From 6eecb8d4666d23d9b91b6740309622071ac1b3e5 Mon Sep 17 00:00:00 2001 From: jtroo Date: Tue, 27 Jun 2023 22:57:39 -0700 Subject: [PATCH 001/819] ci: run clippy and test on subcrates (#468) --- .github/workflows/rust.yml | 19 +++++++++++-------- parser/src/cfg/mod.rs | 28 ++++++++++++++-------------- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index b1567062e..f448f3124 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -6,7 +6,9 @@ on: paths: - Cargo.* - src/**/* + - keyberon/**/* - cfg_samples/**/* + - parser/**/* - test_cfgs/**/* - .github/workflows/**/* pull_request: @@ -15,6 +17,7 @@ on: - Cargo.* - src/**/* - keyberon/**/* + - parser/**/* - cfg_samples/**/* - test_cfgs/**/* - .github/workflows/**/* @@ -42,13 +45,13 @@ jobs: workspaces: ./ - run: rustup component add clippy - name: Run tests - run: cargo test --verbose + run: cargo test --verbose -p kanata -p kanata-parser -p kanata-keyberon - name: Run tests all features - run: cargo test --all-features --verbose + run: cargo test --all-features --verbose -p kanata -p kanata-parser -p kanata-keyberon - name: Run clippy no features - run: cargo clippy -- -D warnings + run: cargo clippy --all -- -D warnings - name: Run clippy all features - run: cargo clippy --all-features -- -D warnings + run: cargo clippy --all --all-features -- -D warnings build-test-clippy-windows: runs-on: windows-latest @@ -64,10 +67,10 @@ jobs: workspaces: ./ - run: rustup component add clippy - name: Run tests - run: cargo test --verbose + run: cargo test --verbose -p kanata -p kanata-parser -p kanata-keyberon - name: Run tests all features - run: cargo test --all-features --verbose + run: cargo test --all-features --verbose -p kanata -p kanata-parser -p kanata-keyberon - name: Run clippy no features - run: cargo clippy + run: cargo clippy --all -- -D warnings - name: Run clippy all features - run: cargo clippy --all-features + run: cargo clippy --all --all-features -- -D warnings diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index 0e540811c..38cd1167b 100644 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -5,28 +5,28 @@ //! //! If the mapped keys are defined as: //! -//! (defsrc -//! esc 1 2 3 4 -//! ) +//! (defsrc +//! esc 1 2 3 4 +//! ) //! //! and the layers are: //! -//! (deflayer one -//! _ a s d _ -//! ) +//! (deflayer one +//! _ a s d _ +//! ) //! -//! (deflayer two -//! _ a o e _ -//! ) +//! (deflayer two +//! _ a o e _ +//! ) //! //! Then the keyberon layers will be as follows: //! -//! xx means unimportant and _ means transparent. +//! (xx means unimportant and _ means transparent) //! -//! layers[0] = { xx, esc, a, s, d, 4, xx... } -//! layers[1] = { xx, _ , a, s, d, _, xx... } -//! layers[2] = { xx, esc, a, o, e, 4, xx... } -//! layers[3] = { xx, _ , a, o, e, _, xx... } +//! layers[0] = { xx, esc, a, s, d, 4, xx... } +//! layers[1] = { xx, _ , a, s, d, _, xx... } +//! layers[2] = { xx, esc, a, o, e, 4, xx... } +//! layers[3] = { xx, _ , a, o, e, _, xx... } //! //! Note that this example isn't practical, but `(defsrc esc 1 2 3 4)` is used because these keys //! are at the beginning of the array. The column index for layers is the numerical value of From bf24e142c19a37e08753a22ab6ae9848be2d65c9 Mon Sep 17 00:00:00 2001 From: rszyma Date: Wed, 28 Jun 2023 22:49:25 +0200 Subject: [PATCH 002/819] fix: cargo features in subcrate (#471) --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e7cada242..5c15f5a78 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,9 +52,9 @@ native-windows-gui = { version = "1.0.12", default_features = false } kanata-interception = { version = "0.2.0", optional = true } [features] -cmd = [] +cmd = ["kanata-parser/cmd"] perf_logging = [] -interception_driver = ["kanata-interception"] +interception_driver = ["kanata-interception", "kanata-parser/interception_driver"] [profile.release] opt-level = "z" From c2bfb764fab679bb7c2a70320cf8771e28425dad Mon Sep 17 00:00:00 2001 From: jtroo Date: Sat, 1 Jul 2023 23:27:55 -0700 Subject: [PATCH 003/819] fix: order of operations for oneshot in Custom branch --- keyberon/src/layout.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/keyberon/src/layout.rs b/keyberon/src/layout.rs index 9ba8391ee..6392eb1e7 100644 --- a/keyberon/src/layout.rs +++ b/keyberon/src/layout.rs @@ -1375,13 +1375,13 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt } Custom(value) => { self.last_press_tracker.coord = coord; - if self.states.push(State::Custom { value, coord }).is_ok() { - return CustomEvent::Press(value); - } if !is_oneshot { self.oneshot .handle_press(OneShotHandlePressKey::Other(coord)); } + if self.states.push(State::Custom { value, coord }).is_ok() { + return CustomEvent::Press(value); + } } ReleaseState(rs) => { self.states.retain(|s| s.release_state(*rs).is_some()); From 31ea28186cf3d2938ce6e79d41e8e9df9fc824a6 Mon Sep 17 00:00:00 2001 From: jtroo Date: Sat, 1 Jul 2023 23:29:50 -0700 Subject: [PATCH 004/819] fix: move repeat rate to after the grab (#472) --- justfile | 7 +++++++ src/kanata/linux.rs | 4 ++++ src/kanata/mod.rs | 6 +++++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/justfile b/justfile index 4f79eb5b0..6e5150561 100644 --- a/justfile +++ b/justfile @@ -16,3 +16,10 @@ build_release_windows output_dir: sha256sums output_dir: rm -f {{output_dir}}/sha256sums cd {{output_dir}}; sha256sum * > sha256sums + +test: + cargo test --verbose -p kanata -p kanata-parser -p kanata-keyberon -- --nocapture + cargo clippy --all + +fmt: + cargo fmt --all diff --git a/src/kanata/linux.rs b/src/kanata/linux.rs index aa2c2563c..97ff87f2e 100644 --- a/src/kanata/linux.rs +++ b/src/kanata/linux.rs @@ -25,6 +25,10 @@ impl Kanata { bail!("failed to open keyboard device(s): {}", e) } }; + + // In some environments, this needs to be done after the input device grab otherwise it + // does not work on kanata startup. + Kanata::set_repeat_rate(&k.defcfg_items)?; drop(k); loop { diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index 5fe719911..85232287d 100644 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -139,6 +139,9 @@ pub struct Kanata { log_layer_changes: bool, /// Tracks the caps-word state. Is Some(...) if caps-word is active and None otherwise. pub caps_word: Option, + /// Config items from `defcfg`. + #[cfg(target_os = "linux")] + pub defcfg_items: HashMap, } pub struct ScrollState { @@ -298,7 +301,6 @@ impl Kanata { .get("linux-dev-names-exclude") .cloned() .map(|paths| parse_colon_separated_text(&paths)); - Kanata::set_repeat_rate(&cfg.items)?; #[cfg(target_os = "windows")] unsafe { @@ -389,6 +391,8 @@ impl Kanata { dynamic_macros: Default::default(), log_layer_changes, caps_word: None, + #[cfg(target_os = "linux")] + defcfg_items: cfg.items, }) } From 31eb7de5aa1f6c3dd495630cfca20ef03dc11453 Mon Sep 17 00:00:00 2001 From: jtroo Date: Sat, 1 Jul 2023 23:30:36 -0700 Subject: [PATCH 005/819] doc: include (#473) --- cfg_samples/included-file.kbd | 3 +++ cfg_samples/kanata.kbd | 6 ++++++ docs/config.adoc | 30 ++++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+) create mode 100644 cfg_samples/included-file.kbd diff --git a/cfg_samples/included-file.kbd b/cfg_samples/included-file.kbd new file mode 100644 index 000000000..1c755703d --- /dev/null +++ b/cfg_samples/included-file.kbd @@ -0,0 +1,3 @@ +(defalias + included-alias (macro i spc a m spc i n c l u d e d) +) diff --git a/cfg_samples/kanata.kbd b/cfg_samples/kanata.kbd index b96ce8ee6..229ba8a7a 100644 --- a/cfg_samples/kanata.kbd +++ b/cfg_samples/kanata.kbd @@ -871,9 +871,15 @@ If you need help, you are welcome to ask. ( 2 4 8) (multi 1 4) (1 2 4 8) (multi 1 5) ) + (defalias ch1 (chord binary 1) ch2 (chord binary 2) ch4 (chord binary 4) ch8 (chord binary 8) ) + +;; The top-level action `include` will read a configuration from a new file. +;; At the time of writing, includes can only be placed at the top level. The +;; included files also cannot contain includes themselves. +(include included-file.kbd) diff --git a/docs/config.adoc b/docs/config.adoc index 05e58894c..1398e8562 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -1666,6 +1666,36 @@ Only zero or one `defoverrides` is allowed in a configuration file. ) ---- +[[include]] +== Include other files +<> + +The `include` optional configuration item +allows you to include other files into the configuration. +This configuration accepts a single string which is a file path. +The file path can be an absolute path or a relative path. +The path will be relative to the defined configuration file. + +At the time of writing, includes can only be placed at the top level. +The included files also cannot contain includes themselves. + +.Example: +---- +;; This is in the file initially read by kanata, e.g. kanata.kbd +(include other-file.kbd) + +;; This is in the other file +(defalias + included-alias XX + ;; ... +) + +;; This is in the other file +(deflayer included-layer + ;; ... +) +---- + [[advanced-weird-features]] == Advanced/weird features From e79b0cb70ea2b2e8ed9013aff15ff2e2ba17f042 Mon Sep 17 00:00:00 2001 From: George Stuyt Date: Thu, 6 Jul 2023 14:58:43 +1000 Subject: [PATCH 006/819] doc: update config.adoc link to key name source (#477) --- docs/config.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config.adoc b/docs/config.adoc index 1398e8562..7c13339ff 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -64,7 +64,7 @@ spaces, tabs, or newlines however you like to visually format `defsrc` to your liking. The the primary source of all key names is the -https://github.com/jtroo/kanata/blob/main/src/keys/mod.rs[str_to_oscode] +https://github.com/jtroo/kanata/blob/main/parser/src/keys/mod.rs[str_to_oscode] function in the source code. Please feel free to file an issue if you're unable to find the key you're looking for. From 14f755f20295b6189773d1c2d2b92a624ffc07d9 Mon Sep 17 00:00:00 2001 From: jtroo Date: Thu, 6 Jul 2023 17:26:36 -0700 Subject: [PATCH 007/819] ver: 1.4.0-prerelease-3 --- Cargo.toml | 12 ++++++------ keyberon/Cargo.toml | 2 +- parser/Cargo.toml | 8 ++++++-- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5c15f5a78..3c4373d39 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kanata" -version = "1.4.0-prerelease-2" +version = "1.4.0-prerelease-3" authors = ["jtroo "] description = "Multi-layer keyboard customization" keywords = ["cli", "linux", "windows", "keyboard", "layout"] @@ -25,13 +25,13 @@ rustc-hash = "1.1.0" miette = { version = "5.7.0", features = ["fancy"] } dirs = "5.0.1" -# kanata-keyberon = "0.17.0" -# Uncomment below and comment out above for testing local keyberon changes. +kanata-keyberon = "0.19.0" +kanata-parser = "0.19.0" +# Uncomment below and comment out above for testing local changes. # Otherwise any changes to the local files will not reflect in the compiled # binary. -kanata-keyberon = { path = "keyberon" } - -kanata-parser = { path = "parser" } +# kanata-keyberon = { path = "keyberon" } +# kanata-parser = { path = "parser" } [target.'cfg(target_os = "linux")'.dependencies] evdev = "=0.12.0" diff --git a/keyberon/Cargo.toml b/keyberon/Cargo.toml index 9ea485711..8be288936 100644 --- a/keyberon/Cargo.toml +++ b/keyberon/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kanata-keyberon" -version = "0.18.0" +version = "0.19.0" authors = ["Guillaume Pinot ", "Robin Krahl ", "jtroo "] edition = "2018" description = "Pure Rust keyboard firmware. Fork intended for use with kanata." diff --git a/parser/Cargo.toml b/parser/Cargo.toml index 91c3c20c7..56383fdbc 100644 --- a/parser/Cargo.toml +++ b/parser/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kanata-parser" -version = "0.1.0" +version = "0.19.0" authors = ["jtroo "] description = "A parser for configuration language of kanata, a keyboard remapper." keywords = ["kanata", "parser"] @@ -20,7 +20,11 @@ rustc-hash = "1.1.0" miette = { version = "5.7.0", features = ["fancy"] } thiserror = "1.0.38" -kanata-keyberon = { path = "../keyberon" } +kanata-keyberon = "0.19.0" +# Uncomment below and comment out above for testing local changes. +# Otherwise any changes to the local files will not reflect in the compiled +# binary. +# kanata-keyberon = { path = "keyberon" } [features] cmd = [] From 68446dae107bf83f31ebab34d7cf655acd8ed83d Mon Sep 17 00:00:00 2001 From: jtroo Date: Thu, 6 Jul 2023 21:55:30 -0700 Subject: [PATCH 008/819] fix: make tests work after crates.io publish (#480) --- Cargo.lock | 6 +- Cargo.toml | 8 +-- parser/Cargo.toml | 4 +- parser/src/cfg/tests.rs | 60 +----------------- .../test_cfgs}/transparent_default.kbd | 0 src/main.rs | 3 + src/tests.rs | 62 +++++++++++++++++++ 7 files changed, 75 insertions(+), 68 deletions(-) rename {cfg_samples => parser/test_cfgs}/transparent_default.kbd (100%) create mode 100644 src/tests.rs diff --git a/Cargo.lock b/Cargo.lock index ec29f5ce8..51db58dfc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -383,7 +383,7 @@ checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" [[package]] name = "kanata" -version = "1.4.0-prerelease-2" +version = "1.4.0-prerelease-3" dependencies = [ "anyhow", "clap", @@ -425,7 +425,7 @@ dependencies = [ [[package]] name = "kanata-keyberon" -version = "0.18.0" +version = "0.19.0" dependencies = [ "arraydeque", "heapless", @@ -444,7 +444,7 @@ dependencies = [ [[package]] name = "kanata-parser" -version = "0.1.0" +version = "0.19.0" dependencies = [ "anyhow", "evdev", diff --git a/Cargo.toml b/Cargo.toml index 3c4373d39..5b2335a8a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,13 +25,13 @@ rustc-hash = "1.1.0" miette = { version = "5.7.0", features = ["fancy"] } dirs = "5.0.1" -kanata-keyberon = "0.19.0" -kanata-parser = "0.19.0" +# kanata-keyberon = "0.19.0" +# kanata-parser = "0.19.0" # Uncomment below and comment out above for testing local changes. # Otherwise any changes to the local files will not reflect in the compiled # binary. -# kanata-keyberon = { path = "keyberon" } -# kanata-parser = { path = "parser" } +kanata-keyberon = { path = "keyberon" } +kanata-parser = { path = "parser" } [target.'cfg(target_os = "linux")'.dependencies] evdev = "=0.12.0" diff --git a/parser/Cargo.toml b/parser/Cargo.toml index 56383fdbc..b01fe2d98 100644 --- a/parser/Cargo.toml +++ b/parser/Cargo.toml @@ -20,11 +20,11 @@ rustc-hash = "1.1.0" miette = { version = "5.7.0", features = ["fancy"] } thiserror = "1.0.38" -kanata-keyberon = "0.19.0" +# kanata-keyberon = "0.19.0" # Uncomment below and comment out above for testing local changes. # Otherwise any changes to the local files will not reflect in the compiled # binary. -# kanata-keyberon = { path = "keyberon" } +kanata-keyberon = { path = "../keyberon" } [features] cmd = [] diff --git a/parser/src/cfg/tests.rs b/parser/src/cfg/tests.rs index 6b5cbf544..42963c44f 100644 --- a/parser/src/cfg/tests.rs +++ b/parser/src/cfg/tests.rs @@ -24,52 +24,6 @@ fn span_works() { assert_eq!(&s[tlevel[1].span.start..tlevel[1].span.end], "(row two)"); } -#[test] -fn parse_simple() { - let _lk = match CFG_PARSE_LOCK.lock() { - Ok(guard) => guard, - Err(poisoned) => poisoned.into_inner(), - }; - new_from_file(&std::path::PathBuf::from("../cfg_samples/simple.kbd")).unwrap(); -} - -#[test] -fn parse_minimal() { - let _lk = match CFG_PARSE_LOCK.lock() { - Ok(guard) => guard, - Err(poisoned) => poisoned.into_inner(), - }; - new_from_file(&std::path::PathBuf::from("../cfg_samples/minimal.kbd")).unwrap(); -} - -#[test] -fn parse_default() { - let _lk = match CFG_PARSE_LOCK.lock() { - Ok(guard) => guard, - Err(poisoned) => poisoned.into_inner(), - }; - new_from_file(&std::path::PathBuf::from("../cfg_samples/kanata.kbd")).unwrap(); -} - -#[test] -fn parse_jtroo() { - let _lk = match CFG_PARSE_LOCK.lock() { - Ok(guard) => guard, - Err(poisoned) => poisoned.into_inner(), - }; - let cfg = new_from_file(&std::path::PathBuf::from("../cfg_samples/jtroo.kbd")).unwrap(); - assert_eq!(cfg.layer_info.len(), 16); -} - -#[test] -fn parse_f13_f24() { - let _lk = match CFG_PARSE_LOCK.lock() { - Ok(guard) => guard, - Err(poisoned) => poisoned.into_inner(), - }; - new_from_file(&std::path::PathBuf::from("../cfg_samples/f13_f24.kbd")).unwrap(); -} - #[test] fn parse_action_vars() { let _lk = match CFG_PARSE_LOCK.lock() { @@ -230,7 +184,7 @@ fn parse_transparent_default() { }; let mut s = ParsedState::default(); let (_, _, layer_strings, layers, _, _) = parse_cfg_raw( - &std::path::PathBuf::from("../cfg_samples/transparent_default.kbd"), + &std::path::PathBuf::from("./test_cfgs/transparent_default.kbd"), &mut s, ) .unwrap(); @@ -269,18 +223,6 @@ fn parse_transparent_default() { assert_eq!(layers[3][0][usize::from(OsCode::KEY_F15)], Action::Trans); } -#[test] -fn parse_all_keys() { - let _lk = match CFG_PARSE_LOCK.lock() { - Ok(guard) => guard, - Err(poisoned) => poisoned.into_inner(), - }; - new_from_file(&std::path::PathBuf::from( - "../cfg_samples/all_keys_in_defsrc.kbd", - )) - .unwrap(); -} - #[test] fn parse_multiline_comment() { let _lk = match CFG_PARSE_LOCK.lock() { diff --git a/cfg_samples/transparent_default.kbd b/parser/test_cfgs/transparent_default.kbd similarity index 100% rename from cfg_samples/transparent_default.kbd rename to parser/test_cfgs/transparent_default.kbd diff --git a/src/main.rs b/src/main.rs index 3bbb2c2de..be5b3c431 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,9 @@ mod kanata; mod oskbd; mod tcp_server; +#[cfg(test)] +mod tests; + use clap::Parser; use kanata::Kanata; use tcp_server::TcpServer; diff --git a/src/tests.rs b/src/tests.rs new file mode 100644 index 000000000..c714694e4 --- /dev/null +++ b/src/tests.rs @@ -0,0 +1,62 @@ +use kanata_parser::cfg::*; +use std::sync::Mutex; + +static CFG_PARSE_LOCK: Mutex<()> = Mutex::new(()); + +#[test] +fn parse_simple() { + let _lk = match CFG_PARSE_LOCK.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + }; + new_from_file(&std::path::PathBuf::from("./cfg_samples/simple.kbd")).unwrap(); +} + +#[test] +fn parse_minimal() { + let _lk = match CFG_PARSE_LOCK.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + }; + new_from_file(&std::path::PathBuf::from("./cfg_samples/minimal.kbd")).unwrap(); +} + +#[test] +fn parse_default() { + let _lk = match CFG_PARSE_LOCK.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + }; + new_from_file(&std::path::PathBuf::from("./cfg_samples/kanata.kbd")).unwrap(); +} + +#[test] +fn parse_jtroo() { + let _lk = match CFG_PARSE_LOCK.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + }; + let cfg = new_from_file(&std::path::PathBuf::from("./cfg_samples/jtroo.kbd")).unwrap(); + assert_eq!(cfg.layer_info.len(), 16); +} + +#[test] +fn parse_f13_f24() { + let _lk = match CFG_PARSE_LOCK.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + }; + new_from_file(&std::path::PathBuf::from("./cfg_samples/f13_f24.kbd")).unwrap(); +} + +#[test] +fn parse_all_keys() { + let _lk = match CFG_PARSE_LOCK.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + }; + new_from_file(&std::path::PathBuf::from( + "./cfg_samples/all_keys_in_defsrc.kbd", + )) + .unwrap(); +} From 370cd52cd552268a62748fa46d79a5eae855cd04 Mon Sep 17 00:00:00 2001 From: rszyma Date: Sun, 9 Jul 2023 06:05:47 +0200 Subject: [PATCH 009/819] fix!: disallow invalid boolean defcfg values (#483) --- parser/src/cfg/mod.rs | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index 38cd1167b..0c015bd35 100644 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -230,6 +230,7 @@ fn parse_cfg( pub const FALSE_VALUES: [&str; 3] = ["no", "false", "0"]; pub const TRUE_VALUES: [&str; 3] = ["yes", "true", "1"]; +pub const BOOLEAN_VALUES: [&str; 6] = ["yes", "true", "1", "no", "false", "0"]; #[cfg(all(not(feature = "interception_driver"), target_os = "windows"))] const DEF_LOCAL_KEYS: &str = "deflocalkeys-win"; @@ -651,24 +652,26 @@ fn check_first_expr<'a>( /// Parse configuration entries from an expression starting with defcfg. fn parse_defcfg(expr: &[SExpr]) -> Result> { - let valid_cfg_keys = &[ - "process-unmapped-keys", - "danger-enable-cmd", + let non_bool_cfg_keys = &[ "sequence-timeout", "sequence-input-mode", - "sequence-backtrack-modcancel", - "log-layer-changes", - "delegate-to-first-layer", "linux-dev", "linux-dev-names-include", "linux-dev-names-exclude", - "linux-continue-if-no-devs-found", "linux-unicode-u-code", "linux-unicode-termination", "linux-x11-repeat-delay-rate", "windows-altgr", "windows-interception-mouse-hwid", ]; + let bool_cfg_keys = &[ + "process-unmapped-keys", + "danger-enable-cmd", + "sequence-backtrack-modcancel", + "log-layer-changes", + "delegate-to-first-layer", + "linux-continue-if-no-devs-found", + ]; let mut cfg = HashMap::default(); let mut exprs = check_first_expr(expr.iter(), "defcfg")?; // Read k-v pairs from the configuration @@ -683,7 +686,18 @@ fn parse_defcfg(expr: &[SExpr]) -> Result> { }; match (&key, &val) { (SExpr::Atom(k), SExpr::Atom(v)) => { - if !valid_cfg_keys.iter().any(|valid_key| &k.t == valid_key) { + if non_bool_cfg_keys.contains(&&*k.t) { + // nothing to do + } else if bool_cfg_keys.contains(&&*k.t) { + if !BOOLEAN_VALUES.contains(&&*v.t) { + bail_expr!( + val, + "The value for {} must be one of: {}", + k.t, + BOOLEAN_VALUES.join(", ") + ); + } + } else { bail_expr!(key, "Unknown defcfg option {}", k.t); } if cfg From ca25292be4a1c016057793dd79d4bad23fd962a7 Mon Sep 17 00:00:00 2001 From: rszyma Date: Tue, 11 Jul 2023 06:52:13 +0200 Subject: [PATCH 010/819] refactor: make parser compilable to wasm (#484) --- Cargo.lock | 1 - parser/Cargo.toml | 5 --- parser/src/cfg/mod.rs | 2 +- parser/src/keys/linux.rs | 25 +----------- parser/src/keys/mod.rs | 65 +++++------------------------- src/kanata/mod.rs | 2 +- src/kanata/windows/interception.rs | 3 +- src/oskbd/linux.rs | 23 ++++++++++- src/oskbd/mod.rs | 50 +++++++++++++++++++++++ src/oskbd/windows/interception.rs | 4 +- src/oskbd/windows/llhook.rs | 1 + src/oskbd/windows/mod.rs | 2 +- 12 files changed, 90 insertions(+), 93 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 51db58dfc..31a42347e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -447,7 +447,6 @@ name = "kanata-parser" version = "0.19.0" dependencies = [ "anyhow", - "evdev", "kanata-keyberon", "log", "miette", diff --git a/parser/Cargo.toml b/parser/Cargo.toml index b01fe2d98..3ef34f312 100644 --- a/parser/Cargo.toml +++ b/parser/Cargo.toml @@ -29,8 +29,3 @@ kanata-keyberon = { path = "../keyberon" } [features] cmd = [] interception_driver = [] - -[target.'cfg(target_os = "linux")'.dependencies] -evdev = "=0.12.0" - -[target.'cfg(target_os = "windows")'.dependencies] diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index 0c015bd35..a673fa159 100644 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -236,7 +236,7 @@ pub const BOOLEAN_VALUES: [&str; 6] = ["yes", "true", "1", "no", "false", "0"]; const DEF_LOCAL_KEYS: &str = "deflocalkeys-win"; #[cfg(all(feature = "interception_driver", target_os = "windows"))] const DEF_LOCAL_KEYS: &str = "deflocalkeys-wintercept"; -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "unknown"))] const DEF_LOCAL_KEYS: &str = "deflocalkeys-linux"; #[allow(clippy::type_complexity)] // return type is not pub diff --git a/parser/src/keys/linux.rs b/parser/src/keys/linux.rs index 2cd90a64b..b981ff9d1 100644 --- a/parser/src/keys/linux.rs +++ b/parser/src/keys/linux.rs @@ -1,10 +1,6 @@ // This file is taken from the original ktrl project's keys.rs file with modifications. -use evdev::{EventType, InputEvent}; -use std::convert::TryFrom; - -use super::{KeyEvent, KeyValue, OsCode}; - +use super::OsCode; impl OsCode { pub const fn as_u16(self) -> u16 { self as u16 @@ -763,25 +759,6 @@ impl OsCode { } } -impl TryFrom for KeyEvent { - type Error = (); - fn try_from(item: InputEvent) -> Result { - match item.kind() { - evdev::InputEventKind::Key(k) => Ok(Self { - code: OsCode::from_u16(k.0).ok_or(())?, - value: KeyValue::from(item.value()), - }), - _ => Err(()), - } - } -} - -impl From for InputEvent { - fn from(item: KeyEvent) -> Self { - InputEvent::new(EventType::KEY, item.code as u16, item.value as i32) - } -} - use crate::custom_action::Btn; impl From for OsCode { fn from(btn: Btn) -> Self { diff --git a/parser/src/keys/mod.rs b/parser/src/keys/mod.rs index 14cbb9b4a..4cb36a6c6 100644 --- a/parser/src/keys/mod.rs +++ b/parser/src/keys/mod.rs @@ -5,9 +5,9 @@ use once_cell::sync::Lazy; use parking_lot::Mutex; use rustc_hash::FxHashMap as HashMap; -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "unknown"))] mod linux; -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "unknown"))] pub use linux::*; #[cfg(target_os = "windows")] @@ -216,17 +216,17 @@ pub fn str_to_oscode(s: &str) -> Option { "f24" => OsCode::KEY_F24, #[cfg(target_os = "windows")] "kana" | "katakana" | "katakanahiragana" => OsCode::KEY_HANGEUL, - #[cfg(target_os = "linux")] + #[cfg(any(target_os = "linux", target_os = "unknown"))] "kana" | "katakanahiragana" => OsCode::KEY_KATAKANAHIRAGANA, - #[cfg(target_os = "linux")] + #[cfg(any(target_os = "linux", target_os = "unknown"))] "hiragana" => OsCode::KEY_HIRAGANA, - #[cfg(target_os = "linux")] + #[cfg(any(target_os = "linux", target_os = "unknown"))] "katakana" => OsCode::KEY_KATAKANA, "cnv" | "conv" | "henk" | "hnk" | "henkan" => OsCode::KEY_HENKAN, "ncnv" | "mhnk" | "muhenkan" => OsCode::KEY_MUHENKAN, "ro" => OsCode::KEY_RO, - #[cfg(target_os = "linux")] + #[cfg(any(target_os = "linux", target_os = "unknown"))] "prtsc" | "prnt" => OsCode::KEY_SYSRQ, #[cfg(target_os = "windows")] "prtsc" | "prnt" => OsCode::KEY_PRINT, @@ -244,11 +244,11 @@ pub fn str_to_oscode(s: &str) -> Option { "calc" => OsCode::KEY_CALC, // NOTE: these are linux-only right now due to missing the mappings in windows.rs - #[cfg(target_os = "linux")] + #[cfg(any(target_os = "linux", target_os = "unknown"))] "plyr" | "player" => OsCode::KEY_PLAYER, - #[cfg(target_os = "linux")] + #[cfg(any(target_os = "linux", target_os = "unknown"))] "powr" | "power" => OsCode::KEY_POWER, - #[cfg(target_os = "linux")] + #[cfg(any(target_os = "linux", target_os = "unknown"))] "zzz" | "sleep" => OsCode::KEY_SLEEP, _ => { @@ -1066,50 +1066,3 @@ impl From<&KeyCode> for OsCode { (*item).into() } } - -// ------------------ KeyValue -------------------- - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum KeyValue { - Release = 0, - Press = 1, - Repeat = 2, -} - -impl From for KeyValue { - fn from(item: i32) -> Self { - match item { - 0 => Self::Release, - 1 => Self::Press, - 2 => Self::Repeat, - _ => unreachable!(), - } - } -} - -impl From for KeyValue { - fn from(up: bool) -> Self { - match up { - true => Self::Release, - false => Self::Press, - } - } -} - -impl From for bool { - fn from(val: KeyValue) -> Self { - matches!(val, KeyValue::Release) - } -} -#[derive(Debug, Clone, Copy)] -pub struct KeyEvent { - pub code: OsCode, - pub value: KeyValue, -} - -#[cfg(not(all(feature = "interception_driver", target_os = "windows")))] -impl KeyEvent { - pub fn new(code: OsCode, value: KeyValue) -> Self { - Self { code, value } - } -} diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index 85232287d..112075212 100644 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -16,7 +16,7 @@ use std::sync::atomic::{AtomicBool, AtomicU32, Ordering::SeqCst}; use std::sync::Arc; use std::time; -use crate::oskbd::*; +use crate::oskbd::{KeyEvent, *}; use crate::tcp_server::ServerMessage; use crate::ValidatedArgs; use kanata_parser::cfg; diff --git a/src/kanata/windows/interception.rs b/src/kanata/windows/interception.rs index a7fe3bd8a..c68d2602c 100644 --- a/src/kanata/windows/interception.rs +++ b/src/kanata/windows/interception.rs @@ -6,7 +6,8 @@ use std::sync::Arc; use super::PRESSED_KEYS; use crate::kanata::*; -use kanata_parser::keys::{KeyValue, OsCode}; +use crate::oskbd::KeyValue; +use kanata_parser::keys::OsCode; const HWID_ARR_SZ: usize = 128; diff --git a/src/oskbd/linux.rs b/src/oskbd/linux.rs index 371f79160..8989a2fff 100644 --- a/src/oskbd/linux.rs +++ b/src/oskbd/linux.rs @@ -10,14 +10,16 @@ use signal_hook::{ iterator::Signals, }; +use std::convert::TryFrom; use std::fs; use std::io; use std::os::unix::io::AsRawFd; use std::path::PathBuf; use std::thread; +use super::*; +use crate::oskbd::KeyEvent; use kanata_parser::custom_action::*; -use kanata_parser::keys::KeyEvent; use kanata_parser::keys::*; pub struct KbdIn { @@ -257,6 +259,25 @@ pub fn is_input_device(device: &Device) -> bool { } } +impl TryFrom for KeyEvent { + type Error = (); + fn try_from(item: InputEvent) -> Result { + match item.kind() { + evdev::InputEventKind::Key(k) => Ok(Self { + code: OsCode::from_u16(k.0).ok_or(())?, + value: KeyValue::from(item.value()), + }), + _ => Err(()), + } + } +} + +impl From for InputEvent { + fn from(item: KeyEvent) -> Self { + InputEvent::new(EventType::KEY, item.code as u16, item.value as i32) + } +} + #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum UnicodeTermination { Enter, diff --git a/src/oskbd/mod.rs b/src/oskbd/mod.rs index 092717cb4..32a9a3761 100644 --- a/src/oskbd/mod.rs +++ b/src/oskbd/mod.rs @@ -9,3 +9,53 @@ pub use linux::*; mod windows; #[cfg(target_os = "windows")] pub use windows::*; + +// ------------------ KeyValue -------------------- + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum KeyValue { + Release = 0, + Press = 1, + Repeat = 2, +} + +impl From for KeyValue { + fn from(item: i32) -> Self { + match item { + 0 => Self::Release, + 1 => Self::Press, + 2 => Self::Repeat, + _ => unreachable!(), + } + } +} + +impl From for KeyValue { + fn from(up: bool) -> Self { + match up { + true => Self::Release, + false => Self::Press, + } + } +} + +impl From for bool { + fn from(val: KeyValue) -> Self { + matches!(val, KeyValue::Release) + } +} + +use kanata_parser::keys::OsCode; + +#[derive(Debug, Clone, Copy)] +pub struct KeyEvent { + pub code: OsCode, + pub value: KeyValue, +} + +#[cfg(not(all(feature = "interception_driver", target_os = "windows")))] +impl KeyEvent { + pub fn new(code: OsCode, value: KeyValue) -> Self { + Self { code, value } + } +} diff --git a/src/oskbd/windows/interception.rs b/src/oskbd/windows/interception.rs index 87c254ec8..b3fe124f8 100644 --- a/src/oskbd/windows/interception.rs +++ b/src/oskbd/windows/interception.rs @@ -4,11 +4,11 @@ use std::io; use kanata_interception::{Interception, KeyState, MouseFlags, MouseState, Stroke}; +use super::OsCodeWrapper; +use crate::oskbd::KeyValue; use kanata_parser::custom_action::*; use kanata_parser::keys::*; -use super::OsCodeWrapper; - /// Key event received by the low level keyboard hook. #[derive(Debug, Clone, Copy)] pub struct InputEvent(pub Stroke); diff --git a/src/oskbd/windows/llhook.rs b/src/oskbd/windows/llhook.rs index f751d916f..f0c2f1c57 100644 --- a/src/oskbd/windows/llhook.rs +++ b/src/oskbd/windows/llhook.rs @@ -12,6 +12,7 @@ use winapi::shared::minwindef::*; use winapi::shared::windef::*; use winapi::um::winuser::*; +use crate::oskbd::{KeyEvent, KeyValue}; use kanata_parser::custom_action::*; use kanata_parser::keys::*; diff --git a/src/oskbd/windows/mod.rs b/src/oskbd/windows/mod.rs index d78bb013c..d9c952999 100644 --- a/src/oskbd/windows/mod.rs +++ b/src/oskbd/windows/mod.rs @@ -4,7 +4,7 @@ use winapi::um::winuser::*; use encode_unicode::CharExt; -use kanata_parser::keys::KeyValue; +use crate::oskbd::KeyValue; #[cfg(not(feature = "interception_driver"))] mod llhook; From 1a145f51096b72d859b5a77ff1a3086fff21fe3f Mon Sep 17 00:00:00 2001 From: jtroo Date: Sat, 15 Jul 2023 15:41:32 -0700 Subject: [PATCH 011/819] feat: switch action (#485) --- docs/switch-design | 198 ++++++++++++++ keyberon/src/action.rs | 9 + keyberon/src/action/switch.rs | 502 ++++++++++++++++++++++++++++++++++ keyberon/src/key_code.rs | 65 ----- keyberon/src/layout.rs | 16 +- parser/src/cfg/mod.rs | 128 +++++++++ parser/src/cfg/tests.rs | 97 +++++++ parser/src/trie.rs | 1 + src/kanata/mod.rs | 1 + 9 files changed, 950 insertions(+), 67 deletions(-) create mode 100644 docs/switch-design create mode 100644 keyberon/src/action/switch.rs diff --git a/docs/switch-design b/docs/switch-design new file mode 100644 index 000000000..1d0d94e12 --- /dev/null +++ b/docs/switch-design @@ -0,0 +1,198 @@ +# Preface: + +This document is a scratch space for the design of the switch action. +It may be out of date and is kept around for posterity. + +.syntax: +---- +(switch + (or a b c) (cmd ) break + (and a b (or c d)) (cmd ) fallthrough + (and a b (or c d) (or e f)) fallthrough + () +) +---- + + +.opcode format examples: +---- +(or a b c) +OR-4 a b c + +(and a b (or c d)) +AND-6 a b OR-6 c d + +(and a b (or c d) (or e f)) +AND-9 a b OR-6 c d OR-9 e f +---- + +.opcodes: +---- +key: all values < 1024 +OR/AND: OP & 0xF000 + OR : 0x1000 + AND: 0x2000 + length: OP & 0x0FFF +---- + +.Rough algorithm for opcodes: +---- +value=true +push first opcode +WHILE stack is not empty + WHILE index <= ending_index + switch + opcode: + push, continue + key(OR): + value=true: skip to index, pop + value=false: continue + key(AND): + value=true: continue + value=false: skip to index, pop + pop + switch + current_value(OR): + value=true: skip to index, pop + value=false: continue + current_value(AND): + value=true: continue + value=false: skip to index, pop +return value +---- + +.statestruct: +---- + value + current_index + current_end_index + current_op + stack (op, ending_index) +---- + +.rough sequence 1: +---- +pressed: y y y y y y +opcodes: AND-9 a b OR-6 c d OR-9 e f + +index: 0 + push: AND-9 + stack: AND-9 + +index: 1 + val: true + +index: 2 + val: true + +index: 3 + push: OR-6 + stack: AND-9 OR-6 + +index: 4 + val: true + skip to 6 + pop + stack: AND-9 + +index: 6 + push: OR-9 + stack: AND-9 OR-9 + +index: 7 + val: true + skip to 9 + pop + stack: AND-9-true + +index 9: + pop + stack: empty + return val: true +---- + +.rough sequence 2: +---- +pressed: y y n n y y +opcodes: AND-9 a b OR-6 c d OR-9 e f + +index: 0 + push: AND-9 + stack: AND-9 + +index: 1 + val: true + +index: 2 + val: true + +index: 3 + push: OR-6 + stack: AND-9 OR-6 + val: true + +index: 4 + val: false + +index: 5 + val: false + +index: 6 + val: false + pop + stack: AND-9 + skip to 9 + pop + stack: empty + return val: false +---- + +.rough sequence 3: +---- +pressed: n y n n y y +opcodes: AND-9 a b OR-6 c d OR-9 e f + +index: 0 + push: AND-9 + stack: AND-9 + +index: 1 + val: false + skip to 9 + pop + stack: empty + return val: false +---- + + +.pseudo code again: +---- +let mut value = true +let mut current_index = 1 +let mut current_end_index = first_opcode - end_index +let mut current_op = OR +while current_index < slice_length { + if index >= current_end_index: + if stack is empty: + break + else: + pop stack to current_op and current_end_index + switch + current_value(OR): + value=true: skip to current_end_index; continue + current_value(AND): + value=false: skip to current_end_index; continue + switch + opcode: + push (current_end_index,current_op) + update (current_end_index,current_op) with opcode + key(OR): + value=true: skip to current_end_index; continue + value=false + key(AND): + value=true + value=false: skip to current_end_index; continue + current_index++; +} +return value +---- diff --git a/keyberon/src/action.rs b/keyberon/src/action.rs index 004c88184..e23539203 100644 --- a/keyberon/src/action.rs +++ b/keyberon/src/action.rs @@ -4,6 +4,9 @@ use crate::key_code::KeyCode; use crate::layout::{QueuedIter, WaitingAction}; use core::fmt::Debug; +pub mod switch; +pub use switch::*; + /// The different types of actions we support for key sequences/macros #[non_exhaustive] #[derive(Clone, Copy, Eq, PartialEq)] @@ -373,6 +376,12 @@ where /// Fork action that can activate one of two potential actions depending on what keys are /// currently active. Fork(&'a ForkConfig<'a, T>), + /// Action that can activate 0 to N actions based on what keys are currently + /// active and the boolean logic of each case. + /// + /// The maximum number of actions that can activate the same time is governed by + /// `ACTION_QUEUE_LEN`. + Switch(&'a Switch<'a, T>), } impl<'a, T> Action<'a, T> { diff --git a/keyberon/src/action/switch.rs b/keyberon/src/action/switch.rs new file mode 100644 index 000000000..f3294e31a --- /dev/null +++ b/keyberon/src/action/switch.rs @@ -0,0 +1,502 @@ +//! Handle processing of the switch action for Keyberon. +//! +//! Limitations: +//! - Maximum opcode length: 4095 +//! - Maximum boolean expression depth: 8 +//! +//! The intended use is to build up a `Switch` struct and use that in the `Layout`. +//! +//! The `Layout` will use `Switch::actions` to iterate over the actions that should be activated +//! when the corresponding key is pressed. + +use super::*; + +use crate::key_code::*; + +use BooleanOperator::*; +use BreakOrFallthrough::*; + +pub const MAX_OPCODE_LEN: u16 = 0x0FFF; +pub const MAX_BOOL_EXPR_DEPTH: usize = 8; + +pub type Case<'a, T> = (&'a [OpCode], &'a Action<'a, T>, BreakOrFallthrough); + +#[derive(Debug, Clone, Copy, PartialEq)] +/// Behaviour of a switch action. Each case is a 3-tuple of: +/// +/// - the boolean expression (array of opcodes) +/// - the action to evaluate if the expression evaluates to true +/// - whether to break or fallthrough to the next case if the expression evaluates to true +pub struct Switch<'a, T: 'a> { + pub cases: &'a [Case<'a, T>], +} + +const OR_VAL: u16 = 0x1000; +const AND_VAL: u16 = 0x2000; + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +/// Boolean operator. Notably missing today is Not. +pub enum BooleanOperator { + Or, + And, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +/// OpCode for a switch case boolean expression. +pub struct OpCode(u16); + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +/// The more useful interpretion of an OpCode. +enum OpCodeType { + BooleanOp(OperatorAndEndIndex), + KeyCode(u16), +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +/// The operation type and the opcode index at which evaluating this type ends. +struct OperatorAndEndIndex { + pub op: BooleanOperator, + pub idx: usize, +} + +#[derive(Debug, Copy, Clone, PartialEq)] +/// Whether or not a case should break out of the switch if it evaluates to true or fallthrough to +/// the next case. +pub enum BreakOrFallthrough { + Break, + Fallthrough, +} + +impl<'a, T> Switch<'a, T> { + /// Iterates over the actions (if any) that are activated in the `Switch` based on its cases + /// and the currently active keys. + pub fn actions(&self, active_keys: T2) -> SwitchActions<'a, T, T2> + where + T2: Iterator + Clone, + { + SwitchActions { + cases: self.cases, + active_keys, + case_index: 0, + } + } +} + +#[derive(Debug, Clone)] +/// Iterator returned by `Switch::actions`. +pub struct SwitchActions<'a, T, T2> +where + T2: Iterator + Clone, +{ + cases: &'a [(&'a [OpCode], &'a Action<'a, T>, BreakOrFallthrough)], + active_keys: T2, + case_index: usize, +} + +impl<'a, T, T2> Iterator for SwitchActions<'a, T, T2> +where + T2: Iterator + Clone, +{ + type Item = &'a Action<'a, T>; + + fn next(&mut self) -> Option { + while self.case_index < self.cases.len() { + let case = &self.cases[self.case_index]; + if evaluate_boolean(case.0, self.active_keys.clone()) { + let ret_ac = case.1; + match case.2 { + Break => self.case_index = self.cases.len(), + Fallthrough => self.case_index += 1, + } + return Some(ret_ac); + } else { + self.case_index += 1; + } + } + None + } +} + +impl BooleanOperator { + fn to_u16(self) -> u16 { + match self { + Or => OR_VAL, + And => AND_VAL, + } + } +} + +impl OpCode { + /// Return a new OpCode that checks if the key active or not. + pub fn new_key(kc: KeyCode) -> Self { + assert!((kc as u16) <= MAX_OPCODE_LEN); + Self(kc as u16 & MAX_OPCODE_LEN) + } + + /// Return a new OpCode for a boolean operation that ends (non-inclusive) at the specified + /// index. + pub fn new_bool(op: BooleanOperator, end_idx: u16) -> Self { + Self((end_idx & MAX_OPCODE_LEN) + op.to_u16()) + } + /// Return the interpretation of this `OpCode`. + fn opcode_type(self) -> OpCodeType { + if self.0 < MAX_OPCODE_LEN { + OpCodeType::KeyCode(self.0) + } else { + OpCodeType::BooleanOp(OperatorAndEndIndex::from(self.0)) + } + } +} + +impl From for OperatorAndEndIndex { + fn from(value: u16) -> Self { + Self { + op: match value & 0xF000 { + OR_VAL => Or, + AND_VAL => And, + _ => unreachable!("public interface should protect from this"), + }, + idx: usize::from(value & MAX_OPCODE_LEN), + } + } +} + +/// Evaluate the return value of an expression evaluated on the given key codes. +fn evaluate_boolean( + bool_expr: &[OpCode], + key_codes: impl Iterator + Clone, +) -> bool { + let mut ret = true; + let mut current_index = 0; + let mut current_end_index = bool_expr.len(); + let mut current_op = Or; + let mut stack: arraydeque::ArrayDeque< + [OperatorAndEndIndex; MAX_BOOL_EXPR_DEPTH], + arraydeque::behavior::Saturating, + > = Default::default(); + while current_index < bool_expr.len() { + if current_index >= current_end_index { + match stack.pop_back() { + Some(operator) => { + (current_op, current_end_index) = (operator.op, operator.idx); + } + None => break, + } + if matches!((ret, current_op), (true, Or) | (false, And)) { + current_index = current_end_index; + continue; + } + } + match bool_expr[current_index].opcode_type() { + OpCodeType::KeyCode(kc) => { + ret = key_codes.clone().any(|kc_input| kc_input as u16 == kc); + if matches!((ret, current_op), (true, Or) | (false, And)) { + current_index = current_end_index; + continue; + } + } + OpCodeType::BooleanOp(operator) => { + let res = stack.push_back(OperatorAndEndIndex { + op: current_op, + idx: current_end_index, + }); + assert!( + res.is_ok(), + "exceeded boolean op depth {}", + MAX_BOOL_EXPR_DEPTH + ); + (current_op, current_end_index) = (operator.op, operator.idx); + } + }; + current_index += 1; + } + ret +} + +#[test] +fn bool_evaluation_test_0() { + let opcodes = [ + OpCode::new_bool(And, 9), + OpCode::new_key(KeyCode::A), + OpCode::new_key(KeyCode::B), + OpCode::new_bool(Or, 6), + OpCode::new_key(KeyCode::C), + OpCode::new_key(KeyCode::D), + OpCode::new_bool(Or, 9), + OpCode::new_key(KeyCode::E), + OpCode::new_key(KeyCode::F), + ]; + let keycodes = [KeyCode::A, KeyCode::B, KeyCode::D, KeyCode::F]; + assert_eq!( + evaluate_boolean(opcodes.as_slice(), keycodes.iter().copied()), + true + ); +} + +#[test] +fn bool_evaluation_test_1() { + let opcodes = [ + OpCode::new_bool(And, 9), + OpCode::new_key(KeyCode::A), + OpCode::new_key(KeyCode::B), + OpCode::new_bool(Or, 6), + OpCode::new_key(KeyCode::C), + OpCode::new_key(KeyCode::D), + OpCode::new_bool(Or, 9), + OpCode::new_key(KeyCode::E), + OpCode::new_key(KeyCode::F), + ]; + let keycodes = [ + KeyCode::A, + KeyCode::B, + KeyCode::C, + KeyCode::D, + KeyCode::E, + KeyCode::F, + ]; + assert_eq!( + evaluate_boolean(opcodes.as_slice(), keycodes.iter().copied()), + true + ); +} + +#[test] +fn bool_evaluation_test_2() { + let opcodes = [ + OpCode(0x2009), + OpCode(KeyCode::A as u16), + OpCode(KeyCode::B as u16), + OpCode(0x1006), + OpCode(KeyCode::C as u16), + OpCode(KeyCode::D as u16), + OpCode(0x1009), + OpCode(KeyCode::E as u16), + OpCode(KeyCode::F as u16), + ]; + let keycodes = [KeyCode::A, KeyCode::B, KeyCode::E, KeyCode::F]; + assert_eq!( + evaluate_boolean(opcodes.as_slice(), keycodes.iter().copied()), + false + ); +} + +#[test] +fn bool_evaluation_test_3() { + let opcodes = [ + OpCode(0x2009), + OpCode(KeyCode::A as u16), + OpCode(KeyCode::B as u16), + OpCode(0x1006), + OpCode(KeyCode::C as u16), + OpCode(KeyCode::D as u16), + OpCode(0x1009), + OpCode(KeyCode::E as u16), + OpCode(KeyCode::F as u16), + ]; + let keycodes = [KeyCode::B, KeyCode::C, KeyCode::D, KeyCode::E, KeyCode::F]; + assert_eq!( + evaluate_boolean(opcodes.as_slice(), keycodes.iter().copied()), + false + ); +} + +#[test] +fn bool_evaluation_test_4() { + let opcodes = []; + let keycodes = []; + assert_eq!( + evaluate_boolean(opcodes.as_slice(), keycodes.iter().copied()), + true + ); +} + +#[test] +fn bool_evaluation_test_5() { + let opcodes = []; + let keycodes = [ + KeyCode::A, + KeyCode::B, + KeyCode::C, + KeyCode::D, + KeyCode::E, + KeyCode::F, + ]; + assert_eq!( + evaluate_boolean(opcodes.as_slice(), keycodes.iter().copied()), + true + ); +} + +#[test] +fn bool_evaluation_test_6() { + let opcodes = [OpCode(KeyCode::A as u16), OpCode(KeyCode::B as u16)]; + let keycodes = [ + KeyCode::A, + KeyCode::B, + KeyCode::C, + KeyCode::D, + KeyCode::E, + KeyCode::F, + ]; + assert_eq!( + evaluate_boolean(opcodes.as_slice(), keycodes.iter().copied()), + true + ); +} + +#[test] +fn bool_evaluation_test_7() { + let opcodes = [OpCode(KeyCode::A as u16), OpCode(KeyCode::B as u16)]; + let keycodes = [KeyCode::C, KeyCode::D, KeyCode::E, KeyCode::F]; + assert_eq!( + evaluate_boolean(opcodes.as_slice(), keycodes.iter().copied()), + false + ); +} + +#[test] +fn bool_evaluation_test_9() { + let opcodes = [ + OpCode(0x2003), + OpCode(KeyCode::A as u16), + OpCode(KeyCode::B as u16), + OpCode(KeyCode::C as u16), + ]; + let keycodes = [KeyCode::C, KeyCode::D, KeyCode::E, KeyCode::F]; + assert_eq!( + evaluate_boolean(opcodes.as_slice(), keycodes.iter().copied()), + true + ); +} + +#[test] +fn bool_evaluation_test_10() { + let opcodes = [ + OpCode(0x2004), + OpCode(KeyCode::A as u16), + OpCode(KeyCode::B as u16), + OpCode(KeyCode::C as u16), + ]; + let keycodes = [KeyCode::C, KeyCode::D, KeyCode::E, KeyCode::F]; + assert_eq!( + evaluate_boolean(opcodes.as_slice(), keycodes.iter().copied()), + false + ); +} + +#[test] +fn bool_evaluation_test_11() { + let opcodes = [ + OpCode(0x1003), + OpCode(KeyCode::A as u16), + OpCode(KeyCode::B as u16), + ]; + let keycodes = [KeyCode::C, KeyCode::D, KeyCode::E, KeyCode::F]; + assert_eq!( + evaluate_boolean(opcodes.as_slice(), keycodes.iter().copied()), + false + ); +} + +#[test] +fn bool_evaluation_test_12() { + let opcodes = [ + OpCode(0x1005), + OpCode(0x2004), + OpCode(KeyCode::A as u16), + OpCode(KeyCode::B as u16), + OpCode(KeyCode::C as u16), + ]; + let keycodes = [KeyCode::C, KeyCode::D, KeyCode::E, KeyCode::F]; + assert_eq!( + evaluate_boolean(opcodes.as_slice(), keycodes.iter().copied()), + true + ); +} + +#[test] +fn bool_evaluation_test_max_depth_does_not_panic() { + let opcodes = [ + OpCode(0x1008), + OpCode(0x1008), + OpCode(0x1008), + OpCode(0x1008), + OpCode(0x1008), + OpCode(0x1008), + OpCode(0x1008), + OpCode(0x1008), + ]; + let keycodes = []; + assert_eq!( + evaluate_boolean(opcodes.as_slice(), keycodes.iter().copied()), + true + ); +} + +#[test] +#[should_panic] +fn bool_evaluation_test_more_than_max_depth_panics() { + let opcodes = [ + OpCode(0x1009), + OpCode(0x1009), + OpCode(0x1009), + OpCode(0x1009), + OpCode(0x1009), + OpCode(0x1009), + OpCode(0x1009), + OpCode(0x1009), + OpCode(0x1009), + ]; + let keycodes = []; + assert_eq!( + evaluate_boolean(opcodes.as_slice(), keycodes.iter().copied()), + true + ); +} + +#[test] +fn switch_fallthrough() { + let sw = Switch { + cases: &[ + (&[], &Action::<()>::KeyCode(KeyCode::A), Fallthrough), + (&[], &Action::<()>::KeyCode(KeyCode::B), Fallthrough), + ], + }; + let mut actions = sw.actions([].iter().copied()); + assert_eq!(actions.next(), Some(&Action::<()>::KeyCode(KeyCode::A))); + assert_eq!(actions.next(), Some(&Action::<()>::KeyCode(KeyCode::B))); + assert_eq!(actions.next(), None); +} + +#[test] +fn switch_break() { + let sw = Switch { + cases: &[ + (&[], &Action::<()>::KeyCode(KeyCode::A), Break), + (&[], &Action::<()>::KeyCode(KeyCode::B), Break), + ], + }; + let mut actions = sw.actions([].iter().copied()); + assert_eq!(actions.next(), Some(&Action::<()>::KeyCode(KeyCode::A))); + assert_eq!(actions.next(), None); +} + +#[test] +fn switch_no_actions() { + let sw = Switch { + cases: &[ + ( + &[OpCode::new_key(KeyCode::A)], + &Action::<()>::KeyCode(KeyCode::A), + Break, + ), + ( + &[OpCode::new_key(KeyCode::A)], + &Action::<()>::KeyCode(KeyCode::B), + Break, + ), + ], + }; + let mut actions = sw.actions([].iter().copied()); + assert_eq!(actions.next(), None); +} diff --git a/keyberon/src/key_code.rs b/keyberon/src/key_code.rs index 50784fd17..f526bd0e9 100644 --- a/keyberon/src/key_code.rs +++ b/keyberon/src/key_code.rs @@ -810,68 +810,3 @@ pub enum KeyCode { K743, K744, } - -impl KeyCode { - /// Returns `true` if the key code corresponds to a modifier (sent - /// separately on the USB HID report). - pub fn is_modifier(self) -> bool { - KeyCode::LCtrl <= self && self <= KeyCode::RGui - } - - /// Returns the byte with the bit corresponding to the USB HID - /// modifier bitfield set. - pub fn as_modifier_bit(self) -> u8 { - if self.is_modifier() { - 1 << (self as u8 - KeyCode::LCtrl as u8) - } else { - 0 - } - } -} - -/// A standard keyboard USB HID report. -/// -/// It can handle any modifier and 6 keys. -#[derive(Default, Debug, Clone, Eq, PartialEq)] -pub struct KbHidReport([u8; 8]); - -impl core::iter::FromIterator for KbHidReport { - fn from_iter(iter: T) -> Self - where - T: IntoIterator, - { - let mut res = Self::default(); - for kc in iter { - res.pressed(kc); - } - res - } -} - -impl KbHidReport { - /// Returns the byte slice corresponding to the report. - pub fn as_bytes(&self) -> &[u8] { - &self.0 - } - - /// Add the given key code to the report. If the report is full, - /// it will be set to `ErrorRollOver`. - pub fn pressed(&mut self, kc: KeyCode) { - use KeyCode::*; - match kc { - No => (), - ErrorRollOver | PostFail | ErrorUndefined => self.set_all(kc), - kc if kc.is_modifier() => self.0[0] |= kc.as_modifier_bit(), - _ => self.0[2..] - .iter_mut() - .find(|c| **c == 0) - .map(|c| *c = kc as u8) - .unwrap_or_else(|| self.set_all(ErrorRollOver)), - } - } - fn set_all(&mut self, kc: KeyCode) { - for c in &mut self.0[2..] { - *c = kc as u8; - } - } -} diff --git a/keyberon/src/layout.rs b/keyberon/src/layout.rs index 6392eb1e7..369d0d4dc 100644 --- a/keyberon/src/layout.rs +++ b/keyberon/src/layout.rs @@ -53,11 +53,16 @@ type Queue = ArrayDeque<[Queued; QUEUE_SIZE], arraydeque::behavior::Wrapping>; /// that occur during a Waiting event. type PressedQueue = ArrayDeque<[KCoord; QUEUE_SIZE]>; +/// The maximum number of actions that can be activated concurrently via chord decomposition or +/// activation of multiple switch cases using fallthrough. +pub const ACTION_QUEUE_LEN: usize = 8; + /// The queue is currently only used for chord decomposition when a longer chord does not result in /// an action, but splitting it into smaller chords would. The buffer size of 8 should be more than /// enough for real world usage, but if one wanted to be extra safe, this should be ChordKeys::BITS /// since that should guarantee that all potentially queueable actions can fit. -type ActionQueue<'a, T> = ArrayDeque<[QueuedAction<'a, T>; 8], arraydeque::behavior::Wrapping>; +type ActionQueue<'a, T> = + ArrayDeque<[QueuedAction<'a, T>; ACTION_QUEUE_LEN], arraydeque::behavior::Wrapping>; type QueuedAction<'a, T> = Option<(KCoord, &'a Action<'a, T>)>; /// The layout manager. It takes `Event`s and `tick`s as input, and @@ -840,7 +845,7 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt } } /// Iterates on the key codes of the current state. - pub fn keycodes(&self) -> impl Iterator + '_ { + pub fn keycodes(&self) -> impl Iterator + Clone + '_ { self.states.iter().filter_map(State::keycode) } fn waiting_into_hold(&mut self) -> CustomEvent<'a, T> { @@ -1401,6 +1406,13 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt true => self.do_action(&fcfg.right, coord, delay, false), }; } + Switch(sw) => { + let kcs = self.states.iter().filter_map(State::keycode); + let action_queue = &mut self.action_queue; + for ac in sw.actions(kcs.clone()) { + action_queue.push_back(Some((coord, ac))); + } + } } CustomEvent::NoEvent } diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index a673fa159..926e6ec57 100644 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -1201,6 +1201,7 @@ fn parse_action_list(ac: &[SExpr], s: &ParsedState) -> Result<&'static KanataAct "caps-word" => parse_caps_word(&ac[1..], s), "caps-word-custom" => parse_caps_word_custom(&ac[1..], s), "dynamic-macro-record-stop-truncate" => parse_macro_record_stop_truncate(&ac[1..], s), + "switch" => parse_switch(&ac[1..], s), _ => bail_expr!(&ac[0], "Unknown action type: {ac_type}"), } } @@ -1972,6 +1973,11 @@ fn find_chords_coords(chord_groups: &mut [ChordGroup], coord: (u8, u16), action: find_chords_coords(chord_groups, coord, left); find_chords_coords(chord_groups, coord, right); } + Action::Switch(Switch { cases }) => { + for case in cases.iter() { + find_chords_coords(chord_groups, coord, case.1); + } + } } } @@ -2069,6 +2075,21 @@ fn fill_chords( None } } + Action::Switch(Switch { cases }) => { + let mut new_cases = vec![]; + for case in cases.iter() { + new_cases.push(( + case.0, + fill_chords(chord_groups, &case.1, s) + .map(|ac| s.a.sref(ac)) + .unwrap_or(case.1), + case.2, + )); + } + Some(Action::Switch(s.a.sref(Switch { + cases: s.a.sref_vec(new_cases), + }))) + } } } @@ -2696,6 +2717,108 @@ fn parse_macro_record_stop_truncate( )))) } +fn parse_switch(ac_params: &[SExpr], s: &ParsedState) -> Result<&'static KanataAction> { + const ERR_STR: &str = + "switch expects triples of params: "; + + let mut cases = vec![]; + + let mut params = ac_params.iter(); + loop { + let Some(key_match) = params.next() else { + break; + }; + let Some(action) = params.next() else { + bail!("{ERR_STR}\nMissing and for the final triple"); + }; + let Some(break_or_fallthrough_expr) = params.next() else { + bail!("{ERR_STR}\nMissing for the final triple"); + }; + + let Some(key_match) = key_match.list(s.vars()) else { + bail_expr!(key_match, "{ERR_STR}\n must be a list") + }; + let mut ops = vec![]; + let mut current_index = 0; + for op in key_match.iter() { + current_index = parse_switch_case_bool(current_index, 1, op, &mut ops, s)?; + } + + let action = parse_action(action, s)?; + + let Some(break_or_fallthrough) = break_or_fallthrough_expr.atom(s.vars()) else { + bail_expr!(break_or_fallthrough_expr, "{ERR_STR}\nthis must be one of: break, fallthrough"); + }; + let break_or_fallthrough = match break_or_fallthrough { + "break" => BreakOrFallthrough::Break, + "fallthrough" => BreakOrFallthrough::Fallthrough, + _ => bail_expr!( + break_or_fallthrough_expr, + "{ERR_STR}\nthis must be one of: break, fallthrough" + ), + }; + cases.push((s.a.sref_vec(ops), action, break_or_fallthrough)); + } + Ok(s.a.sref(Action::Switch(s.a.sref(Switch { + cases: s.a.sref_vec(cases), + })))) +} + +fn parse_switch_case_bool( + mut current_index: u16, + depth: u8, + op_expr: &SExpr, + ops: &mut Vec, + s: &ParsedState, +) -> Result { + if current_index > MAX_OPCODE_LEN { + bail_expr!( + op_expr, + "maximum key match size of {MAX_OPCODE_LEN} items is exceeded" + ); + } + if usize::from(depth) > MAX_BOOL_EXPR_DEPTH { + bail_expr!( + op_expr, + "maximum key match expression depth {MAX_BOOL_EXPR_DEPTH} is exceeded" + ); + } + if let Some(a) = op_expr.atom(s.vars()) { + let osc = str_to_oscode(a).ok_or_else(|| anyhow_expr!(op_expr, "invalid key name"))?; + ops.push(OpCode::new_key(osc.into())); + Ok(current_index + 1) + } else { + let l = op_expr + .list(s.vars()) + .expect("must be a list, checked atom"); + if l.len() < 1 { + bail_expr!(op_expr, "key match cannot contain empty lists inside"); + } + let op = l[0] + .atom(s.vars()) + .and_then(|s| match s { + "or" => Some(BooleanOperator::Or), + "and" => Some(BooleanOperator::And), + _ => None, + }) + .ok_or_else(|| { + anyhow_expr!( + op_expr, + "lists inside key match must begin with one of: or, and" + ) + })?; + // insert a placeholder for now, don't know the end index yet. + let placeholder_index = current_index; + ops.push(OpCode::new_bool(op, placeholder_index)); + current_index += 1; + for op in l.iter().skip(1) { + current_index = parse_switch_case_bool(current_index, depth + 1, op, ops, s)?; + } + ops[placeholder_index as usize] = OpCode::new_bool(op, current_index); + Ok(current_index) + } +} + /// Creates a `KeyOutputs` from `layers::LAYERS`. fn create_key_outputs(layers: &KanataLayers, overrides: &Overrides) -> KeyOutputs { let mut outs = KeyOutputs::new(); @@ -2767,6 +2890,11 @@ fn add_key_output_from_action_to_key_pos( add_key_output_from_action_to_key_pos(osc_slot, ac, outputs, overrides); } } + Action::Switch(Switch { cases }) => { + for case in cases.iter() { + add_key_output_from_action_to_key_pos(osc_slot, case.1, outputs, overrides); + } + } Action::NoOp | Action::Trans | Action::Repeat diff --git a/parser/src/cfg/tests.rs b/parser/src/cfg/tests.rs index 42963c44f..7bd0e2dfa 100644 --- a/parser/src/cfg/tests.rs +++ b/parser/src/cfg/tests.rs @@ -1,5 +1,6 @@ use super::*; use crate::cfg::sexpr::parse; +use kanata_keyberon::action::BooleanOperator::*; use std::sync::Mutex; @@ -553,3 +554,99 @@ fn test_include_bad2_has_original_filename() { std::path::MAIN_SEPARATOR ))); } + +#[test] +fn parse_switch() { + let _lk = match CFG_PARSE_LOCK.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + }; + let mut s = ParsedState::default(); + let source = r#" +(defvar var1 a) +(defsrc a) +(deflayer base + (switch + ((and a b (or c d) (or e f))) XX break + () _ fallthrough + (a b c) $var1 fallthrough + ((or (or (or (or (or (or (or (or))))))))) $var1 fallthrough + ) +) +"#; + let res = parse_cfg_raw_string(source, &mut s, "test") + .map_err(|e| { + eprintln!("{:?}", error_with_source(e)); + "" + }) + .unwrap(); + assert_eq!( + res.3[0][0][OsCode::KEY_A.as_u16() as usize], + Action::Switch(&Switch { + cases: &[ + ( + &[ + OpCode::new_bool(And, 9), + OpCode::new_key(KeyCode::A), + OpCode::new_key(KeyCode::B), + OpCode::new_bool(Or, 6), + OpCode::new_key(KeyCode::C), + OpCode::new_key(KeyCode::D), + OpCode::new_bool(Or, 9), + OpCode::new_key(KeyCode::E), + OpCode::new_key(KeyCode::F), + ], + &Action::NoOp, + BreakOrFallthrough::Break + ), + (&[], &Action::Trans, BreakOrFallthrough::Fallthrough), + ( + &[ + OpCode::new_key(KeyCode::A), + OpCode::new_key(KeyCode::B), + OpCode::new_key(KeyCode::C), + ], + &Action::KeyCode(KeyCode::A), + BreakOrFallthrough::Fallthrough + ), + ( + &[ + OpCode::new_bool(Or, 8), + OpCode::new_bool(Or, 8), + OpCode::new_bool(Or, 8), + OpCode::new_bool(Or, 8), + OpCode::new_bool(Or, 8), + OpCode::new_bool(Or, 8), + OpCode::new_bool(Or, 8), + OpCode::new_bool(Or, 8), + ], + &Action::KeyCode(KeyCode::A), + BreakOrFallthrough::Fallthrough + ), + ] + }) + ); +} + +#[test] +fn parse_switch_exceed_depth() { + let _lk = match CFG_PARSE_LOCK.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + }; + let mut s = ParsedState::default(); + let source = r#" +(defsrc a) +(deflayer base + (switch + ((or (or (or (or (or (or (or (or (or)))))))))) XX break + ) +) +"#; + parse_cfg_raw_string(source, &mut s, "test") + .map_err(|e| { + eprintln!("{:?}", error_with_source(e)); + "" + }) + .unwrap_err(); +} diff --git a/parser/src/trie.rs b/parser/src/trie.rs index 31f26968c..e3621ec5f 100644 --- a/parser/src/trie.rs +++ b/parser/src/trie.rs @@ -5,6 +5,7 @@ use radix_trie::TrieCommon; pub type TrieKey = Vec; pub type TrieVal = (u8, u16); +#[derive(Debug, Clone)] pub struct Trie { inner: radix_trie::Trie, } diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index 112075212..f9b445f02 100644 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -1552,6 +1552,7 @@ impl Kanata { && (self.layout.b().oneshot.timeout == 0 || self.layout.b().oneshot.keys.is_empty()) && self.layout.b().active_sequences.is_empty() && self.layout.b().tap_dance_eager.is_none() + && self.layout.b().action_queue.is_empty() && self.sequence_state.is_none() && self.scroll_state.is_none() && self.hscroll_state.is_none() From f08aff1751888f290e3b0f9fa844743d23998716 Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 16 Jul 2023 13:40:20 -0700 Subject: [PATCH 012/819] fix: handle waiting states in action queue (#487) --- keyberon/src/layout.rs | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/keyberon/src/layout.rs b/keyberon/src/layout.rs index 369d0d4dc..ddd02f206 100644 --- a/keyberon/src/layout.rs +++ b/keyberon/src/layout.rs @@ -1162,7 +1162,7 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt delay: u16, is_oneshot: bool, ) -> CustomEvent<'a, T> { - assert!(self.waiting.is_none() || matches!(action, Action::Custom(..))); + self.clear_and_handle_waiting(action); if self.last_press_tracker.coord != coord { self.last_press_tracker.tap_hold_timeout = 0; } @@ -1417,6 +1417,41 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt CustomEvent::NoEvent } + /// Clear the waiting state if it is about to be overwritten by a new waiting state. + /// + /// If something is waiting **and** another waiting action is currently being activated, that + /// probably means that there were multiple actions in the action queue caused by a single + /// terminal state. In this scenario, do some sensible default for the waiting state and end it + /// early, since a new action should interrupt the waiting action anyway. + /// + /// Another potential concern is if there is some processing in the event queue that needs to + /// happen as part of the cleanup, i.e. the code runs in `handle_tap_dance`, `handle_chord` + /// where some queued events are consumed. I'm fairly sure that there is no extra processing + /// that needs to happen. Actions in the action queue should be activated on subsequent ticks + /// with no room for key events to be a factor when handling this case. + fn clear_and_handle_waiting(&mut self, action: &'a Action<'a, T>) { + if !matches!( + action, + Action::HoldTap(_) | Action::TapDance(_) | Action::Chords(_) + ) { + return; + } + let mut waiting_action = None; + if let Some(waiting) = &self.waiting { + waiting_action = match waiting.config { + WaitingConfig::HoldTap(_) => Some((waiting.tap, waiting.coord, waiting.delay)), + WaitingConfig::TapDance(tdc) => { + Some((tdc.actions[0], waiting.coord, waiting.delay)) + } + WaitingConfig::Chord(_) => None, + }; + self.waiting = None; + }; + if let Some((action, coord, delay)) = waiting_action { + self.do_action(action, coord, delay, false); + }; + } + /// Obtain the index of the current active layer pub fn current_layer(&self) -> usize { self.states From 4b1f14f6387ab9f165bba33056916554fb1f2f2f Mon Sep 17 00:00:00 2001 From: DarkKronicle <38167691+DarkKronicle@users.noreply.github.com> Date: Thu, 20 Jul 2023 10:44:28 -0600 Subject: [PATCH 013/819] feat: scnl and sequence start with override of defaults (#492) Co-authored-by: jtroo --- cfg_samples/kanata.kbd | 17 +++++ docs/config.adoc | 34 ++++++++++ parser/src/cfg/mod.rs | 56 ++++++++++++++++- parser/src/custom_action.rs | 40 +++++++++++- src/kanata/mod.rs | 122 +++++++++++------------------------- 5 files changed, 181 insertions(+), 88 deletions(-) diff --git a/cfg_samples/kanata.kbd b/cfg_samples/kanata.kbd index 229ba8a7a..c0fbf587f 100644 --- a/cfg_samples/kanata.kbd +++ b/cfg_samples/kanata.kbd @@ -793,6 +793,11 @@ If you need help, you are welcome to ask. ;; You can add an entry to defcfg to change the sequence timeout (default is 1000): ;; sequence-timeout ;; +;; If you want multiple timeouts with different leaders, you can also activate the +;; sequence action: +;; (sequence ) +;; This acts like `sldr` but uses a different timeout. +;; ;; There is also an option to customize the key sequence input mode. Its default ;; value when not configured is `hidden-suppressed`. ;; @@ -817,6 +822,18 @@ If you need help, you are welcome to ask. (deffakekeys git-status (macro g i t spc s t a t u s)) (defalias rcl (tap-hold-release 200 200 sldr rctl)) +(defseq + dotcom (. S-3) + dotorg (. S-4) +) +(deffakekeys + dotcom (macro . c o m) + dotorg (macro . o r g) +) +;; Enter sequence mode and input . +(defalias dot-sequence (macro (sequence 250) 10 .)) +(defalias dot-sequence-inputmode (macro (sequence 250 hidden-delay-type) 10 .)) + ;; Input chording. ;; ;; Not to be confused with output chords (like C-S-a or the chords layer diff --git a/docs/config.adoc b/docs/config.adoc index 7c13339ff..15a2f1d86 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -1824,6 +1824,15 @@ precisely, the action triggered is: (defseq git-status (g s t)) (deffakekeys git-status (macro g i t spc s t a t u s)) (defalias rcl (tap-hold-release 200 200 sldr rctl)) + +(defseq + dotcom (. S-3) + dotorg (. S-4) +) +(deffakekeys + dotcom (macro . c o m) + dotorg (macro . c o m) +) ---- For more context, you can read the @@ -1833,6 +1842,31 @@ https://github.com/jtroo/kanata/blob/main/docs/sequence-adding-chords-ideas.md[ the document describing chords is sequences] to read about how chords in sequences behave. +==== Override the global timeout and input mode + +An alternative to using `sldr` is the `sequence` action. +The syntax is `(sequence )`. +This enters sequence mode with a sequence timeout +different from the globally configured one. + +The `sequence` action can also be called with a second parameter. +The second parameter is an override for `sequence-input-mode`: + +---- +(sequence ) +---- + + +.Example: +[source] +---- +;; Enter sequence mode and input . with a timeout of 250 +(defalias dot-sequence (macro (sequence 250) 10 .)) + +;; Enter sequence mode and input . with a timeout of 250 and using hidden-delay-type +(defalias dot-sequence (macro (sequence 250 hidden-delay-type) 10 .)) +---- + [[input-chords]] === Input chords <> diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index 926e6ec57..92502b536 100644 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -485,6 +485,17 @@ fn parse_cfg_raw_string( false } }), + default_sequence_timeout: cfg + .get(SEQUENCE_TIMEOUT_CFG_NAME) + .map(|s| match str::parse::(s) { + Ok(0) | Err(_) => Err(anyhow!("{SEQUENCE_TIMEOUT_ERR}")), + Ok(t) => Ok(t), + }) + .unwrap_or(Ok(SEQUENCE_TIMEOUT_DEFAULT))?, + default_sequence_input_mode: cfg + .get(SEQUENCE_INPUT_MODE_CFG_NAME) + .map(|s| SequenceInputMode::try_from_str(s.as_str())) + .unwrap_or(Ok(SequenceInputMode::HiddenSuppressed))?, ..Default::default() }; @@ -852,6 +863,8 @@ struct ParsedState { defsrc_layer: [KanataAction; KEYS_IN_ROW], is_cmd_enabled: bool, delegate_to_first_layer: bool, + default_sequence_timeout: u16, + default_sequence_input_mode: SequenceInputMode, vars: HashMap, a: Arc, } @@ -862,6 +875,12 @@ impl ParsedState { } } +const SEQUENCE_TIMEOUT_CFG_NAME: &str = "sequence-timeout"; +const SEQUENCE_INPUT_MODE_CFG_NAME: &str = "sequence-input-mode"; +const SEQUENCE_TIMEOUT_ERR: &str = "sequence-timeout should be a number (1-65535)"; +const SEQUENCE_TIMEOUT_DEFAULT: u16 = 1000; +const SEQUENCE_INPUT_MODE_DEFAULT: SequenceInputMode = SequenceInputMode::HiddenSuppressed; + impl Default for ParsedState { fn default() -> Self { Self { @@ -875,6 +894,8 @@ impl Default for ParsedState { is_cmd_enabled: false, delegate_to_first_layer: false, vars: Default::default(), + default_sequence_timeout: SEQUENCE_TIMEOUT_DEFAULT, + default_sequence_input_mode: SEQUENCE_INPUT_MODE_DEFAULT, a: unsafe { Allocations::new() }, } } @@ -1042,8 +1063,16 @@ fn parse_action_atom(ac: &Spanned, s: &ParsedState) -> Result<&'static K ))) } "sldr" => { + return Ok(s.a.sref(Action::Custom(s.a.sref(s.a.sref_slice( + CustomAction::SequenceLeader( + s.default_sequence_timeout, + s.default_sequence_input_mode, + ), + ))))) + } + "scnl" => { return Ok(s.a.sref(Action::Custom( - s.a.sref(s.a.sref_slice(CustomAction::SequenceLeader)), + s.a.sref(s.a.sref_slice(CustomAction::SequenceCancel)), ))) } "mlft" | "mouseleft" => { @@ -1202,6 +1231,7 @@ fn parse_action_list(ac: &[SExpr], s: &ParsedState) -> Result<&'static KanataAct "caps-word-custom" => parse_caps_word_custom(&ac[1..], s), "dynamic-macro-record-stop-truncate" => parse_macro_record_stop_truncate(&ac[1..], s), "switch" => parse_switch(&ac[1..], s), + "sequence" => parse_sequence_start(&ac[1..], s), _ => bail_expr!(&ac[0], "Unknown action type: {ac_type}"), } } @@ -2717,6 +2747,30 @@ fn parse_macro_record_stop_truncate( )))) } +fn parse_sequence_start(ac_params: &[SExpr], s: &ParsedState) -> Result<&'static KanataAction> { + const ERR_MSG: &str = + "sequence expects one or two params: "; + if !matches!(ac_params.len(), 1 | 2) { + bail!("{ERR_MSG}\nfound {} items", ac_params.len()); + } + let timeout = parse_non_zero_u16(&ac_params[0], s, "timeout-override")?; + let input_mode = if ac_params.len() > 1 { + if let Some(Ok(input_mode)) = ac_params[1] + .atom(s.vars()) + .map(|config_str| SequenceInputMode::try_from_str(config_str)) + { + input_mode + } else { + bail_expr!(&ac_params[1], "{ERR_MSG}\n{}", SequenceInputMode::err_msg()); + } + } else { + s.default_sequence_input_mode + }; + Ok(s.a.sref(Action::Custom(s.a.sref( + s.a.sref_slice(CustomAction::SequenceLeader(timeout, input_mode)), + )))) +} + fn parse_switch(ac_params: &[SExpr], s: &ParsedState) -> Result<&'static KanataAction> { const ERR_STR: &str = "switch expects triples of params: "; diff --git a/parser/src/custom_action.rs b/parser/src/custom_action.rs index d5ace1ce3..3f119f3f3 100644 --- a/parser/src/custom_action.rs +++ b/parser/src/custom_action.rs @@ -3,6 +3,7 @@ //! When adding a new custom action, the macro section of the config.adoc documentation may need to //! be updated, to include the new action to the documented list of supported actions in macro. +use anyhow::{anyhow, Result}; use kanata_keyberon::key_code::KeyCode; #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -39,7 +40,8 @@ pub enum CustomAction { min_distance: u16, max_distance: u16, }, - SequenceLeader, + SequenceCancel, + SequenceLeader(u16, SequenceInputMode), LiveReload, LiveReloadNext, LiveReloadPrev, @@ -100,3 +102,39 @@ pub struct CapsWordCfg { pub keys_nonterminal: &'static [KeyCode], pub timeout: u16, } + +/// This controls the behaviour of kanata when sequence mode is initiated by the sequence leader +/// action. +/// +/// - `HiddenSuppressed` hides the keys typed as part of the sequence and does not output the keys +/// typed when an invalid sequence is the result of an invalid sequence character or a timeout. +/// - `HiddenDelayType` hides the keys typed as part of the sequence and outputs the keys when an +/// typed when an invalid sequence is the result of an invalid sequence character or a timeout. +/// - `VisibleBackspaced` will type the keys that are typed as part of the sequence but will +/// backspace the typed sequence keys before performing the fake key tap when a valid sequence is +/// the result. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum SequenceInputMode { + HiddenSuppressed, + HiddenDelayType, + VisibleBackspaced, +} + +const SEQ_VISIBLE_BACKSPACED: &str = "visible-backspaced"; +const SEQ_HIDDEN_SUPPRESSED: &str = "hidden-suppressed"; +const SEQ_HIDDEN_DELAY_TYPE: &str = "hidden-delay-type"; + +impl SequenceInputMode { + pub fn try_from_str(s: &str) -> Result { + match s { + SEQ_VISIBLE_BACKSPACED => Ok(SequenceInputMode::VisibleBackspaced), + SEQ_HIDDEN_SUPPRESSED => Ok(SequenceInputMode::HiddenSuppressed), + SEQ_HIDDEN_DELAY_TYPE => Ok(SequenceInputMode::HiddenDelayType), + _ => Err(anyhow!(SequenceInputMode::err_msg())), + } + } + + pub fn err_msg() -> String { + format!("sequence input mode must be one of: {SEQ_VISIBLE_BACKSPACED}, {SEQ_HIDDEN_SUPPRESSED}, {SEQ_HIDDEN_DELAY_TYPE}") + } +} diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index f9b445f02..d2cfa0523 100644 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -85,8 +85,6 @@ pub struct Kanata { /// Horizontal mouse movement state. Is Some(...) when horizontal mouse movement is active and /// None otherwise. pub move_mouse_state_horizontal: Option, - /// The number of ticks defined in the user configuration for sequence timeout. - pub sequence_timeout: u16, /// The user configuration for backtracking to find valid sequences. See /// <../../docs/sequence-adding-chords-ideas.md> for more info. pub sequence_backtrack_modcancel: bool, @@ -94,8 +92,6 @@ pub struct Kanata { pub sequence_state: Option, /// Valid sequences defined in the user configuration. pub sequences: cfg::KeySeqsToFKeys, - /// Stores the user configuration for the sequence input mode. - pub sequence_input_mode: SequenceInputMode, /// Stores the user recored dynamic macros. pub dynamic_macros: HashMap>, /// Tracks the progress of an active dynamic macro. Is Some(...) when a dynamic macro is being @@ -169,40 +165,9 @@ pub struct MoveMouseAccelState { pub struct SequenceState { pub sequence: Vec, + pub sequence_input_mode: SequenceInputMode, pub ticks_until_timeout: u16, -} - -/// This controls the behaviour of kanata when sequence mode is initiated by the sequence leader -/// action. -/// -/// - `HiddenSuppressed` hides the keys typed as part of the sequence and does not output the keys -/// typed when an invalid sequence is the result of an invalid sequence character or a timeout. -/// - `HiddenDelayType` hides the keys typed as part of the sequence and outputs the keys when an -/// typed when an invalid sequence is the result of an invalid sequence character or a timeout. -/// - `VisibleBackspaced` will type the keys that are typed as part of the sequence but will -/// backspace the typed sequence keys before performing the fake key tap when a valid sequence is -/// the result. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum SequenceInputMode { - HiddenSuppressed, - HiddenDelayType, - VisibleBackspaced, -} - -const SEQ_INPUT_MODE_CFG_NAME: &str = "sequence-input-mode"; -const SEQ_VISIBLE_BACKSPACED: &str = "visible-backspaced"; -const SEQ_HIDDEN_SUPPRESSED: &str = "hidden-suppressed"; -const SEQ_HIDDEN_DELAY_TYPE: &str = "hidden-delay-type"; - -impl SequenceInputMode { - fn try_from_str(s: &str) -> Result { - match s { - SEQ_VISIBLE_BACKSPACED => Ok(SequenceInputMode::VisibleBackspaced), - SEQ_HIDDEN_SUPPRESSED => Ok(SequenceInputMode::HiddenSuppressed), - SEQ_HIDDEN_DELAY_TYPE => Ok(SequenceInputMode::HiddenDelayType), - _ => Err(anyhow!("{SEQ_INPUT_MODE_CFG_NAME} mode must be one of: {SEQ_VISIBLE_BACKSPACED}, {SEQ_HIDDEN_SUPPRESSED}, {SEQ_HIDDEN_DELAY_TYPE}")) - } - } + pub sequence_timeout: u16, } pub struct DynamicMacroReplayState { @@ -235,9 +200,6 @@ impl DynamicMacroRecordState { static LAST_PRESSED_KEY: AtomicU32 = AtomicU32::new(0); -const SEQUENCE_TIMEOUT_ERR: &str = "sequence-timeout should be a number (1-65535)"; -const SEQUENCE_TIMEOUT_DEFAULT: u16 = 1000; - use once_cell::sync::Lazy; static MAPPED_KEYS: Lazy> = @@ -322,24 +284,11 @@ impl Kanata { update_kbd_out(&cfg.items, &kbd_out)?; set_altgr_behaviour(&cfg)?; - let sequence_timeout = cfg - .items - .get("sequence-timeout") - .map(|s| match str::parse::(s) { - Ok(0) | Err(_) => Err(anyhow!("{SEQUENCE_TIMEOUT_ERR}")), - Ok(t) => Ok(t), - }) - .unwrap_or(Ok(SEQUENCE_TIMEOUT_DEFAULT))?; let sequence_backtrack_modcancel = cfg .items .get("sequence-backtrack-modcancel") .map(|s| !FALSE_VALUES.contains(&s.to_lowercase().as_str())) .unwrap_or(true); - let sequence_input_mode = cfg - .items - .get(SEQ_INPUT_MODE_CFG_NAME) - .map(|s| SequenceInputMode::try_from_str(s.as_str())) - .unwrap_or(Ok(SequenceInputMode::HiddenSuppressed))?; let log_layer_changes = cfg .items .get("log-layer-changes") @@ -362,11 +311,9 @@ impl Kanata { hscroll_state: None, move_mouse_state_vertical: None, move_mouse_state_horizontal: None, - sequence_timeout, sequence_backtrack_modcancel, sequence_state: None, sequences: cfg.sequences, - sequence_input_mode, last_tick: time::Instant::now(), time_remainder: 0, live_reload_requested: false, @@ -411,19 +358,6 @@ impl Kanata { }; update_kbd_out(&cfg.items, &self.kbd_out)?; set_altgr_behaviour(&cfg).map_err(|e| anyhow!("failed to set altgr behaviour {e})"))?; - self.sequence_timeout = cfg - .items - .get("sequence-timeout") - .map(|s| match str::parse::(s) { - Ok(0) | Err(_) => Err(anyhow!("{SEQUENCE_TIMEOUT_ERR}")), - Ok(t) => Ok(t), - }) - .unwrap_or(Ok(SEQUENCE_TIMEOUT_DEFAULT))?; - self.sequence_input_mode = cfg - .items - .get(SEQ_INPUT_MODE_CFG_NAME) - .map(|s| SequenceInputMode::try_from_str(s.as_str())) - .unwrap_or(Ok(SequenceInputMode::HiddenSuppressed))?; let log_layer_changes = cfg .items .get("log-layer-changes") @@ -591,17 +525,7 @@ impl Kanata { state.ticks_until_timeout -= 1; if state.ticks_until_timeout == 0 { log::debug!("sequence timeout; exiting sequence state"); - match self.sequence_input_mode { - SequenceInputMode::HiddenDelayType => { - for code in state.sequence.iter().copied() { - if let Some(osc) = OsCode::from_u16(code) { - self.kbd_out.press_key(osc)?; - self.kbd_out.release_key(osc)?; - } - } - } - SequenceInputMode::HiddenSuppressed | SequenceInputMode::VisibleBackspaced => {} - } + cancel_sequence(state, &mut self.kbd_out)?; self.sequence_state = None; } } @@ -696,7 +620,7 @@ impl Kanata { } } Some(state) => { - state.ticks_until_timeout = self.sequence_timeout; + state.ticks_until_timeout = state.sequence_timeout; // Transform to OsCode and convert modifiers other than altgr/ralt (same key // different names) to the left version, since that's how chords get @@ -719,7 +643,7 @@ impl Kanata { }; state.sequence.push(pushed_into_seq); - match self.sequence_input_mode { + match state.sequence_input_mode { SequenceInputMode::VisibleBackspaced => { self.kbd_out.press_key(osc)?; } @@ -755,7 +679,7 @@ impl Kanata { }; if is_invalid_termination { log::debug!("got invalid sequence; exiting sequence mode"); - match self.sequence_input_mode { + match state.sequence_input_mode { SequenceInputMode::HiddenDelayType => { for code in state.sequence.iter().copied() { if let Some(osc) = OsCode::from_u16(code) { @@ -775,7 +699,7 @@ impl Kanata { // Check for and handle valid termination. if let HasValue((i, j)) = res { log::debug!("sequence complete; tapping fake key"); - match self.sequence_input_mode { + match state.sequence_input_mode { SequenceInputMode::HiddenSuppressed | SequenceInputMode::HiddenDelayType => {} SequenceInputMode::VisibleBackspaced => { @@ -1014,14 +938,25 @@ impl Kanata { log::debug!("on-press: sleeping for {delay} ms"); std::thread::sleep(std::time::Duration::from_millis((*delay).into())); } - CustomAction::SequenceLeader => { + CustomAction::SequenceCancel => { + if self.sequence_state.is_some() { + log::debug!("exiting sequence"); + let state = self.sequence_state.as_ref().unwrap(); + cancel_sequence(state, &mut self.kbd_out)?; + self.sequence_state = None; + } + } + CustomAction::SequenceLeader(timeout, input_mode) => { if self.sequence_state.is_none() - || self.sequence_input_mode == SequenceInputMode::HiddenSuppressed + || self.sequence_state.as_ref().unwrap().sequence_input_mode + == SequenceInputMode::HiddenSuppressed { log::debug!("entering sequence mode"); self.sequence_state = Some(SequenceState { sequence: vec![], - ticks_until_timeout: self.sequence_timeout, + sequence_input_mode: *input_mode, + ticks_until_timeout: *timeout, + sequence_timeout: *timeout, }); } } @@ -1642,3 +1577,18 @@ fn update_kbd_out(_cfg: &HashMap, _kbd_out: &KbdOut) -> Result<( } Ok(()) } + +fn cancel_sequence(state: &SequenceState, kbd_out: &mut KbdOut) -> Result<()> { + match state.sequence_input_mode { + SequenceInputMode::HiddenDelayType => { + for code in state.sequence.iter().copied() { + if let Some(osc) = OsCode::from_u16(code) { + kbd_out.press_key(osc)?; + kbd_out.release_key(osc)?; + } + } + } + SequenceInputMode::HiddenSuppressed | SequenceInputMode::VisibleBackspaced => {} + } + Ok(()) +} From 26dfa545e91bf9d0c1d9579e95177f73114734dd Mon Sep 17 00:00:00 2001 From: rszyma Date: Fri, 21 Jul 2023 08:03:55 +0200 Subject: [PATCH 014/819] github: use forms for issue templates (#495) --- .github/ISSUE_TEMPLATE/bug_report.md | 32 --------- .github/ISSUE_TEMPLATE/bug_report.yml | 76 ++++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 5 ++ .github/ISSUE_TEMPLATE/feature_request.md | 20 ------ .github/ISSUE_TEMPLATE/feature_request.yml | 35 ++++++++++ 5 files changed, 116 insertions(+), 52 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 19905a3b8..000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: 'Bug: title_goes_here' -labels: bug -assignees: jtroo - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**Version** -The kanata version prints in the log on startup, or you can also print it by passing the `--version` flag when running on the command line. - -**Relevant kanata configs** -E.g. defcfg, defsrc, deflayer, defalias items. If in doubt, feel free to include your entire config. - -**To reproduce** -Steps to reproduce the behaviour. - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Debug logs** -If you think it might help with a non-obvious issue, run kanata from the command line and pass the `--debug` flag. This will print more info. Include the relevant log outputs this section if you did so. - -**Operating system** -Linux or Windows? - -**Additional context** -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 000000000..09d44efe7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,76 @@ +name: "Bug report" +description: Create a report to help us improve. +labels: ["bug"] +assignees: ["jtroo"] +title: "Bug: title_goes_here" +body: + - type: checkboxes + attributes: + label: Requirements + description: Before you create a bug report, please check the following + options: + - label: I've searched [issues](https://github.com/jtroo/kanata/issues) to see if this has not been reported before. + required: true + - type: textarea + id: summary + attributes: + label: Describe the bug + description: A clear and concise description of what the bug is. + validations: + required: true + - type: textarea + id: config + attributes: + label: Relevant kanata config + description: E.g. defcfg, defsrc, deflayer, defalias items. If in doubt, feel free to include your entire config. + validations: + required: false + - type: textarea + id: reproduce + attributes: + label: To Reproduce + description: | + Walk us through the steps needed to reproduce the bug. + value: | + 1. + 2. + 3. + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected behavior + description: A clear and concise description of what you expected to happen. + validations: + required: true + - type: input + id: version + attributes: + label: Kanata version + description: The kanata version prints in the log on startup, or you can also print it by passing the `--version` flag when running on the command line. + placeholder: e.g. kanata 1.3.0 + validations: + required: true + - type: textarea + id: logs + attributes: + label: Debug logs + description: If you think it might help with a non-obvious issue, run kanata from the command line and pass the `--debug` flag. This will print more info. Include the relevant log outputs this section if you did so. + validations: + required: false + - type: input + id: os + attributes: + label: Operating system + description: Linux or Windows? + placeholder: e.g. Windows 11 + validations: + required: true + - type: textarea + id: additional + attributes: + label: Additional context + description: Add any other context about the problem here. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..8a1826f17 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: true +contact_links: + - name: Discussions + url: https://github.com/jtroo/kanata/discussions + about: Ask for help or interact with the community. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index a988ccbe9..000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: 'Feature request: feature_summary_goes_here' -labels: enhancement -assignees: '' - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 000000000..f6f08c91c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,35 @@ +name: "Feature request" +description: Suggest an idea for this project +title: 'Feature request: feature_summary_goes_here' +labels: ["enhancement"] +assignees: [] +body: + - type: textarea + attributes: + label: Is your feature request related to a problem? Please describe. + description: | + A clear and concise description of what the problem is. + placeholder: Ex. I'm always frustrated when [...] + validations: + required: true + - type: textarea + attributes: + label: Describe the solution you'd like. + description: | + A clear and concise description of what you want to happen. + validations: + required: true + - type: textarea + attributes: + label: Describe alternatives you've considered. + description: | + A clear and concise description of any alternative solutions or features you've considered. + validations: + required: true + - type: textarea + attributes: + label: Additional context + description: | + Add any other context or screenshots about the feature request here. + validations: + required: false From ab5cb68212af00b69e1f42296c5c830585fc33ba Mon Sep 17 00:00:00 2001 From: jtroo Date: Thu, 20 Jul 2023 23:36:44 -0700 Subject: [PATCH 015/819] fix: clarity of uinput permission log text (#498) --- src/kanata/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index d2cfa0523..1dcb45235 100644 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -239,7 +239,7 @@ impl Kanata { ) { Ok(kbd_out) => kbd_out, Err(err) => { - error!("Failed to open the output uinput device. Make sure you've added kanata to the `uinput` group"); + error!("Failed to open the output uinput device. Make sure you've added the user executing kanata to the `uinput` group"); bail!(err) } }; From d5ce74eafccbb35534fe1c149aa5a28100d372be Mon Sep 17 00:00:00 2001 From: ArijanJ <56356662+ArijanJ@users.noreply.github.com> Date: Fri, 21 Jul 2023 18:30:34 +0200 Subject: [PATCH 016/819] feat: movemouse-speed (#490) --- cfg_samples/kanata.kbd | 8 +++- docs/config.adoc | 27 ++++++++++++- parser/src/cfg/mod.rs | 14 +++++++ parser/src/custom_action.rs | 3 ++ src/kanata/mod.rs | 78 ++++++++++++++++++++++++++++++++++++- 5 files changed, 126 insertions(+), 4 deletions(-) diff --git a/cfg_samples/kanata.kbd b/cfg_samples/kanata.kbd index c0fbf587f..e789bdc08 100644 --- a/cfg_samples/kanata.kbd +++ b/cfg_samples/kanata.kbd @@ -631,7 +631,7 @@ If you need help, you are welcome to ask. _ @mwu @mwd @mwl @mwr _ _ _ _ _ @ma↑ _ _ _ _ pgup bck _ fwd _ _ _ _ @ma← @ma↓ @ma→ _ _ _ pgdn mlft _ mrgt mmid _ mbck mfwd _ @ms↑ _ _ - _ _ mltp _ mrtp mmtp _ mbtp mftp @ms← @ms↓ @ms→ + @fms _ mltp _ mrtp mmtp _ mbtp mftp @ms← @ms↓ @ms→ _ _ _ _ _ _ _ ) @@ -680,6 +680,12 @@ If you need help, you are welcome to ask. ;; treated as one giant screen, which may make it a bit confusing for how to ;; set up the pixels. You will need to experiment. sm (setmouse 32228 32228) + + ;; movemouse-speed takes a percentage by which it then scales all of the + ;; mouse movements while held. You can have as many of these active at a + ;; given time as you would like, but be warned that some values, such as 33 + ;; may not have correct pixel distance representations. + fms (movemouse-speed 200) ) (defalias diff --git a/docs/config.adoc b/docs/config.adoc index 15a2f1d86..2d0f352bf 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -1157,6 +1157,29 @@ This can make it a little confusing for how to set the `x, y` values to get the positions that you want. Experimentation will be needed. +[[mouse-speed]] +==== Modify the speed of mouse movements +<> + +The action `movemouse-speed` modifies the speed at which `movemouse` and +`movemouse-accel` function at runtime. It does this by expanding or shrinking +`min_distance` and `max_distance` while the action key is pressed. + +This action accepts one number (unit: percentage) by which the +mouse movements will be accelerated. + +WARNING: Due to the nature of pixels being whole numbers, some values such as +33 may not result in an exact third of the distance. + +.Example: +[source] +---- +(defalias + fst (movemouse-speed 200) + slw (movemouse-speed 50) +) +---- + [[mouse-all-actions-example]] ==== Mouse all actions example <> @@ -1180,13 +1203,15 @@ Experimentation will be needed. ma→ (movemouse-accel-right 1 1000 1 5) sm (setmouse 32228 32228) + + fst (movemouse-speed 200) ) (deflayer mouse _ @mwu @mwd @mwl @mwr _ _ _ _ _ @ma↑ _ _ _ _ pgup bck _ fwd _ _ _ _ @ma← @ma↓ @ma→ _ _ _ pgdn mlft _ mrgt mmid _ mbck mfwd _ @ms↑ _ _ - _ _ mltp _ mrtp mmtp _ mbtp mftp @ms← @ms↓ @ms→ + @fst _ mltp _ mrtp mmtp _ mbtp mftp @ms← @ms↓ @ms→ _ _ _ _ _ _ _ ) ---- diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index 92502b536..28042fbe4 100644 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -1220,6 +1220,7 @@ fn parse_action_list(ac: &[SExpr], s: &ParsedState) -> Result<&'static KanataAct "movemouse-accel-down" => parse_move_mouse_accel(&ac[1..], MoveDirection::Down, s), "movemouse-accel-left" => parse_move_mouse_accel(&ac[1..], MoveDirection::Left, s), "movemouse-accel-right" => parse_move_mouse_accel(&ac[1..], MoveDirection::Right, s), + "movemouse-speed" => parse_move_mouse_speed(&ac[1..], s), "setmouse" => parse_set_mouse(&ac[1..], s), "dynamic-macro-record" => parse_dynamic_macro_record(&ac[1..], s), "dynamic-macro-play" => parse_dynamic_macro_play(&ac[1..], s), @@ -2325,6 +2326,19 @@ fn parse_move_mouse_accel( ))))) } +fn parse_move_mouse_speed(ac_params: &[SExpr], s: &ParsedState) -> Result<&'static KanataAction> { + if ac_params.len() != 1 { + bail!( + "movemouse-speed expects one parameter, found {}\n", + ac_params.len() + ); + } + let speed = parse_non_zero_u16(&ac_params[0], s, "speed scaling %")?; + Ok(s.a.sref(Action::Custom( + s.a.sref(s.a.sref_slice(CustomAction::MoveMouseSpeed { speed })), + ))) +} + fn parse_set_mouse(ac_params: &[SExpr], s: &ParsedState) -> Result<&'static KanataAction> { if ac_params.len() != 2 { bail!( diff --git a/parser/src/custom_action.rs b/parser/src/custom_action.rs index 3f119f3f3..517c8d5e0 100644 --- a/parser/src/custom_action.rs +++ b/parser/src/custom_action.rs @@ -40,6 +40,9 @@ pub enum CustomAction { min_distance: u16, max_distance: u16, }, + MoveMouseSpeed { + speed: u16, + }, SequenceCancel, SequenceLeader(u16, SequenceInputMode), LiveReload, diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index 1dcb45235..dd47660d3 100644 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -85,6 +85,8 @@ pub struct Kanata { /// Horizontal mouse movement state. Is Some(...) when horizontal mouse movement is active and /// None otherwise. pub move_mouse_state_horizontal: Option, + /// A list of mouse speed modifiers in percentages by which mouse travel distance is scaled. + pub move_mouse_speed_modifiers: Vec, /// The user configuration for backtracking to find valid sequences. See /// <../../docs/sequence-adding-chords-ideas.md> for more info. pub sequence_backtrack_modcancel: bool, @@ -311,6 +313,7 @@ impl Kanata { hscroll_state: None, move_mouse_state_vertical: None, move_mouse_state_horizontal: None, + move_mouse_speed_modifiers: Vec::new(), sequence_backtrack_modcancel, sequence_state: None, sequences: cfg.sequences, @@ -493,7 +496,10 @@ impl Kanata { } if mmsv.ticks_until_move == 0 { mmsv.ticks_until_move = mmsv.interval - 1; - self.kbd_out.move_mouse(mmsv.direction, mmsv.distance)?; + let scaled_distance = + apply_mouse_distance_modifiers(mmsv.distance, &self.move_mouse_speed_modifiers); + log::debug!("handle_move_mouse: scaled vdistance: {}", scaled_distance); + self.kbd_out.move_mouse(mmsv.direction, scaled_distance)?; } else { mmsv.ticks_until_move -= 1; } @@ -512,7 +518,10 @@ impl Kanata { } if mmsh.ticks_until_move == 0 { mmsh.ticks_until_move = mmsh.interval - 1; - self.kbd_out.move_mouse(mmsh.direction, mmsh.distance)?; + let scaled_distance = + apply_mouse_distance_modifiers(mmsh.distance, &self.move_mouse_speed_modifiers); + log::debug!("handle_move_mouse: scaled hdistance: {}", scaled_distance); + self.kbd_out.move_mouse(mmsh.direction, scaled_distance)?; } else { mmsh.ticks_until_move -= 1; } @@ -903,6 +912,13 @@ impl Kanata { } } } + CustomAction::MoveMouseSpeed { speed } => { + self.move_mouse_speed_modifiers.push(*speed); + log::debug!( + "movemousespeed modifiers: {:?}", + self.move_mouse_speed_modifiers + ); + } CustomAction::Cmd(_cmd) => { #[cfg(feature = "cmd")] cmds.push(_cmd.clone()); @@ -1171,6 +1187,20 @@ impl Kanata { } pbtn } + CustomAction::MoveMouseSpeed { speed, .. } => { + if let Some(idx) = self + .move_mouse_speed_modifiers + .iter() + .position(|s| *s == *speed) + { + self.move_mouse_speed_modifiers.remove(idx); + } + log::debug!( + "movemousespeed modifiers: {:?}", + self.move_mouse_speed_modifiers + ); + pbtn + } CustomAction::Delay(delay) => { log::debug!("on-press: sleeping for {delay} ms"); std::thread::sleep(std::time::Duration::from_millis((*delay).into())); @@ -1521,6 +1551,50 @@ fn run_multi_cmd(cmds: Vec>) { }); } +fn apply_mouse_distance_modifiers(initial_distance: u16, mods: &Vec) -> u16 { + let mut scaled_distance = initial_distance; + for &modifier in mods { + scaled_distance = u16::max( + 1, + f32::min( + scaled_distance as f32 * (modifier as f32 / 100f32), + u16::MAX as f32, + ) + .round() as u16, + ); + } + scaled_distance +} + +#[test] +fn apply_speed_modifiers() { + assert_eq!(apply_mouse_distance_modifiers(15, &vec![]), 15); + + assert_eq!(apply_mouse_distance_modifiers(10, &vec![200u16]), 20); + assert_eq!(apply_mouse_distance_modifiers(20, &vec![50u16]), 10); + + assert_eq!(apply_mouse_distance_modifiers(5, &vec![33u16]), 2); // 1.65 + assert_eq!(apply_mouse_distance_modifiers(100, &vec![99u16]), 99); + + // Clamping + assert_eq!( + apply_mouse_distance_modifiers(65535, &vec![65535u16]), + 65535 + ); + assert_eq!(apply_mouse_distance_modifiers(1, &vec![1u16]), 1); + + // Nice, round calculations equal themselves + assert_eq!( + apply_mouse_distance_modifiers(10, &vec![50u16, 200u16]), + apply_mouse_distance_modifiers(10, &vec![200u16, 50u16]) + ); + + // 33% of 20 + assert_eq!(apply_mouse_distance_modifiers(10, &vec![200u16, 33u16]), 7); + // 200% of 3 + assert_eq!(apply_mouse_distance_modifiers(10, &vec![33u16, 200u16]), 6); +} + /// Checks if kanata should exit based on the fixed key combination of: /// Lctl+Spc+Esc fn check_for_exit(event: &KeyEvent) { From e99ee1c347b0ca70d292cd0fcb46b74abde558f1 Mon Sep 17 00:00:00 2001 From: jtroo Date: Mon, 31 Jul 2023 05:50:12 -0700 Subject: [PATCH 017/819] doc: add docs for switch action (#509) --- cfg_samples/kanata.kbd | 32 ++++++++++++++++ docs/config.adoc | 83 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) diff --git a/cfg_samples/kanata.kbd b/cfg_samples/kanata.kbd index e789bdc08..8b354000d 100644 --- a/cfg_samples/kanata.kbd +++ b/cfg_samples/kanata.kbd @@ -437,6 +437,38 @@ If you need help, you are welcome to ask. ;; the keys in the third parameter (right-trigger-keys) are currently active. frk (fork @🙃 @🙁 (lsft rsft)) + ;; switch accepts triples of keys check, action, and fallthrough|break. + ;; The default usage of keys check behaves similarly to fork. + ;; However, it also accepts boolean operators and|or to allow more + ;; complex use cases. + ;; + ;; The order of cases matters. If two different cases match the + ;; currently pressed keys, the case listed earlier in the configuration + ;; will activate first. If the early case uses break, the second case will + ;; not activate at all. Otherwise if fallthrough is used, the second case + ;; will also activate sequentially after the first case. + swt (switch + + ;; translating this keys check to some other common languages + ;; this might look like: + ;; + ;; (a && b && (c || d) && (e || f)) + ((and a b (or c d) (or e f))) a break + + ;; this case behaves like fork, i.e. + ;; + ;; (or a b c) + ;; + ;; or for some other common languages: + ;; + ;; a || b || c + (a b c) b fallthrough + + ;; default case, empty list always evaluates to true. + ;; break vs. fallthrough doesn't matter here + () c break + ) + ;; Having a cmd action in your configuration without explicitly enabling ;; `danger-enable-cmd` **and** using the cmd-enabled executable will make ;; kanata refuse to load your configuration. The aliases below are commented diff --git a/docs/config.adoc b/docs/config.adoc index 2d0f352bf..e2f86ca74 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -2039,6 +2039,89 @@ VAR_NAME=var_value === Custom tap-hold behaviour <> +The `switch` action accepts multiple cases. +One case is a triple of: + +- keys check +- action: to activate if keys check succeeds +- fallthrough|break: stop evaluating cases + +The default use of keys check behaves similarly to fork. + +For example, the keys check `(a b c)` will activate the corresponding action +if any of a, b, or c are currently pressed. + +The keys check also accepts the boolean operators and|or to allow more +complex use cases. + +The order of cases matters. +For example, if two different cases match the currently pressed keys, +the case listed earlier in the configuration will activate first. +If the early case uses break, the second case will not activate. +Otherwise if fallthrough is used, +the second case will activate sequentially after the first case. +This idea generalizes to more than two cases, +but the two case example is hopefully simple and effective enough. + +.Example: +[source] +---- +(defalias + swt (switch + ;; case 1 + ((and a b (or c d) (or e f))) @ac1 break + ;; case 2 + (a b c) @ac2 fallthrough + ;; case 3 + () @ac3 break + ) +) +---- + +Below is a description of how this example behaves. + +==== Case 1 + +---- +((and a b (or c d) (or e f))) a break +---- + +Translating case 1's keys check to some other common languages +might look like: + +---- +(a && b && (c || d) && (e || f)) +---- + +If the keys check passes, the action `@ac1` will activate. +No other action will activate since `break` is used. + +==== Cases 2 and 3 + +---- +(a b c) c fallthrough +() b break +---- + +Case 2's key check behaves like that of `fork`, i.e. + + (or a b c) + +or for some other common languages: + + a || b || c + +If this keys check passes and the case 1 does not pass, +the action `@ac2` will activate first. +Since the keys check of case 3 always passes, `@ac3` will activate next. + +If neither case 1 or case 2 pass their keys checks, +case 3 will always activate with `@ac3`. + +[[custom-tap-hold-behaviour]] +=== Custom tap-hold behaviour +<> + This is not currently configurable without modifying the source code, but if you're willing and/or capable, there is a tap-hold behaviour that is currently not exposed. Using this behaviour, one can be very particular about when and how From d6461425764aec10b9f919968eb70b6d13c72ea6 Mon Sep 17 00:00:00 2001 From: Jan Tache Date: Mon, 31 Jul 2023 09:02:41 -0700 Subject: [PATCH 018/819] ver: 1.4.0 --- Cargo.lock | 10 +++++++--- Cargo.toml | 10 +++++----- keyberon/Cargo.toml | 2 +- parser/Cargo.toml | 6 +++--- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 31a42347e..c7cb233ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -383,7 +383,7 @@ checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" [[package]] name = "kanata" -version = "1.4.0-prerelease-3" +version = "1.4.0" dependencies = [ "anyhow", "clap", @@ -425,7 +425,9 @@ dependencies = [ [[package]] name = "kanata-keyberon" -version = "0.19.0" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3ba168f7f70812ae955bec71750b80677208c93a02495d32503b3e190ad47c" dependencies = [ "arraydeque", "heapless", @@ -444,7 +446,9 @@ dependencies = [ [[package]] name = "kanata-parser" -version = "0.19.0" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a44c2ab3d2c5441c4bd03aa6a8a12e2a031e2d08027db08d1637deb10639d3" dependencies = [ "anyhow", "kanata-keyberon", diff --git a/Cargo.toml b/Cargo.toml index 5b2335a8a..e58a42852 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kanata" -version = "1.4.0-prerelease-3" +version = "1.4.0" authors = ["jtroo "] description = "Multi-layer keyboard customization" keywords = ["cli", "linux", "windows", "keyboard", "layout"] @@ -25,13 +25,13 @@ rustc-hash = "1.1.0" miette = { version = "5.7.0", features = ["fancy"] } dirs = "5.0.1" -# kanata-keyberon = "0.19.0" -# kanata-parser = "0.19.0" +kanata-keyberon = "0.20.0" +kanata-parser = "0.20.0" # Uncomment below and comment out above for testing local changes. # Otherwise any changes to the local files will not reflect in the compiled # binary. -kanata-keyberon = { path = "keyberon" } -kanata-parser = { path = "parser" } +# kanata-keyberon = { path = "keyberon" } +# kanata-parser = { path = "parser" } [target.'cfg(target_os = "linux")'.dependencies] evdev = "=0.12.0" diff --git a/keyberon/Cargo.toml b/keyberon/Cargo.toml index 8be288936..ccd438f3a 100644 --- a/keyberon/Cargo.toml +++ b/keyberon/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kanata-keyberon" -version = "0.19.0" +version = "0.20.0" authors = ["Guillaume Pinot ", "Robin Krahl ", "jtroo "] edition = "2018" description = "Pure Rust keyboard firmware. Fork intended for use with kanata." diff --git a/parser/Cargo.toml b/parser/Cargo.toml index 3ef34f312..d863d4053 100644 --- a/parser/Cargo.toml +++ b/parser/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kanata-parser" -version = "0.19.0" +version = "0.20.0" authors = ["jtroo "] description = "A parser for configuration language of kanata, a keyboard remapper." keywords = ["kanata", "parser"] @@ -20,11 +20,11 @@ rustc-hash = "1.1.0" miette = { version = "5.7.0", features = ["fancy"] } thiserror = "1.0.38" -# kanata-keyberon = "0.19.0" +kanata-keyberon = "0.20.0" # Uncomment below and comment out above for testing local changes. # Otherwise any changes to the local files will not reflect in the compiled # binary. -kanata-keyberon = { path = "../keyberon" } +# kanata-keyberon = { path = "../keyberon" } [features] cmd = [] From 91deb073b487bd3f3d59eed737039d159eb1710e Mon Sep 17 00:00:00 2001 From: rszyma Date: Mon, 31 Jul 2023 18:24:38 +0200 Subject: [PATCH 019/819] doc: fix switch action header (#510) --- docs/config.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/config.adoc b/docs/config.adoc index e2f86ca74..87d69f8f9 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -2035,8 +2035,8 @@ $env:VAR_NAME = "var_value" VAR_NAME=var_value ---- -[[custom-tap-hold-behaviour]] -=== Custom tap-hold behaviour +[[switch]] +=== switch <> The `switch` action accepts multiple cases. From 3515e919de8841f635bb5ca164d5d22852f5bc63 Mon Sep 17 00:00:00 2001 From: jtroo Date: Wed, 2 Aug 2023 20:38:23 -0700 Subject: [PATCH 020/819] fix: macro parsing - off-by-one and bad chord (#517) --- parser/src/cfg/mod.rs | 8 +++++++- parser/src/cfg/tests.rs | 21 +++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index 28042fbe4..b201c0d03 100644 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -1575,9 +1575,15 @@ fn parse_macro_item_impl<'a>( let submacro = match maybe_list_var.list(s.vars()) { Some(l) => l, None => { + // Ensure that the unparsed text is empty since otherwise it means there is + // invalid text there + if !unparsed_str.is_empty() { + bail_expr!(&acs[0], "{MACRO_ERR}") + } + // Check for a follow-up list rem_start = 2; if acs.len() < 2 { - bail_expr!(&acs[1], "{MACRO_ERR}") + bail_expr!(&acs[0], "{MACRO_ERR}") } acs[1] .list(s.vars()) diff --git a/parser/src/cfg/tests.rs b/parser/src/cfg/tests.rs index 7bd0e2dfa..81c9100cf 100644 --- a/parser/src/cfg/tests.rs +++ b/parser/src/cfg/tests.rs @@ -555,6 +555,27 @@ fn test_include_bad2_has_original_filename() { ))); } +#[test] +fn parse_submacro() { + let _lk = match CFG_PARSE_LOCK.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + }; + let mut s = ParsedState::default(); + let source = r#" +(defsrc a) +(deflayer base + (macro M-S-()) +) +"#; + parse_cfg_raw_string(source, &mut s, "test") + .map_err(|e| { + eprintln!("{:?}", error_with_source(e)); + "" + }) + .unwrap_err(); +} + #[test] fn parse_switch() { let _lk = match CFG_PARSE_LOCK.lock() { From ef8c025fc8d5ae1aff6e22cf55f396a5b95cb99e Mon Sep 17 00:00:00 2001 From: jtroo Date: Sat, 5 Aug 2023 00:15:21 -0700 Subject: [PATCH 021/819] feat: on-idle-fakekey action (#512) --- Cargo.lock | 4 - Cargo.toml | 8 +- cfg_samples/kanata.kbd | 20 +++-- docs/config.adoc | 6 +- parser/Cargo.toml | 4 +- parser/src/cfg/mod.rs | 74 +++++++++++++++--- parser/src/cfg/tests.rs | 146 +++++++++++++++++++++++++++++++++++- parser/src/custom_action.rs | 9 +++ parser/src/sequences.rs | 6 ++ src/kanata/mod.rs | 64 ++++++++++++++-- 10 files changed, 303 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c7cb233ca..e22ef2b84 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -426,8 +426,6 @@ dependencies = [ [[package]] name = "kanata-keyberon" version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3ba168f7f70812ae955bec71750b80677208c93a02495d32503b3e190ad47c" dependencies = [ "arraydeque", "heapless", @@ -447,8 +445,6 @@ dependencies = [ [[package]] name = "kanata-parser" version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a44c2ab3d2c5441c4bd03aa6a8a12e2a031e2d08027db08d1637deb10639d3" dependencies = [ "anyhow", "kanata-keyberon", diff --git a/Cargo.toml b/Cargo.toml index e58a42852..65543d3cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,13 +25,13 @@ rustc-hash = "1.1.0" miette = { version = "5.7.0", features = ["fancy"] } dirs = "5.0.1" -kanata-keyberon = "0.20.0" -kanata-parser = "0.20.0" +# kanata-keyberon = "0.20.0" +# kanata-parser = "0.20.0" # Uncomment below and comment out above for testing local changes. # Otherwise any changes to the local files will not reflect in the compiled # binary. -# kanata-keyberon = { path = "keyberon" } -# kanata-parser = { path = "parser" } +kanata-keyberon = { path = "keyberon" } +kanata-parser = { path = "parser" } [target.'cfg(target_os = "linux")'.dependencies] evdev = "=0.12.0" diff --git a/cfg_samples/kanata.kbd b/cfg_samples/kanata.kbd index 8b354000d..818f5d848 100644 --- a/cfg_samples/kanata.kbd +++ b/cfg_samples/kanata.kbd @@ -746,18 +746,25 @@ If you need help, you are welcome to ask. ;; Press and release fake keys. ;; ;; Fake keys can't be pressed by any physical keyboard buttons and can only be -;; acted upon by the actions on-press-fakekey and on-release-fakekey. The -;; purpose of fake keys is for a use case such as holding modifier keys for -;; any number of keypresses and then releasing the modifiers when desired. +;; acted upon by the actions: +;; - on-press-fakekey +;; - on-release-fakekey +;; - on-idle-fakekey +;; +;; One use case of fake keys is for holding modifier keys +;; for any number of keypresses and then releasing the modifiers when desired. ;; ;; The actions associated with fake keys in deffakekeys are parsed before ;; aliases, so you can't use aliases within deffakekeys. Other than the lack ;; of alias support, fake keys can do any action that a normal key can, ;; including doing operations on previously defined fake keys. ;; -;; Operations on fake keys can occur either on press (on-press-fakekey) or -;; on release (on-release-fakekey). The use cases for the on-release variant -;; are left up to your own creativity. +;; Operations on fake keys can occur either on press (on-press-fakekey), +;; on release (on-release-fakekey), or on idle for a specified time +;; (on-idle-fakekey). +;; +;; Fake keys are flexible in usage but can be obscure to discover how they +;; can be useful to you. (deflayer fakekeys _ @fcp @fsp @fmp @pal _ _ _ _ _ _ _ _ _ _ @fcr @fsr @fap @ral _ _ _ _ _ _ _ _ _ @@ -802,6 +809,7 @@ If you need help, you are welcome to ask. ) pal (on-press-fakekey pal tap) ral (on-press-fakekey ral tap) + rdl (on-idle-fakekey ral tap 1000) ;; Test of on-press-fakekey and on-release-fakekey in a macro t1 (macro-release-cancel @fsp 5 a b c @fsr 5 c b a) diff --git a/docs/config.adoc b/docs/config.adoc index 87d69f8f9..60e98cddc 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -1735,6 +1735,9 @@ physical key presses and can only be activated via these actions: action when pressing the key mapped to this action. * `+(on-release-fakekey )+`: Activate a fake key action when releasing the key mapped to this action. +* `+(on-idle-fakekey )+`: + Activate a fake key action + when the keyboard is idle for `idle time` milliseconds A fake key can be defined in a `+deffakekeys+` configuration entry. Configuring this entry is similar to `+defalias+`, but you cannot make use of aliases @@ -1780,10 +1783,11 @@ The aforementioned `++` can be one of three values: pal (on-press-fakekey pal tap) ral (on-press-fakekey ral tap) + rsf (on-idle-fakekey ral tap 1000) ) (deflayer use-fake-keys - @psf @rsf @pal @ral a s d f + @psf @rsf @pal @ral a s d @rsf ) ---- diff --git a/parser/Cargo.toml b/parser/Cargo.toml index d863d4053..d2a8c5391 100644 --- a/parser/Cargo.toml +++ b/parser/Cargo.toml @@ -20,11 +20,11 @@ rustc-hash = "1.1.0" miette = { version = "5.7.0", features = ["fancy"] } thiserror = "1.0.38" -kanata-keyberon = "0.20.0" +# kanata-keyberon = "0.20.0" # Uncomment below and comment out above for testing local changes. # Otherwise any changes to the local files will not reflect in the compiled # binary. -# kanata-keyberon = { path = "../keyberon" } +kanata-keyberon = { path = "../keyberon" } [features] cmd = [] diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index b201c0d03..7b70825a6 100644 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -1208,6 +1208,7 @@ fn parse_action_list(ac: &[SExpr], s: &ParsedState) -> Result<&'static KanataAct "on-release-fakekey" => parse_on_release_fake_key_op(&ac[1..], s), "on-press-fakekey-delay" => parse_fake_key_delay(&ac[1..], s), "on-release-fakekey-delay" => parse_on_release_fake_key_delay(&ac[1..], s), + "on-idle-fakekey" => parse_on_idle_fakekey(&ac[1..], s), "mwheel-up" => parse_mwheel(&ac[1..], MWheelDirection::Up, s), "mwheel-down" => parse_mwheel(&ac[1..], MWheelDirection::Down, s), "mwheel-left" => parse_mwheel(&ac[1..], MWheelDirection::Left, s), @@ -2195,29 +2196,31 @@ fn parse_fake_key_op_coord_action( let y = match s.fake_keys.get(ac_params[0].atom(s.vars()).ok_or_else(|| { anyhow_expr!( &ac_params[0], - "{ERR_MSG}\nA list is not allowed for a fake key name", + "{ERR_MSG}\nInvalid first parameter: a fake key name cannot be a list", ) })?) { - Some((y, _)) => *y as u8, // cast should be safe; checked in `parse_fake_keys` - None => bail_expr!(&ac_params[0], "unknown fake key name {:?}", &ac_params[0]), + Some((y, _)) => *y as u16, // cast should be safe; checked in `parse_fake_keys` + None => bail_expr!( + &ac_params[0], + "{ERR_MSG}\nInvalid first parameter: unknown fake key name {:?}", + &ac_params[0] + ), }; let action = ac_params[1] .atom(s.vars()) .map(|a| match a { - "tap" => Ok(FakeKeyAction::Tap), - "press" => Ok(FakeKeyAction::Press), - "release" => Ok(FakeKeyAction::Release), - _ => bail_expr!( - &ac_params[1], - "{ERR_MSG}\nInvalid second parameter, it must be one of: tap, press, release", - ), + "tap" => Some(FakeKeyAction::Tap), + "press" => Some(FakeKeyAction::Press), + "release" => Some(FakeKeyAction::Release), + _ => None, }) + .flatten() .ok_or_else(|| { anyhow_expr!( &ac_params[1], "{ERR_MSG}\nInvalid second parameter, it must be one of: tap, press, release", ) - })??; + })?; let (x, y) = get_fake_key_coords(y); Ok((Coord { x, y }, action)) } @@ -2893,6 +2896,55 @@ fn parse_switch_case_bool( } } +fn parse_on_idle_fakekey(ac_params: &[SExpr], s: &ParsedState) -> Result<&'static KanataAction> { + const ERR_MSG: &str = + "on-idle-fakekey expects three parameters:\n <(tap|press|release)> \n"; + if ac_params.len() != 3 { + bail!("{ERR_MSG}"); + } + let y = match s.fake_keys.get(ac_params[0].atom(s.vars()).ok_or_else(|| { + anyhow_expr!( + &ac_params[0], + "{ERR_MSG}\nInvalid first parameter: a fake key name cannot be a list", + ) + })?) { + Some((y, _)) => *y as u16, // cast should be safe; checked in `parse_fake_keys` + None => bail_expr!( + &ac_params[0], + "{ERR_MSG}\nInvalid first parameter: unknown fake key name {:?}", + &ac_params[0] + ), + }; + let action = ac_params[1] + .atom(s.vars()) + .map(|a| match a { + "tap" => Some(FakeKeyAction::Tap), + "press" => Some(FakeKeyAction::Press), + "release" => Some(FakeKeyAction::Release), + _ => None, + }) + .flatten() + .ok_or_else(|| { + anyhow_expr!( + &ac_params[1], + "{ERR_MSG}\nInvalid second parameter, it must be one of: tap, press, release", + ) + })?; + let idle_duration = parse_u16(&ac_params[2], s, "idle time").map_err(|mut e| { + e.help_msg = format!("{ERR_MSG}\nInvalid third parameter: {}", e.help_msg); + e + })?; + let (x, y) = get_fake_key_coords(y); + let coord = Coord { x, y }; + Ok(s.a.sref(Action::Custom(s.a.sref(s.a.sref_slice( + CustomAction::FakeKeyOnIdle(FakeKeyOnIdle { + coord, + action, + idle_duration, + }), + ))))) +} + /// Creates a `KeyOutputs` from `layers::LAYERS`. fn create_key_outputs(layers: &KanataLayers, overrides: &Overrides) -> KeyOutputs { let mut outs = KeyOutputs::new(); diff --git a/parser/src/cfg/tests.rs b/parser/src/cfg/tests.rs index 81c9100cf..814c78038 100644 --- a/parser/src/cfg/tests.rs +++ b/parser/src/cfg/tests.rs @@ -665,9 +665,151 @@ fn parse_switch_exceed_depth() { ) "#; parse_cfg_raw_string(source, &mut s, "test") - .map_err(|e| { - eprintln!("{:?}", error_with_source(e)); + .map_err(|_e| { + // uncomment to see what this looks like when running test + // eprintln!("{:?}", error_with_source(_e)); + "" + }) + .unwrap_err(); +} + +#[test] +fn parse_on_idle_fakekey() { + let _lk = match CFG_PARSE_LOCK.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + }; + let mut s = ParsedState::default(); + let source = r#" +(defvar var1 a) +(defsrc a) +(deffakekeys hello a) +(deflayer base + (on-idle-fakekey hello tap 200) +) +"#; + let res = parse_cfg_raw_string(source, &mut s, "test") + .map_err(|_e| { + // uncomment to see what this looks like when running test + eprintln!("{:?}", error_with_source(_e)); + "" + }) + .unwrap(); + assert_eq!( + res.3[0][0][OsCode::KEY_A.as_u16() as usize], + Action::Custom( + &[&CustomAction::FakeKeyOnIdle(FakeKeyOnIdle { + coord: Coord { x: 1, y: 0 }, + action: FakeKeyAction::Tap, + idle_duration: 200 + })] + .as_ref() + ), + ); +} + +#[test] +fn parse_on_idle_fakekey_errors() { + let _lk = match CFG_PARSE_LOCK.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + }; + let mut s = ParsedState::default(); + let source = r#" +(defvar var1 a) +(defsrc a) +(deffakekeys hello a) +(deflayer base + (on-idle-fakekey hello bap 200) +) +"#; + parse_cfg_raw_string(source, &mut s, "test") + .map_err(|_e| { + // comment out to see what this looks like when running test + // eprintln!("{:?}", error_with_source(_e)); + "" + }) + .unwrap_err(); + + let source = r#" +(defvar var1 a) +(defsrc a) +(deffakekeys hello a) +(deflayer base + (on-idle-fakekey jello tap 200) +) +"#; + parse_cfg_raw_string(source, &mut s, "test") + .map_err(|_e| { + // uncomment to see what this looks like when running test + // eprintln!("{:?}", error_with_source(_e)); + "" + }) + .unwrap_err(); + + let source = r#" +(defvar var1 a) +(defsrc a) +(deffakekeys hello a) +(deflayer base + (on-idle-fakekey (hello) tap 200) +) +"#; + parse_cfg_raw_string(source, &mut s, "test") + .map_err(|_e| { + // uncomment to see what this looks like when running test + // eprintln!("{:?}", error_with_source(_e)); + "" + }) + .unwrap_err(); + + let source = r#" +(defvar var1 a) +(defsrc a) +(deffakekeys hello a) +(deflayer base + (on-idle-fakekey hello tap -1) +) +"#; + parse_cfg_raw_string(source, &mut s, "test") + .map_err(|_e| { + // uncomment to see what this looks like when running test + // eprintln!("{:?}", error_with_source(_e)); "" }) .unwrap_err(); } + +#[test] +fn parse_fake_keys_errors_on_too_many() { + let mut s = ParsedState::default(); + let mut checked_for_err = false; + for n in 0..1000 { + let exprs = [&vec![ + SExpr::Atom(Spanned { + t: "deffakekeys".to_string(), + span: Default::default(), + }), + SExpr::Atom(Spanned { + t: "a".repeat(n), + span: Default::default(), + }), + SExpr::Atom(Spanned { + t: "a".to_string(), + span: Default::default(), + }), + ]]; + if n < 500 { + // fill up fake keys, expect first bunch to succeed + parse_fake_keys(&exprs, &mut s).unwrap(); + } else if n < 999 { + // at some point they start failing, ignore result + let _ = parse_fake_keys(&exprs, &mut s); + } else { + // last iteration, check for error. probably happened before this, but just check here + let _ = parse_fake_keys(&exprs, &mut s).unwrap_err(); + checked_for_err = true; + } + } + assert!(checked_for_err); +} diff --git a/parser/src/custom_action.rs b/parser/src/custom_action.rs index 517c8d5e0..31ab3cda2 100644 --- a/parser/src/custom_action.rs +++ b/parser/src/custom_action.rs @@ -21,6 +21,7 @@ pub enum CustomAction { coord: Coord, action: FakeKeyAction, }, + FakeKeyOnIdle(FakeKeyOnIdle), Delay(u16), DelayOnRelease(u16), MWheel { @@ -83,6 +84,14 @@ pub enum FakeKeyAction { Tap, } +/// An active waiting-for-idle state. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct FakeKeyOnIdle { + pub coord: Coord, + pub action: FakeKeyAction, + pub idle_duration: u16, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum MWheelDirection { Up, diff --git a/parser/src/sequences.rs b/parser/src/sequences.rs index 0936a6197..2e5c35f76 100644 --- a/parser/src/sequences.rs +++ b/parser/src/sequences.rs @@ -14,3 +14,9 @@ pub fn mod_mask_for_keycode(kc: KeyCode) -> u16 { _ => 0, } } + +#[test] +fn keys_fit_within_mask() { + use crate::keys::OsCode; + assert!(MASK_KEYCODES >= u16::from(OsCode::KEY_MAX)); +} diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index dd47660d3..8acd95ad0 100644 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -140,6 +140,10 @@ pub struct Kanata { /// Config items from `defcfg`. #[cfg(target_os = "linux")] pub defcfg_items: HashMap, + /// Fake key actions that are waiting for a certain duration of keyboard idling. + pub waiting_for_idle: HashSet, + /// Number of ticks since kanata was idle. + pub ticks_since_idle: u16, } pub struct ScrollState { @@ -343,6 +347,8 @@ impl Kanata { caps_word: None, #[cfg(target_os = "linux")] defcfg_items: cfg.items, + waiting_for_idle: HashSet::default(), + ticks_since_idle: 0, }) } @@ -387,6 +393,7 @@ impl Kanata { fn handle_key_event(&mut self, event: &KeyEvent) -> Result<()> { log::debug!("process recv ev {event:?}"); let evc: u16 = event.code.into(); + self.ticks_since_idle = 0; let kbrn_ev = match event.value { KeyValue::Press => { if let Some(state) = &mut self.dynamic_macro_record_state { @@ -426,6 +433,7 @@ impl Kanata { self.handle_move_mouse()?; self.tick_sequence_state()?; self.tick_dynamic_macro_state()?; + self.tick_idle_timeout(); if self.live_reload_requested && self.prev_keys.is_empty() && self.cur_keys.is_empty() { self.live_reload_requested = false; @@ -570,6 +578,30 @@ impl Kanata { Ok(()) } + fn tick_idle_timeout(&mut self) { + if self.waiting_for_idle.is_empty() { + return; + } + self.waiting_for_idle.retain(|wfd| { + if self.ticks_since_idle >= wfd.idle_duration { + // Process this and return false so that it is not retained. + let layout = self.layout.bm(); + let Coord { x, y } = wfd.coord; + match wfd.action { + FakeKeyAction::Press => layout.event(Event::Press(x, y)), + FakeKeyAction::Release => layout.event(Event::Release(x, y)), + FakeKeyAction::Tap => { + layout.event(Event::Press(x, y)); + layout.event(Event::Release(x, y)); + } + }; + false + } else { + true + } + }) + } + /// Sends OS key events according to the change in key state between the current and the /// previous keyberon keystate. Also processes any custom actions. /// @@ -1110,6 +1142,9 @@ impl Kanata { CustomAction::FakeKeyOnRelease { .. } | CustomAction::DelayOnRelease(_) | CustomAction::CancelMacroOnRelease => {} + CustomAction::FakeKeyOnIdle(fkd) => { + self.waiting_for_idle.insert(*fkd); + } } } #[cfg(feature = "cmd")] @@ -1414,7 +1449,21 @@ impl Kanata { info!("Starting kanata proper"); let err = loop { - if kanata.lock().can_block() { + let can_block = { + let mut k = kanata.lock(); + let is_idle = k.is_idle(); + // Note: checking waiting_for_idle can not be part of the computation for + // is_idle() since incrementing ticks_since_idle is dependent on the return + // value of is_idle(). + let counting_idle_ticks = !k.waiting_for_idle.is_empty(); + if !is_idle { + k.ticks_since_idle = 0; + } else if is_idle && counting_idle_ticks { + k.ticks_since_idle = k.ticks_since_idle.saturating_add(1); + } + is_idle && !counting_idle_ticks + }; + if can_block { log::trace!("blocking on channel"); match rx.recv() { Ok(kev) => { @@ -1510,7 +1559,8 @@ impl Kanata { }); } - pub fn can_block(&self) -> bool { + pub fn is_idle(&self) -> bool { + let pressed_keys_means_not_idle = !self.waiting_for_idle.is_empty(); self.layout.b().queue.is_empty() && self.layout.b().waiting.is_none() && self.layout.b().last_press_tracker.tap_hold_timeout == 0 @@ -1525,12 +1575,10 @@ impl Kanata { && self.move_mouse_state_horizontal.is_none() && self.dynamic_macro_replay_state.is_none() && self.caps_word.is_none() - && !self - .layout - .b() - .states - .iter() - .any(|s| matches!(s, State::SeqCustomPending(_) | State::SeqCustomActive(_))) + && !self.layout.b().states.iter().any(|s| { + matches!(s, State::SeqCustomPending(_) | State::SeqCustomActive(_)) + || (pressed_keys_means_not_idle && matches!(s, State::NormalKey { .. })) + }) } } From b12b6eb9ea359f3de9cbc6e29333d3ca3ebd70e6 Mon Sep 17 00:00:00 2001 From: Jan Tache Date: Sat, 5 Aug 2023 00:50:27 -0700 Subject: [PATCH 022/819] fix: bad test expectation --- parser/src/cfg/tests.rs | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/parser/src/cfg/tests.rs b/parser/src/cfg/tests.rs index 814c78038..11ab5fc2f 100644 --- a/parser/src/cfg/tests.rs +++ b/parser/src/cfg/tests.rs @@ -556,7 +556,8 @@ fn test_include_bad2_has_original_filename() { } #[test] -fn parse_submacro() { +fn parse_bad_submacro() { + // Test exists since it used to crash. It should not crash. let _lk = match CFG_PARSE_LOCK.lock() { Ok(guard) => guard, Err(poisoned) => poisoned.into_inner(), @@ -565,7 +566,29 @@ fn parse_submacro() { let source = r#" (defsrc a) (deflayer base - (macro M-S-()) + (macro M-s-()) +) +"#; + parse_cfg_raw_string(source, &mut s, "test") + .map_err(|e| { + eprintln!("{:?}", error_with_source(e)); + "" + }) + .unwrap_err(); +} + +#[test] +fn parse_bad_submacro_2() { + // Test exists since it used to crash. It should not crash. + let _lk = match CFG_PARSE_LOCK.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + }; + let mut s = ParsedState::default(); + let source = r#" +(defsrc a) +(deflayer base + (macro M-s-g) ) "#; parse_cfg_raw_string(source, &mut s, "test") From 96c7558c43a7411ccd561abc8ad48e8442b8bf33 Mon Sep 17 00:00:00 2001 From: rszyma Date: Sun, 6 Aug 2023 23:20:54 +0200 Subject: [PATCH 023/819] parser: add better support for external tools (#513) * parser: support tokenizing comments and whitespace * parser: add LSP-style position to Span * parser: add FileContentProvider Motivation: In LS / Web-based UI configurator the config files can't be loaded directly from disk. * refactor: decouple miette diagnostic from parser Reason for these changes: unable to get lsp-style span from CfgError. I didn't add Position to CfgError because it would be ugly to have 2 almost identical fields that indicate span. * expose ParseError methods * remove column from Position and add line_beginning Reason: this makes Position work with all character encodings For more info see: https://reedmullanix.com/posts/unicode-source-spans.html * add more tests for span * fix a mistake in test * add wrapper around bytes iterator in Lexer Reason: using a wrapper for Bytes iterator simplifies setting line and line_beginning. --- parser/.gitignore | 1 + parser/src/cfg/error.rs | 126 ++++++++------- parser/src/cfg/mod.rs | 149 +++++++++++------ parser/src/cfg/sexpr.rs | 295 ++++++++++++++++++++++----------- parser/src/cfg/tests.rs | 350 +++++++++++++++++++++++++++++----------- src/kanata/cmd.rs | 4 +- 6 files changed, 624 insertions(+), 301 deletions(-) diff --git a/parser/.gitignore b/parser/.gitignore index 62e2af0b8..cb4f2dd90 100644 --- a/parser/.gitignore +++ b/parser/.gitignore @@ -1,2 +1,3 @@ # Ignore Cargo.lock since this is a library crate Cargo.lock +target \ No newline at end of file diff --git a/parser/src/cfg/error.rs b/parser/src/cfg/error.rs index f1bbe2eb9..8d3afa732 100644 --- a/parser/src/cfg/error.rs +++ b/parser/src/cfg/error.rs @@ -1,22 +1,80 @@ use miette::{Diagnostic, NamedSource, SourceSpan}; use thiserror::Error; -use super::*; +use super::{sexpr::Span, *}; pub type MResult = miette::Result; -pub type Result = std::result::Result; +pub type Result = std::result::Result; + +#[derive(Debug, Clone)] +pub struct ParseError { + pub msg: String, + pub span: Option, +} + +impl ParseError { + pub fn new(span: Span, err_msg: impl AsRef) -> Self { + Self { + msg: err_msg.as_ref().to_string(), + span: Some(span), + } + } + + pub fn new_without_span(err_msg: impl AsRef) -> Self { + Self { + msg: err_msg.as_ref().to_string(), + span: None, + } + } + + pub fn from_expr(expr: &sexpr::SExpr, err_msg: impl AsRef) -> Self { + Self::new(expr.span(), err_msg) + } + + pub fn from_spanned(spanned: &Spanned, err_msg: impl AsRef) -> Self { + Self::new(spanned.span.clone(), err_msg) + } +} + +impl From for ParseError { + fn from(value: anyhow::Error) -> Self { + Self::new_without_span(value.to_string()) + } +} + +impl From for miette::Error { + fn from(val: ParseError) -> Self { + let diagnostic = CfgError { + err_span: val + .span + .as_ref() + .map(|s| SourceSpan::new(s.start().into(), (s.end() - s.start()).into())), + help_msg: help(val.msg), + file_name: val.span.as_ref().map(|s| s.file_name()), + file_content: val.span.as_ref().map(|s| s.file_content()), + }; + + let report: miette::Error = diagnostic.into(); + + if let Some(span) = val.span { + report.with_source_code(NamedSource::new(span.file_name(), span.file_content())) + } else { + report + } + } +} #[derive(Error, Debug, Diagnostic, Clone)] #[error("Error in configuration file")] #[diagnostic()] -pub struct CfgError { +struct CfgError { // Snippets and highlights can be included in the diagnostic! #[label("Error here")] - pub err_span: Option, + err_span: Option, #[help] - pub help_msg: String, - pub file_name: Option, - pub file_content: Option, + help_msg: String, + file_name: Option, + file_content: Option, } pub(super) fn help(err_msg: impl AsRef) -> String { @@ -29,57 +87,3 @@ For more info, see the configuration guide or ask in GitHub discussions. err_msg.as_ref(), ) } - -pub(super) fn error_expr(expr: &sexpr::SExpr, err_msg: impl AsRef) -> CfgError { - CfgError { - err_span: Some(expr_err_span(expr)), - help_msg: help(err_msg), - file_name: Some(expr.span().file_name()), - file_content: Some(expr.span().file_content()), - } -} - -pub(super) fn error_spanned(expr: &Spanned, err_msg: impl AsRef) -> CfgError { - CfgError { - err_span: Some(spanned_err_span(expr)), - help_msg: help(err_msg), - file_name: Some(expr.span.file_name()), - file_content: Some(expr.span.file_content()), - } -} - -pub(super) fn span_start_len(start: usize, len: usize) -> SourceSpan { - SourceSpan::new(start.into(), len.into()) -} - -pub(super) fn expr_err_span(expr: &sexpr::SExpr) -> SourceSpan { - let span = expr.span(); - SourceSpan::new(span.start.into(), (span.end - span.start).into()) -} - -pub(super) fn spanned_err_span(spanned: &Spanned) -> SourceSpan { - let span = spanned.span.clone(); - SourceSpan::new(span.start.into(), (span.end - span.start).into()) -} - -pub(super) fn error_with_source(e: CfgError) -> miette::Error { - let filename = e.file_name.clone(); - let source = e.file_content.clone(); - let e2: miette::Error = e.into(); - if let (Some(f), Some(s)) = (filename, source) { - e2.with_source_code(NamedSource::new(f, s)) - } else { - e2 - } -} - -impl From for CfgError { - fn from(value: anyhow::Error) -> Self { - Self { - err_span: None, - help_msg: help(value.to_string()), - file_name: None, - file_content: None, - } - } -} diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index 7b70825a6..d38e28631 100644 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -53,12 +53,13 @@ use crate::keys::*; use crate::layers::*; mod error; -use error::*; +pub use error::*; use crate::trie::Trie; use anyhow::anyhow; use std::collections::hash_map::Entry; use std::path::Path; +use std::path::PathBuf; use std::sync::Arc; type HashSet = rustc_hash::FxHashSet; @@ -79,49 +80,68 @@ pub use sexpr::parse; macro_rules! bail { ($err:expr $(,)?) => { - return Err(CfgError::from(anyhow!($err))) + return Err(ParseError::from(anyhow!($err))) }; ($fmt:expr, $($arg:tt)*) => { - return Err(CfgError::from(anyhow!($fmt, $($arg)*))) + return Err(ParseError::from(anyhow!($fmt, $($arg)*))) }; } macro_rules! bail_expr { ($expr:expr, $fmt:expr $(,)?) => { - return Err(error_expr($expr, format!($fmt))) + return Err(ParseError::from_expr($expr, format!($fmt))) }; ($expr:expr, $fmt:expr, $($arg:tt)*) => { - return Err(error_expr($expr, format!($fmt, $($arg)*))) + return Err(ParseError::from_expr($expr, format!($fmt, $($arg)*))) }; } macro_rules! bail_span { ($expr:expr, $fmt:expr $(,)?) => { - return Err(error_spanned($expr, format!($fmt))) + return Err(ParseError::from_spanned($expr, format!($fmt))) }; ($expr:expr, $fmt:expr, $($arg:tt)*) => { - return Err(error_spanned($expr, format!($fmt, $($arg)*))) + return Err(ParseError::from_spanned($expr, format!($fmt, $($arg)*))) }; } macro_rules! anyhow_expr { ($expr:expr, $fmt:expr $(,)?) => { - error_expr($expr, format!($fmt)) + ParseError::from_expr($expr, format!($fmt)) }; ($expr:expr, $fmt:expr, $($arg:tt)*) => { - error_expr($expr, format!($fmt, $($arg)*)) + ParseError::from_expr($expr, format!($fmt, $($arg)*)) }; } macro_rules! anyhow_span { ($expr:expr, $fmt:expr $(,)?) => { - error_spanned($expr, format!($fmt)) + ParseError::from_spanned($expr, format!($fmt)) }; ($expr:expr, $fmt:expr, $($arg:tt)*) => { - error_spanned($expr, format!($fmt, $($arg)*)) + ParseError::from_spanned($expr, format!($fmt, $($arg)*)) }; } +pub struct FileContentProvider<'a> { + /// A function to load content of a file from a filepath. + /// Optionally, it could implement caching and a mechanism preventing "file" and "./file" from loading twice. + get_file_content_fn: &'a mut dyn FnMut(&Path) -> std::result::Result, +} + +impl<'a> FileContentProvider<'a> { + pub fn new( + get_file_content_fn: &'a mut impl FnMut(&Path) -> std::result::Result, + ) -> Self { + Self { + get_file_content_fn, + } + } + pub fn get_file_content(&mut self, filename: &Path) -> std::result::Result { + (self.get_file_content_fn)(filename) + } +} + pub type KanataAction = Action<'static, &'static &'static [&'static CustomAction]>; type KLayout = Layout<'static, KEYS_IN_ROW, 2, ACTUAL_NUM_LAYERS, &'static &'static [&'static CustomAction]>; @@ -177,7 +197,7 @@ pub struct Cfg { } /// Parse a new configuration from a file. -pub fn new_from_file(p: &std::path::Path) -> MResult { +pub fn new_from_file(p: &Path) -> MResult { let (items, mapped_keys, layer_info, key_outputs, layout, sequences, overrides) = parse_cfg(p)?; log::info!("config parsed"); Ok(Cfg { @@ -205,7 +225,7 @@ pub struct LayerInfo { #[allow(clippy::type_complexity)] // return type is not pub fn parse_cfg( - p: &std::path::Path, + p: &Path, ) -> MResult<( HashMap, MappedKeys, @@ -241,7 +261,7 @@ const DEF_LOCAL_KEYS: &str = "deflocalkeys-linux"; #[allow(clippy::type_complexity)] // return type is not pub fn parse_cfg_raw( - p: &std::path::Path, + p: &Path, s: &mut ParsedState, ) -> MResult<( HashMap, @@ -251,12 +271,57 @@ fn parse_cfg_raw( KeySeqsToFKeys, Overrides, )> { - let text = std::fs::read_to_string(p).map_err(|e| miette::miette!("{e}"))?; - let cfg_filename = p.to_string_lossy().to_string(); - parse_cfg_raw_string(&text, s, &cfg_filename).map_err(error_with_source) + const INVALID_PATH_ERROR: &str = "The provided config file path is not valid"; + + let mut loaded_files: HashSet = HashSet::default(); + + let mut get_file_content_fn_impl = |filepath: &Path| { + // Make the include paths relative to main config file instead of kanata executable. + let filepath_relative_to_loaded_kanata_cfg = if filepath.is_absolute() { + filepath.to_owned() + } else { + let relative_main_cfg_file_dir = p.parent().ok_or(INVALID_PATH_ERROR)?; + relative_main_cfg_file_dir.join(filepath) + }; + + // Forbid loading the same file multiple times. + // This prevents a potential recursive infinite loop of includes + // (if includes within includes were to be allowed). + let abs_filepath: PathBuf = filepath_relative_to_loaded_kanata_cfg + .canonicalize() + .map_err(|e| { + format!( + "Failed to resolve absolute path: {}: {}", + filepath_relative_to_loaded_kanata_cfg.to_string_lossy(), + e + ) + })?; + if !loaded_files.insert(abs_filepath.clone()) { + return Err("The provided config file was already included before".to_string()); + }; + + std::fs::read_to_string(abs_filepath.to_str().ok_or(INVALID_PATH_ERROR)?) + .map_err(|e| format!("Failed to include file: {e}")) + }; + let mut file_content_provider = FileContentProvider::new(&mut get_file_content_fn_impl); + + // `get_file_content_fn_impl` already uses CWD of the main config path, + // so we need to provide only the name, not the whole path. + let cfg_file_name: PathBuf = p + .file_name() + .ok_or_else(|| miette::miette!(INVALID_PATH_ERROR))? + .into(); + let text = file_content_provider + .get_file_content(&cfg_file_name) + .map_err(|e| miette::miette!(e))?; + + parse_cfg_raw_string(&text, s, p, &mut file_content_provider).map_err(|e| e.into()) } -fn expand_includes(xs: Vec, main_config_filepath: &str) -> Result> { +fn expand_includes( + xs: Vec, + file_content_provider: &mut FileContentProvider, +) -> Result> { let include_is_first_atom = gen_first_atom_filter("include"); xs.iter().try_fold(Vec::new(), |mut acc, spanned_exprs| { if include_is_first_atom(&&spanned_exprs.t) { @@ -281,22 +346,9 @@ fn expand_includes(xs: Vec, main_config_filepath: &str) -> Result, main_config_filepath: &str) -> Result Result<( HashMap, MappedKeys, @@ -320,11 +373,9 @@ fn parse_cfg_raw_string( KeySeqsToFKeys, Overrides, )> { - let spanned_root_exprs = - sexpr::parse(text, cfg_filename).and_then(|xs| expand_includes(xs, cfg_filename))?; + let spanned_root_exprs = sexpr::parse(text, &cfg_path.to_string_lossy()) + .and_then(|xs| expand_includes(xs, file_content_provider))?; - // NOTE: If nested included were to be allowed in the future, - // a mechanism preventing circular includes should be incorporated. if let Some(spanned) = spanned_root_exprs .iter() .find(gen_first_atom_filter_spanned("include")) @@ -853,7 +904,7 @@ fn parse_layer_indexes(exprs: &[Spanned>], expected_len: usize) -> Re } #[derive(Debug)] -struct ParsedState { +pub struct ParsedState { layer_exprs: Vec>, aliases: Aliases, layer_idxs: LayerIndexes, @@ -1032,11 +1083,9 @@ fn parse_action(expr: &SExpr, s: &ParsedState) -> Result<&'static KanataAction> .expect("must be atom or list") }) .map_err(|mut e| { - if e.err_span.is_none() { - e.err_span = Some(expr_err_span(expr)); - e.file_name = Some(expr.span().file_name()); - e.file_content = Some(expr.span().file_content()); - } + if e.span.is_none() { + e.span = Some(expr.span()) + }; e }) } @@ -2118,7 +2167,7 @@ fn fill_chords( for case in cases.iter() { new_cases.push(( case.0, - fill_chords(chord_groups, &case.1, s) + fill_chords(chord_groups, case.1, s) .map(|ac| s.a.sref(ac)) .unwrap_or(case.1), case.2, @@ -2560,7 +2609,7 @@ fn parse_sequence_keys(exprs: &[SExpr], s: &ParsedState) -> Result> { (seq, res.1) } Err(mut e) => { - e.help_msg = format!("{SEQ_ERR}\nFound invalid key/chord in key_list"); + e.msg = format!("{SEQ_ERR}\nFound invalid key/chord in key_list"); return Err(e); } }; @@ -2868,7 +2917,7 @@ fn parse_switch_case_bool( let l = op_expr .list(s.vars()) .expect("must be a list, checked atom"); - if l.len() < 1 { + if l.is_empty() { bail_expr!(op_expr, "key match cannot contain empty lists inside"); } let op = l[0] @@ -2931,7 +2980,7 @@ fn parse_on_idle_fakekey(ac_params: &[SExpr], s: &ParsedState) -> Result<&'stati ) })?; let idle_duration = parse_u16(&ac_params[2], s, "idle time").map_err(|mut e| { - e.help_msg = format!("{ERR_MSG}\nInvalid third parameter: {}", e.help_msg); + e.msg = format!("{ERR_MSG}\nInvalid third parameter: {}", e.msg); e })?; let (x, y) = get_fake_key_coords(y); diff --git a/parser/src/cfg/sexpr.rs b/parser/src/cfg/sexpr.rs index 0431f93d7..aee983286 100644 --- a/parser/src/cfg/sexpr.rs +++ b/parser/src/cfg/sexpr.rs @@ -1,19 +1,38 @@ +use std::iter; use std::ops::Index; use std::rc::Rc; use std::str::Bytes; -use std::{cmp, iter}; -type ParseError = Spanned; type HashMap = rustc_hash::FxHashMap; -type ParseResult = Result; +use super::{ParseError, Result}; -use super::error::{span_start_len, CfgError}; +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +pub struct Position { + /// The position (since the beginning of the file), in bytes. + pub absolute: usize, + /// The number of newline characters since the beginning of the file. + pub line: usize, + /// The position of beginning of line, in bytes. + pub line_beginning: usize, +} + +impl Position { + fn new(absolute: usize, line: usize, line_beginning: usize) -> Self { + assert!(line <= absolute); + assert!(line_beginning <= absolute); + Self { + absolute, + line, + line_beginning, + } + } +} #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Span { - pub start: usize, - pub end: usize, + pub start: Position, + pub end: Position, pub file_name: Rc, pub file_content: Rc, } @@ -21,8 +40,8 @@ pub struct Span { impl Default for Span { fn default() -> Self { Self { - start: 0, - end: 0, + start: Position::default(), + end: Position::default(), file_name: Rc::from(""), file_content: Rc::from(""), } @@ -30,8 +49,9 @@ impl Default for Span { } impl Span { - fn new(start: usize, end: usize, file_name: Rc, file_content: Rc) -> Span { - assert!(start <= end); + fn new(start: Position, end: Position, file_name: Rc, file_content: Rc) -> Span { + assert!(start.absolute <= end.absolute); + assert!(start.line <= end.line); Span { start, end, @@ -40,24 +60,35 @@ impl Span { } } - pub fn start(&self) -> usize { - self.start + pub fn cover(&self, other: &Span) -> Span { + assert!(self.file_name == other.file_name); + + let start: Position = if self.start() <= other.start() { + self.start + } else { + other.start + }; + + let end: Position = if self.end() >= other.end() { + self.end + } else { + other.end + }; + + Span::new( + start, + end, + self.file_name.clone(), + self.file_content.clone(), + ) } - pub fn end(&self) -> usize { - self.end + pub fn start(&self) -> usize { + self.start.absolute } - /// # Panics - /// - /// Panics if `other` has a different `file_name` - pub fn cover(self, other: Span) -> Span { - let start = cmp::min(self.start(), other.start()); - let end = cmp::max(self.end(), other.end()); - if self.file_name != other.file_name { - panic!("Can't create span across different files."); - } - Span::new(start, end, self.file_name, self.file_content) + pub fn end(&self) -> usize { + self.end.absolute } pub fn file_name(&self) -> String { @@ -160,39 +191,97 @@ impl std::fmt::Debug for SExpr { } } +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +/// Complementary to SExpr metadata items. +pub enum SExprMetaData { + LineComment(Spanned), + BlockComment(Spanned), + Whitespace(Spanned), +} + #[derive(Debug)] enum Token { Open, Close, StringTok, + BlockComment, + LineComment, + Whitespace, } -pub struct Lexer<'a> { - s: &'a str, + +#[derive(Clone)] +/// A wrapper around [`Bytes`] that keeps track of current [`Position`]. +struct PositionCountingBytesIterator<'a> { bytes: Bytes<'a>, + source_length: usize, + line: usize, + line_beginning: usize, +} + +impl<'a> PositionCountingBytesIterator<'a> { + fn new(s: &'a str) -> Self { + Self { + bytes: s.bytes(), + source_length: s.len(), + line: 0, + line_beginning: 0, + } + } + + fn pos(&self) -> Position { + let absolute = self.source_length - self.bytes.len(); + Position::new(absolute, self.line, self.line_beginning) + } +} + +impl<'a> Iterator for PositionCountingBytesIterator<'a> { + type Item = u8; + + fn next(&mut self) -> Option { + self.bytes.next().map(|b| { + if b == b'\n' { + self.line += 1; + self.line_beginning = self.source_length - self.bytes.len() + } + b + }) + } +} + +pub struct Lexer<'a> { + bytes: PositionCountingBytesIterator<'a>, + ignore_whitespace_and_comments: bool, } fn is_start(b: u8) -> bool { matches!(b, b'(' | b')' | b'"') || b.is_ascii_whitespace() } -type TokenRes = Result; +type TokenRes = std::result::Result; impl<'a> Lexer<'a> { #[allow(clippy::new_ret_no_self)] /// `file_name` is used only for indicating a file, where /// a fragment of `source` that caused parsing error came from. - fn new(source: &'a str, file_name: &'a str) -> impl Iterator> + 'a { + fn new( + source: &'a str, + file_name: &'a str, + ignore_whitespace_and_comments: bool, + ) -> impl Iterator> + 'a { + let _bytes = source.bytes().next(); + let mut lexer = Lexer { - s: source, - bytes: source.bytes(), + bytes: PositionCountingBytesIterator::new(source), + ignore_whitespace_and_comments, }; let file_name: Rc = Rc::from(file_name); let file_content: Rc = Rc::from(source); iter::from_fn(move || { lexer.next_token().map(|(start, t)| { + let end = lexer.bytes.pos(); Spanned::new( t, - Span::new(start, lexer.pos(), file_name.clone(), file_content.clone()), + Span::new(start, end, file_name.clone(), file_content.clone()), ) }) }) @@ -209,36 +298,24 @@ impl<'a> Lexer<'a> { } } - /// Looks for "|#", consuming bytes until found. If not found, returns Some(Err(...)); - /// otherwise returns None. - fn read_until_multiline_comment_end(&mut self) -> Option { - let mut found_comment_end = false; + /// Looks for "|#", consuming bytes until found. If not found, returns Err(...); + fn read_until_multiline_comment_end(&mut self) -> TokenRes { for b2 in self.bytes.clone().skip(1) { // Iterating over a clone of this iterator that's 1 item ahead - this is guaranteed to // be Some. let b1 = self.bytes.next().expect("iter lag"); if b1 == b'|' && b2 == b'#' { - found_comment_end = true; - break; + self.bytes.next(); + return Ok(Token::BlockComment); } } - if !found_comment_end { - return Some(Err( - "Unterminated multiline comment. Add |# after the end of your comment.".to_string(), - )); - } - self.bytes.next(); - None + Err("Unterminated multiline comment. Add |# after the end of your comment.".to_string()) } - fn pos(&self) -> usize { - self.s.len() - self.bytes.len() - } - - fn next_token(&mut self) -> Option<(usize, TokenRes)> { + fn next_token(&mut self) -> Option<(Position, TokenRes)> { use Token::*; loop { - let start = self.pos(); + let start = self.bytes.pos(); break match self.bytes.next() { Some(b) => Some(( start, @@ -256,8 +333,11 @@ impl<'a> Lexer<'a> { Some(b';') => { self.next_while(|b| b != b'\n'); // possibly consume the newline (or EOF handled in next iteration) - let _ = self.bytes.next(); - continue; + self.bytes.next(); + if self.ignore_whitespace_and_comments { + continue; + } + Token::LineComment } _ => self.next_string(), }, @@ -265,16 +345,23 @@ impl<'a> Lexer<'a> { Some(b'|') => { // consume the '|' self.bytes.next(); - if let Some(e) = self.read_until_multiline_comment_end() { - return Some((start, e)); + let tok: Token = match self.read_until_multiline_comment_end() { + Ok(t) => t, + e @ Err(_) => return Some((start, e)), + }; + if self.ignore_whitespace_and_comments { + continue; } - continue; + tok } _ => self.next_string(), }, b if b.is_ascii_whitespace() => { - self.next_while(|b| b.is_ascii_whitespace()); - continue; + let tok = self.next_whitespace(); + if self.ignore_whitespace_and_comments { + continue; + } + tok } _ => self.next_string(), }), @@ -289,30 +376,57 @@ impl<'a> Lexer<'a> { self.next_while(|b| !is_start(b)); Token::StringTok } + + fn next_whitespace(&mut self) -> Token { + self.next_while(|b| b.is_ascii_whitespace()); + Token::Whitespace + } } pub type TopLevel = Spanned>; -pub fn parse(cfg: &str, file_name: &str) -> Result, CfgError> { - parse_(cfg, file_name).map_err(transform_error) +pub fn parse(cfg: &str, file_name: &str) -> std::result::Result, ParseError> { + let ignore_whitespace_and_comments = true; + parse_(cfg, file_name, ignore_whitespace_and_comments) + .map_err(|e| { + if e.msg.contains("Unterminated multiline comment") { + if let Some(mut span) = e.span { + span.end = span.start; + span.end.absolute += 2; + ParseError::new(span, e.msg) + } else { + e + } + } else { + e + } + }) + .map(|(x, _)| x) } -pub fn parse_(cfg: &str, file_name: &str) -> ParseResult> { - parse_with(cfg, Lexer::new(cfg, file_name)) +pub fn parse_( + cfg: &str, + file_name: &str, + ignore_whitespace_and_comments: bool, +) -> Result<(Vec, Vec)> { + parse_with( + cfg, + Lexer::new(cfg, file_name, ignore_whitespace_and_comments), + ) } fn parse_with( s: &str, mut tokens: impl Iterator>, -) -> ParseResult> { - use SExpr::*; +) -> Result<(Vec, Vec)> { use Token::*; let mut stack = vec![Spanned::new(vec![], Span::default())]; + let mut metadata: Vec = vec![]; loop { match tokens.next() { None => break, - Some(Spanned { t, span }) => match t.map_err(|s| Spanned::new(s, span.clone()))? { - Open => stack.push(Spanned::new(vec![], span.clone())), + Some(Spanned { t, span }) => match t.map_err(|s| ParseError::new(span.clone(), s))? { + Open => stack.push(Spanned::new(vec![], span)), Close => { let Spanned { t: exprs, @@ -321,22 +435,28 @@ fn parse_with( // if the stack is ever empty, return an error. } = stack.pop().expect("placeholder unpopped"); if stack.is_empty() { - return Err(Spanned::new( - "Unexpected closing parenthesis".to_string(), - span, - )); + return Err(ParseError::new(span, "Unexpected closing parenthesis")); } - let expr = List(Spanned::new(exprs, stack_span.cover(span.clone()))); + let expr = SExpr::List(Spanned::new(exprs, stack_span.cover(&span))); stack.last_mut().expect("not empty").t.push(expr); } StringTok => stack .last_mut() .expect("not empty") .t - .push(Atom(Spanned::new( - s[span.clone()].to_string(), - span.clone(), - ))), + .push(SExpr::Atom(Spanned::new(s[span.clone()].to_string(), span))), + BlockComment => metadata.push(SExprMetaData::BlockComment(Spanned::new( + s[span.clone()].to_string(), + span, + ))), + LineComment => metadata.push(SExprMetaData::LineComment(Spanned::new( + s[span.clone()].to_string(), + span, + ))), + Whitespace => metadata.push(SExprMetaData::Whitespace(Spanned::new( + s[span.clone()].to_string(), + span, + ))), }, } } @@ -344,19 +464,16 @@ fn parse_with( // empty, return an error. let Spanned { t: exprs, span: sp } = stack.pop().expect("placeholder unpopped"); if !stack.is_empty() { - return Err(Spanned::new("Unclosed opening parenthesis".to_string(), sp)); + return Err(ParseError::new(sp, "Unclosed opening parenthesis")); } let exprs = exprs .into_iter() .map(|expr| match expr { SExpr::List(es) => Ok(es), - SExpr::Atom(s) => Err(Spanned::new( - "Everything must be in a list".to_string(), - s.span, - )), + SExpr::Atom(s) => Err(ParseError::new(s.span, "Everything must be in a list")), }) - .collect::>()?; - Ok(exprs) + .collect::>()?; + Ok((exprs, metadata)) } use miette::{Diagnostic, SourceSpan}; @@ -372,19 +489,3 @@ pub struct LexError { #[help] pub help_msg: String, } - -pub fn transform_error(e: ParseError) -> CfgError { - let start = e.span.start(); - let end = e.span.end(); - let mut len = end - start; - if e.t.contains("Unterminated multiline comment") { - len = 2; - }; - - CfgError { - err_span: Some(span_start_len(start, len)), - help_msg: e.t, - file_name: Some(e.span.file_name()), - file_content: Some(e.span.file_content()), - } -} diff --git a/parser/src/cfg/tests.rs b/parser/src/cfg/tests.rs index 11ab5fc2f..9d3080cec 100644 --- a/parser/src/cfg/tests.rs +++ b/parser/src/cfg/tests.rs @@ -15,14 +15,114 @@ fn sizeof_action_is_two_usizes() { } #[test] -fn span_works() { +fn test_span_absolute_ranges() { let s = "(hello world my oyster)\n(row two)"; let tlevel = parse(s, "test").unwrap(); assert_eq!( - &s[tlevel[0].span.start..tlevel[0].span.end], + &s[tlevel[0].span.start()..tlevel[0].span.end()], "(hello world my oyster)" ); - assert_eq!(&s[tlevel[1].span.start..tlevel[1].span.end], "(row two)"); + assert_eq!( + &s[tlevel[1].span.start()..tlevel[1].span.end()], + "(row two)" + ); +} + +#[test] +fn span_works_with_unicode_characters() { + let _lk = match CFG_PARSE_LOCK.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + }; + let mut s = ParsedState::default(); + let source = r#"(defsrc a) ;; 😊 +(deflayer base @😊) +"#; + let span = parse_cfg_raw_string( + source, + &mut s, + &PathBuf::from("test"), + &mut FileContentProvider { + get_file_content_fn: &mut |_| unimplemented!(), + }, + ) + .expect_err("should be an error because @😊 is not defined") + .span + .expect("span should be Some"); + + assert_eq!(&source[span.start()..span.end()], "@😊"); + + assert_eq!(span.start.line, 1); + assert_eq!(span.end.line, 1); + + assert_eq!("😊".len(), 4); + assert_eq!("(defsrc a) ;; 😊\n".len(), 19); + assert_eq!(span.start.line_beginning, 19); + assert_eq!(span.end.line_beginning, 19); +} + +#[test] +fn test_multiline_error_span() { + let _lk = match CFG_PARSE_LOCK.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + }; + let mut s = ParsedState::default(); + let source = r#"(defsrc a) +( + 🍍 + 🍕 +) +(defalias a b) +"#; + let span = parse_cfg_raw_string( + source, + &mut s, + &PathBuf::from("test"), + &mut FileContentProvider { + get_file_content_fn: &mut |_| unimplemented!(), + }, + ) + .expect_err("should error on unknown top level block") + .span + .expect("span should be Some"); + + assert_eq!(&source[span.start()..span.end()], "(\n 🍍\n 🍕\n)"); + + assert_eq!(span.start.line, 1); + assert_eq!(span.end.line, 4); + + assert_eq!(span.start.line_beginning, "(defsrc a)\n".len()); + assert_eq!(span.end.line_beginning, "(defsrc a)\n(\n 🍍\n 🍕\n".len()); +} + +#[test] +fn test_span_of_an_unterminated_block_comment_error() { + let _lk = match CFG_PARSE_LOCK.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + }; + let mut s = ParsedState::default(); + let source = r#"(defsrc a) |# I'm an unterminated block comment..."#; + let span = parse_cfg_raw_string( + source, + &mut s, + &PathBuf::from("test"), + &mut FileContentProvider { + get_file_content_fn: &mut |_| unimplemented!(), + }, + ) + .expect_err("should be an unterminated comment error") + .span + .expect("span should be Some"); + + assert_eq!(&source[span.start()..span.end()], "|#"); + + assert_eq!(span.start.line, 0); + assert_eq!(span.end.line, 0); + + assert_eq!(span.start.line_beginning, 0); + assert_eq!(span.end.line_beginning, 0); } #[test] @@ -94,12 +194,15 @@ fn parse_action_vars() { (defchords $e $one $1 $two) (defchords $e2 $one ($one) $two) "#; - parse_cfg_raw_string(source, &mut s, "test") - .map_err(|e| { - eprintln!("{:?}", error_with_source(e)); - "" - }) - .unwrap(); + parse_cfg_raw_string( + source, + &mut s, + &PathBuf::from("test"), + &mut FileContentProvider { + get_file_content_fn: &mut |_| unimplemented!(), + }, + ) + .unwrap(); } #[test] @@ -115,12 +218,15 @@ fn parse_delegate_to_default_layer_yes() { (deflayer base b) (deflayer other _) "#; - let res = parse_cfg_raw_string(source, &mut s, "test") - .map_err(|e| { - eprintln!("{:?}", error_with_source(e)); - "" - }) - .unwrap(); + let res = parse_cfg_raw_string( + source, + &mut s, + &PathBuf::from("test"), + &mut FileContentProvider { + get_file_content_fn: &mut |_| unimplemented!(), + }, + ) + .unwrap(); assert_eq!( res.3[2][0][OsCode::KEY_A.as_u16() as usize], Action::KeyCode(KeyCode::B), @@ -140,12 +246,15 @@ fn parse_delegate_to_default_layer_yes_but_base_transparent() { (deflayer base _) (deflayer other _) "#; - let res = parse_cfg_raw_string(source, &mut s, "test") - .map_err(|e| { - eprintln!("{:?}", error_with_source(e)); - "" - }) - .unwrap(); + let res = parse_cfg_raw_string( + source, + &mut s, + &PathBuf::from("test"), + &mut FileContentProvider { + get_file_content_fn: &mut |_| unimplemented!(), + }, + ) + .unwrap(); assert_eq!( res.3[2][0][OsCode::KEY_A.as_u16() as usize], Action::KeyCode(KeyCode::A), @@ -165,12 +274,15 @@ fn parse_delegate_to_default_layer_no() { (deflayer base b) (deflayer other _) "#; - let res = parse_cfg_raw_string(source, &mut s, "test") - .map_err(|e| { - eprintln!("{:?}", error_with_source(e)); - "" - }) - .unwrap(); + let res = parse_cfg_raw_string( + source, + &mut s, + &PathBuf::from("test"), + &mut FileContentProvider { + get_file_content_fn: &mut |_| unimplemented!(), + }, + ) + .unwrap(); assert_eq!( res.3[2][0][OsCode::KEY_A.as_u16() as usize], Action::KeyCode(KeyCode::A), @@ -519,10 +631,7 @@ fn test_include_bad_has_filename_included() { .map(|_| ()) .unwrap_err() ); - assert!(err.contains(&format!( - "test_cfgs{}included-bad.kbd", - std::path::MAIN_SEPARATOR - ))); + assert!(err.contains("included-bad.kbd")); assert!(!err.contains(&format!( "test_cfgs{}include-bad.kbd", std::path::MAIN_SEPARATOR @@ -569,12 +678,19 @@ fn parse_bad_submacro() { (macro M-s-()) ) "#; - parse_cfg_raw_string(source, &mut s, "test") - .map_err(|e| { - eprintln!("{:?}", error_with_source(e)); - "" - }) - .unwrap_err(); + parse_cfg_raw_string( + source, + &mut s, + &PathBuf::from("test"), + &mut FileContentProvider { + get_file_content_fn: &mut |_| unimplemented!(), + }, + ) + .map_err(|e| { + eprintln!("{:?}", e); + "" + }) + .unwrap_err(); } #[test] @@ -591,12 +707,19 @@ fn parse_bad_submacro_2() { (macro M-s-g) ) "#; - parse_cfg_raw_string(source, &mut s, "test") - .map_err(|e| { - eprintln!("{:?}", error_with_source(e)); - "" - }) - .unwrap_err(); + parse_cfg_raw_string( + source, + &mut s, + &PathBuf::from("test"), + &mut FileContentProvider { + get_file_content_fn: &mut |_| unimplemented!(), + }, + ) + .map_err(|e| { + eprintln!("{:?}", e); + "" + }) + .unwrap_err(); } #[test] @@ -618,12 +741,15 @@ fn parse_switch() { ) ) "#; - let res = parse_cfg_raw_string(source, &mut s, "test") - .map_err(|e| { - eprintln!("{:?}", error_with_source(e)); - "" - }) - .unwrap(); + let res = parse_cfg_raw_string( + source, + &mut s, + &PathBuf::from("test"), + &mut FileContentProvider { + get_file_content_fn: &mut |_| unimplemented!(), + }, + ) + .unwrap(); assert_eq!( res.3[0][0][OsCode::KEY_A.as_u16() as usize], Action::Switch(&Switch { @@ -687,13 +813,20 @@ fn parse_switch_exceed_depth() { ) ) "#; - parse_cfg_raw_string(source, &mut s, "test") - .map_err(|_e| { - // uncomment to see what this looks like when running test - // eprintln!("{:?}", error_with_source(_e)); - "" - }) - .unwrap_err(); + parse_cfg_raw_string( + source, + &mut s, + &PathBuf::from("test"), + &mut FileContentProvider { + get_file_content_fn: &mut |_| unimplemented!(), + }, + ) + .map_err(|_e| { + // uncomment to see what this looks like when running test + // eprintln!("{:?}", _e); + "" + }) + .unwrap_err(); } #[test] @@ -711,13 +844,20 @@ fn parse_on_idle_fakekey() { (on-idle-fakekey hello tap 200) ) "#; - let res = parse_cfg_raw_string(source, &mut s, "test") - .map_err(|_e| { - // uncomment to see what this looks like when running test - eprintln!("{:?}", error_with_source(_e)); - "" - }) - .unwrap(); + let res = parse_cfg_raw_string( + source, + &mut s, + &PathBuf::from("test"), + &mut FileContentProvider { + get_file_content_fn: &mut |_| unimplemented!(), + }, + ) + .map_err(|_e| { + // uncomment to see what this looks like when running test + eprintln!("{:?}", _e); + "" + }) + .unwrap(); assert_eq!( res.3[0][0][OsCode::KEY_A.as_u16() as usize], Action::Custom( @@ -746,13 +886,20 @@ fn parse_on_idle_fakekey_errors() { (on-idle-fakekey hello bap 200) ) "#; - parse_cfg_raw_string(source, &mut s, "test") - .map_err(|_e| { - // comment out to see what this looks like when running test - // eprintln!("{:?}", error_with_source(_e)); - "" - }) - .unwrap_err(); + parse_cfg_raw_string( + source, + &mut s, + &PathBuf::from("test"), + &mut FileContentProvider { + get_file_content_fn: &mut |_| unimplemented!(), + }, + ) + .map_err(|_e| { + // comment out to see what this looks like when running test + // eprintln!("{:?}", _e); + "" + }) + .unwrap_err(); let source = r#" (defvar var1 a) @@ -762,13 +909,20 @@ fn parse_on_idle_fakekey_errors() { (on-idle-fakekey jello tap 200) ) "#; - parse_cfg_raw_string(source, &mut s, "test") - .map_err(|_e| { - // uncomment to see what this looks like when running test - // eprintln!("{:?}", error_with_source(_e)); - "" - }) - .unwrap_err(); + parse_cfg_raw_string( + source, + &mut s, + &PathBuf::from("test"), + &mut FileContentProvider { + get_file_content_fn: &mut |_| unimplemented!(), + }, + ) + .map_err(|_e| { + // uncomment to see what this looks like when running test + // eprintln!("{:?}", _e); + "" + }) + .unwrap_err(); let source = r#" (defvar var1 a) @@ -778,13 +932,20 @@ fn parse_on_idle_fakekey_errors() { (on-idle-fakekey (hello) tap 200) ) "#; - parse_cfg_raw_string(source, &mut s, "test") - .map_err(|_e| { - // uncomment to see what this looks like when running test - // eprintln!("{:?}", error_with_source(_e)); - "" - }) - .unwrap_err(); + parse_cfg_raw_string( + source, + &mut s, + &PathBuf::from("test"), + &mut FileContentProvider { + get_file_content_fn: &mut |_| unimplemented!(), + }, + ) + .map_err(|_e| { + // uncomment to see what this looks like when running test + // eprintln!("{:?}", _e); + "" + }) + .unwrap_err(); let source = r#" (defvar var1 a) @@ -794,13 +955,20 @@ fn parse_on_idle_fakekey_errors() { (on-idle-fakekey hello tap -1) ) "#; - parse_cfg_raw_string(source, &mut s, "test") - .map_err(|_e| { - // uncomment to see what this looks like when running test - // eprintln!("{:?}", error_with_source(_e)); - "" - }) - .unwrap_err(); + parse_cfg_raw_string( + source, + &mut s, + &PathBuf::from("test"), + &mut FileContentProvider { + get_file_content_fn: &mut |_| unimplemented!(), + }, + ) + .map_err(|_e| { + // uncomment to see what this looks like when running test + // eprintln!("{:?}", _e); + "" + }) + .unwrap_err(); } #[test] diff --git a/src/kanata/cmd.rs b/src/kanata/cmd.rs index 580186606..27a99340d 100644 --- a/src/kanata/cmd.rs +++ b/src/kanata/cmd.rs @@ -91,7 +91,7 @@ fn try_parse_chord<'a>(chord: &str, exprs: &'a [SExpr], items: &mut Vec) - } }, Err(e) => { - log::warn!("{LP} found invalid chord {chord}: {e}"); + log::warn!("{LP} found invalid chord {chord}: {}", e.msg); &exprs[1..] } } @@ -187,7 +187,7 @@ pub(super) fn keys_for_cmd_output(cmd_and_args: &[String]) -> impl Iterator { log::warn!( "{LP} could not parse an S-expression from cmd:\n{stdout}\n{}", - e.help_msg + e.msg ); empty() } From a46477302f3978c3f534267ea2082919e4a5d4d0 Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 6 Aug 2023 15:38:12 -0700 Subject: [PATCH 024/819] doc: fix a header level, add to tap-hold-release-keys --- docs/config.adoc | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/config.adoc b/docs/config.adoc index 60e98cddc..6674c1b10 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -366,7 +366,7 @@ overhead. If you do not care for the logging, you can choose to disable it. ---- [[delegate-to-first-layer]] -== delegate-to-first-layer +=== delegate-to-first-layer <> @@ -1376,9 +1376,15 @@ non `-timeout` variants will activate the hold action in both cases. - `tap-hold-release-keys` -This variant takes a 5th parameter which is a list of keys that trigger an -early tap when they are pressed while the `tap-hold-release-keys` action is -waiting. +This variant takes a 5th parameter which is a list of keys +that trigger an early tap +when they are pressed while the `tap-hold-release-keys` action is waiting. + +The keys in the 5th parameter correspond to the physical input keys, +or in other words the key that corresponds to `defsrc`. +This is in contrast to the `fork` and `switch` actions +which operates on outputted keys, or in other words the outputs +that are in `deflayer`, `defalias`, etc. for the corresponding `defsrc` key. .Example: [source] From f013e5aef2fb3c3f9d97b108fcc180dd6baee316 Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 6 Aug 2023 15:41:16 -0700 Subject: [PATCH 025/819] doc: tap-hold formatting+more additions --- docs/config.adoc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/config.adoc b/docs/config.adoc index 6674c1b10..937700333 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -1366,8 +1366,8 @@ but you should be wary of activating the hold action unintentionally. There are further additional variants of `tap-hold-press` and `tap-hold-release`: -. `tap-hold-press-timeout` -. `tap-hold-release-timeout` +- `tap-hold-press-timeout` +- `tap-hold-release-timeout` These variants take a 5th parameter, in addition to the same 4 as the other variants. The 5th parameter is another action, which will activate if the hold @@ -1379,6 +1379,7 @@ non `-timeout` variants will activate the hold action in both cases. This variant takes a 5th parameter which is a list of keys that trigger an early tap when they are pressed while the `tap-hold-release-keys` action is waiting. +Otherwise this behaves as `tap-hold-release`. The keys in the 5th parameter correspond to the physical input keys, or in other words the key that corresponds to `defsrc`. From bfed3f17d88de30abe81191028bbf4cb10f65cbd Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 6 Aug 2023 20:22:14 -0700 Subject: [PATCH 026/819] doc: update macro err msg, fakekey-delay doc (#524) --- docs/config.adoc | 37 ++++++++++--------------------------- parser/src/cfg/mod.rs | 3 ++- 2 files changed, 12 insertions(+), 28 deletions(-) diff --git a/docs/config.adoc b/docs/config.adoc index 937700333..70002de01 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -1798,33 +1798,6 @@ The aforementioned `++` can be one of three values: ) ---- -If you find that an application isn't registering keypresses correctly with -`+multi+` because the sequence activates too quickly, you can try using fake -key actions alongside the delay actions below. - -* `+on-press-fakekey-delay+` -* `+on-release-fakekey-delay+` - -Do note that processing a fakekey-delay and even a sequence of delays will -delay any other inputs from being processed until the fakekey-delays are all -complete, so use with care. - -NOTE: You will likely want to use `+macro+` instead of fake keys with delays now -that `+macro+` supports more actions. - -[source] ----- -(defalias - stm (multi ;; Shift -> middle mouse with a delay - (on-press-fakekey lsft press) - (on-press-fakekey-delay 200) - (on-press-fakekey mmid press) - (on-release-fakekey mmid release) - (on-release-fakekey-delay 200) - (on-release-fakekey lsft release) - ) -) ----- For more context, you can read the https://github.com/jtroo/kanata/issues/80[issue that sparked the creation of fake keys]. @@ -1834,6 +1807,16 @@ of an active `+tap-dance-eager+`. If a `macro` action is assigned to a fake key, this won't interrupt a tap dance. However, most other action types, notably a "normal" key action like `+rsft+` will still interrupt a tap dance. +==== Aside: broken delay action + +The implementation of the following actions are broken in many use cases: +`on-press-fakekey-delay`, `on-release-fakekey-delay`. + +Because these actions exist and might otherwise be discovered anyway, +it seems better to warn about it here in the documentation. +You should use the `macro` action instead. +The `macro` action can operate on fake keys. + [[sequences]] === Sequences <> diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index d38e28631..8f57bbc19 100644 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -1501,7 +1501,7 @@ fn parse_multi(ac_params: &[SExpr], s: &ParsedState) -> Result<&'static KanataAc Ok(s.a.sref(Action::MultipleActions(s.a.sref(s.a.sref_vec(actions))))) } -const MACRO_ERR: &str = "Action macro only accepts delays, keys, chords, and chorded sub-macros"; +const MACRO_ERR: &str = "Action macro only accepts delays, keys, chords, chorded sub-macros, and a subset of special actions.\nThe macro section of the documentation describes this in more detail:\nhttps://github.com/jtroo/kanata/blob/main/docs/config.adoc#macro"; enum RepeatMacro { Yes, No, @@ -2296,6 +2296,7 @@ fn parse_delay( s: &ParsedState, ) -> Result<&'static KanataAction> { const ERR_MSG: &str = "fakekey-delay expects a single number (ms, 0-65535)"; + log::warn!("The configuration contains a fakekey-delay action. This is broken for many use cases. It is recommended to use macro instead."); let delay = ac_params[0] .atom(s.vars()) .map(str::parse::) From b8c36b3742e61f0618e76886ddbca608b99eef10 Mon Sep 17 00:00:00 2001 From: Rappie Date: Wed, 9 Aug 2023 00:05:18 +0200 Subject: [PATCH 027/819] doc: add info about comp for other US keyboards (#526) --- docs/config.adoc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/config.adoc b/docs/config.adoc index 70002de01..d125a8b26 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -82,6 +82,9 @@ approximately 60% keyboard layout: ) ---- +Note that some keyboards have a Compose/Menu key instead of a right Meta key. +In this case you can use `comp` instead of `rmet`. + For non-US keyboards, see <>. [[deflayer]] From 27ca0e6487c442366a5d1658e4d7cf8e3620f339 Mon Sep 17 00:00:00 2001 From: jtroo Date: Tue, 8 Aug 2023 22:26:19 -0700 Subject: [PATCH 028/819] fix!: map unicode yen to KEY_YEN (#527) This commit also allows "yen" to be overridden if desired. --- docs/config.adoc | 9 +++++---- parser/src/keys/mod.rs | 6 +++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/config.adoc b/docs/config.adoc index d125a8b26..d59440802 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -63,10 +63,11 @@ amount of whitespace (spaces, tabs, newlines) are not relevant. You may use spaces, tabs, or newlines however you like to visually format `defsrc` to your liking. -The the primary source of all key names is the -https://github.com/jtroo/kanata/blob/main/parser/src/keys/mod.rs[str_to_oscode] -function in the source code. Please feel free to file an issue if you're unable -to find the key you're looking for. +The the primary source of all key names are the +`str_to_oscode` and `default_mappings` functions in +https://github.com/jtroo/kanata/blob/main/parser/src/keys/mod.rs[the source]. +Please feel welcome to file an issue +if you're unable to find the key you're looking for. An example `defsrc` containing the US QWERTY keyboard keys as an approximately 60% keyboard layout: diff --git a/parser/src/keys/mod.rs b/parser/src/keys/mod.rs index 4cb36a6c6..0b49e541f 100644 --- a/parser/src/keys/mod.rs +++ b/parser/src/keys/mod.rs @@ -66,6 +66,10 @@ fn add_default_str_osc_mappings(mapping: &mut HashMap) { (",", OsCode::KEY_COMMA), (".", OsCode::KEY_DOT), ("\\", OsCode::KEY_BACKSLASH), + // Mapped as backslash because in some locales/fonts, yen=backslash + ("yen", OsCode::KEY_BACKSLASH), + // Unicode yen is probably the yen key, so map this to a separate oscode by default. + ("¥", OsCode::KEY_YEN), ]; for dm in default_mappings { mapping.entry(dm.0.into()).or_insert(dm.1); @@ -109,7 +113,7 @@ pub fn str_to_oscode(s: &str) -> Option { "p" => OsCode::KEY_P, "lbrc" => OsCode::KEY_LEFTBRACE, "rbrc" => OsCode::KEY_RIGHTBRACE, - "bksl" | "yen" | "¥" => OsCode::KEY_BACKSLASH, + "bksl" => OsCode::KEY_BACKSLASH, "caps" => OsCode::KEY_CAPSLOCK, "a" => OsCode::KEY_A, "s" => OsCode::KEY_S, From 3ccbcd1a2c8e482d4b2b1df1ce391934d43043d4 Mon Sep 17 00:00:00 2001 From: jtroo Date: Wed, 9 Aug 2023 10:51:30 -0700 Subject: [PATCH 029/819] fix: precision of on-idle ticks + doc changes (#528) This commit fixes a bug where even if there are multiple ticks of the keyberon state in the processing loop, the idle time ticks would only increment by one. In addition to the bug fix, some documentation improvements are included. --- docs/config.adoc | 16 ++++++++++++---- src/kanata/mod.rs | 37 +++++++++++++++++++++++++------------ 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/docs/config.adoc b/docs/config.adoc index d59440802..025c3a4c1 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -1747,8 +1747,8 @@ physical key presses and can only be activated via these actions: * `+(on-release-fakekey )+`: Activate a fake key action when releasing the key mapped to this action. * `+(on-idle-fakekey )+`: - Activate a fake key action - when the keyboard is idle for `idle time` milliseconds + Activate a fake key action when kanata has been idle + for at least `idle time` milliseconds. A fake key can be defined in a `+deffakekeys+` configuration entry. Configuring this entry is similar to `+defalias+`, but you cannot make use of aliases @@ -1762,6 +1762,14 @@ The aforementioned `++` can be one of three values: * `+release+`: Release the fake key. If it's not already pressed, this does nothing. * `+tap+`: Press and release the fake key. If it's already pressed, this only releases it. +Expanding on the `on-idle-fakekey` action some more, +the wording that "kanata" has been idle is important. +Even if the keyboard is idle, kanata may not yet be idle. +For example, if a long-running macro is playing, +or kanata is waiting for the timeout of actions such as `caps-word` or `tap-dance`, +kanata is not yet idle, and the tick count for the `` parameter +will not yet be counting even if you no longer have any keyboard keys pressed. + .Example: [source] ---- @@ -2042,14 +2050,14 @@ One case is a triple of: - keys check - action: to activate if keys check succeeds -- fallthrough|break: stop evaluating cases +- `fallthrough|break`: choose to continue vs. stop evaluating cases The default use of keys check behaves similarly to fork. For example, the keys check `(a b c)` will activate the corresponding action if any of a, b, or c are currently pressed. -The keys check also accepts the boolean operators and|or to allow more +The keys check also accepts the boolean operators `and|or` to allow more complex use cases. The order of cases matters. diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index 8acd95ad0..6e32a21b1 100644 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -419,7 +419,8 @@ impl Kanata { } /// Advance keyberon layout state and send events based on changes to its state. - fn handle_time_ticks(&mut self, tx: &Option>) -> Result<()> { + /// Returns the number of ticks that elapsed. + fn handle_time_ticks(&mut self, tx: &Option>) -> Result { const NS_IN_MS: u128 = 1_000_000; let now = time::Instant::now(); let ns_elapsed = now.duration_since(self.last_tick).as_nanos(); @@ -464,7 +465,12 @@ impl Kanata { self.check_handle_layer_change(tx); } - Ok(()) + #[cfg(feature = "perf_logging")] + log::info!("ms elapsed: {ms_elapsed}"); + // Note regarding `as` casting. It doesn't really matter if the result would truncate and + // end up being wrong. Prefer to do the cheaper operation, as compared to doing the min of + // u16::MAX and ms_elapsed. + Ok(ms_elapsed as u16) } fn handle_scrolling(&mut self) -> Result<()> { @@ -1143,6 +1149,7 @@ impl Kanata { | CustomAction::DelayOnRelease(_) | CustomAction::CancelMacroOnRelease => {} CustomAction::FakeKeyOnIdle(fkd) => { + self.ticks_since_idle = 0; self.waiting_for_idle.insert(*fkd); } } @@ -1446,6 +1453,7 @@ impl Kanata { std::thread::sleep(time::Duration::from_millis(1)); } } + let mut ms_elapsed = 0; info!("Starting kanata proper"); let err = loop { @@ -1459,7 +1467,9 @@ impl Kanata { if !is_idle { k.ticks_since_idle = 0; } else if is_idle && counting_idle_ticks { - k.ticks_since_idle = k.ticks_since_idle.saturating_add(1); + k.ticks_since_idle = k.ticks_since_idle.saturating_add(ms_elapsed); + #[cfg(feature = "perf_logging")] + log::info!("ticks since idle: {}", k.ticks_since_idle); } is_idle && !counting_idle_ticks }; @@ -1487,9 +1497,10 @@ impl Kanata { #[cfg(feature = "perf_logging")] let start = std::time::Instant::now(); - if let Err(e) = k.handle_time_ticks(&tx) { - break e; - } + match k.handle_time_ticks(&tx) { + Ok(ms) => ms_elapsed = ms, + Err(e) => break e, + }; #[cfg(feature = "perf_logging")] log::info!( @@ -1521,9 +1532,10 @@ impl Kanata { #[cfg(feature = "perf_logging")] let start = std::time::Instant::now(); - if let Err(e) = k.handle_time_ticks(&tx) { - break e; - } + match k.handle_time_ticks(&tx) { + Ok(ms) => ms_elapsed = ms, + Err(e) => break e, + }; #[cfg(feature = "perf_logging")] log::info!( @@ -1535,9 +1547,10 @@ impl Kanata { #[cfg(feature = "perf_logging")] let start = std::time::Instant::now(); - if let Err(e) = k.handle_time_ticks(&tx) { - break e; - } + match k.handle_time_ticks(&tx) { + Ok(ms) => ms_elapsed = ms, + Err(e) => break e, + }; #[cfg(feature = "perf_logging")] log::info!( From f7e05d9eaf77efe76996b0e77d075eb2a4f25e8c Mon Sep 17 00:00:00 2001 From: jtroo Date: Fri, 11 Aug 2023 22:35:33 -0700 Subject: [PATCH 030/819] feat: better handling of Windows LLHOOK stuck keys (#531) There are two changes in this commit. 1) Always live reload after ~1s. After 1 second if live reload is still not done, there might be a key in a stuck state. One known instance where this happens is Win+L to lock the screen when on Windows and using the LLHOOK mechanism. The release of Win and L keys will not be caught by the kanata process when on the lock screen. However, the OS knows that these keys have released - only the kanata state is wrong. And since kanata has a key in a stuck state, without this 1s fallback, live reload would never activate. Having this fallback allows live reload to happen which resets the kanata states. 2) Clear normal states (not fake key) after blocking for a while This code only runs in the LLHOOK Windows version. The reason for this existing is the same as in 1). The main motivator is the Win+L example. The thought is that after locking their screen, the user will probably go away for a while. The software can detect that the zero user inputs were input for extended period of time and assume that keys got stuck. --- parser/src/cfg/mod.rs | 5 ++- src/kanata/mod.rs | 75 +++++++++++++++++++++++++++++++++++++------ 2 files changed, 70 insertions(+), 10 deletions(-) diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index 8f57bbc19..b86855fa2 100644 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -2274,9 +2274,12 @@ fn parse_fake_key_op_coord_action( Ok((Coord { x, y }, action)) } +pub const NORMAL_KEY_ROW: u8 = 0; +pub const FAKE_KEY_ROW: u8 = 1; + fn get_fake_key_coords>(y: T) -> (u8, u16) { let y: usize = y.into(); - (1, y as u16) + (FAKE_KEY_ROW, y as u16) } fn parse_fake_key_delay(ac_params: &[SExpr], s: &ParsedState) -> Result<&'static KanataAction> { diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index 6e32a21b1..16aa178cf 100644 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -436,13 +436,6 @@ impl Kanata { self.tick_dynamic_macro_state()?; self.tick_idle_timeout(); - if self.live_reload_requested && self.prev_keys.is_empty() && self.cur_keys.is_empty() { - self.live_reload_requested = false; - if let Err(e) = self.do_live_reload() { - log::error!("live reload failed {e}"); - } - } - self.prev_keys.clear(); self.prev_keys.append(&mut self.cur_keys); } @@ -465,6 +458,25 @@ impl Kanata { self.check_handle_layer_change(tx); } + if self.live_reload_requested + && ((self.prev_keys.is_empty() && self.cur_keys.is_empty()) + || self.ticks_since_idle > 1000) + { + // Note regarding the ticks_since_idle check above: + // After 1 second if live reload is still not done, there might be a key in a stuck + // state. One known instance where this happens is Win+L to lock the screen in + // Windows with the LLHOOK mechanism. The release of Win and L keys will not be + // caught by the kanata process when on the lock screen. However, the OS knows that + // these keys have released - only the kanata state is wrong. And since kanata has + // a key in a stuck state, without this 1s fallback, live reload would never + // activate. Having this fallback allows live reload to happen which resets the + // kanata states. + self.live_reload_requested = false; + if let Err(e) = self.do_live_reload() { + log::error!("live reload failed {e}"); + } + } + #[cfg(feature = "perf_logging")] log::info!("ms elapsed: {ms_elapsed}"); // Note regarding `as` casting. It doesn't really matter if the result would truncate and @@ -1463,7 +1475,8 @@ impl Kanata { // Note: checking waiting_for_idle can not be part of the computation for // is_idle() since incrementing ticks_since_idle is dependent on the return // value of is_idle(). - let counting_idle_ticks = !k.waiting_for_idle.is_empty(); + let counting_idle_ticks = + !k.waiting_for_idle.is_empty() || k.live_reload_requested; if !is_idle { k.ticks_since_idle = 0; } else if is_idle && counting_idle_ticks { @@ -1478,9 +1491,53 @@ impl Kanata { match rx.recv() { Ok(kev) => { let mut k = kanata.lock(); - k.last_tick = time::Instant::now() + let now = time::Instant::now() .checked_sub(time::Duration::from_millis(1)) .expect("subtract 1ms from current time"); + #[cfg(all( + not(feature = "interception_driver"), + target_os = "windows" + ))] + { + // If kanata has been blocking for long enough, clear all states. + // This won't trigger if there are macros running, or if a key is + // held down for a long time and is sending OS repeats. The reason + // for this code is in case like Win+L which locks the Windows + // desktop. When this happens, the Win key and L key will be stuck + // as pressed in the kanata state because LLHOOK kanata cannot read + // keys in the lock screen or administrator applications. So this + // is heuristic to detect such an issue and clear states assuming + // that's what happened. + // + // Only states in the normal key row are cleared, since those are + // the states that might be stuck. A real use case might be to have + // a fake key pressed for a long period of time, so make sure those + // are not cleared. + if (now - k.last_tick) > time::Duration::from_secs(60) { + log::debug!( + "clearing keyberon normal key states due to blocking for a while" + ); + k.layout.bm().states.retain(|s| { + !matches!( + s, + State::NormalKey { + coord: (NORMAL_KEY_ROW, _), + .. + } | State::LayerModifier { + coord: (NORMAL_KEY_ROW, _), + .. + } | State::Custom { + coord: (NORMAL_KEY_ROW, _), + .. + } | State::RepeatingSequence { + coord: (NORMAL_KEY_ROW, _), + .. + } + ) + }); + } + } + k.last_tick = now; #[cfg(feature = "perf_logging")] let start = std::time::Instant::now(); From c6e77c0430be852566e85e198e8c47bec48ca664 Mon Sep 17 00:00:00 2001 From: jtroo Date: Sat, 12 Aug 2023 00:16:11 -0700 Subject: [PATCH 031/819] fix: do not idle pending lrld with pressed keys --- src/kanata/mod.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index 16aa178cf..13d1f99b8 100644 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -1630,7 +1630,8 @@ impl Kanata { } pub fn is_idle(&self) -> bool { - let pressed_keys_means_not_idle = !self.waiting_for_idle.is_empty(); + let pressed_keys_means_not_idle = + !self.waiting_for_idle.is_empty() || self.live_reload_requested; self.layout.b().queue.is_empty() && self.layout.b().waiting.is_none() && self.layout.b().last_press_tracker.tap_hold_timeout == 0 From 91a084ba04d705a43358bde3f7fb9cd627fc9f87 Mon Sep 17 00:00:00 2001 From: jtroo Date: Mon, 14 Aug 2023 22:17:35 -0700 Subject: [PATCH 032/819] fix(wintercept): mbck and mfwd up/down were reversed (#533) --- src/oskbd/windows/interception.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/oskbd/windows/interception.rs b/src/oskbd/windows/interception.rs index b3fe124f8..a13ddca39 100644 --- a/src/oskbd/windows/interception.rs +++ b/src/oskbd/windows/interception.rs @@ -41,10 +41,10 @@ impl InputEvent { (Btn::Right, false) => MouseState::RIGHT_BUTTON_DOWN, (Btn::Mid, true) => MouseState::MIDDLE_BUTTON_UP, (Btn::Mid, false) => MouseState::MIDDLE_BUTTON_DOWN, - (Btn::Backward, true) => MouseState::BUTTON_4_DOWN, - (Btn::Backward, false) => MouseState::BUTTON_4_UP, - (Btn::Forward, true) => MouseState::BUTTON_5_DOWN, - (Btn::Forward, false) => MouseState::BUTTON_5_UP, + (Btn::Backward, true) => MouseState::BUTTON_4_UP, + (Btn::Backward, false) => MouseState::BUTTON_4_DOWN, + (Btn::Forward, true) => MouseState::BUTTON_5_UP, + (Btn::Forward, false) => MouseState::BUTTON_5_DOWN, }, flags: MouseFlags::empty(), rolling: 0, From 0cf5829e4df4d659e0e37178742599b3a7ab41ff Mon Sep 17 00:00:00 2001 From: jtroo Date: Wed, 16 Aug 2023 22:30:23 -0700 Subject: [PATCH 033/819] fix: specify rpt-any behaviour better (#534) --- keyberon/src/layout.rs | 59 ++++++++++++++++++++++++++++++------------ 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/keyberon/src/layout.rs b/keyberon/src/layout.rs index ddd02f206..7512ab04f 100644 --- a/keyberon/src/layout.rs +++ b/keyberon/src/layout.rs @@ -82,7 +82,7 @@ where pub last_press_tracker: LastPressTracker, pub active_sequences: ArrayDeque<[SequenceState<'a, T>; 4], arraydeque::behavior::Wrapping>, pub action_queue: ActionQueue<'a, T>, - pub prev_action: Option<&'a Action<'a, T>>, + pub rpt_action: Option<&'a Action<'a, T>>, } /// An event on the key matrix. @@ -841,7 +841,7 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt last_press_tracker: Default::default(), active_sequences: ArrayDeque::new(), action_queue: ArrayDeque::new(), - prev_action: None, + rpt_action: None, } } /// Iterates on the key codes of the current state. @@ -1167,18 +1167,30 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt self.last_press_tracker.tap_hold_timeout = 0; } use Action::*; - if !matches!(action, Repeat | Layer(..) | DefaultLayer(..)) { - self.prev_action = Some(action); - } match action { NoOp | Trans => { if !is_oneshot { self.oneshot .handle_press(OneShotHandlePressKey::Other(coord)); } + self.rpt_action = Some(action); } Repeat => { - if let Some(ac) = self.prev_action { + // Notes around repeat: + // + // Though this action seems conceptually simple, in reality there are a lot of + // decisions to be made around how exactly actions repeat. For example: in a + // tap-dance action, would one expect the tap-dance to be repeated or the inner + // action that was most activated within the tap-dance? + // + // Currently the answer to these questions is: what is easy/possible to do? E.g. + // fork and switch are inconsistent with each other even though the actions are + // conceptually very similar. This is because switch can potentially activate + // multiple actions (but not always), so uses the action queue, while fork does + // not. As another example, tap-dance and tap-hold will repeat the inner action and + // not the outer (tap-dance|hold) but multi will repeat the entire outer multi + // action. + if let Some(ac) = self.rpt_action { self.do_action(ac, coord, delay, is_oneshot); } } @@ -1218,6 +1230,9 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt &OneShot(oneshot) => { self.last_press_tracker.coord = coord; let custom = self.do_action(oneshot.action, coord, delay, true); + // Note - set rpt_action after doing the inner oneshot action. This means that the + // whole oneshot will be repeated by rpt-any rather than only the inner action. + self.rpt_action = Some(action); self.oneshot .handle_press(OneShotHandlePressKey::OneShotKey(coord)); self.oneshot.timeout = oneshot.timeout; @@ -1293,6 +1308,7 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt self.oneshot .handle_press(OneShotHandlePressKey::Other(coord)); } + self.rpt_action = Some(action); } &MultipleKeyCodes(v) => { self.last_press_tracker.coord = coord; @@ -1303,6 +1319,7 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt self.oneshot .handle_press(OneShotHandlePressKey::Other(coord)); } + self.rpt_action = Some(action); } &MultipleActions(v) => { self.last_press_tracker.coord = coord; @@ -1310,15 +1327,9 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt for action in *v { custom.update(self.do_action(action, coord, delay, is_oneshot)); } - // Multi is probably the one action where it is desirable to repeat the top-level - // action instead of the final action. The final action meaning a hold action in - // `tap-hold` or the 3rd tap of a `tap-dance`. - // - // Set the prev_action again since it was probably overwritten by the - // `do_action` recursion. - if !matches!(action, Action::Repeat) { - self.prev_action = Some(action); - } + // Save the whole multi action instead of the final action in multi so that Repeat + // repeats all of the actions in this multi. + self.rpt_action = Some(action); return custom; } Sequence { events } => { @@ -1332,6 +1343,7 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt self.oneshot .handle_press(OneShotHandlePressKey::Other(coord)); } + self.rpt_action = Some(action); } RepeatableSequence { events } => { self.active_sequences.push_back(SequenceState { @@ -1348,6 +1360,7 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt self.oneshot .handle_press(OneShotHandlePressKey::Other(coord)); } + self.rpt_action = Some(action); } CancelSequences => { // Clear any and all running sequences then clean up any leftover FakeKey events @@ -1361,6 +1374,7 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt self.oneshot .handle_press(OneShotHandlePressKey::Other(coord)); } + self.rpt_action = Some(action); } &Layer(value) => { self.last_press_tracker.coord = coord; @@ -1369,6 +1383,9 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt self.oneshot .handle_press(OneShotHandlePressKey::Other(coord)); } + // Notably missing in Layer and below in DefaultLayer is setting rpt_action. This + // is so that if the Repeat key is on a different layer than the base, it can still + // be used to repeat the previous non-layer-changing action. } DefaultLayer(value) => { self.last_press_tracker.coord = coord; @@ -1384,6 +1401,7 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt self.oneshot .handle_press(OneShotHandlePressKey::Other(coord)); } + self.rpt_action = Some(action); if self.states.push(State::Custom { value, coord }).is_ok() { return CustomEvent::Press(value); } @@ -1394,9 +1412,10 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt self.oneshot .handle_press(OneShotHandlePressKey::Other(coord)); } + self.rpt_action = Some(action); } Fork(fcfg) => { - return match self.states.iter().any(|s| match s { + let ret = match self.states.iter().any(|s| match s { NormalKey { keycode, .. } | FakeKey { keycode } => { fcfg.right_triggers.contains(keycode) } @@ -1405,6 +1424,9 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt false => self.do_action(&fcfg.left, coord, delay, false), true => self.do_action(&fcfg.right, coord, delay, false), }; + // Repeat the fork rather than the terminal action. + self.rpt_action = Some(action); + return ret; } Switch(sw) => { let kcs = self.states.iter().filter_map(State::keycode); @@ -1412,6 +1434,11 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt for ac in sw.actions(kcs.clone()) { action_queue.push_back(Some((coord, ac))); } + // Switch is not properly repeatable. This has to use the action queue for the + // purpose of proper Custom action handling, because a single switch action can + // activate multiple inner actions. But because of the use of the action queue, + // switch has no way to set `rpt_action` after the queue is depleted. I suppose + // that can be fixable, but for now will keep it as-is. } } CustomEvent::NoEvent From 8cd2c9e22026c0a3cf5bd84eb18c0f1ddbb9d8fe Mon Sep 17 00:00:00 2001 From: jtroo Date: Sat, 19 Aug 2023 12:38:27 -0700 Subject: [PATCH 034/819] doc: update platform-known-issues.adoc --- docs/platform-known-issues.adoc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/platform-known-issues.adoc b/docs/platform-known-issues.adoc index a6f7724e2..354bbe1d5 100644 --- a/docs/platform-known-issues.adoc +++ b/docs/platform-known-issues.adoc @@ -36,7 +36,7 @@ which map to the binaries * AltGr can misbehave ** https://github.com/jtroo/kanata/blob/main/docs/config.adoc#windows-only-windows-altgr -== Windows Interception +== Windows 10+11 Interception * Sleeping your system or unplugging/replugging devices enough times causes inputs to stop working @@ -44,6 +44,7 @@ which map to the binaries * Some less-frequently used keys are not supported or handled correctly ** https://github.com/jtroo/kanata/issues/164 ** https://github.com/jtroo/kanata/issues/425 +** https://github.com/jtroo/kanata/issues/532 == Linux From c6fa1eb9ed74b84d238c5a2156bd0ddc2db96e07 Mon Sep 17 00:00:00 2001 From: jtroo Date: Sat, 19 Aug 2023 21:06:03 -0700 Subject: [PATCH 035/819] doc: update platform-known-issues.adoc again Update with issue #127 as an interception issue. --- docs/platform-known-issues.adoc | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/platform-known-issues.adoc b/docs/platform-known-issues.adoc index 354bbe1d5..de8de31cc 100644 --- a/docs/platform-known-issues.adoc +++ b/docs/platform-known-issues.adoc @@ -42,6 +42,7 @@ which map to the binaries inputs to stop working ** https://github.com/oblitum/Interception/issues/25 * Some less-frequently used keys are not supported or handled correctly +** https://github.com/jtroo/kanata/issues/127 ** https://github.com/jtroo/kanata/issues/164 ** https://github.com/jtroo/kanata/issues/425 ** https://github.com/jtroo/kanata/issues/532 From a0d8207b22c026b4af62078e5130b4fe1d35a39c Mon Sep 17 00:00:00 2001 From: jtroo Date: Sat, 19 Aug 2023 23:36:52 -0700 Subject: [PATCH 036/819] feat: add toggle action for fake keys (#538) --- cfg_samples/kanata.kbd | 1 + docs/config.adoc | 1 + parser/src/cfg/mod.rs | 3 +- parser/src/custom_action.rs | 1 + src/kanata/mod.rs | 61 ++++++++++++++++++++++--------------- 5 files changed, 42 insertions(+), 25 deletions(-) diff --git a/cfg_samples/kanata.kbd b/cfg_samples/kanata.kbd index 818f5d848..47b0dd892 100644 --- a/cfg_samples/kanata.kbd +++ b/cfg_samples/kanata.kbd @@ -801,6 +801,7 @@ If you need help, you are welcome to ask. fsp (on-release-fakekey sft press) fsr (on-release-fakekey sft release) fst (on-release-fakekey sft tap) + fsg (on-release-fakekey sft toggle) fmp (on-press-fakekey met press) fap (on-press-fakekey alt press) rma (multi diff --git a/docs/config.adoc b/docs/config.adoc index 025c3a4c1..cbdc4a8b6 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -1761,6 +1761,7 @@ The aforementioned `++` can be one of three values: triggers a release or tap. * `+release+`: Release the fake key. If it's not already pressed, this does nothing. * `+tap+`: Press and release the fake key. If it's already pressed, this only releases it. +* `+toggle+`: Press the fake key if not already pressed, otherwise release it. Expanding on the `on-idle-fakekey` action some more, the wording that "kanata" has been idle is important. diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index b86855fa2..9083273d9 100644 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -2238,7 +2238,7 @@ fn parse_fake_key_op_coord_action( s: &ParsedState, ) -> Result<(Coord, FakeKeyAction)> { const ERR_MSG: &str = - "on-(press|release)-fakekey expects two parameters: <(tap|press|release)>"; + "on-(press|release)-fakekey expects two parameters: <(tap|press|release|toggle)>"; if ac_params.len() != 2 { bail!("{ERR_MSG}"); } @@ -2261,6 +2261,7 @@ fn parse_fake_key_op_coord_action( "tap" => Some(FakeKeyAction::Tap), "press" => Some(FakeKeyAction::Press), "release" => Some(FakeKeyAction::Release), + "toggle" => Some(FakeKeyAction::Toggle), _ => None, }) .flatten() diff --git a/parser/src/custom_action.rs b/parser/src/custom_action.rs index 31ab3cda2..3d94049c2 100644 --- a/parser/src/custom_action.rs +++ b/parser/src/custom_action.rs @@ -82,6 +82,7 @@ pub enum FakeKeyAction { Press, Release, Tap, + Toggle, } /// An active waiting-for-idle state. diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index 13d1f99b8..1554214e0 100644 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -605,14 +605,7 @@ impl Kanata { // Process this and return false so that it is not retained. let layout = self.layout.bm(); let Coord { x, y } = wfd.coord; - match wfd.action { - FakeKeyAction::Press => layout.event(Event::Press(x, y)), - FakeKeyAction::Release => layout.event(Event::Release(x, y)), - FakeKeyAction::Tap => { - layout.event(Event::Press(x, y)); - layout.event(Event::Release(x, y)); - } - }; + handle_fakekey_action(wfd.action, layout, x, y); false } else { true @@ -991,14 +984,7 @@ impl Kanata { layout.default_layer, layout.layers[layout.default_layer][x as usize][y as usize] ); - match action { - FakeKeyAction::Press => layout.event(Event::Press(x, y)), - FakeKeyAction::Release => layout.event(Event::Release(x, y)), - FakeKeyAction::Tap => { - layout.event(Event::Press(x, y)); - layout.event(Event::Release(x, y)); - } - } + handle_fakekey_action(*action, layout, x, y); } CustomAction::Delay(delay) => { log::debug!("on-press: sleeping for {delay} ms"); @@ -1263,14 +1249,7 @@ impl Kanata { CustomAction::FakeKeyOnRelease { coord, action } => { let (x, y) = (coord.x, coord.y); log::debug!("fake key on release {action:?} {x:?},{y:?}"); - match action { - FakeKeyAction::Press => layout.event(Event::Press(x, y)), - FakeKeyAction::Release => layout.event(Event::Release(x, y)), - FakeKeyAction::Tap => { - layout.event(Event::Press(x, y)); - layout.event(Event::Release(x, y)); - } - } + handle_fakekey_action(*action, layout, x, y); pbtn } CustomAction::CancelMacroOnRelease => { @@ -1785,3 +1764,37 @@ fn cancel_sequence(state: &SequenceState, kbd_out: &mut KbdOut) -> Result<()> { } Ok(()) } + +fn handle_fakekey_action<'a, const C: usize, const R: usize, const L: usize, T>( + action: FakeKeyAction, + layout: &mut Layout<'a, C, R, L, T>, + x: u8, + y: u16, +) where + T: 'a + std::fmt::Debug + Copy, +{ + match action { + FakeKeyAction::Press => layout.event(Event::Press(x, y)), + FakeKeyAction::Release => layout.event(Event::Release(x, y)), + FakeKeyAction::Tap => { + layout.event(Event::Press(x, y)); + layout.event(Event::Release(x, y)); + } + FakeKeyAction::Toggle => { + match states_has_coord(&layout.states, x, y) { + true => layout.event(Event::Release(x, y)), + false => layout.event(Event::Press(x, y)), + }; + } + }; +} + +fn states_has_coord(states: &[State], x: u8, y: u16) -> bool { + states.iter().any(|s| match s { + State::NormalKey { coord, .. } + | State::LayerModifier { coord, .. } + | State::Custom { coord, .. } + | State::RepeatingSequence { coord, .. } => *coord == (x, y), + _ => false, + }) +} From d609049e685b5ef03afc758d8a949f7a47ea3c85 Mon Sep 17 00:00:00 2001 From: jtroo Date: Sat, 19 Aug 2023 23:50:00 -0700 Subject: [PATCH 037/819] github: create pull_request_template.md Experimental change for PR template --- .github/pull_request_template.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..547cdf986 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,15 @@ +## Describe your changes. Use imperative present tense. + +## Checklist + +- Add documentation to docs/config.adoc- + - [ ] Yes + - [ ] N/A +- Add example to cfg_samples/kanata.kbd + - [ ] Yes + - [ ] N/A +- Update error messages + - [ ] Yes + - [ ] N/A +- Added tests, or did manual testing + - [ ] Yes From a87cd0f299817748fd73a45e2f1e990063d00086 Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 20 Aug 2023 00:26:31 -0700 Subject: [PATCH 038/819] github: update pull_request_template.md Fix extra dash, combine Yes and N/A --- .github/pull_request_template.md | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 547cdf986..223435050 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -2,14 +2,11 @@ ## Checklist -- Add documentation to docs/config.adoc- - - [ ] Yes - - [ ] N/A +- Add documentation to docs/config.adoc + - [ ] Yes or N/A - Add example to cfg_samples/kanata.kbd - - [ ] Yes - - [ ] N/A + - [ ] Yes or N/A - Update error messages - - [ ] Yes - - [ ] N/A + - [ ] Yes or N/A - Added tests, or did manual testing - [ ] Yes From da05276b2e280a81990901592d4d776df27e4e03 Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 20 Aug 2023 00:31:07 -0700 Subject: [PATCH 039/819] ver: 1.5.0-prerelease-1 + cargo update (#539) Update Kanata+subcrate versions and update cargo dependencies. Pin serde-derive to version 1.0.171 due to later versions including a binary blob. Pin is-terminal due to later versions causing another transitive dependency to require two incompatible versions. --- Cargo.lock | 417 +++++++++++++++++--------------------------- Cargo.toml | 11 +- keyberon/Cargo.toml | 2 +- parser/Cargo.toml | 4 +- src/oskbd/linux.rs | 4 +- 5 files changed, 175 insertions(+), 263 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e22ef2b84..3c5b38bfe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" +checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" dependencies = [ "gimli", ] @@ -17,37 +17,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" -[[package]] -name = "ahash" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" -dependencies = [ - "getrandom", - "once_cell", - "version_check", -] - -[[package]] -name = "aho-corasick" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" -dependencies = [ - "memchr", -] - [[package]] name = "anstyle" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" +checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" [[package]] name = "anyhow" -version = "1.0.71" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "arraydeque" @@ -72,9 +52,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" -version = "0.3.67" +version = "0.3.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" +checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" dependencies = [ "addr2line", "cc", @@ -120,9 +100,12 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "cc" -version = "1.0.79" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "305fe645edc1442a0fa8b6726ba61d422798d37a52e12eaecf4b022ebbb88f01" +dependencies = [ + "libc", +] [[package]] name = "cfg-if" @@ -132,9 +115,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.3.0" +version = "4.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93aae7a4192245f70fe75dd9157fc7b4a5bf53e88d30bd4396f7d8f9284d5acc" +checksum = "03aef18ddf7d879c15ce20f04826ef8418101c7e528014c3eeea13321047dca3" dependencies = [ "clap_builder", "clap_derive", @@ -143,21 +126,20 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.3.0" +version = "4.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f423e341edefb78c9caba2d9c7f7687d0e72e89df3ce3394554754393ac3990" +checksum = "f8ce6fffb678c9b80a70b6b6de0aad31df727623a70fd9a842c30cd573e2fa98" dependencies = [ "anstyle", - "bitflags", "clap_lex", "strsim", ] [[package]] name = "clap_derive" -version = "4.3.0" +version = "4.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "191d9573962933b4027f932c600cd252ce27a8ad5979418fe78e43c07996f27b" +checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050" dependencies = [ "heck", "proc-macro2", @@ -173,9 +155,15 @@ checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" [[package]] name = "critical-section" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6548a0ad5d2549e111e1f6a11a6c2e2d00ce6a3dafe22948d67c2b443f775e52" +checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216" + +[[package]] +name = "deranged" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" [[package]] name = "dirs" @@ -195,7 +183,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -210,15 +198,21 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "errno" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" dependencies = [ "errno-dragonfly", "libc", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -252,9 +246,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "getrandom" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", "libc", @@ -263,9 +257,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.27.2" +version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4" +checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" [[package]] name = "hash32" @@ -278,12 +272,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.12.3" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -dependencies = [ - "ahash", -] +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" [[package]] name = "heapless" @@ -306,25 +297,25 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" [[package]] name = "indexmap" -version = "1.9.3" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" dependencies = [ - "autocfg", + "equivalent", "hashbrown", ] [[package]] name = "inotify" -version = "0.10.0" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abf888f9575c290197b2c948dc9e9ff10bd1a39ad1ea8585f734585fa6b9d3f9" +checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc" dependencies = [ "bitflags", "inotify-sys", @@ -348,13 +339,13 @@ checksum = "abe875cf6439eb5d98f0fdbcc6128fdef4870c47e0f898735105ff77685dfcd1" [[package]] name = "io-lifetimes" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -366,7 +357,7 @@ dependencies = [ "hermit-abi", "io-lifetimes", "rustix", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -377,13 +368,13 @@ checksum = "616cde7c720bb2bb5824a224687d8f77bfd38922027f01d825cd7453be5099fb" [[package]] name = "itoa" -version = "1.0.6" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "kanata" -version = "1.4.0" +version = "1.5.0-prerelease-1" dependencies = [ "anyhow", "clap", @@ -391,6 +382,7 @@ dependencies = [ "encode_unicode", "evdev", "inotify", + "is-terminal", "kanata-interception", "kanata-keyberon", "kanata-parser", @@ -405,6 +397,7 @@ dependencies = [ "rustc-hash", "sd-notify", "serde", + "serde_derive", "serde_json", "signal-hook", "simplelog", @@ -425,7 +418,7 @@ dependencies = [ [[package]] name = "kanata-keyberon" -version = "0.20.0" +version = "0.150.0" dependencies = [ "arraydeque", "heapless", @@ -444,7 +437,7 @@ dependencies = [ [[package]] name = "kanata-parser" -version = "0.20.0" +version = "0.150.0" dependencies = [ "anyhow", "kanata-keyberon", @@ -465,9 +458,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.144" +version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" [[package]] name = "linux-raw-sys" @@ -477,9 +470,9 @@ checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "lock_api" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" dependencies = [ "autocfg", "scopeguard", @@ -487,12 +480,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.17" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "memchr" @@ -520,9 +510,9 @@ dependencies = [ [[package]] name = "miette" -version = "5.9.0" +version = "5.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a236ff270093b0b67451bc50a509bd1bad302cb1d3c7d37d5efe931238581fa9" +checksum = "59bb584eaeeab6bd0226ccf3509a69d7936d148cf3d036ad350abe35e8c6856e" dependencies = [ "backtrace", "backtrace-ext", @@ -541,9 +531,9 @@ dependencies = [ [[package]] name = "miette-derive" -version = "5.9.0" +version = "5.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4901771e1d44ddb37964565c654a3223ba41a594d02b8da471cc4464912b5cfa" +checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c" dependencies = [ "proc-macro2", "quote", @@ -552,23 +542,23 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.6.2" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" dependencies = [ "adler", ] [[package]] name = "mio" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", "log", "wasi", - "windows-sys 0.45.0", + "windows-sys", ] [[package]] @@ -651,18 +641,18 @@ dependencies = [ [[package]] name = "object" -version = "0.30.3" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea86265d3d3dcb6a27fc51bd29a4bf387fae9d2986b823079d4986af253eb439" +checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.17.1" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "option-ext" @@ -688,15 +678,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.3.5", "smallvec", - "windows-sys 0.45.0", + "windows-targets", ] [[package]] @@ -717,18 +707,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.58" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa1fb82fc0c281dd9671101b66b771ebbe1eaf967b96ac8740dcba4b70005ca8" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.27" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -759,33 +749,25 @@ dependencies = [ ] [[package]] -name = "redox_users" -version = "0.4.3" +name = "redox_syscall" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" dependencies = [ - "getrandom", - "redox_syscall", - "thiserror", + "bitflags", ] [[package]] -name = "regex" -version = "1.8.2" +name = "redox_users" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1a59b5d8e97dee33696bf13c5ba8ab85341c002922fba050069326b9c498974" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", + "getrandom", + "redox_syscall 0.2.16", + "thiserror", ] -[[package]] -name = "regex-syntax" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" - [[package]] name = "rustc-demangle" version = "0.1.23" @@ -809,29 +791,29 @@ dependencies = [ [[package]] name = "rustix" -version = "0.37.19" +version = "0.37.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" +checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" dependencies = [ "bitflags", "errno", "io-lifetimes", "libc", "linux-raw-sys", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] name = "ryu" -version = "1.0.13" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" [[package]] name = "scopeguard" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sd-notify" @@ -841,24 +823,24 @@ checksum = "621e3680f3e07db4c9c2c3fb07c6223ab2fab2e54bd3c04c3ae037990f428c32" [[package]] name = "semver" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" +checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" [[package]] name = "serde" -version = "1.0.163" +version = "1.0.171" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" +checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.163" +version = "1.0.171" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" +checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682" dependencies = [ "proc-macro2", "quote", @@ -867,9 +849,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.96" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" dependencies = [ "itoa", "ryu", @@ -878,9 +860,9 @@ dependencies = [ [[package]] name = "signal-hook" -version = "0.3.15" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" dependencies = [ "libc", "signal-hook-registry", @@ -908,9 +890,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" [[package]] name = "smawk" @@ -975,9 +957,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.16" +version = "2.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6f671d4b5ffdb8eadec19c0ae67fe2639df8684bd7bc4b83d986b8db549cf01" +checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" dependencies = [ "proc-macro2", "quote", @@ -1022,18 +1004,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.40" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.40" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b" dependencies = [ "proc-macro2", "quote", @@ -1042,10 +1024,11 @@ dependencies = [ [[package]] name = "time" -version = "0.3.21" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc" +checksum = "a79d09ac6b08c1ab3906a2f7cc2e81a0e27c7ae89c63812df75e52bef0751e07" dependencies = [ + "deranged", "itoa", "libc", "num_threads", @@ -1062,24 +1045,24 @@ checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" [[package]] name = "time-macros" -version = "0.2.9" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" +checksum = "75c65469ed6b3a4809d987a41eb1dc918e9bc1d92211cbad7ae82931846f7451" dependencies = [ "time-core", ] [[package]] name = "toml_datetime" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" [[package]] name = "toml_edit" -version = "0.19.9" +version = "0.19.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d964908cec0d030b812013af25a0e57fddfadb1e066ecc6681d86253129d4f" +checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" dependencies = [ "indexmap", "toml_datetime", @@ -1088,19 +1071,15 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.8" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" [[package]] name = "unicode-linebreak" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137" -dependencies = [ - "hashbrown", - "regex", -] +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] name = "unicode-width" @@ -1108,12 +1087,6 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -1157,143 +1130,77 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.0", + "windows-targets", ] [[package]] name = "windows-targets" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - -[[package]] -name = "windows-targets" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" -dependencies = [ - "windows_aarch64_gnullvm 0.48.0", - "windows_aarch64_msvc 0.48.0", - "windows_i686_gnu 0.48.0", - "windows_i686_msvc 0.48.0", - "windows_x86_64_gnu 0.48.0", - "windows_x86_64_gnullvm 0.48.0", - "windows_x86_64_msvc 0.48.0", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_msvc" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_i686_gnu" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_x86_64_gnu" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "winnow" -version = "0.4.6" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61de7bac303dc551fe038e2b3cef0f571087a47571ea6e79a87692ac99b99699" +checksum = "d09770118a7eb1ccaf4a594a221334119a44a814fcb0d31c5b85e83e97227a97" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 65543d3cf..03e089b3d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kanata" -version = "1.4.0" +version = "1.5.0-prerelease-1" authors = ["jtroo "] description = "Multi-layer keyboard customization" keywords = ["cli", "linux", "windows", "keyboard", "layout"] @@ -25,8 +25,13 @@ rustc-hash = "1.1.0" miette = { version = "5.7.0", features = ["fancy"] } dirs = "5.0.1" -# kanata-keyberon = "0.20.0" -# kanata-parser = "0.20.0" +# Pinned to avoid including multiple versions of a dependency +is-terminal = "=0.4.7" +# Pinned to avoid downloading and running binary blobs on Linux +serde_derive = "=1.0.171" + +# kanata-keyberon = "0.150.0" +# kanata-parser = "0.150.0" # Uncomment below and comment out above for testing local changes. # Otherwise any changes to the local files will not reflect in the compiled # binary. diff --git a/keyberon/Cargo.toml b/keyberon/Cargo.toml index ccd438f3a..32866eddd 100644 --- a/keyberon/Cargo.toml +++ b/keyberon/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kanata-keyberon" -version = "0.20.0" +version = "0.150.0" authors = ["Guillaume Pinot ", "Robin Krahl ", "jtroo "] edition = "2018" description = "Pure Rust keyboard firmware. Fork intended for use with kanata." diff --git a/parser/Cargo.toml b/parser/Cargo.toml index d2a8c5391..7aafd7b71 100644 --- a/parser/Cargo.toml +++ b/parser/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kanata-parser" -version = "0.20.0" +version = "0.150.0" authors = ["jtroo "] description = "A parser for configuration language of kanata, a keyboard remapper." keywords = ["kanata", "parser"] @@ -20,7 +20,7 @@ rustc-hash = "1.1.0" miette = { version = "5.7.0", features = ["fancy"] } thiserror = "1.0.38" -# kanata-keyberon = "0.20.0" +# kanata-keyberon = "0.150.0" # Uncomment below and comment out above for testing local changes. # Otherwise any changes to the local files will not reflect in the compiled # binary. diff --git a/src/oskbd/linux.rs b/src/oskbd/linux.rs index 8989a2fff..68c57ebfa 100644 --- a/src/oskbd/linux.rs +++ b/src/oskbd/linux.rs @@ -621,8 +621,8 @@ fn discover_devices( } fn watch_devinput() -> Result { - let mut inotify = Inotify::init().expect("Failed to initialize inotify"); - inotify.add_watch("/dev/input", WatchMask::CREATE)?; + let inotify = Inotify::init().expect("Failed to initialize inotify"); + inotify.watches().add("/dev/input", WatchMask::CREATE)?; Ok(inotify) } From 5dc329686fd0c63a0d21d098499fbead53e17600 Mon Sep 17 00:00:00 2001 From: rszyma Date: Wed, 23 Aug 2023 05:32:12 +0200 Subject: [PATCH 040/819] refactor: parse all deflocalkeys variants regardless of target os (#540) Parse all `deflocalkeys` variants for errors regardless of target OS, but apply only the one based on target OS. This PR fixes https://github.com/rszyma/vscode-kanata/issues/1 ## Checklist - Add documentation to docs/config.adoc - [x] N/A - Add example to cfg_samples/kanata.kbd - [x] N/A - Update error messages - [x] Yes - Added tests, or did manual testing - [x] Yes (manual testing) --- parser/src/cfg/mod.rs | 86 ++++++++++++++++++++++++++---------------- parser/src/keys/mod.rs | 2 + 2 files changed, 55 insertions(+), 33 deletions(-) diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index 9083273d9..33f3f7718 100644 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -404,22 +404,37 @@ pub fn parse_cfg_raw_string( ) } - if let Some(result) = root_exprs - .iter() - .find(gen_first_atom_filter(DEF_LOCAL_KEYS)) - .map(|custom_keys| parse_deflocalkeys(custom_keys)) - { - result?; + let mut local_keys: Option> = None; + clear_custom_str_oscode_mapping(); + for def_local_keys_variant in [ + "deflocalkeys-win", + "deflocalkeys-wintercept", + "deflocalkeys-linux", + ] { + if let Some(result) = root_exprs + .iter() + .find(gen_first_atom_filter(def_local_keys_variant)) + .map(|custom_keys| parse_deflocalkeys(def_local_keys_variant, custom_keys)) + { + let mapping = result?; + if def_local_keys_variant == DEF_LOCAL_KEYS { + local_keys = Some(mapping); + } + } + + if let Some(spanned) = spanned_root_exprs + .iter() + .filter(gen_first_atom_filter_spanned(def_local_keys_variant)) + .nth(1) + { + bail_span!( + spanned, + "Only one {def_local_keys_variant} is allowed, found more. Delete the extras." + ) + } } - if let Some(spanned) = spanned_root_exprs - .iter() - .filter(gen_first_atom_filter_spanned(DEF_LOCAL_KEYS)) - .nth(1) - { - bail_span!( - spanned, - "Only one {DEF_LOCAL_KEYS} is allowed, found more. Delete the extras." - ) + if let Some(mapping) = local_keys { + replace_custom_str_oscode_mapping(&mapping); } let src_expr = root_exprs @@ -782,44 +797,49 @@ fn parse_defcfg(expr: &[SExpr]) -> Result> { } } -/// Parse custom keys from an expression starting with deflocalkeys. Statefully updates the `keys` -/// module using the custom keys parsed. -fn parse_deflocalkeys(expr: &[SExpr]) -> Result<()> { +/// Parse custom keys from an expression starting with deflocalkeys. +fn parse_deflocalkeys( + def_local_keys_variant: &str, + expr: &[SExpr], +) -> Result> { let mut cfg = HashMap::default(); - let mut exprs = check_first_expr(expr.iter(), DEF_LOCAL_KEYS)?; - clear_custom_str_oscode_mapping(); + let mut exprs = check_first_expr(expr.iter(), def_local_keys_variant)?; // Read k-v pairs from the configuration while let Some(key_expr) = exprs.next() { - let key = key_expr - .atom(None) - .ok_or_else(|| anyhow_expr!(key_expr, "No lists are allowed in {DEF_LOCAL_KEYS}"))?; + let key = key_expr.atom(None).ok_or_else(|| { + anyhow_expr!(key_expr, "No lists are allowed in {def_local_keys_variant}") + })?; if str_to_oscode(key).is_some() { bail_expr!( key_expr, - "Cannot use {key} in {DEF_LOCAL_KEYS} because it is a default key name" + "Cannot use {key} in {def_local_keys_variant} because it is a default key name" ); } else if cfg.contains_key(key) { - bail_expr!(key_expr, "Duplicate {key} found in {DEF_LOCAL_KEYS}"); + bail_expr!( + key_expr, + "Duplicate {key} found in {def_local_keys_variant}" + ); } let osc = match exprs.next() { Some(v) => v .atom(None) - .ok_or_else(|| anyhow_expr!(v, "No lists are allowed in {DEF_LOCAL_KEYS}")) + .ok_or_else(|| anyhow_expr!(v, "No lists are allowed in {def_local_keys_variant}")) .and_then(|osc| { - osc.parse::() - .map_err(|_| anyhow_expr!(v, "Unknown number in {DEF_LOCAL_KEYS}: {osc}")) + osc.parse::().map_err(|_| { + anyhow_expr!(v, "Unknown number in {def_local_keys_variant}: {osc}") + }) }) .and_then(|osc| { - OsCode::from_u16(osc) - .ok_or_else(|| anyhow_expr!(v, "Unknown number in {DEF_LOCAL_KEYS}: {osc}")) + OsCode::from_u16(osc).ok_or_else(|| { + anyhow_expr!(v, "Unknown number in {def_local_keys_variant}: {osc}") + }) })?, - None => bail_expr!(key_expr, "Key without a number in {DEF_LOCAL_KEYS}"), + None => bail_expr!(key_expr, "Key without a number in {def_local_keys_variant}"), }; log::debug!("custom mapping: {key} {}", osc.as_u16()); cfg.insert(key.to_owned(), osc); } - replace_custom_str_oscode_mapping(&cfg); - Ok(()) + Ok(cfg) } /// Parse mapped keys from an expression starting with defsrc. Returns the key mapping as well as diff --git a/parser/src/keys/mod.rs b/parser/src/keys/mod.rs index 0b49e541f..9e86fbb46 100644 --- a/parser/src/keys/mod.rs +++ b/parser/src/keys/mod.rs @@ -45,6 +45,8 @@ pub fn replace_custom_str_oscode_mapping(mapping: &HashMap) { pub fn clear_custom_str_oscode_mapping() { let mut local_mapping = CUSTOM_STRS_TO_OSCODES.lock(); local_mapping.clear(); + add_default_str_osc_mappings(&mut local_mapping); + local_mapping.shrink_to_fit(); } /// Used for backwards compatibility. If there is hardcoded key name in `str_to_oscode` that would From 50ac6757bff5e3e698e50251ed3a5f82861bc046 Mon Sep 17 00:00:00 2001 From: jtroo Date: Thu, 24 Aug 2023 13:30:43 -0700 Subject: [PATCH 041/819] doc: fix fake keys example using on-idle (#543) --- docs/config.adoc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/config.adoc b/docs/config.adoc index cbdc4a8b6..e56e26bd5 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -1803,11 +1803,12 @@ will not yet be counting even if you no longer have any keyboard keys pressed. pal (on-press-fakekey pal tap) ral (on-press-fakekey ral tap) - rsf (on-idle-fakekey ral tap 1000) + + isf (on-idle-fakekey sft tap 1000) ) (deflayer use-fake-keys - @psf @rsf @pal @ral a s d @rsf + @psf @rsf @pal @ral a s d f @isf ) ---- From 90beaafc09726b93b31d4854ebfbf98fb8e13452 Mon Sep 17 00:00:00 2001 From: jtroo Date: Sat, 26 Aug 2023 22:05:09 -0700 Subject: [PATCH 042/819] fix: incorrect handling of deflocalkeys default mappings (#546) Commit 5dc3296 added code to add the default overridable mappings when clearing the deflocalkeys mappings. This was incorrect because it made it so these default mappings were no longer overridable. In addition to the fix, tests are added to prevent future regressions. ## Checklist - Add documentation to docs/config.adoc - [x] N/A - Add example to cfg_samples/kanata.kbd - [x] N/A - Update error messages - [x] N/A - Added tests, or did manual testing - [x] Yes --- parser/src/cfg/mod.rs | 8 ++-- parser/src/cfg/tests.rs | 101 ++++++++++++++++++++++++++++++++++++++++ parser/src/keys/mod.rs | 1 - 3 files changed, 106 insertions(+), 4 deletions(-) diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index 33f3f7718..b7f4b40cb 100644 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -418,6 +418,10 @@ pub fn parse_cfg_raw_string( { let mapping = result?; if def_local_keys_variant == DEF_LOCAL_KEYS { + assert!( + local_keys.is_none(), + ">1 mutually exclusive deflocalkeys variants was parsed" + ); local_keys = Some(mapping); } } @@ -433,9 +437,7 @@ pub fn parse_cfg_raw_string( ) } } - if let Some(mapping) = local_keys { - replace_custom_str_oscode_mapping(&mapping); - } + replace_custom_str_oscode_mapping(&local_keys.unwrap_or_default()); let src_expr = root_exprs .iter() diff --git a/parser/src/cfg/tests.rs b/parser/src/cfg/tests.rs index 9d3080cec..c5ba35040 100644 --- a/parser/src/cfg/tests.rs +++ b/parser/src/cfg/tests.rs @@ -1004,3 +1004,104 @@ fn parse_fake_keys_errors_on_too_many() { } assert!(checked_for_err); } + +#[test] +fn parse_deflocalkeys_overridden() { + let _lk = match CFG_PARSE_LOCK.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + }; + let source = r#" +(deflocalkeys-win ++ 300 +[ 301 +] 302 +{ 303 +} 304 +/ 305 +; 306 +` 307 += 308 +- 309 +' 310 +, 311 +. 312 +\ 313 +yen 314 +¥ 315 +new 316 +) +(deflocalkeys-wintercept ++ 300 +[ 301 +] 302 +{ 303 +} 304 +/ 305 +; 306 +` 307 += 308 +- 309 +' 310 +, 311 +. 312 +\ 313 +yen 314 +¥ 315 +new 316 +) +(deflocalkeys-linux ++ 300 +[ 301 +] 302 +{ 303 +} 304 +/ 305 +; 306 +` 307 += 308 +- 309 +' 310 +, 311 +. 312 +\ 313 +yen 314 +¥ 315 +new 316 +) +(defsrc + [ ] { } / ; ` = - ' , . \ yen ¥ new) +(deflayer base + [ ] { } / ; ` = - ' , . \ yen ¥ new) +"#; + let mut s = ParsedState::default(); + parse_cfg_raw_string( + source, + &mut s, + &PathBuf::from("test"), + &mut FileContentProvider { + get_file_content_fn: &mut |_| unimplemented!(), + }, + ) + .expect("succeeds"); +} + +#[test] +fn use_default_overridable_mappings() { + let _lk = match CFG_PARSE_LOCK.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + }; + let source = r#" +(defsrc + [ ] a b / ; ` = - ' , . 9 yen ¥ ) +(deflayer base + [ ] { } / ; ` = - ' , . \ yen ¥ ) +"#; + let mut s = ParsedState::default(); + parse_cfg_raw_string( + source, + &mut s, + &PathBuf::from("test"), + &mut FileContentProvider { + get_file_content_fn: &mut |_| unimplemented!(), + }, + ) + .expect("succeeds"); +} diff --git a/parser/src/keys/mod.rs b/parser/src/keys/mod.rs index 9e86fbb46..c831a6ebb 100644 --- a/parser/src/keys/mod.rs +++ b/parser/src/keys/mod.rs @@ -45,7 +45,6 @@ pub fn replace_custom_str_oscode_mapping(mapping: &HashMap) { pub fn clear_custom_str_oscode_mapping() { let mut local_mapping = CUSTOM_STRS_TO_OSCODES.lock(); local_mapping.clear(); - add_default_str_osc_mappings(&mut local_mapping); local_mapping.shrink_to_fit(); } From f031c9e428b486499764d8a12da855605b778c80 Mon Sep 17 00:00:00 2001 From: Jan Tache Date: Sun, 27 Aug 2023 22:22:10 -0700 Subject: [PATCH 043/819] ver: prep for publish --- Cargo.lock | 4 ++++ Cargo.toml | 8 ++++---- parser/Cargo.toml | 4 ++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3c5b38bfe..ec8eed272 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -419,6 +419,8 @@ dependencies = [ [[package]] name = "kanata-keyberon" version = "0.150.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abc660b014db42063e40c1c15e95844693807fa0ed1e17dd34b4ef8f95f2702b" dependencies = [ "arraydeque", "heapless", @@ -438,6 +440,8 @@ dependencies = [ [[package]] name = "kanata-parser" version = "0.150.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "160980d507a6e10c9b65766e430ea8f5051d2ece9309108348da84bde8abe771" dependencies = [ "anyhow", "kanata-keyberon", diff --git a/Cargo.toml b/Cargo.toml index 03e089b3d..ce6ee6277 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,13 +30,13 @@ is-terminal = "=0.4.7" # Pinned to avoid downloading and running binary blobs on Linux serde_derive = "=1.0.171" -# kanata-keyberon = "0.150.0" -# kanata-parser = "0.150.0" +kanata-keyberon = "0.150.0" +kanata-parser = "0.150.0" # Uncomment below and comment out above for testing local changes. # Otherwise any changes to the local files will not reflect in the compiled # binary. -kanata-keyberon = { path = "keyberon" } -kanata-parser = { path = "parser" } +# kanata-keyberon = { path = "keyberon" } +# kanata-parser = { path = "parser" } [target.'cfg(target_os = "linux")'.dependencies] evdev = "=0.12.0" diff --git a/parser/Cargo.toml b/parser/Cargo.toml index 7aafd7b71..68215a71b 100644 --- a/parser/Cargo.toml +++ b/parser/Cargo.toml @@ -20,11 +20,11 @@ rustc-hash = "1.1.0" miette = { version = "5.7.0", features = ["fancy"] } thiserror = "1.0.38" -# kanata-keyberon = "0.150.0" +kanata-keyberon = "0.150.0" # Uncomment below and comment out above for testing local changes. # Otherwise any changes to the local files will not reflect in the compiled # binary. -kanata-keyberon = { path = "../keyberon" } +# kanata-keyberon = { path = "../keyberon" } [features] cmd = [] From fbe895ca9121ed69b7a675afe23bf5e4538beb23 Mon Sep 17 00:00:00 2001 From: Jan Tache Date: Sun, 27 Aug 2023 22:27:25 -0700 Subject: [PATCH 044/819] chore: add strip to linux build just job --- justfile | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/justfile b/justfile index 6e5150561..749a9671e 100644 --- a/justfile +++ b/justfile @@ -1,7 +1,11 @@ # Build the release binaries for Linux and put the binaries+cfg in the output directory build_release_linux output_dir: - cargo build --release && cp target/release/kanata "{{output_dir}}/kanata" - cargo build --release --features cmd && cp target/release/kanata "{{output_dir}}/kanata_cmd_allowed" + cargo build --release + cp target/release/kanata "{{output_dir}}/kanata" + strip "{{output_dir}}/kanata" + cargo build --release --features cmd + cp target/release/kanata "{{output_dir}}/kanata_cmd_allowed" + strip "{{output_dir}}/kanata_cmd_allowed" cp cfg_samples/kanata.kbd "{{output_dir}}" # Build the release binaries for Windows and put the binaries+cfg in the output directory. Run as follows: `just --shell powershell.exe --shell-arg -c build_release_windows `. From a5e0c980df24cdaa3ba4a0e41c6074d3f68addf8 Mon Sep 17 00:00:00 2001 From: Jan Tache Date: Tue, 29 Aug 2023 22:30:09 -0700 Subject: [PATCH 045/819] ver: use local crates, update to prerelease-2 --- Cargo.lock | 10 +++------- Cargo.toml | 10 +++++----- keyberon/Cargo.toml | 2 +- keyberon/README.md | 2 ++ parser/Cargo.toml | 6 +++--- parser/README.md | 2 ++ 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ec8eed272..55f759901 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -374,7 +374,7 @@ checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "kanata" -version = "1.5.0-prerelease-1" +version = "1.5.0-prerelease-2" dependencies = [ "anyhow", "clap", @@ -418,9 +418,7 @@ dependencies = [ [[package]] name = "kanata-keyberon" -version = "0.150.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abc660b014db42063e40c1c15e95844693807fa0ed1e17dd34b4ef8f95f2702b" +version = "0.150.2" dependencies = [ "arraydeque", "heapless", @@ -439,9 +437,7 @@ dependencies = [ [[package]] name = "kanata-parser" -version = "0.150.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "160980d507a6e10c9b65766e430ea8f5051d2ece9309108348da84bde8abe771" +version = "0.150.2" dependencies = [ "anyhow", "kanata-keyberon", diff --git a/Cargo.toml b/Cargo.toml index ce6ee6277..1a21ec802 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kanata" -version = "1.5.0-prerelease-1" +version = "1.5.0-prerelease-2" authors = ["jtroo "] description = "Multi-layer keyboard customization" keywords = ["cli", "linux", "windows", "keyboard", "layout"] @@ -30,13 +30,13 @@ is-terminal = "=0.4.7" # Pinned to avoid downloading and running binary blobs on Linux serde_derive = "=1.0.171" -kanata-keyberon = "0.150.0" -kanata-parser = "0.150.0" +# kanata-keyberon = "0.150.2" +# kanata-parser = "0.150.2" # Uncomment below and comment out above for testing local changes. # Otherwise any changes to the local files will not reflect in the compiled # binary. -# kanata-keyberon = { path = "keyberon" } -# kanata-parser = { path = "parser" } +kanata-keyberon = { path = "keyberon" } +kanata-parser = { path = "parser" } [target.'cfg(target_os = "linux")'.dependencies] evdev = "=0.12.0" diff --git a/keyberon/Cargo.toml b/keyberon/Cargo.toml index 32866eddd..b42e505b7 100644 --- a/keyberon/Cargo.toml +++ b/keyberon/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kanata-keyberon" -version = "0.150.0" +version = "0.150.2" authors = ["Guillaume Pinot ", "Robin Krahl ", "jtroo "] edition = "2018" description = "Pure Rust keyboard firmware. Fork intended for use with kanata." diff --git a/keyberon/README.md b/keyberon/README.md index fc9c66ca7..72cd51939 100644 --- a/keyberon/README.md +++ b/keyberon/README.md @@ -4,3 +4,5 @@ This is a fork intended for use by the [kanata keyboard remapper software](https://github.com/jtroo/kanata). Please make contributions to the [original project](https://github.com/TeXitoi/keyberon) where applicable. + +This crate does not follow semver. It tracks the version of kanata. diff --git a/parser/Cargo.toml b/parser/Cargo.toml index 68215a71b..fc6f2f6a4 100644 --- a/parser/Cargo.toml +++ b/parser/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kanata-parser" -version = "0.150.0" +version = "0.150.2" authors = ["jtroo "] description = "A parser for configuration language of kanata, a keyboard remapper." keywords = ["kanata", "parser"] @@ -20,11 +20,11 @@ rustc-hash = "1.1.0" miette = { version = "5.7.0", features = ["fancy"] } thiserror = "1.0.38" -kanata-keyberon = "0.150.0" +# kanata-keyberon = "0.150.2" # Uncomment below and comment out above for testing local changes. # Otherwise any changes to the local files will not reflect in the compiled # binary. -# kanata-keyberon = { path = "../keyberon" } +kanata-keyberon = { path = "../keyberon" } [features] cmd = [] diff --git a/parser/README.md b/parser/README.md index 9e0b759a4..3297d6fbf 100644 --- a/parser/README.md +++ b/parser/README.md @@ -1,3 +1,5 @@ # kanata-parser A parser for configuration language of [kanata](https://github.com/jtroo/kanata). + +This crate does not follow semver. It tracks the version of kanata. From 2202a448da0576628b8823b874ded1261f4b96d9 Mon Sep 17 00:00:00 2001 From: Jan Tache Date: Tue, 29 Aug 2023 23:15:51 -0700 Subject: [PATCH 046/819] github: minor update to checklist --- .github/pull_request_template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 223435050..df26b4524 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -4,7 +4,7 @@ - Add documentation to docs/config.adoc - [ ] Yes or N/A -- Add example to cfg_samples/kanata.kbd +- Add example and basic docs to cfg_samples/kanata.kbd - [ ] Yes or N/A - Update error messages - [ ] Yes or N/A From 4da14565846f83c101b1c002e3b6da5be90ff3e3 Mon Sep 17 00:00:00 2001 From: jtroo Date: Tue, 29 Aug 2023 23:16:14 -0700 Subject: [PATCH 047/819] feat!: clear non-oneshot output chord on next action (#552) This commit changes output chord behavior to be more user-friendly by clearing output chord keys on the next action. I have also been told (though have not confirmed) that QMK has similar behaviour. I am not sure that the change in this commit replicates QMK's behaviour perfectly, but it seems good enough. Clearing output chord keys on the next action allows a subsequent typed key to not have modifiers pressed alongside it. Without this change, typing a new key without first releasing an output chord can have the unintended typing result. Users have asked about this few times in issues/discussions and the current workaround is to use a macro. However, this can be hard to discover. Some users may just be living with the annoyance because aren't aware that there is a workaround and they haven't asked. Output chords within a `one-shot` are ignored because someone might do something like `(one-shot C-S-lalt)` to get 3 modifiers within a one-shot action. These are probably intended to remain held. However, other output chords are usually used to type symbols or accented characters, e.g. S-1 or RA-a. If the symbol or accented character is held down, key repeat works just fine because OS key repeats are not keyberon actions. ## Checklist - Add documentation to docs/config.adoc - [x] Yes - Add example to cfg_samples/kanata.kbd - [x] N/A - Update error messages - [x] N/A - Added tests, or did manual testing - [x] Yes --- cfg_samples/kanata.kbd | 8 ++++-- docs/config.adoc | 14 ++++++++++ keyberon/src/layout.rs | 54 +++++++++++++++++++++++++++++++++++++-- parser/src/cfg/mod.rs | 5 +++- src/kanata/windows/mod.rs | 10 ++++++-- src/tests.rs | 12 +++++++++ 6 files changed, 96 insertions(+), 7 deletions(-) diff --git a/cfg_samples/kanata.kbd b/cfg_samples/kanata.kbd index 47b0dd892..e812c0c79 100644 --- a/cfg_samples/kanata.kbd +++ b/cfg_samples/kanata.kbd @@ -27,12 +27,12 @@ be able to configure kanata. If you follow along with the examples, you should be fine. Kanata should also hopefully have helpful error messages in case something goes wrong. -If you need help, you are welcome to ask. +If you need help, please feel welcome to ask in the GitHub discussions. |# ;; One defcfg entry may be added if desired. This is used for configuration ;; key-value pairs that change kanata's behaviour at a global level. -;; All configuration items are optional. +;; All defcfg options are optional. (defcfg ;; Your keyboard device will likely differ from this. I believe /dev/input/by-id/ ;; is preferable; I recall reading that it's less likely to change names on you, @@ -384,6 +384,10 @@ If you need help, you are welcome to ask. ;; same and only one is allowed in a single chord. This chord can be useful for ;; international layouts. ;; + ;; A special behaviour of output chords is that if another key is pressed, + ;; all of the chord keys will be released. For the explanation about why + ;; this is the case, see the configuration guide. + ;; ;; This use case for multi is typing an all-caps string. alp (multi lsft a b c d e f g h i j k l m n o p q r s t u v w x y z) diff --git a/docs/config.adoc b/docs/config.adoc index e56e26bd5..7175a1980 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -932,6 +932,8 @@ These modifiers may be combined together if desired. [source] ---- (defalias + ;; Type exclamation mark (US layout) + ex! S-1 ;; Ctrl+C: send SIGINT to a Linux terminal program int C-c ;; Win+Tab: open Windows' Task View @@ -942,6 +944,18 @@ These modifiers may be combined together if desired. ) ---- +A special behaviour of output chords is that if another key is pressed, +all of the chord keys will be released +before the newly pressed key action activates. +Output chords are typically used do one-off actions such as: + +- type a symbol, e.g. `S-1` +- type a special/accented character, e.g. `RA-a` +- do a special action like `C-c` to send `SIGTERM` in the terminal + +The modifier pressed with these one-off action +is usually not desired for subsequent actions. + [[repeat-key]] === Repeat key <> diff --git a/keyberon/src/layout.rs b/keyberon/src/layout.rs index 7512ab04f..6f46f11ea 100644 --- a/keyberon/src/layout.rs +++ b/keyberon/src/layout.rs @@ -169,11 +169,24 @@ impl<'a, T> CustomEvent<'a, T> { } } +/// Metadata about normal key flags. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub struct NormalKeyFlags(u16); + +const NORMAL_KEY_FLAG_CLEAR_ON_NEXT_ACTION: u16 = 0x0001; + +impl NormalKeyFlags { + pub fn clear_on_next_action(self) -> bool { + (self.0 & NORMAL_KEY_FLAG_CLEAR_ON_NEXT_ACTION) == NORMAL_KEY_FLAG_CLEAR_ON_NEXT_ACTION + } +} + #[derive(Debug, Eq, PartialEq)] pub enum State<'a, T: 'a> { NormalKey { keycode: KeyCode, coord: KCoord, + flags: NormalKeyFlags, }, LayerModifier { value: usize, @@ -1167,6 +1180,10 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt self.last_press_tracker.tap_hold_timeout = 0; } use Action::*; + self.states.retain(|s| match s { + NormalKey { flags, .. } => !flags.clear_on_next_action(), + _ => true, + }); match action { NoOp | Trans => { if !is_oneshot { @@ -1303,7 +1320,11 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt } &KeyCode(keycode) => { self.last_press_tracker.coord = coord; - let _ = self.states.push(NormalKey { coord, keycode }); + let _ = self.states.push(NormalKey { + coord, + keycode, + flags: NormalKeyFlags(0), + }); if !is_oneshot { self.oneshot .handle_press(OneShotHandlePressKey::Other(coord)); @@ -1313,7 +1334,23 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt &MultipleKeyCodes(v) => { self.last_press_tracker.coord = coord; for &keycode in *v { - let _ = self.states.push(NormalKey { coord, keycode }); + let _ = self.states.push(NormalKey { + coord, + keycode, + // In Kanata, this action is only ever used with output chords. Output + // chords within a one-shot are ignored because someone might do something + // like (one-shot C-S-lalt to get 3 modifiers. These are probably intended + // to remain held. However, other output chords are usually used to type + // symbols or accented characters, e.g. S-1 or RA-a. Clearing chord keys on + // the next action allows a subsequent typed key to not have modifiers + // alongside it. But if the symbol or accented character is held down, key + // repeat works just fine. + flags: NormalKeyFlags(if is_oneshot { + 0 + } else { + NORMAL_KEY_FLAG_CLEAR_ON_NEXT_ACTION + }), + }); } if !is_oneshot { self.oneshot @@ -3357,4 +3394,17 @@ mod test { assert_eq!(CustomEvent::NoEvent, layout.tick()); assert_keys(&[], layout.keycodes()); } + + #[test] + fn test_clear_multiple_keycodes() { + static LAYERS: Layers<2, 1, 1> = [[[k(A), MultipleKeyCodes(&[LCtrl, Enter].as_slice())]]]; + let mut layout = Layout::new(&LAYERS); + layout.event(Press(0, 1)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[LCtrl, Enter], layout.keycodes()); + // Cancel chord keys on next keypress. + layout.event(Press(0, 0)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[A], layout.keycodes()); + } } diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index b7f4b40cb..72a5bd20b 100644 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -2900,7 +2900,10 @@ fn parse_switch(ac_params: &[SExpr], s: &ParsedState) -> Result<&'static KanataA let action = parse_action(action, s)?; let Some(break_or_fallthrough) = break_or_fallthrough_expr.atom(s.vars()) else { - bail_expr!(break_or_fallthrough_expr, "{ERR_STR}\nthis must be one of: break, fallthrough"); + bail_expr!( + break_or_fallthrough_expr, + "{ERR_STR}\nthis must be one of: break, fallthrough" + ); }; let break_or_fallthrough = match break_or_fallthrough { "break" => BreakOrFallthrough::Break, diff --git a/src/kanata/windows/mod.rs b/src/kanata/windows/mod.rs index 658d831e3..ce6d26ee9 100644 --- a/src/kanata/windows/mod.rs +++ b/src/kanata/windows/mod.rs @@ -53,9 +53,14 @@ impl Kanata { pub fn check_release_non_physical_shift(&mut self) -> Result<()> { fn state_filter(v: &State<'_, &&[&CustomAction]>) -> Option> { match v { - State::NormalKey { keycode, coord } => Some(State::NormalKey::<()> { + State::NormalKey { + keycode, + coord, + flags, + } => Some(State::NormalKey::<()> { keycode: *keycode, coord: *coord, + flags: *flags, }), State::FakeKey { keycode } => Some(State::FakeKey::<()> { keycode: *keycode }), _ => None, @@ -81,7 +86,7 @@ impl Kanata { // this should not be a problem. State does not implement Hash so can't use a HashSet. A // HashSet might perform worse anyway. for prev_state in prev_states.iter() { - if let State::NormalKey { keycode, coord } = prev_state { + if let State::NormalKey { keycode, coord, .. } = prev_state { if !matches!(keycode, KeyCode::LShift | KeyCode::RShift) || (matches!(keycode, KeyCode::LShift) && coord.1 == u16::from(OsCode::KEY_LEFTSHIFT)) @@ -104,6 +109,7 @@ impl Kanata { State::NormalKey { keycode: cur_kc, coord: cur_coord, + .. } => cur_kc != keycode && *cur_coord != (0, u16::from(OsCode::from(keycode))), _ => true, }); diff --git a/src/tests.rs b/src/tests.rs index c714694e4..9c337ee54 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -60,3 +60,15 @@ fn parse_all_keys() { )) .unwrap(); } + +#[test] +fn sizeof_state() { + assert_eq!( + std::mem::size_of::< + kanata_keyberon::layout::State< + &'static &'static [&'static kanata_parser::custom_action::CustomAction], + >, + >(), + 2 * std::mem::size_of::() + ); +} From e1c40e43bad087ff26d079d817afd81e3463e99d Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 17 Sep 2023 03:24:59 -0700 Subject: [PATCH 048/819] doc(linux,tap-hold): document linux tap-hold repeat issue (#563) --- cfg_samples/kanata.kbd | 2 ++ docs/config.adoc | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/cfg_samples/kanata.kbd b/cfg_samples/kanata.kbd index e812c0c79..991d17d8b 100644 --- a/cfg_samples/kanata.kbd +++ b/cfg_samples/kanata.kbd @@ -277,6 +277,8 @@ If you need help, please feel welcome to ask in the GitHub discussions. fks (layer-while-held fakekeys) ;; tap-hold aliases with tap for dvorak key, and hold for toggle layers + ;; WARNING(Linux only): key repeat with tap-hold can behave unexpectedly. + ;; For full context, see https://github.com/jtroo/kanata/discussions/422 ;; ;; tap-hold parameter order: ;; 1. tap timeout diff --git a/docs/config.adoc b/docs/config.adoc index 7175a1980..255c57405 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -1342,6 +1342,13 @@ The default name `+one-shot+` corresponds to `+one-shot-press+`. === tap-hold <> +WARNING: The `tap-hold` action and all variants can behave unexpectedly on Linux +with respect to repeat of antecedent key presses. +The full context is in https://github.com/jtroo/kanata/discussions/422[discussion #422]. +In brief, the workaround is to use `tap-hold` inside of <>, +combined with another key action that behaves as a no-op like `f24`. + +Example: `(multi f24 (tap-hold ...))` + The `+tap-hold+` action allows you to have one action/key for a "tap" and a different action/key for a "hold". A tap is a rapid press then release of the key whereas a hold is a long press. From b585a5e507d5a2a9326961c6ee06234e730a7ae6 Mon Sep 17 00:00:00 2001 From: jtroo Date: Wed, 20 Sep 2023 23:42:50 -0700 Subject: [PATCH 049/819] feat: improve error message when list action is not in a list (#565) Before this commit, the error message when a list action was not parenthesized was: Unknown key/action/variable: {ac:?} This is not helpful and confusing because the action is valid and exists, but just needs to be in a list. This commit improves the error message to state that the action must be in parentheses. --- parser/src/cfg/list_actions.rs | 120 +++++++++++++++++++++++++++++++ parser/src/cfg/mod.rs | 128 ++++++++++++++++++--------------- parser/src/cfg/tests.rs | 41 +++++++++-- 3 files changed, 226 insertions(+), 63 deletions(-) create mode 100644 parser/src/cfg/list_actions.rs diff --git a/parser/src/cfg/list_actions.rs b/parser/src/cfg/list_actions.rs new file mode 100644 index 000000000..8a593590d --- /dev/null +++ b/parser/src/cfg/list_actions.rs @@ -0,0 +1,120 @@ +//! Contains all list action names and a function to check that an action name is that of a list +//! action. + +// Note: changing any of these constants is a breaking change. +pub const LAYER_SWITCH: &str = "layer-switch"; +pub const LAYER_TOGGLE: &str = "layer-toggle"; +pub const LAYER_WHILE_HELD: &str = "layer-while-held"; +pub const TAP_HOLD: &str = "tap-hold"; +pub const TAP_HOLD_PRESS: &str = "tap-hold-press"; +pub const TAP_HOLD_RELEASE: &str = "tap-hold-release"; +pub const TAP_HOLD_PRESS_TIMEOUT: &str = "tap-hold-press-timeout"; +pub const TAP_HOLD_RELEASE_TIMEOUT: &str = "tap-hold-release-timeout"; +pub const TAP_HOLD_RELEASE_KEYS: &str = "tap-hold-release-keys"; +pub const MULTI: &str = "multi"; +pub const MACRO: &str = "macro"; +pub const MACRO_REPEAT: &str = "macro-repeat"; +pub const MACRO_RELEASE_CANCEL: &str = "macro-release-cancel"; +pub const MACRO_REPEAT_RELEASE_CANCEL: &str = "macro-repeat-release-cancel"; +pub const UNICODE: &str = "unicode"; +pub const ONE_SHOT: &str = "one-shot"; +pub const ONE_SHOT_PRESS: &str = "one-shot-press"; +pub const ONE_SHOT_RELEASE: &str = "one-shot-release"; +pub const ONE_SHOT_PRESS_PCANCEL: &str = "one-shot-press-pcancel"; +pub const ONE_SHOT_RELEASE_PCANCEL: &str = "one-shot-release-pcancel"; +pub const TAP_DANCE: &str = "tap-dance"; +pub const TAP_DANCE_EAGER: &str = "tap-dance-eager"; +pub const CHORD: &str = "chord"; +pub const RELEASE_KEY: &str = "release-key"; +pub const RELEASE_LAYER: &str = "release-layer"; +pub const ON_PRESS_FAKEKEY: &str = "on-press-fakekey"; +pub const ON_RELEASE_FAKEKEY: &str = "on-release-fakekey"; +pub const ON_PRESS_FAKEKEY_DELAY: &str = "on-press-fakekey-delay"; +pub const ON_RELEASE_FAKEKEY_DELAY: &str = "on-release-fakekey-delay"; +pub const ON_IDLE_FAKEKEY: &str = "on-idle-fakekey"; +pub const MWHEEL_UP: &str = "mwheel-up"; +pub const MWHEEL_DOWN: &str = "mwheel-down"; +pub const MWHEEL_LEFT: &str = "mwheel-left"; +pub const MWHEEL_RIGHT: &str = "mwheel-right"; +pub const MOVEMOUSE_UP: &str = "movemouse-up"; +pub const MOVEMOUSE_DOWN: &str = "movemouse-down"; +pub const MOVEMOUSE_LEFT: &str = "movemouse-left"; +pub const MOVEMOUSE_RIGHT: &str = "movemouse-right"; +pub const MOVEMOUSE_ACCEL_UP: &str = "movemouse-accel-up"; +pub const MOVEMOUSE_ACCEL_DOWN: &str = "movemouse-accel-down"; +pub const MOVEMOUSE_ACCEL_LEFT: &str = "movemouse-accel-left"; +pub const MOVEMOUSE_ACCEL_RIGHT: &str = "movemouse-accel-right"; +pub const MOVEMOUSE_SPEED: &str = "movemouse-speed"; +pub const SETMOUSE: &str = "setmouse"; +pub const DYNAMIC_MACRO_RECORD: &str = "dynamic-macro-record"; +pub const DYNAMIC_MACRO_PLAY: &str = "dynamic-macro-play"; +pub const ARBITRARY_CODE: &str = "arbitrary-code"; +pub const CMD: &str = "cmd"; +pub const CMD_OUTPUT_KEYS: &str = "cmd-output-keys"; +pub const FORK: &str = "fork"; +pub const CAPS_WORD: &str = "caps-word"; +pub const CAPS_WORD_CUSTOM: &str = "caps-word-custom"; +pub const DYNAMIC_MACRO_RECORD_STOP_TRUNCATE: &str = "dynamic-macro-record-stop-truncate"; +pub const SWITCH: &str = "switch"; +pub const SEQUENCE: &str = "sequence"; + +pub fn is_list_action(ac: &str) -> bool { + const LIST_ACTIONS: [&str; 55] = [ + LAYER_SWITCH, + LAYER_TOGGLE, + LAYER_WHILE_HELD, + TAP_HOLD, + TAP_HOLD_PRESS, + TAP_HOLD_RELEASE, + TAP_HOLD_PRESS_TIMEOUT, + TAP_HOLD_RELEASE_TIMEOUT, + TAP_HOLD_RELEASE_KEYS, + MULTI, + MACRO, + MACRO_REPEAT, + MACRO_RELEASE_CANCEL, + MACRO_REPEAT_RELEASE_CANCEL, + UNICODE, + ONE_SHOT, + ONE_SHOT_PRESS, + ONE_SHOT_RELEASE, + ONE_SHOT_PRESS_PCANCEL, + ONE_SHOT_RELEASE_PCANCEL, + TAP_DANCE, + TAP_DANCE_EAGER, + CHORD, + RELEASE_KEY, + RELEASE_LAYER, + ON_PRESS_FAKEKEY, + ON_RELEASE_FAKEKEY, + ON_PRESS_FAKEKEY_DELAY, + ON_RELEASE_FAKEKEY_DELAY, + ON_IDLE_FAKEKEY, + MWHEEL_UP, + MWHEEL_DOWN, + MWHEEL_LEFT, + MWHEEL_RIGHT, + MOVEMOUSE_UP, + MOVEMOUSE_DOWN, + MOVEMOUSE_LEFT, + MOVEMOUSE_RIGHT, + MOVEMOUSE_ACCEL_UP, + MOVEMOUSE_ACCEL_DOWN, + MOVEMOUSE_ACCEL_LEFT, + MOVEMOUSE_ACCEL_RIGHT, + MOVEMOUSE_SPEED, + SETMOUSE, + DYNAMIC_MACRO_RECORD, + DYNAMIC_MACRO_PLAY, + ARBITRARY_CODE, + CMD, + CMD_OUTPUT_KEYS, + FORK, + CAPS_WORD, + CAPS_WORD_CUSTOM, + DYNAMIC_MACRO_RECORD_STOP_TRUNCATE, + SWITCH, + SEQUENCE, + ]; + LIST_ACTIONS.contains(&ac) +} diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index 72a5bd20b..ff4a832ed 100644 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -48,6 +48,9 @@ pub use key_override::*; mod custom_tap_hold; use custom_tap_hold::*; +mod list_actions; +use list_actions::*; + use crate::custom_action::*; use crate::keys::*; use crate::layers::*; @@ -1113,8 +1116,14 @@ fn parse_action(expr: &SExpr, s: &ParsedState) -> Result<&'static KanataAction> } /// Parse a `kanata_keyberon::action::Action` from a string. -fn parse_action_atom(ac: &Spanned, s: &ParsedState) -> Result<&'static KanataAction> { - let ac = &*ac.t; +fn parse_action_atom(ac_span: &Spanned, s: &ParsedState) -> Result<&'static KanataAction> { + let ac = &*ac_span.t; + if is_list_action(ac) { + bail_span!( + ac_span, + "This is a list action and must be in parentheses: ({ac} ...)" + ); + } match ac { "_" => return Ok(s.a.sref(Action::Trans)), "XX" => return Ok(s.a.sref(Action::NoOp)), @@ -1239,73 +1248,74 @@ fn parse_action_list(ac: &[SExpr], s: &ParsedState) -> Result<&'static KanataAct } let ac_type = match &ac[0] { SExpr::Atom(a) => &a.t, - _ => bail!("Action list must start with an atom"), + _ => bail!("All list actions must start with string and not a list"), }; + if !is_list_action(ac_type) { + bail_expr!(&ac[0], "Unknown action type: {ac_type}"); + } match ac_type.as_str() { - "layer-switch" => parse_layer_base(&ac[1..], s), - "layer-toggle" | "layer-while-held" => parse_layer_toggle(&ac[1..], s), - "tap-hold" => parse_tap_hold(&ac[1..], s, HoldTapConfig::Default), - "tap-hold-press" => parse_tap_hold(&ac[1..], s, HoldTapConfig::HoldOnOtherKeyPress), - "tap-hold-release" => parse_tap_hold(&ac[1..], s, HoldTapConfig::PermissiveHold), - "tap-hold-press-timeout" => { + LAYER_SWITCH => parse_layer_base(&ac[1..], s), + LAYER_TOGGLE | LAYER_WHILE_HELD => parse_layer_toggle(&ac[1..], s), + TAP_HOLD => parse_tap_hold(&ac[1..], s, HoldTapConfig::Default), + TAP_HOLD_PRESS => parse_tap_hold(&ac[1..], s, HoldTapConfig::HoldOnOtherKeyPress), + TAP_HOLD_RELEASE => parse_tap_hold(&ac[1..], s, HoldTapConfig::PermissiveHold), + TAP_HOLD_PRESS_TIMEOUT => { parse_tap_hold_timeout(&ac[1..], s, HoldTapConfig::HoldOnOtherKeyPress) } - "tap-hold-release-timeout" => { + TAP_HOLD_RELEASE_TIMEOUT => { parse_tap_hold_timeout(&ac[1..], s, HoldTapConfig::PermissiveHold) } - "tap-hold-release-keys" => parse_tap_hold_release_keys(&ac[1..], s), - "multi" => parse_multi(&ac[1..], s), - "macro" => parse_macro(&ac[1..], s, RepeatMacro::No), - "macro-repeat" => parse_macro(&ac[1..], s, RepeatMacro::Yes), - "macro-release-cancel" => parse_macro_release_cancel(&ac[1..], s, RepeatMacro::No), - "macro-repeat-release-cancel" => parse_macro_release_cancel(&ac[1..], s, RepeatMacro::Yes), - "unicode" => parse_unicode(&ac[1..], s), - "one-shot" | "one-shot-press" => { - parse_one_shot(&ac[1..], s, OneShotEndConfig::EndOnFirstPress) - } - "one-shot-release" => parse_one_shot(&ac[1..], s, OneShotEndConfig::EndOnFirstRelease), - "one-shot-press-pcancel" => { + TAP_HOLD_RELEASE_KEYS => parse_tap_hold_release_keys(&ac[1..], s), + MULTI => parse_multi(&ac[1..], s), + MACRO => parse_macro(&ac[1..], s, RepeatMacro::No), + MACRO_REPEAT => parse_macro(&ac[1..], s, RepeatMacro::Yes), + MACRO_RELEASE_CANCEL => parse_macro_release_cancel(&ac[1..], s, RepeatMacro::No), + MACRO_REPEAT_RELEASE_CANCEL => parse_macro_release_cancel(&ac[1..], s, RepeatMacro::Yes), + UNICODE => parse_unicode(&ac[1..], s), + ONE_SHOT | ONE_SHOT_PRESS => parse_one_shot(&ac[1..], s, OneShotEndConfig::EndOnFirstPress), + ONE_SHOT_RELEASE => parse_one_shot(&ac[1..], s, OneShotEndConfig::EndOnFirstRelease), + ONE_SHOT_PRESS_PCANCEL => { parse_one_shot(&ac[1..], s, OneShotEndConfig::EndOnFirstPressOrRepress) } - "one-shot-release-pcancel" => { + ONE_SHOT_RELEASE_PCANCEL => { parse_one_shot(&ac[1..], s, OneShotEndConfig::EndOnFirstReleaseOrRepress) } - "tap-dance" => parse_tap_dance(&ac[1..], s, TapDanceConfig::Lazy), - "tap-dance-eager" => parse_tap_dance(&ac[1..], s, TapDanceConfig::Eager), - "chord" => parse_chord(&ac[1..], s), - "release-key" => parse_release_key(&ac[1..], s), - "release-layer" => parse_release_layer(&ac[1..], s), - "on-press-fakekey" => parse_fake_key_op(&ac[1..], s), - "on-release-fakekey" => parse_on_release_fake_key_op(&ac[1..], s), - "on-press-fakekey-delay" => parse_fake_key_delay(&ac[1..], s), - "on-release-fakekey-delay" => parse_on_release_fake_key_delay(&ac[1..], s), - "on-idle-fakekey" => parse_on_idle_fakekey(&ac[1..], s), - "mwheel-up" => parse_mwheel(&ac[1..], MWheelDirection::Up, s), - "mwheel-down" => parse_mwheel(&ac[1..], MWheelDirection::Down, s), - "mwheel-left" => parse_mwheel(&ac[1..], MWheelDirection::Left, s), - "mwheel-right" => parse_mwheel(&ac[1..], MWheelDirection::Right, s), - "movemouse-up" => parse_move_mouse(&ac[1..], MoveDirection::Up, s), - "movemouse-down" => parse_move_mouse(&ac[1..], MoveDirection::Down, s), - "movemouse-left" => parse_move_mouse(&ac[1..], MoveDirection::Left, s), - "movemouse-right" => parse_move_mouse(&ac[1..], MoveDirection::Right, s), - "movemouse-accel-up" => parse_move_mouse_accel(&ac[1..], MoveDirection::Up, s), - "movemouse-accel-down" => parse_move_mouse_accel(&ac[1..], MoveDirection::Down, s), - "movemouse-accel-left" => parse_move_mouse_accel(&ac[1..], MoveDirection::Left, s), - "movemouse-accel-right" => parse_move_mouse_accel(&ac[1..], MoveDirection::Right, s), - "movemouse-speed" => parse_move_mouse_speed(&ac[1..], s), - "setmouse" => parse_set_mouse(&ac[1..], s), - "dynamic-macro-record" => parse_dynamic_macro_record(&ac[1..], s), - "dynamic-macro-play" => parse_dynamic_macro_play(&ac[1..], s), - "arbitrary-code" => parse_arbitrary_code(&ac[1..], s), - "cmd" => parse_cmd(&ac[1..], s, CmdType::Standard), - "cmd-output-keys" => parse_cmd(&ac[1..], s, CmdType::OutputKeys), - "fork" => parse_fork(&ac[1..], s), - "caps-word" => parse_caps_word(&ac[1..], s), - "caps-word-custom" => parse_caps_word_custom(&ac[1..], s), - "dynamic-macro-record-stop-truncate" => parse_macro_record_stop_truncate(&ac[1..], s), - "switch" => parse_switch(&ac[1..], s), - "sequence" => parse_sequence_start(&ac[1..], s), - _ => bail_expr!(&ac[0], "Unknown action type: {ac_type}"), + TAP_DANCE => parse_tap_dance(&ac[1..], s, TapDanceConfig::Lazy), + TAP_DANCE_EAGER => parse_tap_dance(&ac[1..], s, TapDanceConfig::Eager), + CHORD => parse_chord(&ac[1..], s), + RELEASE_KEY => parse_release_key(&ac[1..], s), + RELEASE_LAYER => parse_release_layer(&ac[1..], s), + ON_PRESS_FAKEKEY => parse_fake_key_op(&ac[1..], s), + ON_RELEASE_FAKEKEY => parse_on_release_fake_key_op(&ac[1..], s), + ON_PRESS_FAKEKEY_DELAY => parse_fake_key_delay(&ac[1..], s), + ON_RELEASE_FAKEKEY_DELAY => parse_on_release_fake_key_delay(&ac[1..], s), + ON_IDLE_FAKEKEY => parse_on_idle_fakekey(&ac[1..], s), + MWHEEL_UP => parse_mwheel(&ac[1..], MWheelDirection::Up, s), + MWHEEL_DOWN => parse_mwheel(&ac[1..], MWheelDirection::Down, s), + MWHEEL_LEFT => parse_mwheel(&ac[1..], MWheelDirection::Left, s), + MWHEEL_RIGHT => parse_mwheel(&ac[1..], MWheelDirection::Right, s), + MOVEMOUSE_UP => parse_move_mouse(&ac[1..], MoveDirection::Up, s), + MOVEMOUSE_DOWN => parse_move_mouse(&ac[1..], MoveDirection::Down, s), + MOVEMOUSE_LEFT => parse_move_mouse(&ac[1..], MoveDirection::Left, s), + MOVEMOUSE_RIGHT => parse_move_mouse(&ac[1..], MoveDirection::Right, s), + MOVEMOUSE_ACCEL_UP => parse_move_mouse_accel(&ac[1..], MoveDirection::Up, s), + MOVEMOUSE_ACCEL_DOWN => parse_move_mouse_accel(&ac[1..], MoveDirection::Down, s), + MOVEMOUSE_ACCEL_LEFT => parse_move_mouse_accel(&ac[1..], MoveDirection::Left, s), + MOVEMOUSE_ACCEL_RIGHT => parse_move_mouse_accel(&ac[1..], MoveDirection::Right, s), + MOVEMOUSE_SPEED => parse_move_mouse_speed(&ac[1..], s), + SETMOUSE => parse_set_mouse(&ac[1..], s), + DYNAMIC_MACRO_RECORD => parse_dynamic_macro_record(&ac[1..], s), + DYNAMIC_MACRO_PLAY => parse_dynamic_macro_play(&ac[1..], s), + ARBITRARY_CODE => parse_arbitrary_code(&ac[1..], s), + CMD => parse_cmd(&ac[1..], s, CmdType::Standard), + CMD_OUTPUT_KEYS => parse_cmd(&ac[1..], s, CmdType::OutputKeys), + FORK => parse_fork(&ac[1..], s), + CAPS_WORD => parse_caps_word(&ac[1..], s), + CAPS_WORD_CUSTOM => parse_caps_word_custom(&ac[1..], s), + DYNAMIC_MACRO_RECORD_STOP_TRUNCATE => parse_macro_record_stop_truncate(&ac[1..], s), + SWITCH => parse_switch(&ac[1..], s), + SEQUENCE => parse_sequence_start(&ac[1..], s), + _ => unreachable!(), } } diff --git a/parser/src/cfg/tests.rs b/parser/src/cfg/tests.rs index c5ba35040..cbdfd9a6e 100644 --- a/parser/src/cfg/tests.rs +++ b/parser/src/cfg/tests.rs @@ -686,8 +686,9 @@ fn parse_bad_submacro() { get_file_content_fn: &mut |_| unimplemented!(), }, ) - .map_err(|e| { - eprintln!("{:?}", e); + .map_err(|_e| { + // uncomment to see what this looks like when running test + // eprintln!("{:?}", _e); "" }) .unwrap_err(); @@ -715,8 +716,9 @@ fn parse_bad_submacro_2() { get_file_content_fn: &mut |_| unimplemented!(), }, ) - .map_err(|e| { - eprintln!("{:?}", e); + .map_err(|_e| { + // uncomment to see what this looks like when running test + // eprintln!("{:?}", _e); "" }) .unwrap_err(); @@ -1105,3 +1107,34 @@ fn use_default_overridable_mappings() { ) .expect("succeeds"); } + +#[test] +fn list_action_not_in_list_error_message_is_good() { + let _lk = match CFG_PARSE_LOCK.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + }; + let mut s = ParsedState::default(); + let source = r#" +(defsrc a) +(defalias hello + one-shot 1 2 +) +(deflayer base hello) +"#; + parse_cfg_raw_string( + source, + &mut s, + &PathBuf::from("test"), + &mut FileContentProvider { + get_file_content_fn: &mut |_| unimplemented!(), + }, + ) + .map_err(|e| { + assert_eq!( + e.msg, + "This is a list action and must be in parentheses: (one-shot ...)" + ); + }) + .unwrap_err(); +} From 4048e49fdf7270bece3da7218ca64202bc2539f8 Mon Sep 17 00:00:00 2001 From: rszyma Date: Mon, 25 Sep 2023 06:00:49 +0200 Subject: [PATCH 050/819] feat: add movemouse-inherit-accel-state cfg option (#558) --- cfg_samples/kanata.kbd | 9 ++++++ docs/config.adoc | 22 +++++++++++++ parser/src/cfg/mod.rs | 1 + src/kanata/mod.rs | 72 +++++++++++++++++++++++++++++++----------- 4 files changed, 86 insertions(+), 18 deletions(-) diff --git a/cfg_samples/kanata.kbd b/cfg_samples/kanata.kbd index 991d17d8b..fd6b890ce 100644 --- a/cfg_samples/kanata.kbd +++ b/cfg_samples/kanata.kbd @@ -153,6 +153,15 @@ If you need help, please feel welcome to ask in the GitHub discussions. ;; which is the layer active upon startup, that is in the same position. ;; ;; delegate-to-first-layer yes + + ;; This config entry alters the behavior of movemouse-accel actions. + ;; By default, this setting is disabled - vertical and horizontal + ;; acceleration are independent. Enabling this setting will emulate QMK mouse + ;; move acceleration behavior, i.e. the acceleration state of new mouse + ;; movement actions are inherited if others are already being pressed. + ;; + ;; movemouse-inherit-accel-state yes + ) ;; deflocalkeys-* enables you to define and use key names that match your locale diff --git a/docs/config.adoc b/docs/config.adoc index 255c57405..0b3de108b 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -392,6 +392,26 @@ For more context, see https://github.com/jtroo/kanata/issues/435. ) ---- + +[[movemouse-inherit-accel-state]] +=== movemouse-inherit-accel-state +<> + +By default `movemouse-accel` actions will track the acceleration +state for vertical and horizontal axes separately. + +When this setting is enabled, `movemouse-accel` will behave exactly like mouse movements in https://qmk.fm[QMK], +i.e. the acceleration state of new mouse +movement actions will be inherited if others are already being pressed. + +.Example: +[source] +---- +(defcfg + movemouse-inherit-accel-state yes +) +---- + [[linux-only-linux-dev]] === Linux only: linux-dev <> @@ -1154,6 +1174,8 @@ takes (unit: ms) to linearly ramp up from the minimum distance to the maximum distance. The third and fourth numbers are the minimum and maximum distances (unit: pixels) of each movement. +There is a toggable defcfg option related to `movemouse-accel` - <>. You might want to enable it, especially if you're coming from QMK. + [[set-mouse]] ==== Set absolute mouse position <> diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index ff4a832ed..d5ec52e15 100644 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -753,6 +753,7 @@ fn parse_defcfg(expr: &[SExpr]) -> Result> { "log-layer-changes", "delegate-to-first-layer", "linux-continue-if-no-devs-found", + "movemouse-inherit-accel-state", ]; let mut cfg = HashMap::default(); let mut exprs = check_first_expr(expr.iter(), "defcfg")?; diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index 1554214e0..de2d6173a 100644 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -144,6 +144,9 @@ pub struct Kanata { pub waiting_for_idle: HashSet, /// Number of ticks since kanata was idle. pub ticks_since_idle: u16, + /// If a mousemove action is active and another mousemove action is activated, + /// reuse the acceleration state. + movemouse_inherit_accel_state: bool, } pub struct ScrollState { @@ -161,6 +164,7 @@ pub struct MoveMouseState { pub move_mouse_accel_state: Option, } +#[derive(Clone, Copy)] pub struct MoveMouseAccelState { pub accel_ticks_from_min: u16, pub accel_ticks_until_max: u16, @@ -345,6 +349,11 @@ impl Kanata { dynamic_macros: Default::default(), log_layer_changes, caps_word: None, + movemouse_inherit_accel_state: cfg + .items + .get("movemouse-inherit-accel-state") + .map(|s| TRUE_VALUES.contains(&s.to_lowercase().as_str())) + .unwrap_or_default(), #[cfg(target_os = "linux")] defcfg_items: cfg.items, waiting_for_idle: HashSet::default(), @@ -383,6 +392,11 @@ impl Kanata { self.sequences = cfg.sequences; self.overrides = cfg.overrides; self.log_layer_changes = log_layer_changes; + self.movemouse_inherit_accel_state = cfg + .items + .get("movemouse-inherit-accel-state") + .map(|s| TRUE_VALUES.contains(&s.to_lowercase().as_str())) + .unwrap_or_default(); *MAPPED_KEYS.lock() = cfg.mapped_keys; Kanata::set_repeat_rate(&cfg.items)?; log::info!("Live reload successful"); @@ -918,10 +932,44 @@ impl Kanata { min_distance, max_distance, } => { - let f_max_distance: f64 = *max_distance as f64; - let f_min_distance: f64 = *min_distance as f64; - let f_accel_time: f64 = *accel_time as f64; - let increment = (f_max_distance - f_min_distance) / f_accel_time; + let move_mouse_accel_state = match ( + self.movemouse_inherit_accel_state, + &self.move_mouse_state_horizontal, + &self.move_mouse_state_vertical, + ) { + ( + true, + Some(MoveMouseState { + move_mouse_accel_state: Some(s), + .. + }), + _, + ) + | ( + true, + _, + Some(MoveMouseState { + move_mouse_accel_state: Some(s), + .. + }), + ) => *s, + _ => { + let f_max_distance: f64 = *max_distance as f64; + let f_min_distance: f64 = *min_distance as f64; + let f_accel_time: f64 = *accel_time as f64; + let increment = + (f_max_distance - f_min_distance) / f_accel_time; + + MoveMouseAccelState { + accel_ticks_from_min: 0, + accel_ticks_until_max: *accel_time, + accel_increment: increment, + min_distance: *min_distance, + max_distance: *max_distance, + } + } + }; + match direction { MoveDirection::Up | MoveDirection::Down => { self.move_mouse_state_vertical = Some(MoveMouseState { @@ -929,13 +977,7 @@ impl Kanata { distance: *min_distance, ticks_until_move: 0, interval: *interval, - move_mouse_accel_state: Some(MoveMouseAccelState { - accel_ticks_from_min: 0, - accel_ticks_until_max: *accel_time, - accel_increment: increment, - min_distance: *min_distance, - max_distance: *max_distance, - }), + move_mouse_accel_state: Some(move_mouse_accel_state), }) } MoveDirection::Left | MoveDirection::Right => { @@ -944,13 +986,7 @@ impl Kanata { distance: *min_distance, ticks_until_move: 0, interval: *interval, - move_mouse_accel_state: Some(MoveMouseAccelState { - accel_ticks_from_min: 0, - accel_ticks_until_max: *accel_time, - accel_increment: increment, - min_distance: *min_distance, - max_distance: *max_distance, - }), + move_mouse_accel_state: Some(move_mouse_accel_state), }) } } From 863f64416fcbf8a152920c8908b40b587ca74638 Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 24 Sep 2023 21:02:26 -0700 Subject: [PATCH 051/819] doc: add movemouse-inherit-accel-state yes to all defcfg opts example --- docs/config.adoc | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/config.adoc b/docs/config.adoc index 0b3de108b..ad9afb985 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -666,6 +666,7 @@ a non-applicable operating system. linux-x11-repeat-delay-rate 400,50 windows-altgr add-lctl-release windows-interception-mouse-hwid "70, 0, 60, 0" + movemouse-inherit-accel-state yes ) ---- From 3cc0a3e9d298fa955895289744018eb7ea7442b4 Mon Sep 17 00:00:00 2001 From: jtroo Date: Thu, 5 Oct 2023 22:26:32 -0700 Subject: [PATCH 052/819] doc: fix header in config.adoc --- docs/config.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config.adoc b/docs/config.adoc index ad9afb985..87c961aff 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -933,7 +933,7 @@ NOTE: If using Linux, make sure to look at the ) ---- -[[output-chords-combos]] +[[output-chordscombos]] === Output chords/combos <> From 5abe16b80ae11ac228e354ab1fab53272acc9b49 Mon Sep 17 00:00:00 2001 From: rszyma Date: Sun, 8 Oct 2023 00:02:13 +0200 Subject: [PATCH 053/819] refactor(parser): allow specifying which deflocalkeys variant to use (#576) Allows passing which deflocalkeys variant to use as argument to parse function. This commit allows allow https://github.com/rszyma/vscode-kanata/pull/4 to move forward. --- parser/src/cfg/mod.rs | 6 ++++-- parser/src/cfg/tests.rs | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index d5ec52e15..476a8b8d3 100644 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -318,7 +318,8 @@ fn parse_cfg_raw( .get_file_content(&cfg_file_name) .map_err(|e| miette::miette!(e))?; - parse_cfg_raw_string(&text, s, p, &mut file_content_provider).map_err(|e| e.into()) + parse_cfg_raw_string(&text, s, p, &mut file_content_provider, DEF_LOCAL_KEYS) + .map_err(|e| e.into()) } fn expand_includes( @@ -368,6 +369,7 @@ pub fn parse_cfg_raw_string( s: &mut ParsedState, cfg_path: &Path, file_content_provider: &mut FileContentProvider, + def_local_keys_variant_to_apply: &str, ) -> Result<( HashMap, MappedKeys, @@ -420,7 +422,7 @@ pub fn parse_cfg_raw_string( .map(|custom_keys| parse_deflocalkeys(def_local_keys_variant, custom_keys)) { let mapping = result?; - if def_local_keys_variant == DEF_LOCAL_KEYS { + if def_local_keys_variant == def_local_keys_variant_to_apply { assert!( local_keys.is_none(), ">1 mutually exclusive deflocalkeys variants was parsed" diff --git a/parser/src/cfg/tests.rs b/parser/src/cfg/tests.rs index cbdfd9a6e..cdac0ef78 100644 --- a/parser/src/cfg/tests.rs +++ b/parser/src/cfg/tests.rs @@ -45,6 +45,7 @@ fn span_works_with_unicode_characters() { &mut FileContentProvider { get_file_content_fn: &mut |_| unimplemented!(), }, + DEF_LOCAL_KEYS, ) .expect_err("should be an error because @😊 is not defined") .span @@ -82,6 +83,7 @@ fn test_multiline_error_span() { &mut FileContentProvider { get_file_content_fn: &mut |_| unimplemented!(), }, + DEF_LOCAL_KEYS, ) .expect_err("should error on unknown top level block") .span @@ -111,6 +113,7 @@ fn test_span_of_an_unterminated_block_comment_error() { &mut FileContentProvider { get_file_content_fn: &mut |_| unimplemented!(), }, + DEF_LOCAL_KEYS, ) .expect_err("should be an unterminated comment error") .span @@ -201,6 +204,7 @@ fn parse_action_vars() { &mut FileContentProvider { get_file_content_fn: &mut |_| unimplemented!(), }, + DEF_LOCAL_KEYS, ) .unwrap(); } @@ -225,6 +229,7 @@ fn parse_delegate_to_default_layer_yes() { &mut FileContentProvider { get_file_content_fn: &mut |_| unimplemented!(), }, + DEF_LOCAL_KEYS, ) .unwrap(); assert_eq!( @@ -253,6 +258,7 @@ fn parse_delegate_to_default_layer_yes_but_base_transparent() { &mut FileContentProvider { get_file_content_fn: &mut |_| unimplemented!(), }, + DEF_LOCAL_KEYS, ) .unwrap(); assert_eq!( @@ -281,6 +287,7 @@ fn parse_delegate_to_default_layer_no() { &mut FileContentProvider { get_file_content_fn: &mut |_| unimplemented!(), }, + DEF_LOCAL_KEYS, ) .unwrap(); assert_eq!( @@ -685,6 +692,7 @@ fn parse_bad_submacro() { &mut FileContentProvider { get_file_content_fn: &mut |_| unimplemented!(), }, + DEF_LOCAL_KEYS, ) .map_err(|_e| { // uncomment to see what this looks like when running test @@ -715,6 +723,7 @@ fn parse_bad_submacro_2() { &mut FileContentProvider { get_file_content_fn: &mut |_| unimplemented!(), }, + DEF_LOCAL_KEYS, ) .map_err(|_e| { // uncomment to see what this looks like when running test @@ -750,6 +759,7 @@ fn parse_switch() { &mut FileContentProvider { get_file_content_fn: &mut |_| unimplemented!(), }, + DEF_LOCAL_KEYS, ) .unwrap(); assert_eq!( @@ -822,6 +832,7 @@ fn parse_switch_exceed_depth() { &mut FileContentProvider { get_file_content_fn: &mut |_| unimplemented!(), }, + DEF_LOCAL_KEYS, ) .map_err(|_e| { // uncomment to see what this looks like when running test @@ -853,6 +864,7 @@ fn parse_on_idle_fakekey() { &mut FileContentProvider { get_file_content_fn: &mut |_| unimplemented!(), }, + DEF_LOCAL_KEYS, ) .map_err(|_e| { // uncomment to see what this looks like when running test @@ -895,6 +907,7 @@ fn parse_on_idle_fakekey_errors() { &mut FileContentProvider { get_file_content_fn: &mut |_| unimplemented!(), }, + DEF_LOCAL_KEYS, ) .map_err(|_e| { // comment out to see what this looks like when running test @@ -918,6 +931,7 @@ fn parse_on_idle_fakekey_errors() { &mut FileContentProvider { get_file_content_fn: &mut |_| unimplemented!(), }, + DEF_LOCAL_KEYS, ) .map_err(|_e| { // uncomment to see what this looks like when running test @@ -941,6 +955,7 @@ fn parse_on_idle_fakekey_errors() { &mut FileContentProvider { get_file_content_fn: &mut |_| unimplemented!(), }, + DEF_LOCAL_KEYS, ) .map_err(|_e| { // uncomment to see what this looks like when running test @@ -964,6 +979,7 @@ fn parse_on_idle_fakekey_errors() { &mut FileContentProvider { get_file_content_fn: &mut |_| unimplemented!(), }, + DEF_LOCAL_KEYS, ) .map_err(|_e| { // uncomment to see what this looks like when running test @@ -1082,6 +1098,7 @@ new 316 &mut FileContentProvider { get_file_content_fn: &mut |_| unimplemented!(), }, + DEF_LOCAL_KEYS, ) .expect("succeeds"); } @@ -1104,6 +1121,7 @@ fn use_default_overridable_mappings() { &mut FileContentProvider { get_file_content_fn: &mut |_| unimplemented!(), }, + DEF_LOCAL_KEYS, ) .expect("succeeds"); } @@ -1129,6 +1147,7 @@ fn list_action_not_in_list_error_message_is_good() { &mut FileContentProvider { get_file_content_fn: &mut |_| unimplemented!(), }, + DEF_LOCAL_KEYS, ) .map_err(|e| { assert_eq!( From ef23c68034dc4fa2541db69712ecc4359cafdeea Mon Sep 17 00:00:00 2001 From: jtroo Date: Sat, 7 Oct 2023 15:18:22 -0700 Subject: [PATCH 054/819] feat: add movemouse-smooth-diagonals (#573) Co-authored-by: rszyma --- cfg_samples/kanata.kbd | 7 ++ docs/config.adoc | 22 +++++- parser/src/cfg/mod.rs | 1 + src/kanata/mod.rs | 120 +++++++++++++++++++++++------- src/oskbd/linux.rs | 28 +++++-- src/oskbd/windows/interception.rs | 33 +++++++- src/oskbd/windows/llhook.rs | 26 ++++++- 7 files changed, 200 insertions(+), 37 deletions(-) diff --git a/cfg_samples/kanata.kbd b/cfg_samples/kanata.kbd index fd6b890ce..7b31751df 100644 --- a/cfg_samples/kanata.kbd +++ b/cfg_samples/kanata.kbd @@ -162,6 +162,13 @@ If you need help, please feel welcome to ask in the GitHub discussions. ;; ;; movemouse-inherit-accel-state yes + ;; This config entry alters the behavior of movemouseaccel actions. + ;; This makes diagonal movements simultaneous to mitigate choppiness in + ;; drawing apps, if you're using kanata mouse movements to draw for + ;; whatever reason. + ;; + ;; movemouse-smooth-diagonals yes + ) ;; deflocalkeys-* enables you to define and use key names that match your locale diff --git a/docs/config.adoc b/docs/config.adoc index 87c961aff..d6bd806a6 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -412,6 +412,25 @@ movement actions will be inherited if others are already being pressed. ) ---- +[[movemouse-smooth-diagonals]] +== movemouse-smooth-diagonals +<> + +By default, mouse movements move one direction at a time +and vertical/horizontal movements are on independent timers. + +This can result in non-smooth diagonals when drawing a line in some app. +This option adds a small imperceptible amount of latency to +synchronize the mouse movements. + +.Example: +[source] +---- +(defcfg + movemouse-smooth-diagonals yes +) +---- + [[linux-only-linux-dev]] === Linux only: linux-dev <> @@ -657,6 +676,8 @@ a non-applicable operating system. sequence-backtrack-modcancel no log-layer-changes no delegate-to-first-layer yes + movemouse-inherit-accel-state yes + movemouse-smooth-diagonals yes linux-dev /dev/input/dev1:/dev/input/dev2 linux-dev-names-include "Name 1:Name 2" linux-dev-names-exclude "Name 3:Name 4" @@ -666,7 +687,6 @@ a non-applicable operating system. linux-x11-repeat-delay-rate 400,50 windows-altgr add-lctl-release windows-interception-mouse-hwid "70, 0, 60, 0" - movemouse-inherit-accel-state yes ) ---- diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index 476a8b8d3..4feeb332d 100644 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -755,6 +755,7 @@ fn parse_defcfg(expr: &[SExpr]) -> Result> { "log-layer-changes", "delegate-to-first-layer", "linux-continue-if-no-devs-found", + "movemouse-smooth-diagonals", "movemouse-inherit-accel-state", ]; let mut cfg = HashMap::default(); diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index de2d6173a..f8b147cbf 100644 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -147,6 +147,34 @@ pub struct Kanata { /// If a mousemove action is active and another mousemove action is activated, /// reuse the acceleration state. movemouse_inherit_accel_state: bool, + /// Removes jaggedneess of vertical and horizontal mouse movements when used + /// simultaneously at the cost of increased mousemove actions latency. + movemouse_smooth_diagonals: bool, + /// If movemouse_smooth_diagonals is enabled, the previous mouse actions + /// gets stored in this buffer and if the next movemouse action is opposite axis + /// than the one stored in the buffer, both events are outputted at the same time. + movemouse_buffer: Option<(Axis, CalculatedMouseMove)>, +} + +#[derive(PartialEq, Clone, Copy)] +pub enum Axis { + Vertical, + Horizontal, +} + +impl From for Axis { + fn from(val: MoveDirection) -> Axis { + match val { + MoveDirection::Up | MoveDirection::Down => Axis::Vertical, + MoveDirection::Left | MoveDirection::Right => Axis::Horizontal, + } + } +} + +#[derive(Clone, Copy)] +pub struct CalculatedMouseMove { + pub direction: MoveDirection, + pub distance: u16, } pub struct ScrollState { @@ -349,6 +377,11 @@ impl Kanata { dynamic_macros: Default::default(), log_layer_changes, caps_word: None, + movemouse_smooth_diagonals: cfg + .items + .get("movemouse-smooth-diagonals") + .map(|s| TRUE_VALUES.contains(&s.to_lowercase().as_str())) + .unwrap_or_default(), movemouse_inherit_accel_state: cfg .items .get("movemouse-inherit-accel-state") @@ -358,6 +391,7 @@ impl Kanata { defcfg_items: cfg.items, waiting_for_idle: HashSet::default(), ticks_since_idle: 0, + movemouse_buffer: None, }) } @@ -392,6 +426,11 @@ impl Kanata { self.sequences = cfg.sequences; self.overrides = cfg.overrides; self.log_layer_changes = log_layer_changes; + self.movemouse_smooth_diagonals = cfg + .items + .get("movemouse-smooth-diagonals") + .map(|s| TRUE_VALUES.contains(&s.to_lowercase().as_str())) + .unwrap_or_default(); self.movemouse_inherit_accel_state = cfg .items .get("movemouse-inherit-accel-state") @@ -539,7 +578,32 @@ impl Kanata { let scaled_distance = apply_mouse_distance_modifiers(mmsv.distance, &self.move_mouse_speed_modifiers); log::debug!("handle_move_mouse: scaled vdistance: {}", scaled_distance); - self.kbd_out.move_mouse(mmsv.direction, scaled_distance)?; + + let current_move = CalculatedMouseMove { + direction: mmsv.direction, + distance: scaled_distance, + }; + + if self.movemouse_smooth_diagonals { + let axis: Axis = current_move.direction.into(); + match &self.movemouse_buffer { + Some((previous_axis, previous_move)) => { + if axis == *previous_axis { + self.kbd_out.move_mouse(*previous_move)?; + self.movemouse_buffer = Some((axis, current_move)); + } else { + self.kbd_out + .move_mouse_many(&[*previous_move, current_move])?; + self.movemouse_buffer = None; + } + } + None => { + self.movemouse_buffer = Some((axis, current_move)); + } + } + } else { + self.kbd_out.move_mouse(current_move)?; + } } else { mmsv.ticks_until_move -= 1; } @@ -561,7 +625,32 @@ impl Kanata { let scaled_distance = apply_mouse_distance_modifiers(mmsh.distance, &self.move_mouse_speed_modifiers); log::debug!("handle_move_mouse: scaled hdistance: {}", scaled_distance); - self.kbd_out.move_mouse(mmsh.direction, scaled_distance)?; + + let current_move = CalculatedMouseMove { + direction: mmsh.direction, + distance: scaled_distance, + }; + + if self.movemouse_smooth_diagonals { + let axis: Axis = current_move.direction.into(); + match &self.movemouse_buffer { + Some((previous_axis, previous_move)) => { + if axis == *previous_axis { + self.kbd_out.move_mouse(*previous_move)?; + self.movemouse_buffer = Some((axis, current_move)); + } else { + self.kbd_out + .move_mouse_many(&[*previous_move, current_move])?; + self.movemouse_buffer = None; + } + } + None => { + self.movemouse_buffer = Some((axis, current_move)); + } + } + } else { + self.kbd_out.move_mouse(current_move)?; + } } else { mmsh.ticks_until_move -= 1; } @@ -1217,7 +1306,8 @@ impl Kanata { } pbtn } - CustomAction::MoveMouse { direction, .. } => { + CustomAction::MoveMouse { direction, .. } + | CustomAction::MoveMouseAccel { direction, .. } => { match direction { MoveDirection::Up | MoveDirection::Down => { if let Some(move_mouse_state_vertical) = @@ -1238,28 +1328,8 @@ impl Kanata { } } } - pbtn - } - CustomAction::MoveMouseAccel { direction, .. } => { - match direction { - MoveDirection::Up | MoveDirection::Down => { - if let Some(move_mouse_state_vertical) = - &self.move_mouse_state_vertical - { - if move_mouse_state_vertical.direction == *direction { - self.move_mouse_state_vertical = None; - } - } - } - MoveDirection::Left | MoveDirection::Right => { - if let Some(move_mouse_state_horizontal) = - &self.move_mouse_state_horizontal - { - if move_mouse_state_horizontal.direction == *direction { - self.move_mouse_state_horizontal = None; - } - } - } + if self.movemouse_smooth_diagonals { + self.movemouse_buffer = None } pbtn } diff --git a/src/oskbd/linux.rs b/src/oskbd/linux.rs index 68c57ebfa..897361ff1 100644 --- a/src/oskbd/linux.rs +++ b/src/oskbd/linux.rs @@ -18,7 +18,7 @@ use std::path::PathBuf; use std::thread; use super::*; -use crate::oskbd::KeyEvent; +use crate::{kanata::CalculatedMouseMove, oskbd::KeyEvent}; use kanata_parser::custom_action::*; use kanata_parser::keys::*; @@ -540,16 +540,30 @@ impl KbdOut { } } - pub fn move_mouse(&mut self, direction: MoveDirection, distance: u16) -> Result<(), io::Error> { - let (axis, distance) = match direction { - MoveDirection::Up => (RelativeAxisType::REL_Y, -i32::from(distance)), - MoveDirection::Down => (RelativeAxisType::REL_Y, i32::from(distance)), - MoveDirection::Left => (RelativeAxisType::REL_X, -i32::from(distance)), - MoveDirection::Right => (RelativeAxisType::REL_X, i32::from(distance)), + pub fn move_mouse(&mut self, mv: CalculatedMouseMove) -> Result<(), io::Error> { + let (axis, distance) = match mv.direction { + MoveDirection::Up => (RelativeAxisType::REL_Y, -i32::from(mv.distance)), + MoveDirection::Down => (RelativeAxisType::REL_Y, i32::from(mv.distance)), + MoveDirection::Left => (RelativeAxisType::REL_X, -i32::from(mv.distance)), + MoveDirection::Right => (RelativeAxisType::REL_X, i32::from(mv.distance)), }; self.write(InputEvent::new(EventType::RELATIVE, axis.0, distance)) } + pub fn move_mouse_many(&mut self, moves: &[CalculatedMouseMove]) -> Result<(), io::Error> { + let mut events = vec![]; + for mv in moves { + let (axis, distance) = match mv.direction { + MoveDirection::Up => (RelativeAxisType::REL_Y, -i32::from(mv.distance)), + MoveDirection::Down => (RelativeAxisType::REL_Y, i32::from(mv.distance)), + MoveDirection::Left => (RelativeAxisType::REL_X, -i32::from(mv.distance)), + MoveDirection::Right => (RelativeAxisType::REL_X, i32::from(mv.distance)), + }; + events.push(InputEvent::new(EventType::RELATIVE, axis.0, distance)); + } + self.write_many(&events) + } + pub fn set_mouse(&mut self, _x: u16, _y: u16) -> Result<(), io::Error> { log::warn!("setmouse does not work in Linux yet. Maybe try out warpd:\n\thttps://github.com/rvaiya/warpd"); Ok(()) diff --git a/src/oskbd/windows/interception.rs b/src/oskbd/windows/interception.rs index a13ddca39..672f4c9d7 100644 --- a/src/oskbd/windows/interception.rs +++ b/src/oskbd/windows/interception.rs @@ -5,6 +5,7 @@ use std::io; use kanata_interception::{Interception, KeyState, MouseFlags, MouseState, Stroke}; use super::OsCodeWrapper; +use crate::kanata::CalculatedMouseMove; use crate::oskbd::KeyValue; use kanata_parser::custom_action::*; use kanata_parser::keys::*; @@ -94,6 +95,29 @@ impl InputEvent { }) } + fn from_mouse_move_many(moves: &[CalculatedMouseMove]) -> Self { + let mut x_acc = 0; + let mut y_acc = 0; + for mov in moves { + let acc_change = match mov.direction { + MoveDirection::Up => (0, -i32::from(mov.distance)), + MoveDirection::Down => (0, i32::from(mov.distance)), + MoveDirection::Left => (-i32::from(mov.distance), 0), + MoveDirection::Right => (i32::from(mov.distance), 0), + }; + x_acc += acc_change.0; + y_acc += acc_change.1; + } + Self(Stroke::Mouse { + state: MouseState::MOVE, + flags: MouseFlags::empty(), + rolling: 0, + x: x_acc, + y: y_acc, + information: 0, + }) + } + fn from_mouse_set(x: u16, y: u16) -> Self { Self(Stroke::Mouse { state: MouseState::MOVE, @@ -183,8 +207,13 @@ impl KbdOut { Ok(()) } - pub fn move_mouse(&mut self, direction: MoveDirection, distance: u16) -> Result<(), io::Error> { - write_interception(InputEvent::from_mouse_move(direction, distance)); + pub fn move_mouse(&mut self, mv: CalculatedMouseMove) -> Result<(), io::Error> { + write_interception(InputEvent::from_mouse_move(mv.direction, mv.distance)); + Ok(()) + } + + pub fn move_mouse_many(&mut self, moves: &[CalculatedMouseMove]) -> Result<(), io::Error> { + write_interception(InputEvent::from_mouse_move_many(moves)); Ok(()) } diff --git a/src/oskbd/windows/llhook.rs b/src/oskbd/windows/llhook.rs index f0c2f1c57..5b2c98961 100644 --- a/src/oskbd/windows/llhook.rs +++ b/src/oskbd/windows/llhook.rs @@ -12,6 +12,7 @@ use winapi::shared::minwindef::*; use winapi::shared::windef::*; use winapi::um::winuser::*; +use crate::kanata::CalculatedMouseMove; use crate::oskbd::{KeyEvent, KeyValue}; use kanata_parser::custom_action::*; use kanata_parser::keys::*; @@ -216,8 +217,13 @@ impl KbdOut { Ok(()) } - pub fn move_mouse(&mut self, direction: MoveDirection, distance: u16) -> Result<(), io::Error> { - move_mouse(direction, distance); + pub fn move_mouse(&mut self, mv: CalculatedMouseMove) -> Result<(), io::Error> { + move_mouse(mv.direction, mv.distance); + Ok(()) + } + + pub fn move_mouse_many(&mut self, moves: &[CalculatedMouseMove]) -> Result<(), io::Error> { + move_mouse_many(moves); Ok(()) } @@ -303,6 +309,22 @@ fn move_mouse(direction: MoveDirection, distance: u16) { } } +fn move_mouse_many(moves: &[CalculatedMouseMove]) { + let mut x_acc = 0; + let mut y_acc = 0; + for mov in moves { + let acc_change = match mov.direction { + MoveDirection::Up => (0, -i32::from(mov.distance)), + MoveDirection::Down => (0, i32::from(mov.distance)), + MoveDirection::Left => (-i32::from(mov.distance), 0), + MoveDirection::Right => (i32::from(mov.distance), 0), + }; + x_acc += acc_change.0; + y_acc += acc_change.1; + } + move_mouse_xy(x_acc, y_acc); +} + fn move_mouse_xy(x: i32, y: i32) { mouse_event(MOUSEEVENTF_MOVE, 0, x, y); } From 58cfdf6d6c4a13bf4bb58c58ede167d423f5ca7c Mon Sep 17 00:00:00 2001 From: jtroo Date: Wed, 11 Oct 2023 12:44:23 -0700 Subject: [PATCH 055/819] refactor: use bounded channel (#579) This commit attempts to fix an issue with excessive CPU and memory usage. At the time there are no steps to reproduce, but one suspicion is masses of rogue input events from the kernel. Thus the code is changed to use bounded channels with a reasonably large size (100) that I wouldn't expect to get filled up. --- src/kanata/linux.rs | 4 ++-- src/kanata/mod.rs | 4 ++-- src/kanata/windows/interception.rs | 4 ++-- src/kanata/windows/llhook.rs | 6 +++--- src/main.rs | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/kanata/linux.rs b/src/kanata/linux.rs index 97ff87f2e..b251b9df5 100644 --- a/src/kanata/linux.rs +++ b/src/kanata/linux.rs @@ -2,7 +2,7 @@ use anyhow::{anyhow, bail, Result}; use log::info; use parking_lot::Mutex; use std::convert::TryFrom; -use std::sync::mpsc::Sender; +use std::sync::mpsc::SyncSender as Sender; use std::sync::Arc; use super::*; @@ -63,7 +63,7 @@ impl Kanata { } // Send key events to the processing loop - if let Err(e) = tx.send(key_event) { + if let Err(e) = tx.try_send(key_event) { bail!("failed to send on channel: {}", e) } } diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index f8b147cbf..109ec09fc 100644 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -3,7 +3,7 @@ use anyhow::{anyhow, bail, Result}; use log::{error, info}; use parking_lot::Mutex; -use std::sync::mpsc::{Receiver, Sender, TryRecvError}; +use std::sync::mpsc::{Receiver, SyncSender as Sender, TryRecvError}; use kanata_keyberon::key_code::*; use kanata_keyberon::layout::*; @@ -1475,7 +1475,7 @@ impl Kanata { self.print_layer(cur_layer); if let Some(tx) = tx { - match tx.send(ServerMessage::LayerChange { new }) { + match tx.try_send(ServerMessage::LayerChange { new }) { Ok(_) => {} Err(error) => { log::error!("could not send event notification: {}", error); diff --git a/src/kanata/windows/interception.rs b/src/kanata/windows/interception.rs index c68d2602c..df45ef34c 100644 --- a/src/kanata/windows/interception.rs +++ b/src/kanata/windows/interception.rs @@ -1,7 +1,7 @@ use anyhow::Result; use kanata_interception as ic; use parking_lot::Mutex; -use std::sync::mpsc::Sender; +use std::sync::mpsc::SyncSender as Sender; use std::sync::Arc; use super::PRESSED_KEYS; @@ -112,7 +112,7 @@ impl Kanata { } _ => {} } - tx.send(key_event)?; + tx.try_send(key_event)?; } } } diff --git a/src/kanata/windows/llhook.rs b/src/kanata/windows/llhook.rs index 59e7a1153..e501fc49e 100644 --- a/src/kanata/windows/llhook.rs +++ b/src/kanata/windows/llhook.rs @@ -1,6 +1,6 @@ use parking_lot::Mutex; use std::convert::TryFrom; -use std::sync::mpsc::{channel, Receiver, Sender, TryRecvError}; +use std::sync::mpsc::{sync_channel, Receiver, SyncSender as Sender, TryRecvError}; use std::sync::Arc; use std::time; @@ -20,7 +20,7 @@ impl Kanata { }; native_windows_gui::init()?; - let (preprocess_tx, preprocess_rx) = channel(); + let (preprocess_tx, preprocess_rx) = sync_channel(100); start_event_preprocessor(preprocess_rx, tx); // This callback should return `false` if the input event is **not** handled by the @@ -73,7 +73,7 @@ impl Kanata { } fn try_send_panic(tx: &Sender, kev: KeyEvent) { - if let Err(e) = tx.send(kev) { + if let Err(e) = tx.try_send(kev) { panic!("failed to send on channel: {e:?}") } } diff --git a/src/main.rs b/src/main.rs index be5b3c431..9d3e1a444 100644 --- a/src/main.rs +++ b/src/main.rs @@ -172,13 +172,13 @@ fn main_impl() -> Result<()> { let (server, ntx, nrx) = if let Some(port) = args.port { let mut server = TcpServer::new(port); server.start(kanata_arc.clone()); - let (ntx, nrx) = std::sync::mpsc::channel(); + let (ntx, nrx) = std::sync::mpsc::sync_channel(100); (Some(server), Some(ntx), Some(nrx)) } else { (None, None, None) }; - let (tx, rx) = std::sync::mpsc::channel(); + let (tx, rx) = std::sync::mpsc::sync_channel(100); Kanata::start_processing_loop(kanata_arc.clone(), rx, ntx, args.nodelay); if let (Some(server), Some(nrx)) = (server, nrx) { From de5da3ac7cc4bbfa93af804a27e37df8cf09f776 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Donk=C3=B3?= Date: Thu, 19 Oct 2023 20:56:09 +0200 Subject: [PATCH 056/819] doc: fix minor copy-paste error in config.adoc (#589) --- docs/config.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config.adoc b/docs/config.adoc index d6bd806a6..866c8ca69 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -1928,7 +1928,7 @@ precisely, the action triggered is: ) (deffakekeys dotcom (macro . c o m) - dotorg (macro . c o m) + dotorg (macro . o r g) ) ---- From 59938f4f9f5e64a528aaae8b97c83df9b7f8c680 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 22 Oct 2023 01:39:43 +0000 Subject: [PATCH 057/819] chore(deps): bump rustix from 0.37.23 to 0.37.26 (#591) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 55f759901..048a8c20a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -791,9 +791,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.37.23" +version = "0.37.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" +checksum = "84f3f8f960ed3b5a59055428714943298bf3fa2d4a1d53135084e0544829d995" dependencies = [ "bitflags", "errno", From 9516cbb4295f9110c3aa11b2b3f6c66728ed67b0 Mon Sep 17 00:00:00 2001 From: Jan Tache Date: Tue, 24 Oct 2023 19:44:29 -0700 Subject: [PATCH 058/819] ver: use cargo crate versions --- Cargo.toml | 8 ++++---- parser/Cargo.toml | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1a21ec802..bd41974af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,13 +30,13 @@ is-terminal = "=0.4.7" # Pinned to avoid downloading and running binary blobs on Linux serde_derive = "=1.0.171" -# kanata-keyberon = "0.150.2" -# kanata-parser = "0.150.2" +kanata-keyberon = "0.150.2" +kanata-parser = "0.150.2" # Uncomment below and comment out above for testing local changes. # Otherwise any changes to the local files will not reflect in the compiled # binary. -kanata-keyberon = { path = "keyberon" } -kanata-parser = { path = "parser" } +# kanata-keyberon = { path = "keyberon" } +# kanata-parser = { path = "parser" } [target.'cfg(target_os = "linux")'.dependencies] evdev = "=0.12.0" diff --git a/parser/Cargo.toml b/parser/Cargo.toml index fc6f2f6a4..50fb94b02 100644 --- a/parser/Cargo.toml +++ b/parser/Cargo.toml @@ -20,11 +20,11 @@ rustc-hash = "1.1.0" miette = { version = "5.7.0", features = ["fancy"] } thiserror = "1.0.38" -# kanata-keyberon = "0.150.2" +kanata-keyberon = "0.150.2" # Uncomment below and comment out above for testing local changes. # Otherwise any changes to the local files will not reflect in the compiled # binary. -kanata-keyberon = { path = "../keyberon" } +# kanata-keyberon = { path = "../keyberon" } [features] cmd = [] From 49aa3307b95fc3a16c9e8253a5de222083f7b91c Mon Sep 17 00:00:00 2001 From: Fred Vatin <22337329+Fred-Vatin@users.noreply.github.com> Date: Thu, 26 Oct 2023 01:34:15 +0200 Subject: [PATCH 059/819] doc: add french source layout for Windows (#593) --- docs/locales.adoc | 54 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/docs/locales.adoc b/docs/locales.adoc index 86cd80906..c96a9b6c5 100644 --- a/docs/locales.adoc +++ b/docs/locales.adoc @@ -1,14 +1,29 @@ +//// +Commented out since it doesn't seem to add anything for now, but maybe in the future +:sectlinks: +:sectanchors: +//// + +ifdef::env-github[] +:tip-caption: :bulb: +:note-caption: :information_source: +:important-caption: :heavy_exclamation_mark: +:caution-caption: :fire: +:warning-caption: :warning: +endif::[] + = Keyboard locales -:toc: -:toc-placement!: -:toc-title!: -== Table of contents -toc::[] +//// +Commented out since doc is short enough without a ToC for the time being. +:toc: +:toc-title: pass:[TABLE OF CONTENTS] +:toclevels: 3 +//// -== ISO German QWERTZ - Windows (non-interception) +== ISO German QWERTZ (Windows, non-interception)[[german]] -=== Using `deflocalkeys-win`: +=== Using `deflocalkeys-win`:[[german-defwin]] [%collapsible] ==== @@ -35,7 +50,7 @@ toc::[] ---- ==== -=== Without using `deflocalkeys`: +=== Without using `deflocalkeys`:[[german-nodeflocalkeys]] [%collapsible] ==== @@ -50,7 +65,7 @@ toc::[] ---- ==== -=== Example aliases +=== Example aliases[[german-aliases]] [%collapsible] ==== @@ -100,3 +115,24 @@ toc::[] ) ---- ==== + +== ISO French AZERTY (Windows, non-interception)[[french]] + +NOTE: This source is for ISO105 arrangement of the http://kbdlayout.info/kbdfr/overview+virtualkeys[French AZERTY layout]. Tested on Windows only. + +[%collapsible] +==== +---- +(deflocalkeys-win + k252 223 ;; ref to the key [!] (VK_OEM_8) +) + +(defsrc ;; french + ' 1 2 3 4 5 6 7 8 9 0 [ eql bspc + tab a z e r t y u i o p ] ; + caps q s d f g h j k l m ` bksl ret + lsft nubs w x c v b n comm . / k252 rsft + lctl lmet lalt spc ralt rctl +) +---- +==== From 002f656408326922f79d24bb2219845737363b3d Mon Sep 17 00:00:00 2001 From: rszyma Date: Thu, 26 Oct 2023 01:34:46 +0200 Subject: [PATCH 060/819] doc: fix indentation in config.adoc ToC (#594) --- docs/config.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config.adoc b/docs/config.adoc index 866c8ca69..bf2db5bea 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -413,7 +413,7 @@ movement actions will be inherited if others are already being pressed. ---- [[movemouse-smooth-diagonals]] -== movemouse-smooth-diagonals +=== movemouse-smooth-diagonals <> By default, mouse movements move one direction at a time From 57a7440af877805db9d63409c168b6163ade9622 Mon Sep 17 00:00:00 2001 From: Jan Tache Date: Sat, 28 Oct 2023 21:58:32 -0700 Subject: [PATCH 061/819] ver: use local crates, change to 1.5.0-prerelease-3 --- Cargo.lock | 6 +++--- Cargo.toml | 10 +++++----- keyberon/Cargo.toml | 2 +- parser/Cargo.toml | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 048a8c20a..11523ef2e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -374,7 +374,7 @@ checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "kanata" -version = "1.5.0-prerelease-2" +version = "1.5.0-prerelease-3" dependencies = [ "anyhow", "clap", @@ -418,7 +418,7 @@ dependencies = [ [[package]] name = "kanata-keyberon" -version = "0.150.2" +version = "0.150.3" dependencies = [ "arraydeque", "heapless", @@ -437,7 +437,7 @@ dependencies = [ [[package]] name = "kanata-parser" -version = "0.150.2" +version = "0.150.3" dependencies = [ "anyhow", "kanata-keyberon", diff --git a/Cargo.toml b/Cargo.toml index bd41974af..93c09fbc4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kanata" -version = "1.5.0-prerelease-2" +version = "1.5.0-prerelease-3" authors = ["jtroo "] description = "Multi-layer keyboard customization" keywords = ["cli", "linux", "windows", "keyboard", "layout"] @@ -30,13 +30,13 @@ is-terminal = "=0.4.7" # Pinned to avoid downloading and running binary blobs on Linux serde_derive = "=1.0.171" -kanata-keyberon = "0.150.2" -kanata-parser = "0.150.2" +# kanata-keyberon = "0.150.3" +# kanata-parser = "0.150.3" # Uncomment below and comment out above for testing local changes. # Otherwise any changes to the local files will not reflect in the compiled # binary. -# kanata-keyberon = { path = "keyberon" } -# kanata-parser = { path = "parser" } +kanata-keyberon = { path = "keyberon" } +kanata-parser = { path = "parser" } [target.'cfg(target_os = "linux")'.dependencies] evdev = "=0.12.0" diff --git a/keyberon/Cargo.toml b/keyberon/Cargo.toml index b42e505b7..25b4417ca 100644 --- a/keyberon/Cargo.toml +++ b/keyberon/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kanata-keyberon" -version = "0.150.2" +version = "0.150.3" authors = ["Guillaume Pinot ", "Robin Krahl ", "jtroo "] edition = "2018" description = "Pure Rust keyboard firmware. Fork intended for use with kanata." diff --git a/parser/Cargo.toml b/parser/Cargo.toml index 50fb94b02..d072649a7 100644 --- a/parser/Cargo.toml +++ b/parser/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kanata-parser" -version = "0.150.2" +version = "0.150.3" authors = ["jtroo "] description = "A parser for configuration language of kanata, a keyboard remapper." keywords = ["kanata", "parser"] @@ -20,11 +20,11 @@ rustc-hash = "1.1.0" miette = { version = "5.7.0", features = ["fancy"] } thiserror = "1.0.38" -kanata-keyberon = "0.150.2" +# kanata-keyberon = "0.150.3" # Uncomment below and comment out above for testing local changes. # Otherwise any changes to the local files will not reflect in the compiled # binary. -# kanata-keyberon = { path = "../keyberon" } +kanata-keyberon = { path = "../keyberon" } [features] cmd = [] From 20b06e9d5a0792031afbead25bba232a52bf05f2 Mon Sep 17 00:00:00 2001 From: Kabelo Moiloa Date: Tue, 31 Oct 2023 07:47:57 +0000 Subject: [PATCH 062/819] dep: update serde_derive since it no longer ships with binary blobs. (#599) --- Cargo.lock | 209 +++++++++++++++++++++++++---------------------------- Cargo.toml | 4 +- 2 files changed, 102 insertions(+), 111 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 11523ef2e..bdad7e9e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] @@ -19,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "anstyle" -version = "1.0.1" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" [[package]] name = "anyhow" @@ -52,9 +52,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" -version = "0.3.68" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ "addr2line", "cc", @@ -94,15 +94,15 @@ dependencies = [ [[package]] name = "byteorder" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cc" -version = "1.0.82" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "305fe645edc1442a0fa8b6726ba61d422798d37a52e12eaecf4b022ebbb88f01" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ "libc", ] @@ -115,20 +115,19 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.3.23" +version = "4.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03aef18ddf7d879c15ce20f04826ef8418101c7e528014c3eeea13321047dca3" +checksum = "ac495e00dcec98c83465d5ad66c5c4fabd652fd6686e7c6269b117e729a6f17b" dependencies = [ "clap_builder", "clap_derive", - "once_cell", ] [[package]] name = "clap_builder" -version = "4.3.23" +version = "4.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ce6fffb678c9b80a70b6b6de0aad31df727623a70fd9a842c30cd573e2fa98" +checksum = "c77ed9a32a62e6ca27175d00d29d05ca32e396ea1eb5fb01d8256b669cec7663" dependencies = [ "anstyle", "clap_lex", @@ -137,9 +136,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.3.12" +version = "4.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" dependencies = [ "heck", "proc-macro2", @@ -149,9 +148,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" [[package]] name = "critical-section" @@ -161,9 +160,12 @@ checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216" [[package]] name = "deranged" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" +checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" +dependencies = [ + "powerfmt", +] [[package]] name = "dirs" @@ -206,25 +208,14 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.2" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" +checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" dependencies = [ - "errno-dragonfly", "libc", "windows-sys", ] -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] - [[package]] name = "evdev" version = "0.12.0" @@ -257,9 +248,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.27.3" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" [[package]] name = "hash32" @@ -272,9 +263,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.0" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" [[package]] name = "heapless" @@ -297,15 +288,15 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" [[package]] name = "indexmap" -version = "2.0.0" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" dependencies = [ "equivalent", "hashbrown", @@ -390,7 +381,7 @@ dependencies = [ "miette", "mio", "native-windows-gui", - "nix 0.26.2", + "nix 0.26.4", "once_cell", "parking_lot", "radix_trie", @@ -458,9 +449,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.147" +version = "0.2.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" [[package]] name = "linux-raw-sys" @@ -470,9 +461,9 @@ checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "lock_api" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" dependencies = [ "autocfg", "scopeguard", @@ -486,9 +477,9 @@ checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "memchr" -version = "2.5.0" +version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" [[package]] name = "memoffset" @@ -551,9 +542,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" dependencies = [ "libc", "log", @@ -597,16 +588,15 @@ dependencies = [ [[package]] name = "nix" -version = "0.26.2" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" dependencies = [ "bitflags", "cfg-if", "libc", "memoffset 0.7.1", "pin-utils", - "static_assertions", ] [[package]] @@ -641,9 +631,9 @@ dependencies = [ [[package]] name = "object" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" dependencies = [ "memchr", ] @@ -678,13 +668,13 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.3.5", + "redox_syscall 0.4.1", "smallvec", "windows-targets", ] @@ -695,6 +685,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -707,9 +703,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" dependencies = [ "unicode-ident", ] @@ -750,9 +746,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.3.5" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ "bitflags", ] @@ -791,9 +787,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.37.26" +version = "0.37.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84f3f8f960ed3b5a59055428714943298bf3fa2d4a1d53135084e0544829d995" +checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" dependencies = [ "bitflags", "errno", @@ -823,24 +819,24 @@ checksum = "621e3680f3e07db4c9c2c3fb07c6223ab2fab2e54bd3c04c3ae037990f428c32" [[package]] name = "semver" -version = "1.0.18" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" +checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" [[package]] name = "serde" -version = "1.0.171" +version = "1.0.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9" +checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.171" +version = "1.0.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682" +checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3" dependencies = [ "proc-macro2", "quote", @@ -849,9 +845,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.105" +version = "1.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" dependencies = [ "itoa", "ryu", @@ -890,15 +886,15 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" +checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" [[package]] name = "smawk" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" [[package]] name = "spin" @@ -915,12 +911,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - [[package]] name = "strsim" version = "0.10.0" @@ -929,9 +919,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "supports-color" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4950e7174bffabe99455511c39707310e7e9b440364a2fcb1cc21521be57b354" +checksum = "d6398cde53adc3c4557306a96ce67b302968513830a77a95b2b17305d9719a89" dependencies = [ "is-terminal", "is_ci", @@ -957,9 +947,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.29" +version = "2.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" +checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" dependencies = [ "proc-macro2", "quote", @@ -1004,18 +994,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.47" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.47" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", @@ -1024,14 +1014,15 @@ dependencies = [ [[package]] name = "time" -version = "0.3.26" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a79d09ac6b08c1ab3906a2f7cc2e81a0e27c7ae89c63812df75e52bef0751e07" +checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" dependencies = [ "deranged", "itoa", "libc", "num_threads", + "powerfmt", "serde", "time-core", "time-macros", @@ -1039,30 +1030,30 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.12" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75c65469ed6b3a4809d987a41eb1dc918e9bc1d92211cbad7ae82931846f7451" +checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" dependencies = [ "time-core", ] [[package]] name = "toml_datetime" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" [[package]] name = "toml_edit" -version = "0.19.14" +version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ "indexmap", "toml_datetime", @@ -1071,9 +1062,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-linebreak" @@ -1083,9 +1074,9 @@ checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] name = "unicode-width" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] name = "wasi" @@ -1117,9 +1108,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" dependencies = [ "winapi", ] @@ -1198,9 +1189,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "winnow" -version = "0.5.14" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d09770118a7eb1ccaf4a594a221334119a44a814fcb0d31c5b85e83e97227a97" +checksum = "176b6138793677221d420fd2f0aeeced263f197688b36484660da767bca2fa32" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 93c09fbc4..7086f2e8f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,8 @@ parking_lot = "0.12" once_cell = "1" serde = { version = "1", features = ["alloc", "derive"], default_features = false } serde_json = { version = "1", features = ["alloc"], default_features = false } +serde_derive = "1.0" + radix_trie = "0.2" rustc-hash = "1.1.0" miette = { version = "5.7.0", features = ["fancy"] } @@ -27,8 +29,6 @@ dirs = "5.0.1" # Pinned to avoid including multiple versions of a dependency is-terminal = "=0.4.7" -# Pinned to avoid downloading and running binary blobs on Linux -serde_derive = "=1.0.171" # kanata-keyberon = "0.150.3" # kanata-parser = "0.150.3" From 28bed1193e6cc65cb0afe069a17c0ac2228b8517 Mon Sep 17 00:00:00 2001 From: rszyma Date: Sat, 4 Nov 2023 05:42:08 +0100 Subject: [PATCH 063/819] feat: add mouse wheel events to defsrc (#592) This commit allows mapping mouse wheel events to defsrc. This is only supported on Linux and Interception. Co-authored-by: jtroo --- parser/src/cfg/mod.rs | 28 +++++++++ parser/src/custom_action.rs | 19 ++++++ parser/src/keys/mod.rs | 20 +++++++ src/kanata/linux.rs | 96 ++++++++++++++++++++++++++---- src/kanata/mod.rs | 19 ++++-- src/kanata/windows/interception.rs | 39 +++++++++--- src/oskbd/linux.rs | 25 ++++++++ src/oskbd/mod.rs | 1 + src/oskbd/windows/interception.rs | 1 + src/oskbd/windows/mod.rs | 3 + 10 files changed, 228 insertions(+), 23 deletions(-) diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index 4feeb332d..a413caf84 100644 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -1209,6 +1209,34 @@ fn parse_action_atom(ac_span: &Spanned, s: &ParsedState) -> Result<&'sta s.a.sref(s.a.sref_slice(CustomAction::MouseTap(Btn::Backward))), ))) } + "mwu" | "mousewheelup" => { + return Ok(s.a.sref(Action::Custom(s.a.sref(s.a.sref_slice( + CustomAction::MWheelNotch { + direction: MWheelDirection::Up, + }, + ))))) + } + "mwd" | "mousewheeldown" => { + return Ok(s.a.sref(Action::Custom(s.a.sref(s.a.sref_slice( + CustomAction::MWheelNotch { + direction: MWheelDirection::Down, + }, + ))))) + } + "mwl" | "mousewheelleft" => { + return Ok(s.a.sref(Action::Custom(s.a.sref(s.a.sref_slice( + CustomAction::MWheelNotch { + direction: MWheelDirection::Left, + }, + ))))) + } + "mwr" | "mousewheelright" => { + return Ok(s.a.sref(Action::Custom(s.a.sref(s.a.sref_slice( + CustomAction::MWheelNotch { + direction: MWheelDirection::Right, + }, + ))))) + } "rpt" | "repeat" | "rpt-key" => { return Ok(s.a.sref(Action::Custom( s.a.sref(s.a.sref_slice(CustomAction::Repeat)), diff --git a/parser/src/custom_action.rs b/parser/src/custom_action.rs index 3d94049c2..5ea9baa84 100644 --- a/parser/src/custom_action.rs +++ b/parser/src/custom_action.rs @@ -6,6 +6,8 @@ use anyhow::{anyhow, Result}; use kanata_keyberon::key_code::KeyCode; +use crate::keys::OsCode; + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum CustomAction { Cmd(Vec), @@ -29,6 +31,9 @@ pub enum CustomAction { interval: u16, distance: u16, }, + MWheelNotch { + direction: MWheelDirection, + }, MoveMouse { direction: MoveDirection, interval: u16, @@ -101,6 +106,20 @@ pub enum MWheelDirection { Right, } +impl TryFrom for MWheelDirection { + type Error = (); + fn try_from(value: OsCode) -> Result { + use OsCode::*; + Ok(match value { + MouseWheelUp => MWheelDirection::Up, + MouseWheelDown => MWheelDirection::Down, + MouseWheelLeft => MWheelDirection::Left, + MouseWheelRight => MWheelDirection::Right, + _ => return Err(()), + }) + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum MoveDirection { Up, diff --git a/parser/src/keys/mod.rs b/parser/src/keys/mod.rs index c831a6ebb..873014c15 100644 --- a/parser/src/keys/mod.rs +++ b/parser/src/keys/mod.rs @@ -242,6 +242,16 @@ pub fn str_to_oscode(s: &str) -> Option { "mfwd" | "mouseforward" => OsCode::BTN_EXTRA, "mbck" | "mousebackward" => OsCode::BTN_SIDE, + // NOTE: these are linux and interception-only due to missing implementation for LLHOOK + #[cfg(any(target_os = "linux", target_os = "unknown", feature = "interception_driver"))] + "mwu" | "mousewheelup" => OsCode::MouseWheelUp, + #[cfg(any(target_os = "linux", target_os = "unknown", feature = "interception_driver"))] + "mwd" | "mousewheeldown" => OsCode::MouseWheelDown, + #[cfg(any(target_os = "linux", target_os = "unknown", feature = "interception_driver"))] + "mwl" | "mousewheelleft" => OsCode::MouseWheelLeft, + #[cfg(any(target_os = "linux", target_os = "unknown", feature = "interception_driver"))] + "mwr" | "mousewheelright" => OsCode::MouseWheelRight, + "hmpg" | "homepage" => OsCode::KEY_HOMEPAGE, "mdia" | "media" => OsCode::KEY_MEDIA, "mail" => OsCode::KEY_MAIL, @@ -1018,6 +1028,16 @@ pub enum OsCode { BTN_TRIGGER_HAPPY39 = 742, BTN_TRIGGER_HAPPY40 = 743, BTN_MAX = 744, + + // Mouse wheel events are not a part of EV_KEY, so they technically + // shouldn't be there, but they're still there, because this way + // it's easier to implement allowing to add them to defsrc without + // making tons of changes all over the codebase. + MouseWheelUp = 745, + MouseWheelDown = 746, + MouseWheelLeft = 747, + MouseWheelRight = 748, + KEY_MAX = 767, } diff --git a/src/kanata/linux.rs b/src/kanata/linux.rs index b251b9df5..0497b2269 100644 --- a/src/kanata/linux.rs +++ b/src/kanata/linux.rs @@ -1,4 +1,5 @@ use anyhow::{anyhow, bail, Result}; +use evdev::{InputEvent, InputEventKind, RelativeAxisType}; use log::info; use parking_lot::Mutex; use std::convert::TryFrom; @@ -35,11 +36,11 @@ impl Kanata { let events = kbd_in.read().map_err(|e| anyhow!("failed read: {}", e))?; log::trace!("{events:?}"); - for in_event in events.into_iter() { + for in_event in events.iter().copied() { let key_event = match KeyEvent::try_from(in_event) { Ok(ev) => ev, _ => { - // Pass-through non-key events + // Pass-through non-key and non-scroll events let mut kanata = kanata.lock(); kanata .kbd_out @@ -51,15 +52,23 @@ impl Kanata { check_for_exit(&key_event); - // Check if this keycode is mapped in the configuration. If it hasn't been mapped, send - // it immediately. - if !MAPPED_KEYS.lock().contains(&key_event.code) { - let mut kanata = kanata.lock(); - kanata - .kbd_out - .write_key(key_event.code, key_event.value) - .map_err(|e| anyhow!("failed write key: {}", e))?; - continue; + if key_event.value == KeyValue::Tap { + // Scroll event for sure. Only scroll events produce Tap. + if !handle_scroll(&kanata, in_event, key_event.code, &events)? { + continue; + } + } else { + // Handle normal keypresses. + // Check if this keycode is mapped in the configuration. + // If it hasn't been mapped, send it immediately. + if !MAPPED_KEYS.lock().contains(&key_event.code) { + let mut kanata = kanata.lock(); + kanata + .kbd_out + .write_raw(in_event) + .map_err(|e| anyhow!("failed write: {}", e))?; + continue; + }; } // Send key events to the processing loop @@ -113,3 +122,68 @@ impl Kanata { Ok(()) } } + +/// Returns true if the scroll event should be sent to the processing loop, otherwise returns +/// false. +fn handle_scroll( + kanata: &Mutex, + in_event: InputEvent, + code: OsCode, + all_events: &[InputEvent], +) -> Result { + let direction: MWheelDirection = code.try_into().unwrap(); + let scroll_distance = in_event.value().unsigned_abs() as u16; + match in_event.kind() { + InputEventKind::RelAxis(axis_type) => { + match axis_type { + RelativeAxisType::REL_WHEEL | RelativeAxisType::REL_HWHEEL => { + if MAPPED_KEYS.lock().contains(&code) { + return Ok(true); + } + // If we just used `write_raw` here, some of the scrolls issued by kanata would be + // REL_WHEEL_HI_RES + REL_WHEEL and some just REL_WHEEL and an issue like this one + // would happen: https://github.com/jtroo/kanata/issues/395 + // + // So to fix this case, we need to use `scroll` which will also send hi-res scrolls + // along normal scrolls. + // + // However, if this is a normal scroll event, it may be sent alongside a hi-res + // scroll event. In this scenario, the hi-res event should be used to call + // scroll, and not the normal event. Otherwise, too much scrolling will happen. + let mut kanata = kanata.lock(); + if !all_events.iter().any(|ev| { + matches!( + ev.kind(), + InputEventKind::RelAxis( + RelativeAxisType::REL_WHEEL_HI_RES + | RelativeAxisType::REL_HWHEEL_HI_RES + ) + ) + }) { + kanata + .kbd_out + .scroll(direction, scroll_distance * HI_RES_SCROLL_UNITS_IN_LO_RES) + .map_err(|e| anyhow!("failed write: {}", e))?; + } + Ok(false) + } + RelativeAxisType::REL_WHEEL_HI_RES | RelativeAxisType::REL_HWHEEL_HI_RES => { + if !MAPPED_KEYS.lock().contains(&code) { + // Passthrough if the scroll wheel event is not mapped + // in the configuration. + let mut kanata = kanata.lock(); + kanata + .kbd_out + .scroll(direction, scroll_distance) + .map_err(|e| anyhow!("failed write: {}", e))?; + } + // Kanata will not handle high resolution scroll events for now. + // Full notch scrolling only. + Ok(false) + } + _ => unreachable!("expect to be handling a wheel event"), + } + } + _ => unreachable!("expect to be handling a wheel event"), + } +} diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index 109ec09fc..624d12634 100644 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -443,7 +443,7 @@ impl Kanata { } /// Update keyberon layout state for press/release, handle repeat separately - fn handle_key_event(&mut self, event: &KeyEvent) -> Result<()> { + fn handle_input_event(&mut self, event: &KeyEvent) -> Result<()> { log::debug!("process recv ev {event:?}"); let evc: u16 = event.code.into(); self.ticks_since_idle = 0; @@ -466,6 +466,11 @@ impl Kanata { let ret = self.handle_repeat(event); return ret; } + KeyValue::Tap => { + self.layout.bm().event(Event::Press(0, evc)); + self.layout.bm().event(Event::Release(0, evc)); + return Ok(()); + } }; self.layout.bm().event(kbrn_ev); Ok(()) @@ -990,6 +995,10 @@ impl Kanata { }) } }, + CustomAction::MWheelNotch { direction } => { + self.kbd_out + .scroll(*direction, HI_RES_SCROLL_UNITS_IN_LO_RES)?; + } CustomAction::MoveMouse { direction, interval, @@ -1600,8 +1609,8 @@ impl Kanata { // are not cleared. if (now - k.last_tick) > time::Duration::from_secs(60) { log::debug!( - "clearing keyberon normal key states due to blocking for a while" - ); + "clearing keyberon normal key states due to blocking for a while" + ); k.layout.bm().states.retain(|s| { !matches!( s, @@ -1627,7 +1636,7 @@ impl Kanata { #[cfg(feature = "perf_logging")] let start = std::time::Instant::now(); - if let Err(e) = k.handle_key_event(&kev) { + if let Err(e) = k.handle_input_event(&kev) { break e; } @@ -1662,7 +1671,7 @@ impl Kanata { #[cfg(feature = "perf_logging")] let start = std::time::Instant::now(); - if let Err(e) = k.handle_key_event(&kev) { + if let Err(e) = k.handle_input_event(&kev) { break e; } diff --git a/src/kanata/windows/interception.rs b/src/kanata/windows/interception.rs index df45ef34c..88682dce2 100644 --- a/src/kanata/windows/interception.rs +++ b/src/kanata/windows/interception.rs @@ -38,12 +38,7 @@ impl Kanata { if mouse_to_intercept_hwid.is_some() { intrcptn.set_filter( ic::is_mouse, - ic::Filter::MouseFilter( - ic::MouseState::all() - & (!ic::MouseState::WHEEL) - & (!ic::MouseState::HWHEEL) - & (!ic::MouseState::MOVE), - ), + ic::Filter::MouseFilter(ic::MouseState::all() & (!ic::MouseState::MOVE)), ); } let mut is_dev_interceptable: HashMap = HashMap::default(); @@ -70,13 +65,14 @@ impl Kanata { }; KeyEvent { code, value } } - ic::Stroke::Mouse { state, .. } => { + ic::Stroke::Mouse { state, rolling, .. } => { if let Some(hwid) = mouse_to_intercept_hwid { log::trace!("checking mouse stroke {:?}", strokes[i]); if let Some(event) = mouse_state_to_event( dev, &hwid, state, + rolling, &intrcptn, &mut is_dev_interceptable, ) { @@ -123,6 +119,7 @@ fn mouse_state_to_event( input_dev: ic::Device, allowed_hwid: &[u8; HWID_ARR_SZ], state: ic::MouseState, + rolling: i16, intrcptn: &ic::Interception, is_dev_interceptable: &mut HashMap, ) -> Option { @@ -191,6 +188,34 @@ fn mouse_state_to_event( code: OsCode::BTN_EXTRA, value: KeyValue::Release, }) + } else if state.contains(ic::MouseState::WHEEL) { + let osc = if rolling >= 0 { + OsCode::MouseWheelUp + } else { + OsCode::MouseWheelDown + }; + if MAPPED_KEYS.lock().contains(&osc) { + Some(KeyEvent { + code: osc, + value: KeyValue::Tap, + }) + } else { + None + } + } else if state.contains(ic::MouseState::HWHEEL) { + let osc = if rolling >= 0 { + OsCode::MouseWheelRight + } else { + OsCode::MouseWheelLeft + }; + if MAPPED_KEYS.lock().contains(&osc) { + Some(KeyEvent { + code: osc, + value: KeyValue::Tap, + }) + } else { + None + } } else { None } diff --git a/src/oskbd/linux.rs b/src/oskbd/linux.rs index 897361ff1..ca9b348d1 100644 --- a/src/oskbd/linux.rs +++ b/src/oskbd/linux.rs @@ -262,11 +262,36 @@ pub fn is_input_device(device: &Device) -> bool { impl TryFrom for KeyEvent { type Error = (); fn try_from(item: InputEvent) -> Result { + use OsCode::*; match item.kind() { evdev::InputEventKind::Key(k) => Ok(Self { code: OsCode::from_u16(k.0).ok_or(())?, value: KeyValue::from(item.value()), }), + evdev::InputEventKind::RelAxis(axis_type) => { + let dist = item.value(); + let code: OsCode = match axis_type { + RelativeAxisType::REL_WHEEL | RelativeAxisType::REL_WHEEL_HI_RES => { + if dist > 0 { + MouseWheelUp + } else { + MouseWheelDown + } + } + RelativeAxisType::REL_HWHEEL | RelativeAxisType::REL_HWHEEL_HI_RES => { + if dist > 0 { + MouseWheelRight + } else { + MouseWheelLeft + } + } + _ => return Err(()), + }; + Ok(KeyEvent { + code, + value: KeyValue::Tap, + }) + } _ => Err(()), } } diff --git a/src/oskbd/mod.rs b/src/oskbd/mod.rs index 32a9a3761..05664ac1c 100644 --- a/src/oskbd/mod.rs +++ b/src/oskbd/mod.rs @@ -17,6 +17,7 @@ pub enum KeyValue { Release = 0, Press = 1, Repeat = 2, + Tap, } impl From for KeyValue { diff --git a/src/oskbd/windows/interception.rs b/src/oskbd/windows/interception.rs index 672f4c9d7..434575345 100644 --- a/src/oskbd/windows/interception.rs +++ b/src/oskbd/windows/interception.rs @@ -24,6 +24,7 @@ impl InputEvent { match val { KeyValue::Press | KeyValue::Repeat => KeyState::DOWN, KeyValue::Release => KeyState::UP, + KeyValue::Tap => panic!("invalid value attempted to be sent"), }, true, ); diff --git a/src/oskbd/windows/mod.rs b/src/oskbd/windows/mod.rs index d9c952999..fae97fbf1 100644 --- a/src/oskbd/windows/mod.rs +++ b/src/oskbd/windows/mod.rs @@ -20,6 +20,8 @@ pub use self::interception::*; #[cfg(feature = "interception_driver")] pub use interception_convert::*; +pub const HI_RES_SCROLL_UNITS_IN_LO_RES: u16 = 120; + fn send_uc(c: char, up: bool) { log::debug!("sending unicode {c}"); let mut inputs: [INPUT; 2] = unsafe { mem::zeroed() }; @@ -54,6 +56,7 @@ fn write_code(code: u16, value: KeyValue) -> Result<(), std::io::Error> { match value { KeyValue::Press | KeyValue::Repeat => false, KeyValue::Release => true, + KeyValue::Tap => panic!("invalid value attempted to be sent"), }, ); Ok(()) From 36be5b210ec7578181d50b5609054604599c9f31 Mon Sep 17 00:00:00 2001 From: jtroo Date: Fri, 3 Nov 2023 21:51:28 -0700 Subject: [PATCH 064/819] doc: mouse wheel in defsrc (#603) This is intentionally omitted from kanata.kbd right now since it is unsupported in the default LLHOOK mechanism of Windows. --- docs/config.adoc | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/config.adoc b/docs/config.adoc index bf2db5bea..a14177af2 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -1166,6 +1166,17 @@ NOTE: In Linux, not all desktop environments support the `REL_WHEEL_HI_RES` even If this is the case for yours, it will likely be a better experience to use a distance value that is a multiple of 120. +On Linux and Interception, you can also choose to read from a mouse device. +When doing so, using the `mwu`, `mwd`, `mwl`, `mwr` key names in `defsrc` +allow you to remap the mouse scroll up/down/left/right actions like you would +with keyboard keys. + +NOTE: If you are using a high-resolution mouse in Linux, +only a full "notch" of the scroll wheel will activate the action. + +NOTE: If you are using a high-resolution mouse with Interception, +you will probably get way more events than you intended. + [[mouse-movement]] ==== Mouse movement <> From 5f9a164667031aa79b36683c1b1817e2f1b93d42 Mon Sep 17 00:00:00 2001 From: jtroo Date: Fri, 3 Nov 2023 21:56:53 -0700 Subject: [PATCH 065/819] doc: fix toc links (#604) --- docs/config.adoc | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/docs/config.adoc b/docs/config.adoc index a14177af2..44c926443 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -570,8 +570,7 @@ some applications. The termination is configurable with the following options: ) ---- -[[linux-only-x11-repeat-rate]] -=== Linux only: linux-x11-repeat-delay-rate +=== Linux only: linux-x11-repeat-delay-rate[[linux-only-x11-repeat-rate]] <> On Linux, you can tell kanata to run `xset r rate ` @@ -619,8 +618,7 @@ NOTE: Even with these workarounds, putting `+lctl+`+`+ralt+` in your defsrc may work properly with other applications that also use keyboard interception. Known application with issues: GWSL/VcXsrv -[[windows-only--windows-interception-mouse-hwid]] -=== Windows only: windows-interception-mouse-hwid +=== Windows only: windows-interception-mouse-hwid[[windows-only-windows-interception-mouse-hwid]] <> This defcfg item allows you to intercept mouse buttons for a specific mouse @@ -690,8 +688,7 @@ a non-applicable operating system. ) ---- -[[aliases-and-vars]] -== Aliases and variables +== Aliases and variables[[aliases-and-vars]] <> Before learning about actions, @@ -1777,8 +1774,7 @@ Only zero or one `defoverrides` is allowed in a configuration file. ) ---- -[[include]] -== Include other files +== Include other files[[include]] <> The `include` optional configuration item @@ -1807,8 +1803,7 @@ The included files also cannot contain includes themselves. ) ---- -[[advanced-weird-features]] -== Advanced/weird features +== Advanced/weird features[[advanced-weird-features]] [[fake-keys]] === Fake keys From 3be3f88a3d5aa6ef8548d73d7ee5582fa690bd7e Mon Sep 17 00:00:00 2001 From: jtroo Date: Sat, 4 Nov 2023 14:16:46 -0700 Subject: [PATCH 066/819] fix!(seq): release keys in valid visible-backspaced sequence (#605) --- src/kanata/mod.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index 624d12634..5c5f10a73 100644 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -863,6 +863,18 @@ impl Kanata { SequenceInputMode::HiddenSuppressed | SequenceInputMode::HiddenDelayType => {} SequenceInputMode::VisibleBackspaced => { + // Release all keys since they might modify the behaviour of + // backspace into an undesirable behaviour, for example deleting + // more characters than it should. + layout.states.retain(|s| match s { + State::NormalKey { keycode, .. } => { + // Ignore the error, ugly to return it from retain, and + // this is very unlikely to happen anyway. + let _ = self.kbd_out.release_key(keycode.into()); + false + } + _ => true, + }); for k in state.sequence.iter() { // Check for pressed modifiers and don't input backspaces for // those since they don't output characters that can be From 67fd0779e8113fe9104cdba865b49d19953fb5a7 Mon Sep 17 00:00:00 2001 From: jtroo Date: Sat, 4 Nov 2023 23:25:59 -0700 Subject: [PATCH 067/819] feat: add key-history to switch (#607) --- cfg_samples/kanata.kbd | 4 + docs/config.adoc | 23 ++++ keyberon/src/action/switch.rs | 214 +++++++++++++++++++++++++++++----- keyberon/src/layout.rs | 12 +- parser/src/cfg/mod.rs | 80 ++++++++++--- parser/src/cfg/tests.rs | 10 ++ 6 files changed, 296 insertions(+), 47 deletions(-) diff --git a/cfg_samples/kanata.kbd b/cfg_samples/kanata.kbd index 7b31751df..a97529bde 100644 --- a/cfg_samples/kanata.kbd +++ b/cfg_samples/kanata.kbd @@ -486,6 +486,10 @@ If you need help, please feel welcome to ask in the GitHub discussions. ;; a || b || c (a b c) b fallthrough + ;; key-history evaluates to true if the n'th most recent key press, + ;; {n | n ∈ [1, 8]}, matches the given key. + ((key-history a 1) (key-history b 8)) c break + ;; default case, empty list always evaluates to true. ;; break vs. fallthrough doesn't matter here () c break diff --git a/docs/config.adoc b/docs/config.adoc index 44c926443..a9f7aa05e 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -2196,6 +2196,29 @@ Since the keys check of case 3 always passes, `@ac3` will activate next. If neither case 1 or case 2 pass their keys checks, case 3 will always activate with `@ac3`. +==== key-history + +In addition to simple keys, +you can use the `key-history` list item within a case. +This accepts, in order, a key and the key recency. +The key recency must be in the range 1-8, +where 1 is the most recent key that was pressed +and 8 is 8th most recent key pressed. + +.Example: +[source] +---- +(defalias + swh (switch + ((key-history a 1)) S-a break + ((key-history b 1)) S-b break + ((key-history c 1)) S-c break + ((key-history d 8)) (macro d d d) break + () XX break + ) +) +---- + [[custom-tap-hold-behaviour]] === Custom tap-hold behaviour <> diff --git a/keyberon/src/action/switch.rs b/keyberon/src/action/switch.rs index f3294e31a..2254de4ab 100644 --- a/keyberon/src/action/switch.rs +++ b/keyberon/src/action/switch.rs @@ -18,6 +18,7 @@ use BreakOrFallthrough::*; pub const MAX_OPCODE_LEN: u16 = 0x0FFF; pub const MAX_BOOL_EXPR_DEPTH: usize = 8; +pub const MAX_KEY_RECENCY: u8 = 8; pub type Case<'a, T> = (&'a [OpCode], &'a Action<'a, T>, BreakOrFallthrough); @@ -33,6 +34,9 @@ pub struct Switch<'a, T: 'a> { const OR_VAL: u16 = 0x1000; const AND_VAL: u16 = 0x2000; +// Highest bit in u16. Lower 3 bits in the highest nibble are "how far back". This means that +// switch can look back up to 8 keys. +const HISTORICAL_KEYCODE_VAL: u16 = 0x8000; #[derive(Debug, Copy, Clone, PartialEq, Eq)] /// Boolean operator. Notably missing today is Not. @@ -50,6 +54,7 @@ pub struct OpCode(u16); enum OpCodeType { BooleanOp(OperatorAndEndIndex), KeyCode(u16), + HistoricalKeyCode(HistoricalKeyCode), } #[derive(Debug, Copy, Clone, PartialEq, Eq)] @@ -59,6 +64,14 @@ struct OperatorAndEndIndex { pub idx: usize, } +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +/// An op that checks specifically for a key that is a certain number of key presses back in +/// history. +struct HistoricalKeyCode { + key_code: u16, + how_far_back: u8, +} + #[derive(Debug, Copy, Clone, PartialEq)] /// Whether or not a case should break out of the switch if it evaluates to true or fallthrough to /// the next case. @@ -68,15 +81,19 @@ pub enum BreakOrFallthrough { } impl<'a, T> Switch<'a, T> { - /// Iterates over the actions (if any) that are activated in the `Switch` based on its cases - /// and the currently active keys. - pub fn actions(&self, active_keys: T2) -> SwitchActions<'a, T, T2> + /// Iterates over the actions (if any) that are activated in the `Switch` based on its cases, + /// the currently active keys, and historically pressed keys. + /// + /// The `historical_keys` parameter should iterate in the order of most-recent-first. + pub fn actions(&self, active_keys: A, historical_keys: H) -> SwitchActions<'a, T, A, H> where - T2: Iterator + Clone, + A: Iterator + Clone, + H: Iterator + Clone, { SwitchActions { cases: self.cases, active_keys, + historical_keys, case_index: 0, } } @@ -84,25 +101,32 @@ impl<'a, T> Switch<'a, T> { #[derive(Debug, Clone)] /// Iterator returned by `Switch::actions`. -pub struct SwitchActions<'a, T, T2> +pub struct SwitchActions<'a, T, A, H> where - T2: Iterator + Clone, + A: Iterator + Clone, + H: Iterator + Clone, { cases: &'a [(&'a [OpCode], &'a Action<'a, T>, BreakOrFallthrough)], - active_keys: T2, + active_keys: A, + historical_keys: H, case_index: usize, } -impl<'a, T, T2> Iterator for SwitchActions<'a, T, T2> +impl<'a, T, A, H> Iterator for SwitchActions<'a, T, A, H> where - T2: Iterator + Clone, + A: Iterator + Clone, + H: Iterator + Clone, { type Item = &'a Action<'a, T>; fn next(&mut self) -> Option { while self.case_index < self.cases.len() { let case = &self.cases[self.case_index]; - if evaluate_boolean(case.0, self.active_keys.clone()) { + if evaluate_boolean( + case.0, + self.active_keys.clone(), + self.historical_keys.clone(), + ) { let ret_ac = case.1; match case.2 { Break => self.case_index = self.cases.len(), @@ -133,15 +157,29 @@ impl OpCode { Self(kc as u16 & MAX_OPCODE_LEN) } + /// Return a new OpCode that checks if the n'th most recent key, defined by `key_recency`, + /// matches the input keycode. + pub fn new_key_history(kc: KeyCode, key_recency: u8) -> Self { + assert!((kc as u16) <= MAX_OPCODE_LEN); + assert!(key_recency <= MAX_KEY_RECENCY); + Self((kc as u16 & MAX_OPCODE_LEN) | HISTORICAL_KEYCODE_VAL | ((key_recency as u16) << 12)) + } + /// Return a new OpCode for a boolean operation that ends (non-inclusive) at the specified /// index. pub fn new_bool(op: BooleanOperator, end_idx: u16) -> Self { Self((end_idx & MAX_OPCODE_LEN) + op.to_u16()) } + /// Return the interpretation of this `OpCode`. fn opcode_type(self) -> OpCodeType { if self.0 < MAX_OPCODE_LEN { OpCodeType::KeyCode(self.0) + } else if self.0 & HISTORICAL_KEYCODE_VAL == HISTORICAL_KEYCODE_VAL { + OpCodeType::HistoricalKeyCode(HistoricalKeyCode { + key_code: self.0 & 0x0FFF, + how_far_back: ((self.0 & 0x7000) >> 12) as u8, + }) } else { OpCodeType::BooleanOp(OperatorAndEndIndex::from(self.0)) } @@ -165,6 +203,7 @@ impl From for OperatorAndEndIndex { fn evaluate_boolean( bool_expr: &[OpCode], key_codes: impl Iterator + Clone, + historical_keys: impl Iterator + Clone, ) -> bool { let mut ret = true; let mut current_index = 0; @@ -195,6 +234,13 @@ fn evaluate_boolean( continue; } } + OpCodeType::HistoricalKeyCode(hkc) => { + ret = historical_keys + .clone() + .nth(hkc.how_far_back as usize) + .map(|kc| kc as u16 == hkc.key_code) + .unwrap_or(false); + } OpCodeType::BooleanOp(operator) => { let res = stack.push_back(OperatorAndEndIndex { op: current_op, @@ -228,7 +274,11 @@ fn bool_evaluation_test_0() { ]; let keycodes = [KeyCode::A, KeyCode::B, KeyCode::D, KeyCode::F]; assert_eq!( - evaluate_boolean(opcodes.as_slice(), keycodes.iter().copied()), + evaluate_boolean( + opcodes.as_slice(), + keycodes.iter().copied(), + [].iter().copied() + ), true ); } @@ -255,7 +305,11 @@ fn bool_evaluation_test_1() { KeyCode::F, ]; assert_eq!( - evaluate_boolean(opcodes.as_slice(), keycodes.iter().copied()), + evaluate_boolean( + opcodes.as_slice(), + keycodes.iter().copied(), + [].iter().copied() + ), true ); } @@ -275,7 +329,11 @@ fn bool_evaluation_test_2() { ]; let keycodes = [KeyCode::A, KeyCode::B, KeyCode::E, KeyCode::F]; assert_eq!( - evaluate_boolean(opcodes.as_slice(), keycodes.iter().copied()), + evaluate_boolean( + opcodes.as_slice(), + keycodes.iter().copied(), + [].iter().copied() + ), false ); } @@ -295,7 +353,11 @@ fn bool_evaluation_test_3() { ]; let keycodes = [KeyCode::B, KeyCode::C, KeyCode::D, KeyCode::E, KeyCode::F]; assert_eq!( - evaluate_boolean(opcodes.as_slice(), keycodes.iter().copied()), + evaluate_boolean( + opcodes.as_slice(), + keycodes.iter().copied(), + [].iter().copied() + ), false ); } @@ -305,7 +367,11 @@ fn bool_evaluation_test_4() { let opcodes = []; let keycodes = []; assert_eq!( - evaluate_boolean(opcodes.as_slice(), keycodes.iter().copied()), + evaluate_boolean( + opcodes.as_slice(), + keycodes.iter().copied(), + [].iter().copied() + ), true ); } @@ -322,7 +388,11 @@ fn bool_evaluation_test_5() { KeyCode::F, ]; assert_eq!( - evaluate_boolean(opcodes.as_slice(), keycodes.iter().copied()), + evaluate_boolean( + opcodes.as_slice(), + keycodes.iter().copied(), + [].iter().copied() + ), true ); } @@ -339,7 +409,11 @@ fn bool_evaluation_test_6() { KeyCode::F, ]; assert_eq!( - evaluate_boolean(opcodes.as_slice(), keycodes.iter().copied()), + evaluate_boolean( + opcodes.as_slice(), + keycodes.iter().copied(), + [].iter().copied() + ), true ); } @@ -349,7 +423,11 @@ fn bool_evaluation_test_7() { let opcodes = [OpCode(KeyCode::A as u16), OpCode(KeyCode::B as u16)]; let keycodes = [KeyCode::C, KeyCode::D, KeyCode::E, KeyCode::F]; assert_eq!( - evaluate_boolean(opcodes.as_slice(), keycodes.iter().copied()), + evaluate_boolean( + opcodes.as_slice(), + keycodes.iter().copied(), + [].iter().copied() + ), false ); } @@ -364,7 +442,11 @@ fn bool_evaluation_test_9() { ]; let keycodes = [KeyCode::C, KeyCode::D, KeyCode::E, KeyCode::F]; assert_eq!( - evaluate_boolean(opcodes.as_slice(), keycodes.iter().copied()), + evaluate_boolean( + opcodes.as_slice(), + keycodes.iter().copied(), + [].iter().copied() + ), true ); } @@ -379,7 +461,11 @@ fn bool_evaluation_test_10() { ]; let keycodes = [KeyCode::C, KeyCode::D, KeyCode::E, KeyCode::F]; assert_eq!( - evaluate_boolean(opcodes.as_slice(), keycodes.iter().copied()), + evaluate_boolean( + opcodes.as_slice(), + keycodes.iter().copied(), + [].iter().copied() + ), false ); } @@ -393,7 +479,11 @@ fn bool_evaluation_test_11() { ]; let keycodes = [KeyCode::C, KeyCode::D, KeyCode::E, KeyCode::F]; assert_eq!( - evaluate_boolean(opcodes.as_slice(), keycodes.iter().copied()), + evaluate_boolean( + opcodes.as_slice(), + keycodes.iter().copied(), + [].iter().copied() + ), false ); } @@ -409,7 +499,11 @@ fn bool_evaluation_test_12() { ]; let keycodes = [KeyCode::C, KeyCode::D, KeyCode::E, KeyCode::F]; assert_eq!( - evaluate_boolean(opcodes.as_slice(), keycodes.iter().copied()), + evaluate_boolean( + opcodes.as_slice(), + keycodes.iter().copied(), + [].iter().copied() + ), true ); } @@ -428,7 +522,11 @@ fn bool_evaluation_test_max_depth_does_not_panic() { ]; let keycodes = []; assert_eq!( - evaluate_boolean(opcodes.as_slice(), keycodes.iter().copied()), + evaluate_boolean( + opcodes.as_slice(), + keycodes.iter().copied(), + [].iter().copied() + ), true ); } @@ -449,7 +547,11 @@ fn bool_evaluation_test_more_than_max_depth_panics() { ]; let keycodes = []; assert_eq!( - evaluate_boolean(opcodes.as_slice(), keycodes.iter().copied()), + evaluate_boolean( + opcodes.as_slice(), + keycodes.iter().copied(), + [].iter().copied() + ), true ); } @@ -462,7 +564,7 @@ fn switch_fallthrough() { (&[], &Action::<()>::KeyCode(KeyCode::B), Fallthrough), ], }; - let mut actions = sw.actions([].iter().copied()); + let mut actions = sw.actions([].iter().copied(), [].iter().copied()); assert_eq!(actions.next(), Some(&Action::<()>::KeyCode(KeyCode::A))); assert_eq!(actions.next(), Some(&Action::<()>::KeyCode(KeyCode::B))); assert_eq!(actions.next(), None); @@ -476,7 +578,7 @@ fn switch_break() { (&[], &Action::<()>::KeyCode(KeyCode::B), Break), ], }; - let mut actions = sw.actions([].iter().copied()); + let mut actions = sw.actions([].iter().copied(), [].iter().copied()); assert_eq!(actions.next(), Some(&Action::<()>::KeyCode(KeyCode::A))); assert_eq!(actions.next(), None); } @@ -497,6 +599,64 @@ fn switch_no_actions() { ), ], }; - let mut actions = sw.actions([].iter().copied()); + let mut actions = sw.actions([].iter().copied(), [].iter().copied()); assert_eq!(actions.next(), None); } + +#[test] +fn switch_historical_1() { + let opcode_true = [OpCode(0x8000 | KeyCode::A as u16)]; + let opcode_true2 = [OpCode(0xF000 | KeyCode::H as u16)]; + let opcode_false = [OpCode(0x9000 | KeyCode::A as u16)]; + let opcode_false2 = [OpCode(0xE000 | KeyCode::H as u16)]; + assert_eq!( + OpCode::new_key_history(KeyCode::A, 0), + OpCode(0x8000 | KeyCode::A as u16) + ); + assert_eq!( + OpCode::new_key_history(KeyCode::H, 7), + OpCode(0xF000 | KeyCode::H as u16) + ); + let hist_keycodes = [ + KeyCode::A, + KeyCode::B, + KeyCode::C, + KeyCode::D, + KeyCode::E, + KeyCode::F, + KeyCode::G, + KeyCode::H, + ]; + assert_eq!( + evaluate_boolean( + opcode_true.as_slice(), + [].iter().copied(), + hist_keycodes.iter().copied(), + ), + true + ); + assert_eq!( + evaluate_boolean( + opcode_true2.as_slice(), + [].iter().copied(), + hist_keycodes.iter().copied(), + ), + true + ); + assert_eq!( + evaluate_boolean( + opcode_false.as_slice(), + [].iter().copied(), + hist_keycodes.iter().copied(), + ), + false + ); + assert_eq!( + evaluate_boolean( + opcode_false2.as_slice(), + [].iter().copied(), + hist_keycodes.iter().copied(), + ), + false + ); +} diff --git a/keyberon/src/layout.rs b/keyberon/src/layout.rs index 6f46f11ea..77e05efe2 100644 --- a/keyberon/src/layout.rs +++ b/keyberon/src/layout.rs @@ -83,6 +83,7 @@ where pub active_sequences: ArrayDeque<[SequenceState<'a, T>; 4], arraydeque::behavior::Wrapping>, pub action_queue: ActionQueue<'a, T>, pub rpt_action: Option<&'a Action<'a, T>>, + pub historical_keys: ArrayDeque<[KeyCode; 8], arraydeque::behavior::Wrapping>, } /// An event on the key matrix. @@ -855,6 +856,7 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt active_sequences: ArrayDeque::new(), action_queue: ArrayDeque::new(), rpt_action: None, + historical_keys: ArrayDeque::new(), } } /// Iterates on the key codes of the current state. @@ -1016,6 +1018,7 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt Some(SequenceEvent::Press(keycode)) => { // Start tracking this fake key Press() event let _ = self.states.push(FakeKey { keycode }); + self.historical_keys.push_front(keycode); // Fine to fake (0, 0). This is sequences anyway. In Kanata, nothing // valid should be at (0, 0) that this would interfere with. self.oneshot @@ -1024,6 +1027,7 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt Some(SequenceEvent::Tap(keycode)) => { // Same as Press() except we track it for one tick via seq.tapped: let _ = self.states.push(FakeKey { keycode }); + self.historical_keys.push_front(keycode); self.oneshot .handle_press(OneShotHandlePressKey::Other((0, 0))); seq.tapped = Some(keycode); @@ -1320,6 +1324,8 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt } &KeyCode(keycode) => { self.last_press_tracker.coord = coord; + // Most-recent-first! + self.historical_keys.push_front(keycode); let _ = self.states.push(NormalKey { coord, keycode, @@ -1334,6 +1340,7 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt &MultipleKeyCodes(v) => { self.last_press_tracker.coord = coord; for &keycode in *v { + self.historical_keys.push_front(keycode); let _ = self.states.push(NormalKey { coord, keycode, @@ -1466,9 +1473,10 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt return ret; } Switch(sw) => { - let kcs = self.states.iter().filter_map(State::keycode); + let active_keys = self.states.iter().filter_map(State::keycode); + let historical_keys = self.historical_keys.iter().copied(); let action_queue = &mut self.action_queue; - for ac in sw.actions(kcs.clone()) { + for ac in sw.actions(active_keys, historical_keys) { action_queue.push_back(Some((coord, ac))); } // Switch is not properly repeatable. This has to use the action queue for the diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index a413caf84..fbfd413a0 100644 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -1472,6 +1472,21 @@ Params in order: })))) } +fn parse_u8_with_range(expr: &SExpr, s: &ParsedState, label: &str, min: u8, max: u8) -> Result { + expr.atom(s.vars()) + .map(str::parse::) + .and_then(|u| u.ok()) + .and_then(|u| { + assert!(min <= max); + if u >= min && u <= max { + Some(u) + } else { + None + } + }) + .ok_or_else(|| anyhow_expr!(expr, "{label} must be {min}-{max}")) +} + fn parse_u16(expr: &SExpr, s: &ParsedState, label: &str) -> Result { expr.atom(s.vars()) .map(str::parse::) @@ -2934,9 +2949,8 @@ fn parse_switch(ac_params: &[SExpr], s: &ParsedState) -> Result<&'static KanataA bail_expr!(key_match, "{ERR_STR}\n must be a list") }; let mut ops = vec![]; - let mut current_index = 0; for op in key_match.iter() { - current_index = parse_switch_case_bool(current_index, 1, op, &mut ops, s)?; + parse_switch_case_bool(1, op, &mut ops, s)?; } let action = parse_action(action, s)?; @@ -2962,14 +2976,14 @@ fn parse_switch(ac_params: &[SExpr], s: &ParsedState) -> Result<&'static KanataA })))) } +/// Returns the fn parse_switch_case_bool( - mut current_index: u16, depth: u8, op_expr: &SExpr, ops: &mut Vec, s: &ParsedState, -) -> Result { - if current_index > MAX_OPCODE_LEN { +) -> Result<()> { + if ops.len() > MAX_OPCODE_LEN as usize { bail_expr!( op_expr, "maximum key match size of {MAX_OPCODE_LEN} items is exceeded" @@ -2984,7 +2998,7 @@ fn parse_switch_case_bool( if let Some(a) = op_expr.atom(s.vars()) { let osc = str_to_oscode(a).ok_or_else(|| anyhow_expr!(op_expr, "invalid key name"))?; ops.push(OpCode::new_key(osc.into())); - Ok(current_index + 1) + Ok(()) } else { let l = op_expr .list(s.vars()) @@ -2992,28 +3006,58 @@ fn parse_switch_case_bool( if l.is_empty() { bail_expr!(op_expr, "key match cannot contain empty lists inside"); } + #[derive(PartialEq)] + enum AllowedListOps { + Or, + And, + KeyHistory, + } let op = l[0] .atom(s.vars()) .and_then(|s| match s { - "or" => Some(BooleanOperator::Or), - "and" => Some(BooleanOperator::And), + "or" => Some(AllowedListOps::Or), + "and" => Some(AllowedListOps::And), + "key-history" => Some(AllowedListOps::KeyHistory), _ => None, }) .ok_or_else(|| { anyhow_expr!( op_expr, - "lists inside key match must begin with one of: or, and" + "lists inside key match must begin with one of: or, and, key-history" ) })?; - // insert a placeholder for now, don't know the end index yet. - let placeholder_index = current_index; - ops.push(OpCode::new_bool(op, placeholder_index)); - current_index += 1; - for op in l.iter().skip(1) { - current_index = parse_switch_case_bool(current_index, depth + 1, op, ops, s)?; - } - ops[placeholder_index as usize] = OpCode::new_bool(op, current_index); - Ok(current_index) + match op { + AllowedListOps::KeyHistory => { + if l.len() != 3 { + bail_expr!( + op_expr, + "key-history must have 2 parameters: key, key-recency" + ); + } + let osc = l[1] + .atom(s.vars()) + .and_then(str_to_oscode) + .ok_or_else(|| anyhow_expr!(op_expr, "invalid key name"))?; + let key_recency = parse_u8_with_range(&l[2], s, "key-recency", 1, 8)? - 1; + ops.push(OpCode::new_key_history(osc.into(), key_recency)); + Ok(()) + } + AllowedListOps::Or | AllowedListOps::And => { + let op = match op { + AllowedListOps::Or => BooleanOperator::Or, + AllowedListOps::And => BooleanOperator::And, + _ => unreachable!(), + }; + // insert a placeholder for now, don't know the end index yet. + let placeholder_index = ops.len() as u16; + ops.push(OpCode::new_bool(op, placeholder_index)); + for op in l.iter().skip(1) { + parse_switch_case_bool(depth + 1, op, ops, s)?; + } + ops[placeholder_index as usize] = OpCode::new_bool(op, ops.len() as u16); + Ok(()) + } + } } } diff --git a/parser/src/cfg/tests.rs b/parser/src/cfg/tests.rs index cdac0ef78..23af04154 100644 --- a/parser/src/cfg/tests.rs +++ b/parser/src/cfg/tests.rs @@ -749,6 +749,7 @@ fn parse_switch() { () _ fallthrough (a b c) $var1 fallthrough ((or (or (or (or (or (or (or (or))))))))) $var1 fallthrough + ((key-history a 1) (key-history b 5) (key-history c 8)) $var1 fallthrough ) ) "#; @@ -805,6 +806,15 @@ fn parse_switch() { &Action::KeyCode(KeyCode::A), BreakOrFallthrough::Fallthrough ), + ( + &[ + OpCode::new_key_history(KeyCode::A, 0), + OpCode::new_key_history(KeyCode::B, 4), + OpCode::new_key_history(KeyCode::C, 7), + ], + &Action::KeyCode(KeyCode::A), + BreakOrFallthrough::Fallthrough + ), ] }) ); From 6f4805175c1615be69ee6ed00df442bfcc047370 Mon Sep 17 00:00:00 2001 From: jtroo Date: Sat, 4 Nov 2023 23:56:53 -0700 Subject: [PATCH 068/819] doc+log: document and log forceful exit key combo (#608) --- cfg_samples/kanata.kbd | 11 +++++++++-- docs/config.adoc | 13 +++++++++++++ src/main.rs | 4 ++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/cfg_samples/kanata.kbd b/cfg_samples/kanata.kbd index a97529bde..019b3ada9 100644 --- a/cfg_samples/kanata.kbd +++ b/cfg_samples/kanata.kbd @@ -3,8 +3,15 @@ This is a sample configuration file that showcases every feature in kanata. If anything is confusing or hard to discover, please file an issue or contribute a pull request to help improve the document. -This sample file has documentation but it is lower effort than the configuration -guide. The configuration guide can be found at: +Since it may be important for you to know, pressing and holding all of the +three following keys together at the same time will cause kanata to exit: + + Left Control, Space, Escape + +This is for the physical key input rather than after any remappings +that are done by kanata. + +This mechanism works on the key input*before any remappings done by kanata. https://github.com/jtroo/kanata/blob/main/docs/config.adoc |# diff --git a/docs/config.adoc b/docs/config.adoc index a9f7aa05e..81da8ade9 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -21,6 +21,19 @@ part of the project, please be welcome to make a pull request or file an issue. == Table of contents toc::[] +== Forcefully exit kanata [[force-exit]] +<> + +Though this isn't configuration-related, +it may be important for you to know that pressing and holding all of the +three following keys together at the same time will cause kanata to exit: + +- Left Control +- Space +- Escape + +This mechanism works on the key input **before** any remappings done by kanata. + [[comments]] == Comments <> diff --git a/src/main.rs b/src/main.rs index 9d3e1a444..d986d2977 100644 --- a/src/main.rs +++ b/src/main.rs @@ -133,6 +133,10 @@ fn cli_init() -> Result { log::info!("using LLHOOK+SendInput for keyboard IO"); #[cfg(all(feature = "interception_driver", target_os = "windows"))] log::info!("using the Interception driver for keyboard IO"); + log::info!( + "You may forcefully exit kanata by pressing lctl+spc+esc at any time. \ + These keys refer to defsrc input, meaning BEFORE kanata remaps keys." + ); if let Some(config_file) = cfg_paths.first() { if !config_file.exists() { From ed17393d8fbed58eaa482f0298283ba1bc487b07 Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 5 Nov 2023 00:54:56 -0700 Subject: [PATCH 069/819] feat!: add configurable limit to dynamic macro (#609) --- cfg_samples/kanata.kbd | 4 ++++ docs/config.adoc | 15 +++++++++++++++ parser/src/cfg/mod.rs | 1 + src/kanata/mod.rs | 37 +++++++++++++++++++++++++++++++++++-- 4 files changed, 55 insertions(+), 2 deletions(-) diff --git a/cfg_samples/kanata.kbd b/cfg_samples/kanata.kbd index 019b3ada9..baac08eba 100644 --- a/cfg_samples/kanata.kbd +++ b/cfg_samples/kanata.kbd @@ -176,6 +176,10 @@ If you need help, please feel welcome to ask in the GitHub discussions. ;; ;; movemouse-smooth-diagonals yes + ;; This configuration allows you to customize the length limit on dynamic macros. + ;; The default limit is 128 keys. + ;; + ;; dynamic-macro-max-presses 1000 ) ;; deflocalkeys-* enables you to define and use key names that match your locale diff --git a/docs/config.adoc b/docs/config.adoc index 81da8ade9..6946a0c38 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -444,6 +444,20 @@ synchronize the mouse movements. ) ---- +=== dynamic-macro-max-presses [[dynamic-macro-max-presses]] +<> + +This configuration allows you to customize the length limit on dynamic macros. +The default length limit is 128 keys. + +.Example: +[source] +---- +(defcfg + dynamic-macro-max-presses 1000 +) +---- + [[linux-only-linux-dev]] === Linux only: linux-dev <> @@ -689,6 +703,7 @@ a non-applicable operating system. delegate-to-first-layer yes movemouse-inherit-accel-state yes movemouse-smooth-diagonals yes + dynamic-macro-max-presses 1000 linux-dev /dev/input/dev1:/dev/input/dev2 linux-dev-names-include "Name 1:Name 2" linux-dev-names-exclude "Name 3:Name 4" diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index fbfd413a0..7d1aeeb47 100644 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -739,6 +739,7 @@ fn parse_defcfg(expr: &[SExpr]) -> Result> { let non_bool_cfg_keys = &[ "sequence-timeout", "sequence-input-mode", + "dynamic-macro-max-presses", "linux-dev", "linux-dev-names-include", "linux-dev-names-exclude", diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index 5c5f10a73..dea135daf 100644 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -154,6 +154,9 @@ pub struct Kanata { /// gets stored in this buffer and if the next movemouse action is opposite axis /// than the one stored in the buffer, both events are outputted at the same time. movemouse_buffer: Option<(Axis, CalculatedMouseMove)>, + /// Configured maximum for dynamic macro recording, to protect users from themselves if they + /// have accidentally left it on. + dynamic_macro_max_presses: u16, } #[derive(PartialEq, Clone, Copy)] @@ -387,6 +390,12 @@ impl Kanata { .get("movemouse-inherit-accel-state") .map(|s| TRUE_VALUES.contains(&s.to_lowercase().as_str())) .unwrap_or_default(), + dynamic_macro_max_presses: cfg + .items + .get("dynamic-macro-max-presses") + .map(|s| s.parse::()) + .unwrap_or(Ok(128)) + .map_err(|e| anyhow!("dynamic-macro-max-presses must be 0-65535: {e}"))?, #[cfg(target_os = "linux")] defcfg_items: cfg.items, waiting_for_idle: HashSet::default(), @@ -436,6 +445,12 @@ impl Kanata { .get("movemouse-inherit-accel-state") .map(|s| TRUE_VALUES.contains(&s.to_lowercase().as_str())) .unwrap_or_default(); + self.dynamic_macro_max_presses = cfg + .items + .get("dynamic-macro-max-presses") + .map(|s| s.parse::()) + .unwrap_or(Ok(128)) + .map_err(|_| anyhow!("dynamic-macro-max-presses must be 0-65535"))?; *MAPPED_KEYS.lock() = cfg.mapped_keys; Kanata::set_repeat_rate(&cfg.items)?; log::info!("Live reload successful"); @@ -450,7 +465,25 @@ impl Kanata { let kbrn_ev = match event.value { KeyValue::Press => { if let Some(state) = &mut self.dynamic_macro_record_state { - state.macro_items.push(DynamicMacroItem::Press(event.code)); + // This is not 100% accurate since there may be multiple presses before any of + // their relesease are received. But it's probably good enough in practice. + // + // The presses are defined so that a user cares about the number of keys rather + // than events. So rather than the user multiplying by 2 in their config after + // considering the number of keys they want, kanata does the multiplication + // instead. + if state.macro_items.len() > usize::from(self.dynamic_macro_max_presses) * 2 { + log::warn!( + "saving and stopping dynamic macro {} recording due to exceeding limit", + state.starting_macro_id, + ); + state.add_release_for_all_unreleased_presses(); + self.dynamic_macros + .insert(state.starting_macro_id, state.macro_items.clone()); + self.dynamic_macro_record_state = None; + } else { + state.macro_items.push(DynamicMacroItem::Press(event.code)); + } } Event::Press(0, evc) } @@ -1239,7 +1272,7 @@ impl Kanata { state .macro_items .len() - .saturating_sub(*num_actions_to_remove as usize), + .saturating_sub(usize::from(*num_actions_to_remove)), ); state.add_release_for_all_unreleased_presses(); self.dynamic_macros From 5f1450fa5f18b8f632c4d141322151b072d713ab Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 5 Nov 2023 23:32:10 -0800 Subject: [PATCH 070/819] fix(macro): allow unmodded list (#613) --- parser/src/cfg/mod.rs | 13 +++++++++++++ parser/src/cfg/tests.rs | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index 7d1aeeb47..743f124dc 100644 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -1692,6 +1692,19 @@ fn parse_macro_item_impl<'a>( } Ok(Action::Custom(custom)) => Ok((vec![SequenceEvent::Custom(custom)], &acs[1..])), _ => { + if let Some(submacro) = acs[0].list(s.vars()) { + // If it's just a list that's not parsable as a usable action, try parsing the + // content. + let mut submacro_remainder = submacro; + let mut all_events = vec![]; + while !submacro_remainder.is_empty() { + let mut events; + (events, submacro_remainder) = parse_macro_item(submacro_remainder, s)?; + all_events.append(&mut events); + } + return Ok((all_events, &acs[1..])); + } + let (held_mods, unparsed_str) = parse_mods_held_for_submacro(&acs[0], s)?; let mut all_events = vec![]; diff --git a/parser/src/cfg/tests.rs b/parser/src/cfg/tests.rs index 23af04154..fc69003f2 100644 --- a/parser/src/cfg/tests.rs +++ b/parser/src/cfg/tests.rs @@ -733,6 +733,38 @@ fn parse_bad_submacro_2() { .unwrap_err(); } +#[test] +fn parse_nested_macro() { + // Test exists since it used to crash. It should not crash. + let _lk = match CFG_PARSE_LOCK.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + }; + let mut s = ParsedState::default(); + let source = r#" +(defvar m1 (a b c)) +(defsrc a b) +(deflayer base + (macro $m1) + (macro bspc bspc $m1) +) +"#; + parse_cfg_raw_string( + source, + &mut s, + &PathBuf::from("test"), + &mut FileContentProvider { + get_file_content_fn: &mut |_| unimplemented!(), + }, + DEF_LOCAL_KEYS, + ) + .map_err(|e| { + eprintln!("{:?}", miette::Error::from(e)); + "" + }) + .unwrap(); +} + #[test] fn parse_switch() { let _lk = match CFG_PARSE_LOCK.lock() { @@ -877,7 +909,6 @@ fn parse_on_idle_fakekey() { DEF_LOCAL_KEYS, ) .map_err(|_e| { - // uncomment to see what this looks like when running test eprintln!("{:?}", _e); "" }) From b92f549e35438e05d0fa7916992862e6120653f1 Mon Sep 17 00:00:00 2001 From: jtroo Date: Wed, 8 Nov 2023 22:28:04 -0800 Subject: [PATCH 071/819] fix: use correct value for max key recency in code (#616) --- keyberon/src/action/switch.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/keyberon/src/action/switch.rs b/keyberon/src/action/switch.rs index 2254de4ab..c163a4648 100644 --- a/keyberon/src/action/switch.rs +++ b/keyberon/src/action/switch.rs @@ -3,6 +3,7 @@ //! Limitations: //! - Maximum opcode length: 4095 //! - Maximum boolean expression depth: 8 +//! - Maximum key recency: 7, where 0 is the most recent key press //! //! The intended use is to build up a `Switch` struct and use that in the `Layout`. //! @@ -18,7 +19,7 @@ use BreakOrFallthrough::*; pub const MAX_OPCODE_LEN: u16 = 0x0FFF; pub const MAX_BOOL_EXPR_DEPTH: usize = 8; -pub const MAX_KEY_RECENCY: u8 = 8; +pub const MAX_KEY_RECENCY: u8 = 7; pub type Case<'a, T> = (&'a [OpCode], &'a Action<'a, T>, BreakOrFallthrough); From 1d49a7ba49e9fbc53bffba6575c8dd9dbbc6212b Mon Sep 17 00:00:00 2001 From: jtroo Date: Fri, 10 Nov 2023 22:49:32 -0800 Subject: [PATCH 072/819] feat: add unmod action (#617) --- cfg_samples/kanata.kbd | 6 +++++ docs/config.adoc | 21 +++++++++++++++ parser/src/cfg/list_actions.rs | 4 ++- parser/src/cfg/mod.rs | 16 ++++++++++++ parser/src/custom_action.rs | 3 +++ src/kanata/mod.rs | 48 +++++++++++++++++++++++++++++++--- 6 files changed, 94 insertions(+), 4 deletions(-) diff --git a/cfg_samples/kanata.kbd b/cfg_samples/kanata.kbd index baac08eba..83e8797d3 100644 --- a/cfg_samples/kanata.kbd +++ b/cfg_samples/kanata.kbd @@ -460,6 +460,12 @@ If you need help, please feel welcome to ask in the GitHub discussions. dp1 (dynamic-macro-play 1) dp2 (dynamic-macro-play 2) + ;; unmod will release all modifiers temporarily and send the . + ;; So for example holding shift and tapping a @um1 key will still output 1. + um1 (unmod 1) + ;; dead keys é (as opposed to using AltGr) that outputs É when shifted + dké (macro (unmod ') e) + ;; unicode accepts a single unicode character. The unicode character will ;; not be automatically repeated by holding the key down. The alias name ;; is the unicode character itself and is referenced by @🙁 in deflayer. diff --git a/docs/config.adoc b/docs/config.adoc index 6946a0c38..cda9da1ed 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -1533,6 +1533,11 @@ The actions supported in `+macro+` are: * <> * <> * <> +* <> + +NOTE: Some of these actions may need short delays between. +For example, `(macro a (unmod b) 5 (unmod c) d))` +needs the delay of `5` to work correctly. .Example: [source] @@ -1708,6 +1713,22 @@ and what the extra non-terminal+non-capitalized keys should be (3rd parameter). ) ---- +=== unmod[[unmod]] +<> + +The `unmod` action will release all modifiers temporarily and send a key. +After the `unmod` key is released, the released modifiers are pressed again. +The modifiers affected are: `lsft,rsft,lctl,rctl,lmet,rmet,lalt,ralt`. + +.Example: +[source] +---- +;; holding shift and tapping a @um1 key will still output 1. +um1 (unmod 1) +;; dead keys é (as opposed to using AltGr) that outputs É when shifted +dké (macro (unmod ') e) +---- + [[cmd]] === cmd <> diff --git a/parser/src/cfg/list_actions.rs b/parser/src/cfg/list_actions.rs index 8a593590d..237b543bb 100644 --- a/parser/src/cfg/list_actions.rs +++ b/parser/src/cfg/list_actions.rs @@ -57,9 +57,10 @@ pub const CAPS_WORD_CUSTOM: &str = "caps-word-custom"; pub const DYNAMIC_MACRO_RECORD_STOP_TRUNCATE: &str = "dynamic-macro-record-stop-truncate"; pub const SWITCH: &str = "switch"; pub const SEQUENCE: &str = "sequence"; +pub const UNMOD: &str = "unmod"; pub fn is_list_action(ac: &str) -> bool { - const LIST_ACTIONS: [&str; 55] = [ + const LIST_ACTIONS: [&str; 56] = [ LAYER_SWITCH, LAYER_TOGGLE, LAYER_WHILE_HELD, @@ -115,6 +116,7 @@ pub fn is_list_action(ac: &str) -> bool { DYNAMIC_MACRO_RECORD_STOP_TRUNCATE, SWITCH, SEQUENCE, + UNMOD, ]; LIST_ACTIONS.contains(&ac) } diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index 743f124dc..6baa6b4a5 100644 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -1348,6 +1348,7 @@ fn parse_action_list(ac: &[SExpr], s: &ParsedState) -> Result<&'static KanataAct DYNAMIC_MACRO_RECORD_STOP_TRUNCATE => parse_macro_record_stop_truncate(&ac[1..], s), SWITCH => parse_switch(&ac[1..], s), SEQUENCE => parse_sequence_start(&ac[1..], s), + UNMOD => parse_unmod(&ac[1..], s), _ => unreachable!(), } } @@ -3124,6 +3125,21 @@ fn parse_on_idle_fakekey(ac_params: &[SExpr], s: &ParsedState) -> Result<&'stati ))))) } +fn parse_unmod(ac_params: &[SExpr], s: &ParsedState) -> Result<&'static KanataAction> { + const ERR_MSG: &str = "unmod expects one param: key"; + if ac_params.len() != 1 { + bail!("{ERR_MSG}\nfound {} items", ac_params.len()); + } + let key = ac_params[0] + .atom(s.vars()) + .and_then(str_to_oscode) + .ok_or_else(|| anyhow_expr!(&ac_params[0], "{ERR_MSG}"))? + .into(); + Ok(s.a.sref(Action::Custom( + s.a.sref(s.a.sref_slice(CustomAction::Unmodded { key })), + ))) +} + /// Creates a `KeyOutputs` from `layers::LAYERS`. fn create_key_outputs(layers: &KanataLayers, overrides: &Overrides) -> KeyOutputs { let mut outs = KeyOutputs::new(); diff --git a/parser/src/custom_action.rs b/parser/src/custom_action.rs index 5ea9baa84..ebdf7ab54 100644 --- a/parser/src/custom_action.rs +++ b/parser/src/custom_action.rs @@ -65,6 +65,9 @@ pub enum CustomAction { x: u16, y: u16, }, + Unmodded { + key: KeyCode, + }, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index dea135daf..abd29cba4 100644 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -157,6 +157,8 @@ pub struct Kanata { /// Configured maximum for dynamic macro recording, to protect users from themselves if they /// have accidentally left it on. dynamic_macro_max_presses: u16, + /// Keys that should be unmodded. If empty, any modifier should be cleared. + unmodded_keys: Vec, } #[derive(PartialEq, Clone, Copy)] @@ -401,6 +403,7 @@ impl Kanata { waiting_for_idle: HashSet::default(), ticks_since_idle: 0, movemouse_buffer: None, + unmodded_keys: vec![], }) } @@ -774,6 +777,44 @@ impl Kanata { } } + // Deal with unmodded. Unlike other custom actions, this should come before key presses and + // releases. I don't quite remember why custom actions come after the key processing, but I + // remember that it is intentional. However, since unmodded needs to modify the key lists, + // it should come before. + match custom_event { + CustomEvent::Press(custacts) => { + for custact in custacts.iter() { + if let CustomAction::Unmodded { key } = custact { + self.unmodded_keys.push(*key); + } + } + } + CustomEvent::Release(custacts) => { + for custact in custacts.iter() { + if let CustomAction::Unmodded { key } = custact { + self.unmodded_keys.retain(|k| k != key); + } + } + } + _ => {} + } + if !self.unmodded_keys.is_empty() { + cur_keys.retain(|k| { + !matches!( + k, + KeyCode::LShift + | KeyCode::RShift + | KeyCode::LGui + | KeyCode::RGui + | KeyCode::LCtrl + | KeyCode::RCtrl + | KeyCode::LAlt + | KeyCode::RAlt + ) + }); + cur_keys.extend(self.unmodded_keys.iter()); + } + // Release keys that do not exist in the current state but exist in the previous state. // This used to use a HashSet but it was changed to a Vec because the order of operations // matters. @@ -1322,13 +1363,14 @@ impl Kanata { CustomAction::SetMouse { x, y } => { self.kbd_out.set_mouse(*x, *y)?; } - CustomAction::FakeKeyOnRelease { .. } - | CustomAction::DelayOnRelease(_) - | CustomAction::CancelMacroOnRelease => {} CustomAction::FakeKeyOnIdle(fkd) => { self.ticks_since_idle = 0; self.waiting_for_idle.insert(*fkd); } + CustomAction::FakeKeyOnRelease { .. } + | CustomAction::DelayOnRelease(_) + | CustomAction::Unmodded { .. } + | CustomAction::CancelMacroOnRelease => {} } } #[cfg(feature = "cmd")] From 53c3462e135a5ec11d06c34b2be1126ce08f3329 Mon Sep 17 00:00:00 2001 From: rszyma Date: Sat, 11 Nov 2023 23:36:46 +0100 Subject: [PATCH 073/819] feat: improve error message on unknown key/action (#620) --- parser/src/cfg/mod.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index 6baa6b4a5..d6d1a871d 100644 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -1268,7 +1268,16 @@ fn parse_action_atom(ac_span: &Spanned, s: &ParsedState) -> Result<&'sta let (mut keys, unparsed_str) = parse_mod_prefix(ac)?; keys.push( str_to_oscode(unparsed_str) - .ok_or_else(|| anyhow!("Unknown key/action/variable: {ac:?}"))? + .ok_or_else(|| { + // check aliases + if s.aliases.contains_key(ac) { + anyhow!("Unknown key/action: {ac}. If you meant to use an alias, prefix it with '@' symbol: @{ac}") + } else if s.vars.contains_key(ac) { + anyhow!("Unknown key/action: {ac}. If you meant to use a variable, prefix it with '$' symbol: ${ac}") + } else { + anyhow!("Unknown key/action: {ac}") + } + })? .into(), ); Ok(s.a.sref(Action::MultipleKeyCodes(s.a.sref(s.a.sref_vec(keys))))) From abb53c9d7f66ddaf88e707c630f086d918fd57df Mon Sep 17 00:00:00 2001 From: jtroo Date: Sat, 11 Nov 2023 15:08:37 -0800 Subject: [PATCH 074/819] fix: add short-circuiting to key-history (#622) --- keyberon/src/action/switch.rs | 67 +++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/keyberon/src/action/switch.rs b/keyberon/src/action/switch.rs index c163a4648..e6983d160 100644 --- a/keyberon/src/action/switch.rs +++ b/keyberon/src/action/switch.rs @@ -241,6 +241,10 @@ fn evaluate_boolean( .nth(hkc.how_far_back as usize) .map(|kc| kc as u16 == hkc.key_code) .unwrap_or(false); + if matches!((ret, current_op), (true, Or) | (false, And)) { + current_index = current_end_index; + continue; + } } OpCodeType::BooleanOp(operator) => { let res = stack.push_back(OperatorAndEndIndex { @@ -661,3 +665,66 @@ fn switch_historical_1() { false ); } + +#[test] +fn switch_historical_bools() { + let opcodes_true_and = [ + OpCode::new_bool(And, 3), + OpCode::new_key_history(KeyCode::A, 0), + OpCode::new_key_history(KeyCode::B, 1), + ]; + let opcodes_false_and1 = [ + OpCode::new_bool(And, 3), + OpCode::new_key_history(KeyCode::A, 0), + OpCode::new_key_history(KeyCode::B, 2), + ]; + let opcodes_false_and2 = [ + OpCode::new_bool(And, 3), + OpCode::new_key_history(KeyCode::B, 2), + OpCode::new_key_history(KeyCode::A, 0), + ]; + let opcodes_true_or1 = [ + OpCode::new_bool(Or, 3), + OpCode::new_key_history(KeyCode::A, 0), + OpCode::new_key_history(KeyCode::B, 1), + ]; + let opcodes_true_or2 = [ + OpCode::new_bool(Or, 3), + OpCode::new_key_history(KeyCode::A, 0), + OpCode::new_key_history(KeyCode::B, 2), + ]; + let opcodes_true_or3 = [ + OpCode::new_bool(Or, 3), + OpCode::new_key_history(KeyCode::B, 2), + OpCode::new_key_history(KeyCode::A, 0), + ]; + let opcodes_false_or = [ + OpCode::new_bool(Or, 3), + OpCode::new_key_history(KeyCode::A, 1), + OpCode::new_key_history(KeyCode::B, 2), + ]; + let hist_keycodes = [ + KeyCode::A, + KeyCode::B, + KeyCode::C, + KeyCode::D, + KeyCode::E, + KeyCode::F, + KeyCode::G, + KeyCode::H, + ]; + + let test = |opcodes: &[OpCode], expectation: bool| { + assert_eq!( + evaluate_boolean(opcodes, [].iter().copied(), hist_keycodes.iter().copied(),), + expectation + ); + }; + test(&opcodes_true_and, true); + test(&opcodes_true_or1, true); + test(&opcodes_true_or2, true); + test(&opcodes_true_or3, true); + test(&opcodes_false_and1, false); + test(&opcodes_false_and2, false); + test(&opcodes_false_or, false); +} From 3f3a3bc5c837b1c95441904ae74625825a93d742 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Wed, 15 Nov 2023 06:39:17 +0700 Subject: [PATCH 075/819] doc: fix off-by-one error in config.adoc (#630) --- docs/config.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config.adoc b/docs/config.adoc index cda9da1ed..5f1e99ba5 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -1874,7 +1874,7 @@ this entry is similar to `+defalias+`, but you cannot make use of aliases inside of `+deffakekeys+` to shorten an action. You can however refer to previously defined fake keys. -The aforementioned `++` can be one of three values: +The aforementioned `++` can be one of four values: * `+press+`: Press the fake key. It will not be released until another action triggers a release or tap. From 3602d4de3d0e113ac04d86754bf668abffc6c36f Mon Sep 17 00:00:00 2001 From: Evgeny Date: Wed, 15 Nov 2023 06:40:06 +0700 Subject: [PATCH 076/819] doc: fix repeated word in config.adoc (#627) --- docs/config.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config.adoc b/docs/config.adoc index 5f1e99ba5..d35637ef5 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -76,7 +76,7 @@ amount of whitespace (spaces, tabs, newlines) are not relevant. You may use spaces, tabs, or newlines however you like to visually format `defsrc` to your liking. -The the primary source of all key names are the +The primary source of all key names are the `str_to_oscode` and `default_mappings` functions in https://github.com/jtroo/kanata/blob/main/parser/src/keys/mod.rs[the source]. Please feel welcome to file an issue From 84a79317fb06f97a5f8d5b3bcdcb5ba25c9220c1 Mon Sep 17 00:00:00 2001 From: rszyma Date: Fri, 17 Nov 2023 08:34:25 +0100 Subject: [PATCH 077/819] feat: more verbose logging for cmd (#633) Co-authored-by: jtroo --- src/kanata/cmd.rs | 35 +++++++++++++++++++---------------- src/oskbd/linux.rs | 2 +- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/kanata/cmd.rs b/src/kanata/cmd.rs index 27a99340d..a606c5d19 100644 --- a/src/kanata/cmd.rs +++ b/src/kanata/cmd.rs @@ -1,3 +1,5 @@ +use std::fmt::Write; + use kanata_parser::cfg::parse_mod_prefix; use kanata_parser::cfg::sexpr::*; use kanata_parser::keys::*; @@ -8,32 +10,33 @@ const LP: &str = "cmd-out:"; pub(super) fn run_cmd_in_thread(cmd_and_args: Vec) -> std::thread::JoinHandle<()> { std::thread::spawn(move || { let mut args = cmd_and_args.iter(); - let mut cmd = std::process::Command::new( - args.next() - .expect("parsing should have forbidden empty cmd"), - ); + let mut printable_cmd = String::new(); + let executable = args + .next() + .expect("parsing should have forbidden empty cmd"); + write!( + printable_cmd, + "Program: {}, Arguments:", + executable.as_str() + ) + .expect("write to string should succeed"); + let mut cmd = std::process::Command::new(executable); for arg in args { cmd.arg(arg); + printable_cmd.push(' '); + printable_cmd.push_str(arg.as_str()); } + log::info!("Running cmd: {}", printable_cmd); match cmd.output() { Ok(output) => { log::info!( - "Successfully ran cmd {}\nstdout:\n{}\nstderr:\n{}", - { - let mut printable_cmd = Vec::new(); - printable_cmd.push(format!("{:?}", cmd.get_program())); - - let printable_cmd = cmd.get_args().fold(printable_cmd, |mut cmd, arg| { - cmd.push(format!("{arg:?}")); - cmd - }); - printable_cmd.join(" ") - }, + "Successfully ran cmd: {}\nstdout:\n{}\nstderr:\n{}", + printable_cmd, String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); } - Err(e) => log::error!("Failed to execute cmd: {}", e), + Err(e) => log::error!("Failed to execute program {:?}: {}", cmd.get_program(), e), }; }) } diff --git a/src/oskbd/linux.rs b/src/oskbd/linux.rs index ca9b348d1..98b2d8075 100644 --- a/src/oskbd/linux.rs +++ b/src/oskbd/linux.rs @@ -693,7 +693,7 @@ impl Symlink { fn clean_when_killed(symlink: Self) { thread::spawn(|| { let mut signals = Signals::new([SIGINT, SIGTERM]).expect("signals register"); - for signal in &mut signals { + if let Some(signal) = (&mut signals).into_iter().next() { match signal { SIGINT | SIGTERM => { drop(symlink); From 54daa6b49eadf4cf3c2f7fe9c369ce195084d348 Mon Sep 17 00:00:00 2001 From: rszyma Date: Fri, 17 Nov 2023 08:39:55 +0100 Subject: [PATCH 078/819] feat(parser): allow changing oscode mapping variant at runtime (#628) This also adds a minor optimization fix to the `rpt` action. --- parser/src/keys/linux.rs | 4 ++-- parser/src/keys/mod.rs | 47 +++++++++++++++++++++++++++++++++----- parser/src/keys/windows.rs | 4 ++-- src/kanata/mod.rs | 22 ++++++++++-------- 4 files changed, 57 insertions(+), 20 deletions(-) diff --git a/parser/src/keys/linux.rs b/parser/src/keys/linux.rs index b981ff9d1..117b8aa30 100644 --- a/parser/src/keys/linux.rs +++ b/parser/src/keys/linux.rs @@ -2,11 +2,11 @@ use super::OsCode; impl OsCode { - pub const fn as_u16(self) -> u16 { + pub(super) const fn as_u16_linux(self) -> u16 { self as u16 } - pub const fn from_u16(code: u16) -> Option { + pub(super) const fn from_u16_linux(code: u16) -> Option { match code { 0 => Some(OsCode::KEY_RESERVED), 1 => Some(OsCode::KEY_ESC), diff --git a/parser/src/keys/mod.rs b/parser/src/keys/mod.rs index 873014c15..351f32352 100644 --- a/parser/src/keys/mod.rs +++ b/parser/src/keys/mod.rs @@ -7,17 +7,52 @@ use rustc_hash::FxHashMap as HashMap; #[cfg(any(target_os = "linux", target_os = "unknown"))] mod linux; -#[cfg(any(target_os = "linux", target_os = "unknown"))] -pub use linux::*; - -#[cfg(target_os = "windows")] +#[cfg(any(target_os = "windows", target_os = "unknown"))] mod windows; -#[cfg(target_os = "windows")] -pub use windows::*; mod mappings; pub use mappings::*; +#[cfg(target_os = "unknown")] +#[derive(Clone, Copy)] +pub enum Platform { + Win, + Linux, +} + +#[cfg(target_os = "unknown")] +pub static OSCODE_MAPPING_VARIANT: Mutex = Mutex::new(Platform::Linux); + +impl OsCode { + pub fn as_u16(self) -> u16 { + #[cfg(target_os = "unknown")] + return match *OSCODE_MAPPING_VARIANT.lock() { + Platform::Win => self.as_u16_windows(), + Platform::Linux => self.as_u16_linux(), + }; + + #[cfg(target_os = "linux")] + return self.as_u16_linux(); + + #[cfg(target_os = "windows")] + return self.as_u16_windows(); + } + + pub fn from_u16(code: u16) -> Option { + #[cfg(target_os = "unknown")] + return match *OSCODE_MAPPING_VARIANT.lock() { + Platform::Win => OsCode::from_u16_windows(code), + Platform::Linux => OsCode::from_u16_linux(code), + }; + + #[cfg(target_os = "linux")] + return OsCode::from_u16_linux(code); + + #[cfg(target_os = "windows")] + return OsCode::from_u16_windows(code); + } +} + static CUSTOM_STRS_TO_OSCODES: Lazy>> = Lazy::new(|| { let mut mappings = HashMap::default(); add_default_str_osc_mappings(&mut mappings); diff --git a/parser/src/keys/windows.rs b/parser/src/keys/windows.rs index 5c6a1a7a6..136f8962b 100644 --- a/parser/src/keys/windows.rs +++ b/parser/src/keys/windows.rs @@ -203,7 +203,7 @@ mod keys { pub use keys::*; impl OsCode { - pub const fn from_u16(code: u16) -> Option { + pub(super) const fn from_u16_windows(code: u16) -> Option { match code { 0x30 => Some(OsCode::KEY_0), 0x31 => Some(OsCode::KEY_1), @@ -841,7 +841,7 @@ impl OsCode { } } - pub const fn as_u16(self) -> u16 { + pub(super) const fn as_u16_windows(self) -> u16 { match self { OsCode::KEY_0 => 0x30, OsCode::KEY_1 => 0x31, diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index abd29cba4..b4d35dfda 100644 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -12,7 +12,7 @@ use std::collections::VecDeque; use std::io::Write; use std::net::TcpStream; use std::path::PathBuf; -use std::sync::atomic::{AtomicBool, AtomicU32, Ordering::SeqCst}; +use std::sync::atomic::{AtomicBool, Ordering::SeqCst}; use std::sync::Arc; use std::time; @@ -159,6 +159,8 @@ pub struct Kanata { dynamic_macro_max_presses: u16, /// Keys that should be unmodded. If empty, any modifier should be cleared. unmodded_keys: Vec, + /// Keep track of last pressed key for [`CustomAction::Repeat`]. + last_pressed_key: KeyCode, } #[derive(PartialEq, Clone, Copy)] @@ -241,8 +243,6 @@ impl DynamicMacroRecordState { } } -static LAST_PRESSED_KEY: AtomicU32 = AtomicU32::new(0); - use once_cell::sync::Lazy; static MAPPED_KEYS: Lazy> = @@ -404,6 +404,7 @@ impl Kanata { ticks_since_idle: 0, movemouse_buffer: None, unmodded_keys: vec![], + last_pressed_key: KeyCode::No, }) } @@ -845,7 +846,7 @@ impl Kanata { // logic there and is easier to add here since we already have // allocations and logic. self.prev_keys.push(*k); - LAST_PRESSED_KEY.store(OsCode::from(k).into(), SeqCst); + self.last_pressed_key = *k; match &mut self.sequence_state { None => { log::debug!("key press {:?}", k); @@ -1233,12 +1234,13 @@ impl Kanata { } } CustomAction::Repeat => { - let key = OsCode::from(LAST_PRESSED_KEY.load(SeqCst)); - log::debug!("repeating a keypress {key:?}"); + let keycode = self.last_pressed_key; + let osc: OsCode = keycode.into(); + log::debug!("repeating a keypress {osc:?}"); let mut do_caps_word = false; if !cur_keys.contains(&KeyCode::LShift) { if let Some(ref mut cw) = self.caps_word { - cur_keys.push(key.into()); + cur_keys.push(keycode); let prev_len = cur_keys.len(); cw.maybe_add_lsft(cur_keys); if cur_keys.len() > prev_len { @@ -1248,9 +1250,9 @@ impl Kanata { } } // Release key in case the most recently pressed key is still pressed. - self.kbd_out.release_key(key)?; - self.kbd_out.press_key(key)?; - self.kbd_out.release_key(key)?; + self.kbd_out.release_key(osc)?; + self.kbd_out.press_key(osc)?; + self.kbd_out.release_key(osc)?; if do_caps_word { self.kbd_out.release_key(OsCode::KEY_LEFTSHIFT)?; } From 1f7af8fcb6142ee2fbdf9ec8a69f7afabd9d3d31 Mon Sep 17 00:00:00 2001 From: rszyma Date: Wed, 22 Nov 2023 08:59:13 +0100 Subject: [PATCH 079/819] refactor: validate defcfg values in parser (#624) 1. Move validation of cfg values to `parse_defcfg` instead of validating them all over the codebase. Reason: before kanata didn't show location when parsing of a cfg value failed, now it does. 2. Replace `HashMap` with `CfgOptions`, which holds only validated cfg values. Reason: Allows to easily parse lists as values, since now the actual value parsing is no longer delayed after parser function exited. This was done with #121 in mind. 3. Move cfg options defaults to a `CfgOptions::default`. Reason: it's good to have similar code in one place. --- docs/config.adoc | 2 +- keyberon/src/action/switch.rs | 234 +++++++----------- parser/src/cfg/defcfg.rs | 376 +++++++++++++++++++++++++++++ parser/src/cfg/mod.rs | 215 +++++------------ parser/src/cfg/tests.rs | 51 ++++ src/kanata/linux.rs | 30 +-- src/kanata/mod.rs | 164 +++---------- src/kanata/windows/interception.rs | 19 +- src/kanata/windows/mod.rs | 38 +-- src/oskbd/linux.rs | 36 +-- 10 files changed, 623 insertions(+), 542 deletions(-) create mode 100644 parser/src/cfg/defcfg.rs diff --git a/docs/config.adoc b/docs/config.adoc index d35637ef5..79774470a 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -707,7 +707,7 @@ a non-applicable operating system. linux-dev /dev/input/dev1:/dev/input/dev2 linux-dev-names-include "Name 1:Name 2" linux-dev-names-exclude "Name 3:Name 4" - linux-continue-if-no-dev-found yes + linux-continue-if-no-devs-found yes linux-unicode-u-code v linux-unicode-termination space linux-x11-repeat-delay-rate 400,50 diff --git a/keyberon/src/action/switch.rs b/keyberon/src/action/switch.rs index e6983d160..fa0c0359a 100644 --- a/keyberon/src/action/switch.rs +++ b/keyberon/src/action/switch.rs @@ -278,14 +278,11 @@ fn bool_evaluation_test_0() { OpCode::new_key(KeyCode::F), ]; let keycodes = [KeyCode::A, KeyCode::B, KeyCode::D, KeyCode::F]; - assert_eq!( - evaluate_boolean( - opcodes.as_slice(), - keycodes.iter().copied(), - [].iter().copied() - ), - true - ); + assert!(evaluate_boolean( + opcodes.as_slice(), + keycodes.iter().copied(), + [].iter().copied() + )); } #[test] @@ -309,14 +306,11 @@ fn bool_evaluation_test_1() { KeyCode::E, KeyCode::F, ]; - assert_eq!( - evaluate_boolean( - opcodes.as_slice(), - keycodes.iter().copied(), - [].iter().copied() - ), - true - ); + assert!(evaluate_boolean( + opcodes.as_slice(), + keycodes.iter().copied(), + [].iter().copied() + )); } #[test] @@ -333,14 +327,11 @@ fn bool_evaluation_test_2() { OpCode(KeyCode::F as u16), ]; let keycodes = [KeyCode::A, KeyCode::B, KeyCode::E, KeyCode::F]; - assert_eq!( - evaluate_boolean( - opcodes.as_slice(), - keycodes.iter().copied(), - [].iter().copied() - ), - false - ); + assert!(!evaluate_boolean( + opcodes.as_slice(), + keycodes.iter().copied(), + [].iter().copied() + )); } #[test] @@ -357,28 +348,22 @@ fn bool_evaluation_test_3() { OpCode(KeyCode::F as u16), ]; let keycodes = [KeyCode::B, KeyCode::C, KeyCode::D, KeyCode::E, KeyCode::F]; - assert_eq!( - evaluate_boolean( - opcodes.as_slice(), - keycodes.iter().copied(), - [].iter().copied() - ), - false - ); + assert!(!evaluate_boolean( + opcodes.as_slice(), + keycodes.iter().copied(), + [].iter().copied() + )); } #[test] fn bool_evaluation_test_4() { let opcodes = []; let keycodes = []; - assert_eq!( - evaluate_boolean( - opcodes.as_slice(), - keycodes.iter().copied(), - [].iter().copied() - ), - true - ); + assert!(evaluate_boolean( + opcodes.as_slice(), + keycodes.iter().copied(), + [].iter().copied() + )); } #[test] @@ -392,14 +377,11 @@ fn bool_evaluation_test_5() { KeyCode::E, KeyCode::F, ]; - assert_eq!( - evaluate_boolean( - opcodes.as_slice(), - keycodes.iter().copied(), - [].iter().copied() - ), - true - ); + assert!(evaluate_boolean( + opcodes.as_slice(), + keycodes.iter().copied(), + [].iter().copied() + )); } #[test] @@ -413,28 +395,22 @@ fn bool_evaluation_test_6() { KeyCode::E, KeyCode::F, ]; - assert_eq!( - evaluate_boolean( - opcodes.as_slice(), - keycodes.iter().copied(), - [].iter().copied() - ), - true - ); + assert!(evaluate_boolean( + opcodes.as_slice(), + keycodes.iter().copied(), + [].iter().copied() + )); } #[test] fn bool_evaluation_test_7() { let opcodes = [OpCode(KeyCode::A as u16), OpCode(KeyCode::B as u16)]; let keycodes = [KeyCode::C, KeyCode::D, KeyCode::E, KeyCode::F]; - assert_eq!( - evaluate_boolean( - opcodes.as_slice(), - keycodes.iter().copied(), - [].iter().copied() - ), - false - ); + assert!(!evaluate_boolean( + opcodes.as_slice(), + keycodes.iter().copied(), + [].iter().copied() + )); } #[test] @@ -446,14 +422,11 @@ fn bool_evaluation_test_9() { OpCode(KeyCode::C as u16), ]; let keycodes = [KeyCode::C, KeyCode::D, KeyCode::E, KeyCode::F]; - assert_eq!( - evaluate_boolean( - opcodes.as_slice(), - keycodes.iter().copied(), - [].iter().copied() - ), - true - ); + assert!(evaluate_boolean( + opcodes.as_slice(), + keycodes.iter().copied(), + [].iter().copied() + )); } #[test] @@ -465,14 +438,11 @@ fn bool_evaluation_test_10() { OpCode(KeyCode::C as u16), ]; let keycodes = [KeyCode::C, KeyCode::D, KeyCode::E, KeyCode::F]; - assert_eq!( - evaluate_boolean( - opcodes.as_slice(), - keycodes.iter().copied(), - [].iter().copied() - ), - false - ); + assert!(!evaluate_boolean( + opcodes.as_slice(), + keycodes.iter().copied(), + [].iter().copied() + )); } #[test] @@ -483,14 +453,11 @@ fn bool_evaluation_test_11() { OpCode(KeyCode::B as u16), ]; let keycodes = [KeyCode::C, KeyCode::D, KeyCode::E, KeyCode::F]; - assert_eq!( - evaluate_boolean( - opcodes.as_slice(), - keycodes.iter().copied(), - [].iter().copied() - ), - false - ); + assert!(!evaluate_boolean( + opcodes.as_slice(), + keycodes.iter().copied(), + [].iter().copied() + )); } #[test] @@ -503,14 +470,11 @@ fn bool_evaluation_test_12() { OpCode(KeyCode::C as u16), ]; let keycodes = [KeyCode::C, KeyCode::D, KeyCode::E, KeyCode::F]; - assert_eq!( - evaluate_boolean( - opcodes.as_slice(), - keycodes.iter().copied(), - [].iter().copied() - ), - true - ); + assert!(evaluate_boolean( + opcodes.as_slice(), + keycodes.iter().copied(), + [].iter().copied() + )); } #[test] @@ -526,14 +490,11 @@ fn bool_evaluation_test_max_depth_does_not_panic() { OpCode(0x1008), ]; let keycodes = []; - assert_eq!( - evaluate_boolean( - opcodes.as_slice(), - keycodes.iter().copied(), - [].iter().copied() - ), - true - ); + assert!(evaluate_boolean( + opcodes.as_slice(), + keycodes.iter().copied(), + [].iter().copied() + )); } #[test] @@ -551,14 +512,11 @@ fn bool_evaluation_test_more_than_max_depth_panics() { OpCode(0x1009), ]; let keycodes = []; - assert_eq!( - evaluate_boolean( - opcodes.as_slice(), - keycodes.iter().copied(), - [].iter().copied() - ), - true - ); + assert!(evaluate_boolean( + opcodes.as_slice(), + keycodes.iter().copied(), + [].iter().copied() + )); } #[test] @@ -632,38 +590,26 @@ fn switch_historical_1() { KeyCode::G, KeyCode::H, ]; - assert_eq!( - evaluate_boolean( - opcode_true.as_slice(), - [].iter().copied(), - hist_keycodes.iter().copied(), - ), - true - ); - assert_eq!( - evaluate_boolean( - opcode_true2.as_slice(), - [].iter().copied(), - hist_keycodes.iter().copied(), - ), - true - ); - assert_eq!( - evaluate_boolean( - opcode_false.as_slice(), - [].iter().copied(), - hist_keycodes.iter().copied(), - ), - false - ); - assert_eq!( - evaluate_boolean( - opcode_false2.as_slice(), - [].iter().copied(), - hist_keycodes.iter().copied(), - ), - false - ); + assert!(evaluate_boolean( + opcode_true.as_slice(), + [].iter().copied(), + hist_keycodes.iter().copied(), + )); + assert!(evaluate_boolean( + opcode_true2.as_slice(), + [].iter().copied(), + hist_keycodes.iter().copied(), + )); + assert!(!evaluate_boolean( + opcode_false.as_slice(), + [].iter().copied(), + hist_keycodes.iter().copied(), + )); + assert!(!evaluate_boolean( + opcode_false2.as_slice(), + [].iter().copied(), + hist_keycodes.iter().copied(), + )); } #[test] diff --git a/parser/src/cfg/defcfg.rs b/parser/src/cfg/defcfg.rs new file mode 100644 index 000000000..75757804d --- /dev/null +++ b/parser/src/cfg/defcfg.rs @@ -0,0 +1,376 @@ +use super::error::*; +use super::sexpr::SExpr; +use super::HashSet; +use crate::cfg::check_first_expr; +use crate::custom_action::*; +#[allow(unused)] +use crate::{anyhow_expr, anyhow_span, bail, bail_expr}; + +#[derive(Debug)] +pub struct CfgOptions { + pub process_unmapped_keys: bool, + pub enable_cmd: bool, + pub sequence_timeout: u16, + pub sequence_input_mode: SequenceInputMode, + pub sequence_backtrack_modcancel: bool, + pub log_layer_changes: bool, + pub delegate_to_first_layer: bool, + pub movemouse_inherit_accel_state: bool, + pub movemouse_smooth_diagonals: bool, + pub dynamic_macro_max_presses: u16, + #[cfg(any(target_os = "linux", target_os = "unknown"))] + pub linux_dev: Vec, + #[cfg(any(target_os = "linux", target_os = "unknown"))] + pub linux_dev_names_include: Option>, + #[cfg(any(target_os = "linux", target_os = "unknown"))] + pub linux_dev_names_exclude: Option>, + #[cfg(any(target_os = "linux", target_os = "unknown"))] + pub linux_continue_if_no_devs_found: bool, + #[cfg(any(target_os = "linux", target_os = "unknown"))] + pub linux_unicode_u_code: crate::keys::OsCode, + #[cfg(any(target_os = "linux", target_os = "unknown"))] + pub linux_unicode_termination: UnicodeTermination, + #[cfg(any(target_os = "linux", target_os = "unknown"))] + pub linux_x11_repeat_delay_rate: Option, + #[cfg(any(target_os = "windows", target_os = "unknown"))] + pub windows_altgr: AltGrBehaviour, + #[cfg(any( + all(feature = "interception_driver", target_os = "windows"), + target_os = "unknown" + ))] + pub windows_interception_mouse_hwid: Option<[u8; HWID_ARR_SZ]>, +} + +impl Default for CfgOptions { + fn default() -> Self { + Self { + process_unmapped_keys: false, + enable_cmd: false, + sequence_timeout: 1000, + sequence_input_mode: SequenceInputMode::HiddenSuppressed, + sequence_backtrack_modcancel: true, + log_layer_changes: true, + delegate_to_first_layer: false, + movemouse_inherit_accel_state: false, + movemouse_smooth_diagonals: false, + dynamic_macro_max_presses: 128, + #[cfg(any(target_os = "linux", target_os = "unknown"))] + linux_dev: vec![], + #[cfg(any(target_os = "linux", target_os = "unknown"))] + linux_dev_names_include: None, + #[cfg(any(target_os = "linux", target_os = "unknown"))] + linux_dev_names_exclude: None, + #[cfg(any(target_os = "linux", target_os = "unknown"))] + linux_continue_if_no_devs_found: false, + #[cfg(any(target_os = "linux", target_os = "unknown"))] + // historically was the only option, so make KEY_U the default + linux_unicode_u_code: crate::keys::OsCode::KEY_U, + #[cfg(any(target_os = "linux", target_os = "unknown"))] + // historically was the only option, so make Enter the default + linux_unicode_termination: UnicodeTermination::Enter, + #[cfg(any(target_os = "linux", target_os = "unknown"))] + linux_x11_repeat_delay_rate: None, + #[cfg(any(target_os = "windows", target_os = "unknown"))] + windows_altgr: AltGrBehaviour::default(), + #[cfg(any( + all(feature = "interception_driver", target_os = "windows"), + target_os = "unknown" + ))] + windows_interception_mouse_hwid: None, + } + } +} + +/// Parse configuration entries from an expression starting with defcfg. +pub fn parse_defcfg(expr: &[SExpr]) -> Result { + let mut seen_keys = HashSet::default(); + let mut cfg = CfgOptions::default(); + let mut exprs = check_first_expr(expr.iter(), "defcfg")?; + // Read k-v pairs from the configuration + loop { + let key = match exprs.next() { + Some(k) => k, + None => return Ok(cfg), + }; + let val = match exprs.next() { + Some(v) => v, + None => bail_expr!(key, "Found a defcfg option missing a value"), + }; + match (&key, &val) { + (SExpr::Atom(k), SExpr::Atom(v)) => { + if !seen_keys.insert(&k.t) { + bail_expr!(key, "Duplicate defcfg option {}", k.t); + } + match k.t.as_str() { + k @ "sequence-timeout" => { + cfg.sequence_timeout = parse_cfg_val_u16(val, k, true)?; + } + "sequence-input-mode" => { + cfg.sequence_input_mode = + SequenceInputMode::try_from_str(&v.t.trim_matches('"')) + .map_err(|e| anyhow_expr!(val, "{}", e.to_string()))?; + } + k @ "dynamic-macro-max-presses" => { + cfg.dynamic_macro_max_presses = parse_cfg_val_u16(val, k, false)?; + } + "linux-dev" => { + #[cfg(any(target_os = "linux", target_os = "unknown"))] + { + let paths = v.t.trim_matches('"'); + cfg.linux_dev = parse_colon_separated_text(paths); + } + } + "linux-dev-names-include" => { + #[cfg(any(target_os = "linux", target_os = "unknown"))] + { + let paths = v.t.trim_matches('"'); + cfg.linux_dev_names_include = Some(parse_colon_separated_text(paths)); + } + } + "linux-dev-names-exclude" => { + #[cfg(any(target_os = "linux", target_os = "unknown"))] + { + let paths = v.t.trim_matches('"'); + cfg.linux_dev_names_exclude = Some(parse_colon_separated_text(paths)); + } + } + _k @ "linux-unicode-u-code" => { + #[cfg(any(target_os = "linux", target_os = "unknown"))] + { + cfg.linux_unicode_u_code = crate::keys::str_to_oscode( + v.t.trim_matches('"'), + ) + .ok_or_else(|| anyhow_expr!(val, "unknown code for {_k}: {}", v.t))?; + } + } + _k @ "linux-unicode-termination" => { + #[cfg(any(target_os = "linux", target_os = "unknown"))] + { + cfg.linux_unicode_termination = match v.t.trim_matches('"') { + "enter" => UnicodeTermination::Enter, + "space" => UnicodeTermination::Space, + "enter-space" => UnicodeTermination::EnterSpace, + "space-enter" => UnicodeTermination::SpaceEnter, + _ => bail_expr!( + val, + "{_k} got {}. It accepts: enter|space|enter-space|space-enter", + v.t + ), + } + } + } + "linux-x11-repeat-delay-rate" => { + #[cfg(any(target_os = "linux", target_os = "unknown"))] + { + let delay_rate = v.t.trim_matches('"').split(',').collect::>(); + const ERRMSG: &str = "Invalid value for linux-x11-repeat-delay-rate.\nExpected two numbers 0-65535 separated by a comma, e.g. 200,25"; + if delay_rate.len() != 2 { + bail_expr!(val, "{}", ERRMSG) + } + cfg.linux_x11_repeat_delay_rate = Some(KeyRepeatSettings { + delay: match str::parse::(delay_rate[0]) { + Ok(delay) => delay, + Err(_) => bail_expr!(val, "{}", ERRMSG), + }, + rate: match str::parse::(delay_rate[1]) { + Ok(rate) => rate, + Err(_) => bail_expr!(val, "{}", ERRMSG), + }, + }); + } + } + _k @ "windows-altgr" => { + #[cfg(any(target_os = "windows", target_os = "unknown"))] + { + const CANCEL: &str = "cancel-lctl-press"; + const ADD: &str = "add-lctl-release"; + cfg.windows_altgr = match v.t.trim_matches('"') { + CANCEL => AltGrBehaviour::CancelLctlPress, + ADD => AltGrBehaviour::AddLctlRelease, + _ => bail_expr!( + val, + "Invalid value for {_k}: {}. Valid values are {},{}", + v.t, + CANCEL, + ADD + ), + } + } + } + _k @ "windows-interception-mouse-hwid" => { + #[cfg(any( + all(feature = "interception_driver", target_os = "windows"), + target_os = "unknown" + ))] + { + let hwid = v.t.trim_matches('"'); + log::trace!("win hwid: {hwid}"); + let hwid_vec = hwid + .split(',') + .try_fold(vec![], |mut hwid_bytes, hwid_byte| { + hwid_byte.trim_matches(' ').parse::().map(|b| { + hwid_bytes.push(b); + hwid_bytes + }) + }).map_err(|_| anyhow_expr!(val, "{_k} format is invalid. It should consist of integers separated by commas"))?; + let hwid_slice = hwid_vec.iter().copied().enumerate() + .try_fold([0u8; HWID_ARR_SZ], |mut hwid, idx_byte| { + let (i, b) = idx_byte; + if i > HWID_ARR_SZ { + bail_expr!(val, "{_k} is too long; it should be up to {HWID_ARR_SZ} 8-bit unsigned integers") + } + hwid[i] = b; + Ok(hwid) + }); + cfg.windows_interception_mouse_hwid = Some(hwid_slice?); + } + } + + "process-unmapped-keys" => { + cfg.process_unmapped_keys = parse_defcfg_val_bool(val, &k.t)? + } + "danger-enable-cmd" => cfg.enable_cmd = parse_defcfg_val_bool(val, &k.t)?, + "sequence-backtrack-modcancel" => { + cfg.sequence_backtrack_modcancel = parse_defcfg_val_bool(val, &k.t)? + } + "log-layer-changes" => { + cfg.log_layer_changes = parse_defcfg_val_bool(val, &k.t)? + } + "delegate-to-first-layer" => { + cfg.delegate_to_first_layer = parse_defcfg_val_bool(val, &k.t)?; + if cfg.delegate_to_first_layer { + log::info!("delegating transparent keys on other layers to first defined layer"); + } + } + "linux-continue-if-no-devs-found" => { + #[cfg(any(target_os = "linux", target_os = "unknown"))] + { + cfg.linux_continue_if_no_devs_found = parse_defcfg_val_bool(val, &k.t)? + } + } + "movemouse-smooth-diagonals" => { + cfg.movemouse_smooth_diagonals = parse_defcfg_val_bool(val, &k.t)? + } + "movemouse-inherit-accel-state" => { + cfg.movemouse_inherit_accel_state = parse_defcfg_val_bool(val, &k.t)? + } + _ => bail_expr!(key, "Unknown defcfg option {}", &k.t), + }; + } + (SExpr::List(_), _) => { + bail_expr!(key, "Lists are not allowed in defcfg"); + } + (_, SExpr::List(_)) => { + bail_expr!(val, "Lists are not allowed in defcfg"); + } + } + } +} + +pub const FALSE_VALUES: [&str; 3] = ["no", "false", "0"]; +pub const TRUE_VALUES: [&str; 3] = ["yes", "true", "1"]; +pub const BOOLEAN_VALUES: [&str; 6] = ["yes", "true", "1", "no", "false", "0"]; + +fn parse_defcfg_val_bool(expr: &SExpr, label: &str) -> Result { + match &expr { + SExpr::Atom(v) => { + let val = v.t.trim_matches('"').to_ascii_lowercase(); + if TRUE_VALUES.contains(&val.as_str()) { + Ok(true) + } else if FALSE_VALUES.contains(&val.as_str()) { + Ok(false) + } else { + bail_expr!( + expr, + "The value for {label} must be one of: {}", + BOOLEAN_VALUES.join(", ") + ); + } + } + SExpr::List(_) => { + bail_expr!( + expr, + "The value for {label} cannot be a list, it must be one of: {}", + BOOLEAN_VALUES.join(", "), + ) + } + } +} + +fn parse_cfg_val_u16(expr: &SExpr, label: &str, exclude_zero: bool) -> Result { + let start = if exclude_zero { 1 } else { 0 }; + match &expr { + SExpr::Atom(v) => Ok(str::parse::(&v.t.trim_matches('"')) + .ok() + .and_then(|u| { + if exclude_zero && u == 0 { + None + } else { + Some(u) + } + }) + .ok_or_else(|| anyhow_expr!(expr, "{label} must be {start}-65535"))?), + SExpr::List(_) => { + bail_expr!( + expr, + "The value for {label} cannot be a list, it must be a number {start}-65535", + ) + } + } +} + +pub fn parse_colon_separated_text(paths: &str) -> Vec { + let mut all_paths = vec![]; + let mut full_dev_path = String::new(); + let mut dev_path_iter = paths.split(':').peekable(); + while let Some(dev_path) = dev_path_iter.next() { + if dev_path.ends_with('\\') && dev_path_iter.peek().is_some() { + full_dev_path.push_str(dev_path.trim_end_matches('\\')); + full_dev_path.push(':'); + continue; + } else { + full_dev_path.push_str(dev_path); + } + all_paths.push(full_dev_path.clone()); + full_dev_path.clear(); + } + all_paths.shrink_to_fit(); + all_paths +} + +#[cfg(any(target_os = "linux", target_os = "unknown"))] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct KeyRepeatSettings { + pub delay: u16, + pub rate: u16, +} + +#[cfg(any(target_os = "linux", target_os = "unknown"))] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum UnicodeTermination { + Enter, + Space, + SpaceEnter, + EnterSpace, +} + +#[cfg(any(target_os = "windows", target_os = "unknown"))] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AltGrBehaviour { + DoNothing, + CancelLctlPress, + AddLctlRelease, +} + +#[cfg(any(target_os = "windows", target_os = "unknown"))] +impl Default for AltGrBehaviour { + fn default() -> Self { + Self::DoNothing + } +} + +#[cfg(any( + all(feature = "interception_driver", target_os = "windows"), + target_os = "unknown" +))] +pub const HWID_ARR_SZ: usize = 128; diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index d6d1a871d..7d4a7a737 100644 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -51,6 +51,9 @@ use custom_tap_hold::*; mod list_actions; use list_actions::*; +mod defcfg; +pub use defcfg::*; + use crate::custom_action::*; use crate::keys::*; use crate::layers::*; @@ -81,15 +84,17 @@ mod tests; #[cfg(test)] pub use sexpr::parse; +#[macro_export] macro_rules! bail { ($err:expr $(,)?) => { - return Err(ParseError::from(anyhow!($err))) + return Err(ParseError::from(anyhow::anyhow!($err))) }; ($fmt:expr, $($arg:tt)*) => { - return Err(ParseError::from(anyhow!($fmt, $($arg)*))) + return Err(ParseError::from(anyhow::anyhow!($fmt, $($arg)*))) }; } +#[macro_export] macro_rules! bail_expr { ($expr:expr, $fmt:expr $(,)?) => { return Err(ParseError::from_expr($expr, format!($fmt))) @@ -99,6 +104,7 @@ macro_rules! bail_expr { }; } +#[macro_export] macro_rules! bail_span { ($expr:expr, $fmt:expr $(,)?) => { return Err(ParseError::from_spanned($expr, format!($fmt))) @@ -108,6 +114,7 @@ macro_rules! bail_span { }; } +#[macro_export] macro_rules! anyhow_expr { ($expr:expr, $fmt:expr $(,)?) => { ParseError::from_expr($expr, format!($fmt)) @@ -117,6 +124,7 @@ macro_rules! anyhow_expr { }; } +#[macro_export] macro_rules! anyhow_span { ($expr:expr, $fmt:expr $(,)?) => { ParseError::from_spanned($expr, format!($fmt)) @@ -190,7 +198,7 @@ pub struct Cfg { /// Layer info used for printing to the logs. pub layer_info: Vec, /// Configuration items in `defcfg`. - pub items: HashMap, + pub items: CfgOptions, /// The keyberon layout state machine struct. pub layout: KanataLayout, /// Sequences defined in `defseq`. @@ -230,7 +238,7 @@ pub struct LayerInfo { fn parse_cfg( p: &Path, ) -> MResult<( - HashMap, + CfgOptions, MappedKeys, Vec, KeyOutputs, @@ -251,10 +259,6 @@ fn parse_cfg( )) } -pub const FALSE_VALUES: [&str; 3] = ["no", "false", "0"]; -pub const TRUE_VALUES: [&str; 3] = ["yes", "true", "1"]; -pub const BOOLEAN_VALUES: [&str; 6] = ["yes", "true", "1", "no", "false", "0"]; - #[cfg(all(not(feature = "interception_driver"), target_os = "windows"))] const DEF_LOCAL_KEYS: &str = "deflocalkeys-win"; #[cfg(all(feature = "interception_driver", target_os = "windows"))] @@ -267,7 +271,7 @@ fn parse_cfg_raw( p: &Path, s: &mut ParsedState, ) -> MResult<( - HashMap, + CfgOptions, MappedKeys, Vec, Box, @@ -371,7 +375,7 @@ pub fn parse_cfg_raw_string( file_content_provider: &mut FileContentProvider, def_local_keys_variant_to_apply: &str, ) -> Result<( - HashMap, + CfgOptions, MappedKeys, Vec, Box, @@ -392,23 +396,6 @@ pub fn parse_cfg_raw_string( error_on_unknown_top_level_atoms(&spanned_root_exprs)?; - let cfg = root_exprs - .iter() - .find(gen_first_atom_filter("defcfg")) - .map(|cfg| parse_defcfg(cfg)) - .transpose()? - .unwrap_or_default(); - if let Some(spanned) = spanned_root_exprs - .iter() - .filter(gen_first_atom_filter_spanned("defcfg")) - .nth(1) - { - bail_span!( - spanned, - "Only one defcfg is allowed, found more. Delete the extras." - ) - } - let mut local_keys: Option> = None; clear_custom_str_oscode_mapping(); for def_local_keys_variant in [ @@ -425,7 +412,7 @@ pub fn parse_cfg_raw_string( if def_local_keys_variant == def_local_keys_variant_to_apply { assert!( local_keys.is_none(), - ">1 mutually exclusive deflocalkeys variants was parsed" + ">1 mutually exclusive deflocalkeys variants were parsed" ); local_keys = Some(mapping); } @@ -444,6 +431,23 @@ pub fn parse_cfg_raw_string( } replace_custom_str_oscode_mapping(&local_keys.unwrap_or_default()); + let cfg = root_exprs + .iter() + .find(gen_first_atom_filter("defcfg")) + .map(|cfg| parse_defcfg(cfg)) + .transpose()? + .unwrap_or_default(); + if let Some(spanned) = spanned_root_exprs + .iter() + .filter(gen_first_atom_filter_spanned("defcfg")) + .nth(1) + { + bail_span!( + spanned, + "Only one defcfg is allowed, found more. Delete the extras." + ) + } + let src_expr = root_exprs .iter() .find(gen_first_atom_filter("defsrc")) @@ -535,14 +539,12 @@ pub fn parse_cfg_raw_string( is_cmd_enabled: { #[cfg(feature = "cmd")] { - cfg.get("danger-enable-cmd").map_or(false, |s| { - if TRUE_VALUES.contains(&s.to_lowercase().as_str()) { - log::warn!("DANGER! cmd action is enabled."); - true - } else { - false - } - }) + if cfg.enable_cmd { + log::warn!("DANGER! cmd action is enabled."); + true + } else { + false + } } #[cfg(not(feature = "cmd"))] { @@ -550,25 +552,9 @@ pub fn parse_cfg_raw_string( false } }, - delegate_to_first_layer: cfg.get("delegate-to-first-layer").map_or(false, |s| { - if TRUE_VALUES.contains(&s.to_lowercase().as_str()) { - log::info!("delegating transparent keys on other layers to first defined layer"); - true - } else { - false - } - }), - default_sequence_timeout: cfg - .get(SEQUENCE_TIMEOUT_CFG_NAME) - .map(|s| match str::parse::(s) { - Ok(0) | Err(_) => Err(anyhow!("{SEQUENCE_TIMEOUT_ERR}")), - Ok(t) => Ok(t), - }) - .unwrap_or(Ok(SEQUENCE_TIMEOUT_DEFAULT))?, - default_sequence_input_mode: cfg - .get(SEQUENCE_INPUT_MODE_CFG_NAME) - .map(|s| SequenceInputMode::try_from_str(s.as_str())) - .unwrap_or(Ok(SequenceInputMode::HiddenSuppressed))?, + delegate_to_first_layer: cfg.delegate_to_first_layer, + default_sequence_timeout: cfg.sequence_timeout, + default_sequence_input_mode: cfg.sequence_input_mode, ..Default::default() }; @@ -734,85 +720,12 @@ fn check_first_expr<'a>( Ok(exprs) } -/// Parse configuration entries from an expression starting with defcfg. -fn parse_defcfg(expr: &[SExpr]) -> Result> { - let non_bool_cfg_keys = &[ - "sequence-timeout", - "sequence-input-mode", - "dynamic-macro-max-presses", - "linux-dev", - "linux-dev-names-include", - "linux-dev-names-exclude", - "linux-unicode-u-code", - "linux-unicode-termination", - "linux-x11-repeat-delay-rate", - "windows-altgr", - "windows-interception-mouse-hwid", - ]; - let bool_cfg_keys = &[ - "process-unmapped-keys", - "danger-enable-cmd", - "sequence-backtrack-modcancel", - "log-layer-changes", - "delegate-to-first-layer", - "linux-continue-if-no-devs-found", - "movemouse-smooth-diagonals", - "movemouse-inherit-accel-state", - ]; - let mut cfg = HashMap::default(); - let mut exprs = check_first_expr(expr.iter(), "defcfg")?; - // Read k-v pairs from the configuration - loop { - let key = match exprs.next() { - Some(k) => k, - None => return Ok(cfg), - }; - let val = match exprs.next() { - Some(v) => v, - None => bail_expr!(key, "Found a defcfg option missing a value"), - }; - match (&key, &val) { - (SExpr::Atom(k), SExpr::Atom(v)) => { - if non_bool_cfg_keys.contains(&&*k.t) { - // nothing to do - } else if bool_cfg_keys.contains(&&*k.t) { - if !BOOLEAN_VALUES.contains(&&*v.t) { - bail_expr!( - val, - "The value for {} must be one of: {}", - k.t, - BOOLEAN_VALUES.join(", ") - ); - } - } else { - bail_expr!(key, "Unknown defcfg option {}", k.t); - } - if cfg - .insert( - k.t.trim_matches('"').to_owned(), - v.t.trim_matches('"').to_owned(), - ) - .is_some() - { - bail_expr!(key, "Duplicate defcfg option {}", k.t); - } - } - (SExpr::List(_), _) => { - bail_expr!(key, "Lists are not allowed in defcfg"); - } - (_, SExpr::List(_)) => { - bail_expr!(val, "Lists are not allowed in defcfg"); - } - } - } -} - /// Parse custom keys from an expression starting with deflocalkeys. fn parse_deflocalkeys( def_local_keys_variant: &str, expr: &[SExpr], ) -> Result> { - let mut cfg = HashMap::default(); + let mut localkeys = HashMap::default(); let mut exprs = check_first_expr(expr.iter(), def_local_keys_variant)?; // Read k-v pairs from the configuration while let Some(key_expr) = exprs.next() { @@ -824,7 +737,7 @@ fn parse_deflocalkeys( key_expr, "Cannot use {key} in {def_local_keys_variant} because it is a default key name" ); - } else if cfg.contains_key(key) { + } else if localkeys.contains_key(key) { bail_expr!( key_expr, "Duplicate {key} found in {def_local_keys_variant}" @@ -847,18 +760,15 @@ fn parse_deflocalkeys( None => bail_expr!(key_expr, "Key without a number in {def_local_keys_variant}"), }; log::debug!("custom mapping: {key} {}", osc.as_u16()); - cfg.insert(key.to_owned(), osc); + localkeys.insert(key.to_owned(), osc); } - Ok(cfg) + Ok(localkeys) } /// Parse mapped keys from an expression starting with defsrc. Returns the key mapping as well as /// a vec of the indexes in order. The length of the returned vec should be matched by the length /// of all layer declarations. -fn parse_defsrc( - expr: &[SExpr], - defcfg: &HashMap, -) -> Result<(MappedKeys, Vec)> { +fn parse_defsrc(expr: &[SExpr], defcfg: &CfgOptions) -> Result<(MappedKeys, Vec)> { let exprs = check_first_expr(expr.iter(), "defsrc")?; let mut mkeys = MappedKeys::default(); let mut ordered_codes = Vec::new(); @@ -876,12 +786,8 @@ fn parse_defsrc( ordered_codes.push(oscode.into()); } - let process_unmapped_keys = defcfg - .get("process-unmapped-keys") - .map(|s| TRUE_VALUES.contains(&s.to_lowercase().as_str())) - .unwrap_or(false); - log::info!("process unmapped keys: {process_unmapped_keys}"); - if process_unmapped_keys { + log::info!("process unmapped keys: {}", defcfg.process_unmapped_keys); + if defcfg.process_unmapped_keys { for osc in 0..KEYS_IN_ROW as u16 { if let Some(osc) = OsCode::from_u16(osc) { match KeyCode::from(osc) { @@ -942,11 +848,11 @@ pub struct ParsedState { fake_keys: HashMap, chord_groups: HashMap, defsrc_layer: [KanataAction; KEYS_IN_ROW], + vars: HashMap, is_cmd_enabled: bool, delegate_to_first_layer: bool, default_sequence_timeout: u16, default_sequence_input_mode: SequenceInputMode, - vars: HashMap, a: Arc, } @@ -956,14 +862,9 @@ impl ParsedState { } } -const SEQUENCE_TIMEOUT_CFG_NAME: &str = "sequence-timeout"; -const SEQUENCE_INPUT_MODE_CFG_NAME: &str = "sequence-input-mode"; -const SEQUENCE_TIMEOUT_ERR: &str = "sequence-timeout should be a number (1-65535)"; -const SEQUENCE_TIMEOUT_DEFAULT: u16 = 1000; -const SEQUENCE_INPUT_MODE_DEFAULT: SequenceInputMode = SequenceInputMode::HiddenSuppressed; - impl Default for ParsedState { fn default() -> Self { + let default_cfg = CfgOptions::default(); Self { layer_exprs: Default::default(), aliases: Default::default(), @@ -972,11 +873,11 @@ impl Default for ParsedState { defsrc_layer: [KanataAction::Trans; KEYS_IN_ROW], fake_keys: Default::default(), chord_groups: Default::default(), - is_cmd_enabled: false, - delegate_to_first_layer: false, vars: Default::default(), - default_sequence_timeout: SEQUENCE_TIMEOUT_DEFAULT, - default_sequence_input_mode: SEQUENCE_INPUT_MODE_DEFAULT, + is_cmd_enabled: default_cfg.enable_cmd, + delegate_to_first_layer: default_cfg.delegate_to_first_layer, + default_sequence_timeout: default_cfg.sequence_timeout, + default_sequence_input_mode: default_cfg.sequence_input_mode, a: unsafe { Allocations::new() }, } } @@ -2360,14 +2261,13 @@ fn parse_fake_key_op_coord_action( }; let action = ac_params[1] .atom(s.vars()) - .map(|a| match a { + .and_then(|a| match a { "tap" => Some(FakeKeyAction::Tap), "press" => Some(FakeKeyAction::Press), "release" => Some(FakeKeyAction::Release), "toggle" => Some(FakeKeyAction::Toggle), _ => None, }) - .flatten() .ok_or_else(|| { anyhow_expr!( &ac_params[1], @@ -2937,7 +2837,7 @@ fn parse_sequence_start(ac_params: &[SExpr], s: &ParsedState) -> Result<&'static let input_mode = if ac_params.len() > 1 { if let Some(Ok(input_mode)) = ac_params[1] .atom(s.vars()) - .map(|config_str| SequenceInputMode::try_from_str(config_str)) + .map(SequenceInputMode::try_from_str) { input_mode } else { @@ -3106,13 +3006,12 @@ fn parse_on_idle_fakekey(ac_params: &[SExpr], s: &ParsedState) -> Result<&'stati }; let action = ac_params[1] .atom(s.vars()) - .map(|a| match a { + .and_then(|a| match a { "tap" => Some(FakeKeyAction::Tap), "press" => Some(FakeKeyAction::Press), "release" => Some(FakeKeyAction::Release), _ => None, }) - .flatten() .ok_or_else(|| { anyhow_expr!( &ac_params[1], diff --git a/parser/src/cfg/tests.rs b/parser/src/cfg/tests.rs index fc69003f2..fa55ac4f6 100644 --- a/parser/src/cfg/tests.rs +++ b/parser/src/cfg/tests.rs @@ -1198,3 +1198,54 @@ fn list_action_not_in_list_error_message_is_good() { }) .unwrap_err(); } + +#[test] +fn parse_device_paths() { + assert_eq!(parse_colon_separated_text("h:w"), ["h", "w"]); + assert_eq!(parse_colon_separated_text("h\\:w"), ["h:w"]); + assert_eq!(parse_colon_separated_text("h\\:w\\"), ["h:w\\"]); +} + +#[test] +fn parse_all_defcfg() { + let _lk = match CFG_PARSE_LOCK.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + }; + let source = r#" +(defcfg + process-unmapped-keys yes + danger-enable-cmd yes + sequence-timeout 2000 + sequence-input-mode visible-backspaced + sequence-backtrack-modcancel no + log-layer-changes no + delegate-to-first-layer yes + movemouse-inherit-accel-state yes + movemouse-smooth-diagonals yes + dynamic-macro-max-presses 1000 + linux-dev /dev/input/dev1:/dev/input/dev2 + linux-dev-names-include "Name 1:Name 2" + linux-dev-names-exclude "Name 3:Name 4" + linux-continue-if-no-devs-found yes + linux-unicode-u-code v + linux-unicode-termination space + linux-x11-repeat-delay-rate 400,50 + windows-altgr add-lctl-release + windows-interception-mouse-hwid "70, 0, 60, 0" +) +(defsrc a) +(deflayer base a) +"#; + let mut s = ParsedState::default(); + parse_cfg_raw_string( + source, + &mut s, + &PathBuf::from("test"), + &mut FileContentProvider { + get_file_content_fn: &mut |_| unimplemented!(), + }, + DEF_LOCAL_KEYS, + ) + .expect("succeeds"); +} diff --git a/src/kanata/linux.rs b/src/kanata/linux.rs index 0497b2269..8be4cee86 100644 --- a/src/kanata/linux.rs +++ b/src/kanata/linux.rs @@ -29,7 +29,7 @@ impl Kanata { // In some environments, this needs to be done after the input device grab otherwise it // does not work on kanata startup. - Kanata::set_repeat_rate(&k.defcfg_items)?; + Kanata::set_repeat_rate(k.x11_repeat_rate)?; drop(k); loop { @@ -83,28 +83,20 @@ impl Kanata { Ok(()) } - pub fn set_repeat_rate(cfg_items: &HashMap) -> Result<()> { - if let Some(x11_rpt_str) = cfg_items.get("linux-x11-repeat-delay-rate") { - let delay_rate = x11_rpt_str.split(',').collect::>(); - let errmsg = format!("Invalid value for linux-x11-repeat-delay-rate: \"{x11_rpt_str}\".\nExpected two numbers 0-65535 separated by a comma, e.g. 200,25"); - if delay_rate.len() != 2 { - log::error!("{errmsg}"); - } - str::parse::(delay_rate[0]).map_err(|e| { - log::error!("{errmsg}"); - e - })?; - str::parse::(delay_rate[1]).map_err(|e| { - log::error!("{errmsg}"); - e - })?; + pub fn set_repeat_rate(s: Option) -> Result<()> { + if let Some(s) = s { log::info!( "Using xset to set X11 repeat delay to {} and repeat rate to {}", - delay_rate[0], - delay_rate[1] + s.delay, + s.rate, ); let cmd_output = std::process::Command::new("xset") - .args(["r", "rate", delay_rate[0], delay_rate[1]]) + .args([ + "r", + "rate", + s.delay.to_string().as_str(), + s.rate.to_string().as_str(), + ]) .output() .map_err(|e| { log::error!("failed to run xset: {e:?}"); diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index b4d35dfda..957f71f99 100644 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -1,6 +1,6 @@ //! Implements the glue between OS input/output and keyberon state management. -use anyhow::{anyhow, bail, Result}; +use anyhow::{bail, Result}; use log::{error, info}; use parking_lot::Mutex; use std::sync::mpsc::{Receiver, SyncSender as Sender, TryRecvError}; @@ -132,14 +132,14 @@ pub struct Kanata { #[cfg(all(feature = "interception_driver", target_os = "windows"))] /// Used to know which input device to treat as a mouse for intercepting and processing inputs /// by kanata. - intercept_mouse_hwid: Option>, + intercept_mouse_hwid: Option<[u8; HWID_ARR_SZ]>, /// User configuration to do logging of layer changes or not. log_layer_changes: bool, /// Tracks the caps-word state. Is Some(...) if caps-word is active and None otherwise. pub caps_word: Option, /// Config items from `defcfg`. #[cfg(target_os = "linux")] - pub defcfg_items: HashMap, + pub x11_repeat_rate: Option, /// Fake key actions that are waiting for a certain duration of keyboard idling. pub waiting_for_idle: HashSet, /// Number of ticks since kanata was idle. @@ -259,23 +259,6 @@ impl Kanata { } }; - #[cfg(all(feature = "interception_driver", target_os = "windows"))] - let intercept_mouse_hwid = cfg - .items - .get("windows-interception-mouse-hwid") - .map(|hwid: &String| { - log::trace!("win hwid: {hwid}"); - hwid.split_whitespace() - .try_fold(vec![], |mut hwid_bytes, hwid_byte| { - hwid_byte.trim_matches(',').parse::().map(|b| { - hwid_bytes.push(b); - hwid_bytes - }) - }) - .ok() - }) - .unwrap_or_default(); - let kbd_out = match KbdOut::new( #[cfg(target_os = "linux")] &args.symlink_path, @@ -287,26 +270,6 @@ impl Kanata { } }; - #[cfg(target_os = "linux")] - let kbd_in_paths = cfg - .items - .get("linux-dev") - .cloned() - .map(|paths| parse_colon_separated_text(&paths)) - .unwrap_or_default(); - #[cfg(target_os = "linux")] - let include_names = cfg - .items - .get("linux-dev-names-include") - .cloned() - .map(|paths| parse_colon_separated_text(&paths)); - #[cfg(target_os = "linux")] - let exclude_names = cfg - .items - .get("linux-dev-names-exclude") - .cloned() - .map(|paths| parse_colon_separated_text(&paths)); - #[cfg(target_os = "windows")] unsafe { log::info!("Asking Windows to improve timer precision"); @@ -325,18 +288,9 @@ impl Kanata { } update_kbd_out(&cfg.items, &kbd_out)?; - set_altgr_behaviour(&cfg)?; - - let sequence_backtrack_modcancel = cfg - .items - .get("sequence-backtrack-modcancel") - .map(|s| !FALSE_VALUES.contains(&s.to_lowercase().as_str())) - .unwrap_or(true); - let log_layer_changes = cfg - .items - .get("log-layer-changes") - .map(|s| !FALSE_VALUES.contains(&s.to_lowercase().as_str())) - .unwrap_or(true); + + #[cfg(target_os = "windows")] + set_win_altgr_behaviour(cfg.items.windows_altgr); *MAPPED_KEYS.lock() = cfg.mapped_keys; @@ -355,7 +309,7 @@ impl Kanata { move_mouse_state_vertical: None, move_mouse_state_horizontal: None, move_mouse_speed_modifiers: Vec::new(), - sequence_backtrack_modcancel, + sequence_backtrack_modcancel: cfg.items.sequence_backtrack_modcancel, sequence_state: None, sequences: cfg.sequences, last_tick: time::Instant::now(), @@ -364,42 +318,25 @@ impl Kanata { overrides: cfg.overrides, override_states: OverrideStates::new(), #[cfg(target_os = "linux")] - kbd_in_paths, + kbd_in_paths: cfg.items.linux_dev, #[cfg(target_os = "linux")] - continue_if_no_devices: cfg - .items - .get("linux-continue-if-no-devs-found") - .map(|s| TRUE_VALUES.contains(&s.to_lowercase().as_str())) - .unwrap_or_default(), + continue_if_no_devices: cfg.items.linux_continue_if_no_devs_found, #[cfg(target_os = "linux")] - include_names, + include_names: cfg.items.linux_dev_names_include, #[cfg(target_os = "linux")] - exclude_names, + exclude_names: cfg.items.linux_dev_names_exclude, #[cfg(all(feature = "interception_driver", target_os = "windows"))] - intercept_mouse_hwid, + intercept_mouse_hwid: cfg.items.windows_interception_mouse_hwid, dynamic_macro_replay_state: None, dynamic_macro_record_state: None, dynamic_macros: Default::default(), - log_layer_changes, + log_layer_changes: cfg.items.log_layer_changes, caps_word: None, - movemouse_smooth_diagonals: cfg - .items - .get("movemouse-smooth-diagonals") - .map(|s| TRUE_VALUES.contains(&s.to_lowercase().as_str())) - .unwrap_or_default(), - movemouse_inherit_accel_state: cfg - .items - .get("movemouse-inherit-accel-state") - .map(|s| TRUE_VALUES.contains(&s.to_lowercase().as_str())) - .unwrap_or_default(), - dynamic_macro_max_presses: cfg - .items - .get("dynamic-macro-max-presses") - .map(|s| s.parse::()) - .unwrap_or(Ok(128)) - .map_err(|e| anyhow!("dynamic-macro-max-presses must be 0-65535: {e}"))?, + movemouse_smooth_diagonals: cfg.items.movemouse_smooth_diagonals, + movemouse_inherit_accel_state: cfg.items.movemouse_inherit_accel_state, + dynamic_macro_max_presses: cfg.items.dynamic_macro_max_presses, #[cfg(target_os = "linux")] - defcfg_items: cfg.items, + x11_repeat_rate: cfg.items.linux_x11_repeat_delay_rate, waiting_for_idle: HashSet::default(), ticks_since_idle: 0, movemouse_buffer: None, @@ -422,41 +359,22 @@ impl Kanata { } }; update_kbd_out(&cfg.items, &self.kbd_out)?; - set_altgr_behaviour(&cfg).map_err(|e| anyhow!("failed to set altgr behaviour {e})"))?; - let log_layer_changes = cfg - .items - .get("log-layer-changes") - .map(|s| !FALSE_VALUES.contains(&s.to_lowercase().as_str())) - .unwrap_or(true); - self.sequence_backtrack_modcancel = cfg - .items - .get("sequence-backtrack-modcancel") - .map(|s| !FALSE_VALUES.contains(&s.to_lowercase().as_str())) - .unwrap_or(true); + #[cfg(target_os = "windows")] + set_win_altgr_behaviour(cfg.items.windows_altgr); + self.sequence_backtrack_modcancel = cfg.items.sequence_backtrack_modcancel; self.layout = cfg.layout; self.key_outputs = cfg.key_outputs; self.layer_info = cfg.layer_info; self.sequences = cfg.sequences; self.overrides = cfg.overrides; - self.log_layer_changes = log_layer_changes; - self.movemouse_smooth_diagonals = cfg - .items - .get("movemouse-smooth-diagonals") - .map(|s| TRUE_VALUES.contains(&s.to_lowercase().as_str())) - .unwrap_or_default(); - self.movemouse_inherit_accel_state = cfg - .items - .get("movemouse-inherit-accel-state") - .map(|s| TRUE_VALUES.contains(&s.to_lowercase().as_str())) - .unwrap_or_default(); - self.dynamic_macro_max_presses = cfg - .items - .get("dynamic-macro-max-presses") - .map(|s| s.parse::()) - .unwrap_or(Ok(128)) - .map_err(|_| anyhow!("dynamic-macro-max-presses must be 0-65535"))?; + self.log_layer_changes = cfg.items.log_layer_changes; + self.movemouse_smooth_diagonals = cfg.items.movemouse_smooth_diagonals; + self.movemouse_inherit_accel_state = cfg.items.movemouse_inherit_accel_state; + self.dynamic_macro_max_presses = cfg.items.dynamic_macro_max_presses; + *MAPPED_KEYS.lock() = cfg.mapped_keys; - Kanata::set_repeat_rate(&cfg.items)?; + #[cfg(target_os = "linux")] + Kanata::set_repeat_rate(cfg.items.linux_x11_repeat_delay_rate)?; log::info!("Live reload successful"); Ok(()) } @@ -1836,12 +1754,6 @@ impl Kanata { } } -fn set_altgr_behaviour(_cfg: &cfg::Cfg) -> Result<()> { - #[cfg(target_os = "windows")] - set_win_altgr_behaviour(_cfg)?; - Ok(()) -} - #[cfg(feature = "cmd")] fn run_multi_cmd(cmds: Vec>) { std::thread::spawn(move || { @@ -1929,27 +1841,11 @@ fn check_for_exit(event: &KeyEvent) { } } -fn update_kbd_out(_cfg: &HashMap, _kbd_out: &KbdOut) -> Result<()> { +fn update_kbd_out(_cfg: &CfgOptions, _kbd_out: &KbdOut) -> Result<()> { #[cfg(target_os = "linux")] { - _kbd_out.update_unicode_termination( - _cfg.get("linux-unicode-termination").map(|s| { - match s.as_str() { - "enter" => Ok(UnicodeTermination::Enter), - "space" => Ok(UnicodeTermination::Space), - "enter-space" => Ok(UnicodeTermination::EnterSpace), - "space-enter" => Ok(UnicodeTermination::SpaceEnter), - _ => Err(anyhow!("linux-unicode-termination got {s}. It accepts: enter|space|enter-space|space-enter")), - } - }).unwrap_or(Ok(_kbd_out.unicode_termination.get()))?); - _kbd_out.update_unicode_u_code( - _cfg.get("linux-unicode-u-code") - .map(|s| { - str_to_oscode(s) - .ok_or_else(|| anyhow!("unknown code for linux-unicode-u-code {s}")) - }) - .unwrap_or(Ok(_kbd_out.unicode_u_code.get()))?, - ); + _kbd_out.update_unicode_termination(_cfg.linux_unicode_termination); + _kbd_out.update_unicode_u_code(_cfg.linux_unicode_u_code); } Ok(()) } diff --git a/src/kanata/windows/interception.rs b/src/kanata/windows/interception.rs index 88682dce2..56a3487ea 100644 --- a/src/kanata/windows/interception.rs +++ b/src/kanata/windows/interception.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{anyhow, Result}; use kanata_interception as ic; use parking_lot::Mutex; use std::sync::mpsc::SyncSender as Sender; @@ -9,8 +9,6 @@ use crate::kanata::*; use crate::oskbd::KeyValue; use kanata_parser::keys::OsCode; -const HWID_ARR_SZ: usize = 128; - impl Kanata { pub fn event_loop(kanata: Arc>, tx: Sender) -> Result<()> { let intrcptn = ic::Interception::new().ok_or_else(|| anyhow!("interception driver should init: have you completed the interception driver installation?"))?; @@ -21,20 +19,7 @@ impl Kanata { information: 0, }; 32]; - let mouse_to_intercept_hwid: Option<[u8; HWID_ARR_SZ]> = kanata - .lock() - .intercept_mouse_hwid.as_ref() - .map(|hwid| { - hwid.iter().copied().enumerate() - .fold([0u8; HWID_ARR_SZ], |mut hwid, idx_byte| { - let (i, b) = idx_byte; - if i > HWID_ARR_SZ { - panic!("windows-interception-mouse-hwid is too long; it should be up to {HWID_ARR_SZ} 8-bit unsigned integers"); - } - hwid[i] = b; - hwid - }) - }); + let mouse_to_intercept_hwid: Option<[u8; HWID_ARR_SZ]> = kanata.lock().intercept_mouse_hwid; if mouse_to_intercept_hwid.is_some() { intrcptn.set_filter( ic::is_mouse, diff --git a/src/kanata/windows/mod.rs b/src/kanata/windows/mod.rs index ce6d26ee9..609ecc72b 100644 --- a/src/kanata/windows/mod.rs +++ b/src/kanata/windows/mod.rs @@ -1,9 +1,8 @@ -use anyhow::{bail, Result}; +use anyhow::Result; use parking_lot::Mutex; use crate::kanata::*; -use kanata_parser::cfg; #[cfg(not(feature = "interception_driver"))] mod llhook; @@ -15,37 +14,13 @@ mod interception; #[cfg(feature = "interception_driver")] pub use self::interception::*; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum AltGrBehaviour { - DoNothing, - CancelLctlPress, - AddLctlRelease, -} - static PRESSED_KEYS: Lazy>> = Lazy::new(|| Mutex::new(HashSet::default())); pub static ALTGR_BEHAVIOUR: Lazy> = - Lazy::new(|| Mutex::new(AltGrBehaviour::DoNothing)); + Lazy::new(|| Mutex::new(AltGrBehaviour::default())); -pub fn set_win_altgr_behaviour(cfg: &cfg::Cfg) -> Result<()> { - *ALTGR_BEHAVIOUR.lock() = { - const CANCEL: &str = "cancel-lctl-press"; - const ADD: &str = "add-lctl-release"; - match cfg.items.get("windows-altgr") { - None => AltGrBehaviour::DoNothing, - Some(cfg_val) => match cfg_val.as_str() { - CANCEL => AltGrBehaviour::CancelLctlPress, - ADD => AltGrBehaviour::AddLctlRelease, - _ => bail!( - "Invalid value for windows-altgr: {}. Valid values are {},{}", - cfg_val, - CANCEL, - ADD - ), - }, - } - }; - Ok(()) +pub fn set_win_altgr_behaviour(b: AltGrBehaviour) { + *ALTGR_BEHAVIOUR.lock() = b; } impl Kanata { @@ -130,9 +105,4 @@ impl Kanata { pub fn check_release_non_physical_shift(&mut self) -> Result<()> { Ok(()) } - - pub fn set_repeat_rate(_cfg_items: &HashMap) -> Result<()> { - // TODO: no-op right now - Ok(()) - } } diff --git a/src/oskbd/linux.rs b/src/oskbd/linux.rs index 98b2d8075..7ecc245dd 100644 --- a/src/oskbd/linux.rs +++ b/src/oskbd/linux.rs @@ -19,8 +19,8 @@ use std::thread; use super::*; use crate::{kanata::CalculatedMouseMove, oskbd::KeyEvent}; -use kanata_parser::custom_action::*; use kanata_parser::keys::*; +use kanata_parser::{cfg::UnicodeTermination, custom_action::*}; pub struct KbdIn { devices: HashMap, @@ -303,14 +303,6 @@ impl From for InputEvent { } } -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum UnicodeTermination { - Enter, - Space, - SpaceEnter, - EnterSpace, -} - use std::cell::Cell; pub struct KbdOut { @@ -708,25 +700,6 @@ impl Symlink { } } -pub fn parse_colon_separated_text(paths: &str) -> Vec { - let mut all_paths = vec![]; - let mut full_dev_path = String::new(); - let mut dev_path_iter = paths.split(':').peekable(); - while let Some(dev_path) = dev_path_iter.next() { - if dev_path.ends_with('\\') && dev_path_iter.peek().is_some() { - full_dev_path.push_str(dev_path.trim_end_matches('\\')); - full_dev_path.push(':'); - continue; - } else { - full_dev_path.push_str(dev_path); - } - all_paths.push(full_dev_path.clone()); - full_dev_path.clear(); - } - all_paths.shrink_to_fit(); - all_paths -} - // Note for allow: the ioctl_read_buf triggers this clippy lint. // Note: CI does not yet support this lint, so also allowing unknown lints. #[allow(unknown_lints)] @@ -756,13 +729,6 @@ fn wait_for_all_keys_unpressed(dev: &Device) -> Result<(), io::Error> { Ok(()) } -#[test] -fn test_parse_dev_paths() { - assert_eq!(parse_colon_separated_text("h:w"), ["h", "w"]); - assert_eq!(parse_colon_separated_text("h\\:w"), ["h:w"]); - assert_eq!(parse_colon_separated_text("h\\:w\\"), ["h:w\\"]); -} - impl Drop for Symlink { fn drop(&mut self) { let _ = fs::remove_file(&self.dest); From 7f984db97bcb8602ca28d70569cfbe3ffddae55e Mon Sep 17 00:00:00 2001 From: jtroo Date: Wed, 22 Nov 2023 00:15:43 -0800 Subject: [PATCH 080/819] fix(wintercept): send Esc for unmapped oscode (#637) --- src/oskbd/windows/interception.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/oskbd/windows/interception.rs b/src/oskbd/windows/interception.rs index 434575345..825cb2829 100644 --- a/src/oskbd/windows/interception.rs +++ b/src/oskbd/windows/interception.rs @@ -2,7 +2,7 @@ use std::io; -use kanata_interception::{Interception, KeyState, MouseFlags, MouseState, Stroke}; +use kanata_interception::{Interception, KeyState, MouseFlags, MouseState, ScanCode, Stroke}; use super::OsCodeWrapper; use crate::kanata::CalculatedMouseMove; @@ -16,8 +16,14 @@ pub struct InputEvent(pub Stroke); impl InputEvent { fn from_oscode(code: OsCode, val: KeyValue) -> Self { - let mut stroke = - Stroke::try_from(OsCodeWrapper(code)).expect("kanata only sends mapped `OsCode`s"); + let mut stroke = Stroke::try_from(OsCodeWrapper(code)).unwrap_or_else(|_| { + log::error!("Trying to send unmapped oscode '{code:?}', sending esc instead"); + Stroke::Keyboard { + code: ScanCode::Esc, + state: KeyState::empty(), + information: 0, + } + }); match &mut stroke { Stroke::Keyboard { state, .. } => { state.set( From 5dc46c2c502629f1a19e8a1beac8c4d32d706898 Mon Sep 17 00:00:00 2001 From: rszyma Date: Thu, 23 Nov 2023 06:39:52 +0100 Subject: [PATCH 081/819] doc: add "community projects" section to README (#640) --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 8a45922dd..5e607153a 100644 --- a/README.md +++ b/README.md @@ -264,6 +264,11 @@ language and the prior work of the awesome [keyberon crate](https://github.com/T exists. +## Community projects related to kanata + +- [vscode-kanata](https://github.com/rszyma/vscode-kanata): Language support for kanata configuration files in VS Code +- [komokana](https://github.com/LGUG2Z/komokana): Automatic application-aware layer switching for [`komorebi`](https://github.com/LGUG2Z/komorebi) + ## Similar Projects - [kmonad](https://github.com/david-janssen/kmonad): The inspiration for kanata (Linux, Windows, Mac) From ba235f3c7ccbe4a97b24bf1bd501a4956471a3af Mon Sep 17 00:00:00 2001 From: rszyma Date: Thu, 23 Nov 2023 19:54:59 +0100 Subject: [PATCH 082/819] feat: improve error messages for parentheses in deflayer (#642) Improve error messages: - when parentheses are directly put in `deflayer` (#459) - when an attempt is made to escape parentheses with `\` in `deflayer` (#163). Worth noting, that these improved error messages will work only in deflayer, and not in actions. --- parser/src/cfg/mod.rs | 34 ++++++++++++++++++++++++--- parser/src/cfg/tests.rs | 52 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 3 deletions(-) diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index 7d4a7a737..793cd5d58 100644 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -807,8 +807,10 @@ fn parse_defsrc(expr: &[SExpr], defcfg: &CfgOptions) -> Result<(MappedKeys, Vec< type LayerIndexes = HashMap; type Aliases = HashMap; -/// Returns layer names and their indexes into the keyberon layout. This also checks that all -/// layers have the same number of items as the defsrc. Also ensures that there are no duplicate layer names. +/// Returns layer names and their indexes into the keyberon layout. This also checks that: +/// - All layers have the same number of items as the defsrc, +/// - There are no duplicate layer names +/// - Parentheses weren't used directly or kmonad-style escapes for parentheses weren't used. fn parse_layer_indexes(exprs: &[Spanned>], expected_len: usize) -> Result { let mut layer_indexes = HashMap::default(); for (i, expr) in exprs.iter().enumerate() { @@ -823,7 +825,33 @@ fn parse_layer_indexes(exprs: &[Spanned>], expected_len: usize) -> Re if layer_indexes.get(&layer_name).is_some() { bail_expr!(layer_expr, "duplicate layer name: {}", layer_name); } - let num_actions = subexprs.count(); + // Check if user tried to use parentheses directly - `(` and `)` + // or escaped them like in kmonad - `\(` and `\)`. + for subexpr in subexprs { + if let Some(list) = subexpr.list(None) { + if list.is_empty() { + bail_expr!( + subexpr, + "You can't put parentheses in deflayer directly, because they are special characters for delimiting lists.\n\ + To get `(` and `)` in US layout, you should use `S-9` and `S-0` respectively.\n\ + For more context, see: https://github.com/jtroo/kanata/issues/459" + ) + } + if list.len() == 1 + && list + .first() + .is_some_and(|s| s.atom(None).is_some_and(|atom| atom == "\\")) + { + bail_expr!( + subexpr, + "Escaping shifted characters with `\\` is currently not supported in kanata.\n\ + To get `(` and `)` in US layout, you should use `S-9` and `S-0` respectively.\n\ + For more context, see: https://github.com/jtroo/kanata/issues/163" + ) + } + } + } + let num_actions = expr.t.len() - 2; if num_actions != expected_len { bail_span!( expr, diff --git a/parser/src/cfg/tests.rs b/parser/src/cfg/tests.rs index fa55ac4f6..a46ed9eba 100644 --- a/parser/src/cfg/tests.rs +++ b/parser/src/cfg/tests.rs @@ -1249,3 +1249,55 @@ fn parse_all_defcfg() { ) .expect("succeeds"); } + +#[test] +fn using_parentheses_in_deflayer_directly_fails_with_custom_message() { + let _lk = match CFG_PARSE_LOCK.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + }; + let mut s = ParsedState::default(); + let source = r#" +(defsrc a b) +(deflayer base ( )) +"#; + let err = parse_cfg_raw_string( + source, + &mut s, + &PathBuf::from("test"), + &mut FileContentProvider { + get_file_content_fn: &mut |_| unimplemented!(), + }, + DEF_LOCAL_KEYS, + ) + .expect_err("should err"); + assert!(err + .msg + .contains("You can't put parentheses in deflayer directly")); +} + +#[test] +fn using_escaped_parentheses_in_deflayer_fails_with_custom_message() { + let _lk = match CFG_PARSE_LOCK.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + }; + let mut s = ParsedState::default(); + let source = r#" +(defsrc a b) +(deflayer base \( \)) +"#; + let err = parse_cfg_raw_string( + source, + &mut s, + &PathBuf::from("test"), + &mut FileContentProvider { + get_file_content_fn: &mut |_| unimplemented!(), + }, + DEF_LOCAL_KEYS, + ) + .expect_err("should err"); + assert!(err + .msg + .contains("Escaping shifted characters with `\\` is currently not supported")); +} From a870431e150b4993d3a40ae3408cad74b1c4c849 Mon Sep 17 00:00:00 2001 From: jtroo Date: Thu, 23 Nov 2023 23:14:04 -0800 Subject: [PATCH 083/819] feat+fix: unshift, key repeat on unmod+unshift (#638) Implement unshift and make unmod+unshift key repeat work. Implements #615. --- cfg_samples/kanata.kbd | 5 +++ docs/config.adoc | 21 +++++++++--- parser/src/cfg/list_actions.rs | 4 ++- parser/src/cfg/mod.rs | 58 +++++++++++++++++++++++++--------- parser/src/cfg/tests.rs | 29 +++++++++++++++++ parser/src/custom_action.rs | 5 ++- src/kanata/mod.rs | 54 +++++++++++++++++++++++-------- 7 files changed, 141 insertions(+), 35 deletions(-) diff --git a/cfg_samples/kanata.kbd b/cfg_samples/kanata.kbd index 83e8797d3..b1fdbd509 100644 --- a/cfg_samples/kanata.kbd +++ b/cfg_samples/kanata.kbd @@ -466,6 +466,11 @@ If you need help, please feel welcome to ask in the GitHub discussions. ;; dead keys é (as opposed to using AltGr) that outputs É when shifted dké (macro (unmod ') e) + ;; unshift is like unmod but only releases shifts + ;; In ISO German QWERTZ, force unshifted symbols even if shift is held + de{ (unshift ralt 7) + de[ (unshift ralt 8) + ;; unicode accepts a single unicode character. The unicode character will ;; not be automatically repeated by holding the key down. The alias name ;; is the unicode character itself and is referenced by @🙁 in deflayer. diff --git a/docs/config.adoc b/docs/config.adoc index 79774470a..bab770f19 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -1716,17 +1716,28 @@ and what the extra non-terminal+non-capitalized keys should be (3rd parameter). === unmod[[unmod]] <> -The `unmod` action will release all modifiers temporarily and send a key. +The `unmod` action will release all modifiers temporarily +and send one or more keys. After the `unmod` key is released, the released modifiers are pressed again. The modifiers affected are: `lsft,rsft,lctl,rctl,lmet,rmet,lalt,ralt`. +A variant of `unmod` is `unshift`. +This action only releases the `lsft,rsft` keys. +This can be useful for forcing unshifted keys while AltGr is still held. + .Example: [source] ---- -;; holding shift and tapping a @um1 key will still output 1. -um1 (unmod 1) -;; dead keys é (as opposed to using AltGr) that outputs É when shifted -dké (macro (unmod ') e) +(defalias + ;; holding shift and tapping a @um1 key will still output 1. + um1 (unmod 1) + ;; dead keys é (as opposed to using AltGr) that outputs É when shifted + dké (macro (unmod ') e) + + ;; In ISO German QWERTZ, force unshifted symbols even if shift is held + { (unshift ralt 7) + [ (unshift ralt 8) +) ---- [[cmd]] diff --git a/parser/src/cfg/list_actions.rs b/parser/src/cfg/list_actions.rs index 237b543bb..9577dd5a7 100644 --- a/parser/src/cfg/list_actions.rs +++ b/parser/src/cfg/list_actions.rs @@ -58,9 +58,10 @@ pub const DYNAMIC_MACRO_RECORD_STOP_TRUNCATE: &str = "dynamic-macro-record-stop- pub const SWITCH: &str = "switch"; pub const SEQUENCE: &str = "sequence"; pub const UNMOD: &str = "unmod"; +pub const UNSHIFT: &str = "unshift"; pub fn is_list_action(ac: &str) -> bool { - const LIST_ACTIONS: [&str; 56] = [ + const LIST_ACTIONS: [&str; 57] = [ LAYER_SWITCH, LAYER_TOGGLE, LAYER_WHILE_HELD, @@ -117,6 +118,7 @@ pub fn is_list_action(ac: &str) -> bool { SWITCH, SEQUENCE, UNMOD, + UNSHIFT, ]; LIST_ACTIONS.contains(&ac) } diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index 793cd5d58..e304289db 100644 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -1286,7 +1286,8 @@ fn parse_action_list(ac: &[SExpr], s: &ParsedState) -> Result<&'static KanataAct DYNAMIC_MACRO_RECORD_STOP_TRUNCATE => parse_macro_record_stop_truncate(&ac[1..], s), SWITCH => parse_switch(&ac[1..], s), SEQUENCE => parse_sequence_start(&ac[1..], s), - UNMOD => parse_unmod(&ac[1..], s), + UNMOD => parse_unmod(UNMOD, &ac[1..], s), + UNSHIFT => parse_unmod(UNSHIFT, &ac[1..], s), _ => unreachable!(), } } @@ -3061,19 +3062,35 @@ fn parse_on_idle_fakekey(ac_params: &[SExpr], s: &ParsedState) -> Result<&'stati ))))) } -fn parse_unmod(ac_params: &[SExpr], s: &ParsedState) -> Result<&'static KanataAction> { - const ERR_MSG: &str = "unmod expects one param: key"; - if ac_params.len() != 1 { - bail!("{ERR_MSG}\nfound {} items", ac_params.len()); +fn parse_unmod( + unmod_type: &str, + ac_params: &[SExpr], + s: &ParsedState, +) -> Result<&'static KanataAction> { + const ERR_MSG: &str = "expects expects at least one key name"; + if ac_params.len() < 1 { + bail!("{unmod_type} {ERR_MSG}\nfound {} items", ac_params.len()); + } + let mut keys: Vec = ac_params.iter().try_fold(Vec::new(), |mut keys, param| { + keys.push( + param + .atom(s.vars()) + .and_then(str_to_oscode) + .ok_or_else(|| anyhow_expr!(&ac_params[0], "{unmod_type} {ERR_MSG}"))? + .into(), + ); + Ok::<_, ParseError>(keys) + })?; + keys.shrink_to_fit(); + match unmod_type { + UNMOD => Ok(s.a.sref(Action::Custom( + s.a.sref(s.a.sref_slice(CustomAction::Unmodded { keys })), + ))), + UNSHIFT => Ok(s.a.sref(Action::Custom( + s.a.sref(s.a.sref_slice(CustomAction::Unshifted { keys })), + ))), + _ => panic!("Unknown unmod type {unmod_type}"), } - let key = ac_params[0] - .atom(s.vars()) - .and_then(str_to_oscode) - .ok_or_else(|| anyhow_expr!(&ac_params[0], "{ERR_MSG}"))? - .into(); - Ok(s.a.sref(Action::Custom( - s.a.sref(s.a.sref_slice(CustomAction::Unmodded { key })), - ))) } /// Creates a `KeyOutputs` from `layers::LAYERS`. @@ -3152,6 +3169,18 @@ fn add_key_output_from_action_to_key_pos( add_key_output_from_action_to_key_pos(osc_slot, case.1, outputs, overrides); } } + Action::Custom(cacs) => { + for ac in cacs.iter() { + match ac { + CustomAction::Unmodded { keys } | CustomAction::Unshifted { keys } => { + for k in keys.iter() { + add_kc_output(osc_slot, k.into(), outputs, overrides); + } + } + _ => {} + } + } + } Action::NoOp | Action::Trans | Action::Repeat @@ -3160,8 +3189,7 @@ fn add_key_output_from_action_to_key_pos( | Action::Sequence { .. } | Action::RepeatableSequence { .. } | Action::CancelSequences - | Action::ReleaseState(_) - | Action::Custom(_) => {} + | Action::ReleaseState(_) => {} }; } diff --git a/parser/src/cfg/tests.rs b/parser/src/cfg/tests.rs index a46ed9eba..f319e263c 100644 --- a/parser/src/cfg/tests.rs +++ b/parser/src/cfg/tests.rs @@ -1250,6 +1250,35 @@ fn parse_all_defcfg() { .expect("succeeds"); } +#[test] +fn parse_unmod() { + let _lk = match CFG_PARSE_LOCK.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + }; + + let source = r#" +(defsrc a b c d) +(deflayer base + (unmod a) + (unmod a b) + (unshift a) + (unshift a b) +) +"#; + let mut s = ParsedState::default(); + parse_cfg_raw_string( + source, + &mut s, + &PathBuf::from("test"), + &mut FileContentProvider { + get_file_content_fn: &mut |_| unimplemented!(), + }, + DEF_LOCAL_KEYS, + ) + .expect("succeeds"); +} + #[test] fn using_parentheses_in_deflayer_directly_fails_with_custom_message() { let _lk = match CFG_PARSE_LOCK.lock() { diff --git a/parser/src/custom_action.rs b/parser/src/custom_action.rs index ebdf7ab54..dffe40ee6 100644 --- a/parser/src/custom_action.rs +++ b/parser/src/custom_action.rs @@ -66,7 +66,10 @@ pub enum CustomAction { y: u16, }, Unmodded { - key: KeyCode, + keys: Vec, + }, + Unshifted { + keys: Vec, }, } diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index 957f71f99..2c157f7b5 100644 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -157,8 +157,10 @@ pub struct Kanata { /// Configured maximum for dynamic macro recording, to protect users from themselves if they /// have accidentally left it on. dynamic_macro_max_presses: u16, - /// Keys that should be unmodded. If empty, any modifier should be cleared. + /// Keys that should be unmodded. If non-empty, any modifier should be cleared. unmodded_keys: Vec, + /// Keys that should be unshifted. If non-empty, left+right shift keys should be cleared. + unshifted_keys: Vec, /// Keep track of last pressed key for [`CustomAction::Repeat`]. last_pressed_key: KeyCode, } @@ -341,6 +343,7 @@ impl Kanata { ticks_since_idle: 0, movemouse_buffer: None, unmodded_keys: vec![], + unshifted_keys: vec![], last_pressed_key: KeyCode::No, }) } @@ -703,15 +706,27 @@ impl Kanata { match custom_event { CustomEvent::Press(custacts) => { for custact in custacts.iter() { - if let CustomAction::Unmodded { key } = custact { - self.unmodded_keys.push(*key); + match custact { + CustomAction::Unmodded { keys } => { + self.unmodded_keys.extend(keys); + } + CustomAction::Unshifted { keys } => { + self.unshifted_keys.extend(keys); + } + _ => {} } } } CustomEvent::Release(custacts) => { for custact in custacts.iter() { - if let CustomAction::Unmodded { key } = custact { - self.unmodded_keys.retain(|k| k != key); + match custact { + CustomAction::Unmodded { keys } => { + self.unmodded_keys.retain(|k| !keys.contains(k)); + } + CustomAction::Unshifted { keys } => { + self.unshifted_keys.retain(|k| !keys.contains(k)); + } + _ => {} } } } @@ -733,6 +748,10 @@ impl Kanata { }); cur_keys.extend(self.unmodded_keys.iter()); } + if !self.unshifted_keys.is_empty() { + cur_keys.retain(|k| !matches!(k, KeyCode::LShift | KeyCode::RShift)); + cur_keys.extend(self.unshifted_keys.iter()); + } // Release keys that do not exist in the current state but exist in the previous state. // This used to use a HashSet but it was changed to a Vec because the order of operations @@ -1290,6 +1309,7 @@ impl Kanata { CustomAction::FakeKeyOnRelease { .. } | CustomAction::DelayOnRelease(_) | CustomAction::Unmodded { .. } + | CustomAction::Unshifted { .. } | CustomAction::CancelMacroOnRelease => {} } } @@ -1435,10 +1455,14 @@ impl Kanata { // Prioritize checking the active layer in case a layer-while-held is active. if let Some(outputs_for_key) = self.key_outputs[current_layer].get(&event.code) { log::debug!("key outs for active layer-while-held: {outputs_for_key:?};"); - for kc in outputs_for_key.iter().rev() { - if self.cur_keys.contains(&kc.into()) { - log::debug!("repeat {:?}", KeyCode::from(*kc)); - if let Err(e) = self.kbd_out.write_key(*kc, KeyValue::Repeat) { + for osc in outputs_for_key.iter().rev().copied() { + let kc = osc.into(); + if self.cur_keys.contains(&kc) + || self.unshifted_keys.contains(&kc) + || self.unmodded_keys.contains(&kc) + { + log::debug!("repeat {:?}", KeyCode::from(osc)); + if let Err(e) = self.kbd_out.write_key(osc, KeyValue::Repeat) { bail!("could not write key {:?}", e) } return Ok(()); @@ -1460,10 +1484,14 @@ impl Kanata { Some(v) => v, }; log::debug!("key outs for default layer: {outputs_for_key:?};"); - for kc in outputs_for_key.iter().rev() { - if self.cur_keys.contains(&kc.into()) { - log::debug!("repeat {:?}", KeyCode::from(*kc)); - if let Err(e) = self.kbd_out.write_key(*kc, KeyValue::Repeat) { + for osc in outputs_for_key.iter().rev().copied() { + let kc = osc.into(); + if self.cur_keys.contains(&kc) + || self.unshifted_keys.contains(&kc) + || self.unmodded_keys.contains(&kc) + { + log::debug!("repeat {:?}", KeyCode::from(osc)); + if let Err(e) = self.kbd_out.write_key(osc, KeyValue::Repeat) { bail!("could not write key {:?}", e) } return Ok(()); From 0764b798398815830ce7654256f165ba9c2712cb Mon Sep 17 00:00:00 2001 From: jtroo Date: Thu, 23 Nov 2023 23:25:11 -0800 Subject: [PATCH 084/819] feat!(rpt-any): repeat one-shotted chord (#641) Implements #596. This commit adds unsafe code to deal with lifetime/reference shenanigans in keyberon. --- keyberon/src/layout.rs | 75 +++++++++++++++++++++++++---- keyberon/src/lib.rs | 1 + keyberon/src/multikey_buffer.rs | 84 +++++++++++++++++++++++++++++++++ 3 files changed, 152 insertions(+), 8 deletions(-) create mode 100644 keyberon/src/multikey_buffer.rs diff --git a/keyberon/src/layout.rs b/keyberon/src/layout.rs index 77e05efe2..148124697 100644 --- a/keyberon/src/layout.rs +++ b/keyberon/src/layout.rs @@ -22,8 +22,8 @@ /// to do when not using a macro. pub use kanata_keyberon_macros::*; -use crate::action::*; use crate::key_code::KeyCode; +use crate::{action::*, multikey_buffer::MultiKeyBuffer}; use arraydeque::ArrayDeque; use heapless::Vec; @@ -84,6 +84,7 @@ where pub action_queue: ActionQueue<'a, T>, pub rpt_action: Option<&'a Action<'a, T>>, pub historical_keys: ArrayDeque<[KeyCode; 8], arraydeque::behavior::Wrapping>, + rpt_multikey_key_buffer: MultiKeyBuffer<'a, T>, } /// An event on the key matrix. @@ -222,6 +223,18 @@ impl<'a, T: 'a> State<'a, T> { _ => None, } } + fn keycode_in_coords(&self, coords: &OneShotCoords) -> Option { + match self { + NormalKey { keycode, coord, .. } => { + if coords.contains(coord) { + Some(*keycode) + } else { + None + } + } + _ => None, + } + } fn tick(&self) -> Option { Some(*self) } @@ -680,6 +693,8 @@ impl<'a, T: std::fmt::Debug> WaitingState<'a, T> { } } +type OneShotCoords = ArrayDeque<[KCoord; ONE_SHOT_MAX_ACTIVE], arraydeque::behavior::Wrapping>; + #[derive(Debug, Copy, Clone)] pub struct SequenceState<'a, T: 'a> { cur_event: Option>, @@ -730,9 +745,10 @@ impl OneShotState { } } - fn handle_press(&mut self, key: OneShotHandlePressKey) { + fn handle_press(&mut self, key: OneShotHandlePressKey) -> OneShotCoords { + let mut oneshot_coords = ArrayDeque::new(); if self.keys.is_empty() { - return; + return oneshot_coords; } match key { OneShotHandlePressKey::OneShotKey(pressed_coord) => { @@ -743,6 +759,7 @@ impl OneShotState { ) && self.keys.contains(&pressed_coord) { self.release_on_next_tick = true; + oneshot_coords.extend(self.keys.iter().copied()); } self.released_keys.retain(|coord| *coord != pressed_coord); } @@ -755,8 +772,10 @@ impl OneShotState { } else { let _ = self.other_pressed_keys.push_back(pressed_coord); } + oneshot_coords.extend(self.keys.iter().copied()); } - } + }; + oneshot_coords } /// Returns true if the caller should handle the release normally and false otherwise. @@ -857,6 +876,7 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt action_queue: ArrayDeque::new(), rpt_action: None, historical_keys: ArrayDeque::new(), + rpt_multikey_key_buffer: unsafe { MultiKeyBuffer::new() }, } } /// Iterates on the key codes of the current state. @@ -1331,11 +1351,29 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt keycode, flags: NormalKeyFlags(0), }); + let mut oneshot_coords = ArrayDeque::new(); if !is_oneshot { - self.oneshot + oneshot_coords = self + .oneshot .handle_press(OneShotHandlePressKey::Other(coord)); } - self.rpt_action = Some(action); + if oneshot_coords.is_empty() { + self.rpt_action = Some(action); + } else { + self.rpt_action = None; + unsafe { + self.rpt_multikey_key_buffer.clear(); + for kc in self + .states + .iter() + .filter_map(|kc| State::keycode_in_coords(kc, &oneshot_coords)) + { + self.rpt_multikey_key_buffer.push(kc); + } + self.rpt_multikey_key_buffer.push(keycode); + self.rpt_action = Some(self.rpt_multikey_key_buffer.get_ref()); + } + } } &MultipleKeyCodes(v) => { self.last_press_tracker.coord = coord; @@ -1359,11 +1397,32 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt }), }); } + + let mut oneshot_coords = ArrayDeque::new(); if !is_oneshot { - self.oneshot + oneshot_coords = self + .oneshot .handle_press(OneShotHandlePressKey::Other(coord)); } - self.rpt_action = Some(action); + if oneshot_coords.is_empty() { + self.rpt_action = Some(action); + } else { + self.rpt_action = None; + unsafe { + self.rpt_multikey_key_buffer.clear(); + for kc in self + .states + .iter() + .filter_map(|s| s.keycode_in_coords(&oneshot_coords)) + { + self.rpt_multikey_key_buffer.push(kc); + } + for &keycode in *v { + self.rpt_multikey_key_buffer.push(keycode); + } + self.rpt_action = Some(&*self.rpt_multikey_key_buffer.get_ref()); + } + } } &MultipleActions(v) => { self.last_press_tracker.coord = coord; diff --git a/keyberon/src/lib.rs b/keyberon/src/lib.rs index 78258ea05..288dd495c 100644 --- a/keyberon/src/lib.rs +++ b/keyberon/src/lib.rs @@ -4,3 +4,4 @@ pub mod action; pub mod key_code; pub mod layout; +mod multikey_buffer; diff --git a/keyberon/src/multikey_buffer.rs b/keyberon/src/multikey_buffer.rs new file mode 100644 index 000000000..7b8f92548 --- /dev/null +++ b/keyberon/src/multikey_buffer.rs @@ -0,0 +1,84 @@ +//! Module for `MultiKeyBuffer`. + +use std::ptr::null_mut; +use std::{array, slice}; + +use crate::action::{Action, ONE_SHOT_MAX_ACTIVE}; +use crate::key_code::KeyCode; + +// Presumably this should be plenty. +// ONE_SHOT_MAX_ACTIVE is already likely unreasonably large enough. +// This buffer capacity adds more onto that, +// just in case somebody finds a way to use all of the one-shot capacity. +const BUFCAP: usize = ONE_SHOT_MAX_ACTIVE + 4; + +/// This is an unsafe container that enables a mutable Action::MultipleKeyCodes. +pub(crate) struct MultiKeyBuffer<'a, T> { + buf: [KeyCode; BUFCAP], + size: usize, + ptr: *mut &'static [KeyCode], + ac: *mut Action<'a, T>, +} + +unsafe impl<'a, T> Send for MultiKeyBuffer<'a, T> {} + +impl<'a, T> MultiKeyBuffer<'a, T> { + /// Create a new instance of `MultiKeyBuffer`. + /// + /// # Safety + /// + /// The program should not have any references to the inner buffer when the struct is dropped. + pub(crate) unsafe fn new() -> Self { + Self { + buf: array::from_fn(|_| KeyCode::Escape), + size: 0, + ptr: Box::leak(Box::new(slice::from_raw_parts(null_mut(), 0))), + ac: Box::leak(Box::new(Action::NoOp)), + } + } + + /// Set the current size of the buffer to zero. + /// + /// # Safety + /// + /// The program should not have any references to the inner buffer. + pub(crate) unsafe fn clear(&mut self) { + self.size = 0; + } + + /// Push to the end of the buffer. If the buffer is full, this silently fails. + /// + /// # Safety + /// + /// The program should not have any references to the inner buffer. + pub(crate) unsafe fn push(&mut self, kc: KeyCode) { + if self.size < BUFCAP { + self.buf[self.size] = kc; + self.size += 1; + } + } + + /// Get a reference to the inner buffer in the form of an `Action`. + /// The `Action` will be the variant `MultipleKeyCodes`, + /// containing all keys that have been pushed. + /// + /// # Safety + /// + /// The program should not have any references to the inner buffer before calling. + /// The program should not mutate the buffer after calling this function until after the returned reference is dropped. + pub(crate) unsafe fn get_ref(&self) -> &'a Action<'a, T> { + *self.ac = Action::NoOp; + *self.ptr = slice::from_raw_parts(self.buf.as_ptr(), self.size); + *self.ac = Action::MultipleKeyCodes(&*self.ptr); + &*self.ac + } +} + +impl<'a, T> Drop for MultiKeyBuffer<'a, T> { + fn drop(&mut self) { + unsafe { + drop(Box::from_raw(self.ac)); + drop(Box::from_raw(self.ptr)); + } + } +} From 757268377e65b596ab0d41783c4481de50604c34 Mon Sep 17 00:00:00 2001 From: Jan Tache Date: Fri, 24 Nov 2023 00:23:08 -0800 Subject: [PATCH 085/819] ver: publish for v1.5.0-prerelease-3 --- Cargo.lock | 4 ++++ Cargo.toml | 8 ++++---- parser/Cargo.toml | 4 ++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bdad7e9e9..86250dc16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -410,6 +410,8 @@ dependencies = [ [[package]] name = "kanata-keyberon" version = "0.150.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "004d809d2497ded0186bb4cb699d60fd821bf430503a8cfa8099d1019a60e34e" dependencies = [ "arraydeque", "heapless", @@ -429,6 +431,8 @@ dependencies = [ [[package]] name = "kanata-parser" version = "0.150.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c49b3fd5cb0f8255f1552a225ffe016f5aa6e46e7ee47b5e5d71c7cae7d31dc" dependencies = [ "anyhow", "kanata-keyberon", diff --git a/Cargo.toml b/Cargo.toml index 7086f2e8f..b47f9fcff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,13 +30,13 @@ dirs = "5.0.1" # Pinned to avoid including multiple versions of a dependency is-terminal = "=0.4.7" -# kanata-keyberon = "0.150.3" -# kanata-parser = "0.150.3" +kanata-keyberon = "0.150.3" +kanata-parser = "0.150.3" # Uncomment below and comment out above for testing local changes. # Otherwise any changes to the local files will not reflect in the compiled # binary. -kanata-keyberon = { path = "keyberon" } -kanata-parser = { path = "parser" } +# kanata-keyberon = { path = "keyberon" } +# kanata-parser = { path = "parser" } [target.'cfg(target_os = "linux")'.dependencies] evdev = "=0.12.0" diff --git a/parser/Cargo.toml b/parser/Cargo.toml index d072649a7..ca26820c7 100644 --- a/parser/Cargo.toml +++ b/parser/Cargo.toml @@ -20,11 +20,11 @@ rustc-hash = "1.1.0" miette = { version = "5.7.0", features = ["fancy"] } thiserror = "1.0.38" -# kanata-keyberon = "0.150.3" +kanata-keyberon = "0.150.3" # Uncomment below and comment out above for testing local changes. # Otherwise any changes to the local files will not reflect in the compiled # binary. -kanata-keyberon = { path = "../keyberon" } +# kanata-keyberon = { path = "../keyberon" } [features] cmd = [] From 2db1e1277d891b03c9f321c3e37f223155cdc13e Mon Sep 17 00:00:00 2001 From: Jan Tache Date: Fri, 24 Nov 2023 00:59:17 -0800 Subject: [PATCH 086/819] chore: change back to local kanata crate deps --- Cargo.lock | 4 ---- Cargo.toml | 8 ++++---- parser/Cargo.toml | 4 ++-- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 86250dc16..bdad7e9e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -410,8 +410,6 @@ dependencies = [ [[package]] name = "kanata-keyberon" version = "0.150.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "004d809d2497ded0186bb4cb699d60fd821bf430503a8cfa8099d1019a60e34e" dependencies = [ "arraydeque", "heapless", @@ -431,8 +429,6 @@ dependencies = [ [[package]] name = "kanata-parser" version = "0.150.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c49b3fd5cb0f8255f1552a225ffe016f5aa6e46e7ee47b5e5d71c7cae7d31dc" dependencies = [ "anyhow", "kanata-keyberon", diff --git a/Cargo.toml b/Cargo.toml index b47f9fcff..7086f2e8f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,13 +30,13 @@ dirs = "5.0.1" # Pinned to avoid including multiple versions of a dependency is-terminal = "=0.4.7" -kanata-keyberon = "0.150.3" -kanata-parser = "0.150.3" +# kanata-keyberon = "0.150.3" +# kanata-parser = "0.150.3" # Uncomment below and comment out above for testing local changes. # Otherwise any changes to the local files will not reflect in the compiled # binary. -# kanata-keyberon = { path = "keyberon" } -# kanata-parser = { path = "parser" } +kanata-keyberon = { path = "keyberon" } +kanata-parser = { path = "parser" } [target.'cfg(target_os = "linux")'.dependencies] evdev = "=0.12.0" diff --git a/parser/Cargo.toml b/parser/Cargo.toml index ca26820c7..d072649a7 100644 --- a/parser/Cargo.toml +++ b/parser/Cargo.toml @@ -20,11 +20,11 @@ rustc-hash = "1.1.0" miette = { version = "5.7.0", features = ["fancy"] } thiserror = "1.0.38" -kanata-keyberon = "0.150.3" +# kanata-keyberon = "0.150.3" # Uncomment below and comment out above for testing local changes. # Otherwise any changes to the local files will not reflect in the compiled # binary. -# kanata-keyberon = { path = "../keyberon" } +kanata-keyberon = { path = "../keyberon" } [features] cmd = [] From 63f78f223b00727c2e4b3799f1ba91279376c157 Mon Sep 17 00:00:00 2001 From: jtroo Date: Wed, 29 Nov 2023 00:04:39 -0800 Subject: [PATCH 087/819] feat(cmd): allow and flatten lists to allow reuse of defvar (#645) --- parser/src/cfg/mod.rs | 43 ++++++++++++++++++++++++++++++----------- parser/src/cfg/tests.rs | 36 ++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 11 deletions(-) diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index e304289db..80e9dc773 100644 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -1783,21 +1783,15 @@ fn parse_cmd( s: &ParsedState, cmd_type: CmdType, ) -> Result<&'static KanataAction> { - const ERR_STR: &str = "cmd expects one or more strings"; + const ERR_STR: &str = "cmd expects at least one string"; if !s.is_cmd_enabled { - bail!("cmd is not enabled but cmd action is specified somewhere"); + bail!("cmd is not enabled, but cmd is in the configuration"); } - if ac_params.is_empty() { + let mut cmd = vec![]; + collect_strings(ac_params, &mut cmd, s); + if cmd.is_empty() { bail!(ERR_STR); } - let cmd = ac_params - .iter() - .try_fold(vec![], |mut v, p| -> Result> { - p.atom(s.vars()) - .map(|a| v.push(a.trim_matches('"').to_owned())) - .ok_or_else(|| anyhow_expr!(p, "{}, lists are not allowed", ERR_STR))?; - Ok(v) - })?; Ok(s.a .sref(Action::Custom(s.a.sref(s.a.sref_slice(match cmd_type { CmdType::Standard => CustomAction::Cmd(cmd), @@ -1805,6 +1799,33 @@ fn parse_cmd( }))))) } +/// Recurse through all levels of list nesting and collect into a flat list of strings. +/// Recursion is DFS, which matches left-to-right reading of the strings as they appear, +/// if everything was on a single line. +fn collect_strings(params: &[SExpr], strings: &mut Vec, s: &ParsedState) { + for param in params { + if let Some(a) = param.atom(s.vars()) { + strings.push(a.into()); + } else { + // unwrap: this must be a list, since it's not an atom. + let l = param.list(s.vars()).unwrap(); + collect_strings(l, strings, s); + } + } +} + +#[test] +fn test_collect_strings() { + let params = "(gah (squish squash (splish splosh) bah) dah)"; + let params = sexpr::parse(params, "noexist").unwrap(); + let mut strings = vec![]; + collect_strings(¶ms[0].t, &mut strings, &ParsedState::default()); + assert_eq!( + &strings, + &["gah", "squish", "squash", "splish", "splosh", "bah", "dah"] + ); +} + fn parse_one_shot( ac_params: &[SExpr], s: &ParsedState, diff --git a/parser/src/cfg/tests.rs b/parser/src/cfg/tests.rs index f319e263c..627a1fd14 100644 --- a/parser/src/cfg/tests.rs +++ b/parser/src/cfg/tests.rs @@ -1330,3 +1330,39 @@ fn using_escaped_parentheses_in_deflayer_fails_with_custom_message() { .msg .contains("Escaping shifted characters with `\\` is currently not supported")); } + +#[test] +#[cfg(feature = "cmd")] +fn parse_cmd() { + let _lk = match CFG_PARSE_LOCK.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + }; + + let source = r#" +(defcfg danger-enable-cmd yes) +(defsrc a) +(deflayer base a) +(defvar + x blah + y (nyoom) + z (squish squash (splish splosh)) +) +(defalias + 1 (cmd hello world) + 2 (cmd (hello world)) + 3 (cmd $x $y ($z)) +) +"#; + let mut s = ParsedState::default(); + parse_cfg_raw_string( + source, + &mut s, + &PathBuf::from("test"), + &mut FileContentProvider { + get_file_content_fn: &mut |_| unimplemented!(), + }, + DEF_LOCAL_KEYS, + ) + .expect("succeeds"); +} From 40d1979d87ae744b2a5c1139c279159f309c9aa2 Mon Sep 17 00:00:00 2001 From: jtroo Date: Wed, 29 Nov 2023 09:34:27 -0800 Subject: [PATCH 088/819] fix(cmd): strip quotes in collect_strings --- parser/src/cfg/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index 80e9dc773..052be368f 100644 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -1805,7 +1805,7 @@ fn parse_cmd( fn collect_strings(params: &[SExpr], strings: &mut Vec, s: &ParsedState) { for param in params { if let Some(a) = param.atom(s.vars()) { - strings.push(a.into()); + strings.push(a.trim_matches('"').to_owned()); } else { // unwrap: this must be a list, since it's not an atom. let l = param.list(s.vars()).unwrap(); @@ -1816,13 +1816,13 @@ fn collect_strings(params: &[SExpr], strings: &mut Vec, s: &ParsedState) #[test] fn test_collect_strings() { - let params = "(gah (squish squash (splish splosh) bah) dah)"; + let params = r#"(gah (squish "squash" (splish splosh) "bah mah") dah)"#; let params = sexpr::parse(params, "noexist").unwrap(); let mut strings = vec![]; collect_strings(¶ms[0].t, &mut strings, &ParsedState::default()); assert_eq!( &strings, - &["gah", "squish", "squash", "splish", "splosh", "bah", "dah"] + &["gah", "squish", "squash", "splish", "splosh", "bah mah", "dah"] ); } From d0d2fe4f0eb99d330a40d2ae9dcf31fc049d7045 Mon Sep 17 00:00:00 2001 From: jtroo Date: Wed, 29 Nov 2023 09:35:50 -0800 Subject: [PATCH 089/819] chore: clippy --fix --- parser/src/cfg/defcfg.rs | 4 ++-- parser/src/cfg/key_override.rs | 6 ++++++ parser/src/cfg/mod.rs | 2 +- parser/src/trie.rs | 6 ++++++ 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/parser/src/cfg/defcfg.rs b/parser/src/cfg/defcfg.rs index 75757804d..62b3f93cf 100644 --- a/parser/src/cfg/defcfg.rs +++ b/parser/src/cfg/defcfg.rs @@ -107,7 +107,7 @@ pub fn parse_defcfg(expr: &[SExpr]) -> Result { } "sequence-input-mode" => { cfg.sequence_input_mode = - SequenceInputMode::try_from_str(&v.t.trim_matches('"')) + SequenceInputMode::try_from_str(v.t.trim_matches('"')) .map_err(|e| anyhow_expr!(val, "{}", e.to_string()))?; } k @ "dynamic-macro-max-presses" => { @@ -300,7 +300,7 @@ fn parse_defcfg_val_bool(expr: &SExpr, label: &str) -> Result { fn parse_cfg_val_u16(expr: &SExpr, label: &str, exclude_zero: bool) -> Result { let start = if exclude_zero { 1 } else { 0 }; match &expr { - SExpr::Atom(v) => Ok(str::parse::(&v.t.trim_matches('"')) + SExpr::Atom(v) => Ok(str::parse::(v.t.trim_matches('"')) .ok() .and_then(|u| { if exclude_zero && u == 0 { diff --git a/parser/src/cfg/key_override.rs b/parser/src/cfg/key_override.rs index b485b390f..dd23f6452 100644 --- a/parser/src/cfg/key_override.rs +++ b/parser/src/cfg/key_override.rs @@ -15,6 +15,12 @@ pub struct OverrideStates { oscs_to_add: Vec, } +impl Default for OverrideStates { + fn default() -> Self { + Self::new() + } +} + impl OverrideStates { pub fn new() -> Self { Self { diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index 052be368f..a57c6edf6 100644 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -3089,7 +3089,7 @@ fn parse_unmod( s: &ParsedState, ) -> Result<&'static KanataAction> { const ERR_MSG: &str = "expects expects at least one key name"; - if ac_params.len() < 1 { + if ac_params.is_empty() { bail!("{unmod_type} {ERR_MSG}\nfound {} items", ac_params.len()); } let mut keys: Vec = ac_params.iter().try_fold(Vec::new(), |mut keys, param| { diff --git a/parser/src/trie.rs b/parser/src/trie.rs index e3621ec5f..0de3a17af 100644 --- a/parser/src/trie.rs +++ b/parser/src/trie.rs @@ -19,6 +19,12 @@ pub enum GetOrDescendentExistsResult { use GetOrDescendentExistsResult::*; +impl Default for Trie { + fn default() -> Self { + Self::new() + } +} + impl Trie { pub fn new() -> Self { Self { From 3bd68107d41f411c9b7ebe1246cf6a1ceb08bc45 Mon Sep 17 00:00:00 2001 From: jtroo Date: Wed, 29 Nov 2023 09:46:27 -0800 Subject: [PATCH 090/819] chore: define workspace, fix clippy, fmt, ci --- .github/workflows/rust.yml | 8 ++++---- Cargo.toml | 7 +++++++ keyberon/src/layout.rs | 2 +- keyberon/src/multikey_buffer.rs | 6 ++++-- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index f448f3124..a5cd53475 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -45,9 +45,9 @@ jobs: workspaces: ./ - run: rustup component add clippy - name: Run tests - run: cargo test --verbose -p kanata -p kanata-parser -p kanata-keyberon + run: cargo test --all - name: Run tests all features - run: cargo test --all-features --verbose -p kanata -p kanata-parser -p kanata-keyberon + run: cargo test --all --all-features - name: Run clippy no features run: cargo clippy --all -- -D warnings - name: Run clippy all features @@ -67,9 +67,9 @@ jobs: workspaces: ./ - run: rustup component add clippy - name: Run tests - run: cargo test --verbose -p kanata -p kanata-parser -p kanata-keyberon + run: cargo test --all - name: Run tests all features - run: cargo test --all-features --verbose -p kanata -p kanata-parser -p kanata-keyberon + run: cargo test --all --all-features - name: Run clippy no features run: cargo clippy --all -- -D warnings - name: Run clippy all features diff --git a/Cargo.toml b/Cargo.toml index 7086f2e8f..ab43ba051 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,10 @@ +[workspace] +members = [ + "./", + "parser", + "keyberon", +] + [package] name = "kanata" version = "1.5.0-prerelease-3" diff --git a/keyberon/src/layout.rs b/keyberon/src/layout.rs index 148124697..ca49afbe1 100644 --- a/keyberon/src/layout.rs +++ b/keyberon/src/layout.rs @@ -1420,7 +1420,7 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt for &keycode in *v { self.rpt_multikey_key_buffer.push(keycode); } - self.rpt_action = Some(&*self.rpt_multikey_key_buffer.get_ref()); + self.rpt_action = Some(self.rpt_multikey_key_buffer.get_ref()); } } } diff --git a/keyberon/src/multikey_buffer.rs b/keyberon/src/multikey_buffer.rs index 7b8f92548..b36c3d741 100644 --- a/keyberon/src/multikey_buffer.rs +++ b/keyberon/src/multikey_buffer.rs @@ -1,6 +1,5 @@ //! Module for `MultiKeyBuffer`. -use std::ptr::null_mut; use std::{array, slice}; use crate::action::{Action, ONE_SHOT_MAX_ACTIVE}; @@ -32,7 +31,10 @@ impl<'a, T> MultiKeyBuffer<'a, T> { Self { buf: array::from_fn(|_| KeyCode::Escape), size: 0, - ptr: Box::leak(Box::new(slice::from_raw_parts(null_mut(), 0))), + ptr: Box::leak(Box::new(slice::from_raw_parts( + core::ptr::NonNull::dangling().as_ptr(), + 0, + ))), ac: Box::leak(Box::new(Action::NoOp)), } } From 9e5c8d985453594422d622052dd35a0bb6c060ca Mon Sep 17 00:00:00 2001 From: rszyma Date: Sat, 2 Dec 2023 06:17:47 +0100 Subject: [PATCH 091/819] feat: allow using a list as linux dev value (#647) This allows a user to define devices using a sexpr list. The old way (colon separated string) is not removed for compatibility. --- cfg_samples/kanata.kbd | 17 ++++- docs/config.adoc | 34 +++++++-- parser/src/cfg/defcfg.rs | 153 ++++++++++++++++++++++++++------------- parser/src/cfg/tests.rs | 75 ++++++++++++++++++- 4 files changed, 216 insertions(+), 63 deletions(-) diff --git a/cfg_samples/kanata.kbd b/cfg_samples/kanata.kbd index b1fdbd509..246d4f844 100644 --- a/cfg_samples/kanata.kbd +++ b/cfg_samples/kanata.kbd @@ -55,6 +55,15 @@ If you need help, please feel welcome to ask in the GitHub discussions. ;; kanata does not parse it as multiple devices. ;; linux-dev /dev/input/path-to\:device + ;; Alternatively, you can use list syntax, where both backslashes and colons + ;; are parsed literally. List items are separated by spaces or newlines. + ;; Using quotation marks for each item is optional, and only required if an + ;; item contains spaces. + ;; linux-dev ( + ;; /dev/input/by-path/pci-0000:00:14.0-usb-0:1:1.0-event + ;; /dev/input/by-id/usb-Dell_Dell_USB_Keyboard-event-kbd + ;; ) + ;; The linux-dev-names-include entry is parsed identically to linux-dev. It ;; defines a list of device names that should be included. This is only ;; used if linux-dev is omitted. @@ -645,10 +654,10 @@ If you need help, please feel welcome to ask in the GitHub discussions. ;; This example is similar to the default caps-word behaviour but it moves the ;; 0-9 keys to capitalized key list from the extra non-terminating key list. cwc (caps-word-custom - 2000 - (a b c d e f g h i j k l m n o p q r s t u v w x y z 0 1 2 3 4 5 6 7 8 9) - (kp0 kp1 kp2 kp3 kp4 kp5 kp6 kp7 kp8 kp9 bspc del up down left rght) - ) + 2000 + (a b c d e f g h i j k l m n o p q r s t u v w x y z 0 1 2 3 4 5 6 7 8 9) + (kp0 kp1 kp2 kp3 kp4 kp5 kp6 kp7 kp8 kp9 bspc del up down left rght) + ) ) ;; Can see a new action `rpt` in this layer. This repeats the most recently diff --git a/docs/config.adoc b/docs/config.adoc index bab770f19..6736ea9f2 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -461,7 +461,6 @@ The default length limit is 128 keys. [[linux-only-linux-dev]] === Linux only: linux-dev <> - By default, kanata will try to detect which input devices are keyboards and try to intercept them all. However, you may specify exact keyboard devices from the `/dev/input` directories using the `linux-dev` configuration. @@ -495,6 +494,21 @@ its file name, you must escape those colons with backslashes: ) ---- +Alternatively, you can use list syntax, where both backslashes and colons +are parsed literally. List items are separated by spaces or newlines. +Using quotation marks for each item is optional, and only required if an +item contains spaces. + +[source] +---- +(defcfg + linux-dev ( + /dev/input/path:to:device + "/dev/input/path to device" + ) +) +---- + [[linux-only-linux-dev-names-include]] === Linux only: linux-dev-names-include <> @@ -514,7 +528,10 @@ registering /dev/input/eventX: "Name goes here" [source] ---- (defcfg - linux-dev-names-include "Device 1 name:Device \:2\: Name" + linux-dev-names-include ( + "Device name 1" + "Device name 2" + ) ) ---- @@ -526,7 +543,7 @@ In the case that `linux-dev` is omitted, this option defines a list of device names that should be excluded. This option is parsed identically to `linux-dev`. -The `linux-dev-names-include and `linux-dev-names-exclude` options +The `linux-dev-names-include` and `linux-dev-names-exclude` options are not mutually exclusive but in practice it probably only makes sense to use one and not both. @@ -534,7 +551,10 @@ but in practice it probably only makes sense to use one and not both. [source] ---- (defcfg - linux-dev-names-exclude "Device 1 name:Device \:2\: Name" + linux-dev-names-exclude ( + "Device Name 1" + "Device Name 2" + ) ) ---- @@ -704,9 +724,9 @@ a non-applicable operating system. movemouse-inherit-accel-state yes movemouse-smooth-diagonals yes dynamic-macro-max-presses 1000 - linux-dev /dev/input/dev1:/dev/input/dev2 - linux-dev-names-include "Name 1:Name 2" - linux-dev-names-exclude "Name 3:Name 4" + linux-dev (/dev/input/dev1 /dev/input/dev2) + linux-dev-names-include ("Name 1" "Name 2") + linux-dev-names-exclude ("Name 3" "Name 4") linux-continue-if-no-devs-found yes linux-unicode-u-code v linux-unicode-termination space diff --git a/parser/src/cfg/defcfg.rs b/parser/src/cfg/defcfg.rs index 62b3f93cf..071d01246 100644 --- a/parser/src/cfg/defcfg.rs +++ b/parser/src/cfg/defcfg.rs @@ -4,7 +4,7 @@ use super::HashSet; use crate::cfg::check_first_expr; use crate::custom_action::*; #[allow(unused)] -use crate::{anyhow_expr, anyhow_span, bail, bail_expr}; +use crate::{anyhow_expr, anyhow_span, bail, bail_expr, bail_span}; #[derive(Debug)] pub struct CfgOptions { @@ -96,65 +96,74 @@ pub fn parse_defcfg(expr: &[SExpr]) -> Result { Some(v) => v, None => bail_expr!(key, "Found a defcfg option missing a value"), }; - match (&key, &val) { - (SExpr::Atom(k), SExpr::Atom(v)) => { - if !seen_keys.insert(&k.t) { - bail_expr!(key, "Duplicate defcfg option {}", k.t); + match key { + SExpr::Atom(k) => { + let label = k.t.as_str(); + if !seen_keys.insert(label) { + bail_expr!(key, "Duplicate defcfg option {}", label); } - match k.t.as_str() { - k @ "sequence-timeout" => { - cfg.sequence_timeout = parse_cfg_val_u16(val, k, true)?; + match label { + "sequence-timeout" => { + cfg.sequence_timeout = parse_cfg_val_u16(val, label, true)?; } "sequence-input-mode" => { - cfg.sequence_input_mode = - SequenceInputMode::try_from_str(v.t.trim_matches('"')) - .map_err(|e| anyhow_expr!(val, "{}", e.to_string()))?; + let v = sexpr_to_str_or_err(val, label)?; + cfg.sequence_input_mode = SequenceInputMode::try_from_str(v) + .map_err(|e| anyhow_expr!(val, "{}", e.to_string()))?; } - k @ "dynamic-macro-max-presses" => { - cfg.dynamic_macro_max_presses = parse_cfg_val_u16(val, k, false)?; + "dynamic-macro-max-presses" => { + cfg.dynamic_macro_max_presses = parse_cfg_val_u16(val, label, false)?; } "linux-dev" => { #[cfg(any(target_os = "linux", target_os = "unknown"))] { - let paths = v.t.trim_matches('"'); - cfg.linux_dev = parse_colon_separated_text(paths); + cfg.linux_dev = parse_linux_dev(val)?; + if cfg.linux_dev.is_empty() { + bail_expr!( + val, + "device list is empty, no devices will be intercepted" + ); + } } } "linux-dev-names-include" => { #[cfg(any(target_os = "linux", target_os = "unknown"))] { - let paths = v.t.trim_matches('"'); - cfg.linux_dev_names_include = Some(parse_colon_separated_text(paths)); + cfg.linux_dev_names_include = Some(parse_linux_dev(val)?); + if cfg.linux_dev.is_empty() { + log::warn!("linux-dev-names-include is empty"); + } } } "linux-dev-names-exclude" => { #[cfg(any(target_os = "linux", target_os = "unknown"))] { - let paths = v.t.trim_matches('"'); - cfg.linux_dev_names_exclude = Some(parse_colon_separated_text(paths)); + cfg.linux_dev_names_exclude = Some(parse_linux_dev(val)?); } } - _k @ "linux-unicode-u-code" => { + "linux-unicode-u-code" => { #[cfg(any(target_os = "linux", target_os = "unknown"))] { - cfg.linux_unicode_u_code = crate::keys::str_to_oscode( - v.t.trim_matches('"'), - ) - .ok_or_else(|| anyhow_expr!(val, "unknown code for {_k}: {}", v.t))?; + let v = sexpr_to_str_or_err(val, label)?; + cfg.linux_unicode_u_code = + crate::keys::str_to_oscode(v).ok_or_else(|| { + anyhow_expr!(val, "unknown code for {label}: {}", v) + })?; } } - _k @ "linux-unicode-termination" => { + "linux-unicode-termination" => { #[cfg(any(target_os = "linux", target_os = "unknown"))] { - cfg.linux_unicode_termination = match v.t.trim_matches('"') { + let v = sexpr_to_str_or_err(val, label)?; + cfg.linux_unicode_termination = match v { "enter" => UnicodeTermination::Enter, "space" => UnicodeTermination::Space, "enter-space" => UnicodeTermination::EnterSpace, "space-enter" => UnicodeTermination::SpaceEnter, _ => bail_expr!( val, - "{_k} got {}. It accepts: enter|space|enter-space|space-enter", - v.t + "{label} got {}. It accepts: enter|space|enter-space|space-enter", + v ), } } @@ -162,7 +171,8 @@ pub fn parse_defcfg(expr: &[SExpr]) -> Result { "linux-x11-repeat-delay-rate" => { #[cfg(any(target_os = "linux", target_os = "unknown"))] { - let delay_rate = v.t.trim_matches('"').split(',').collect::>(); + let v = sexpr_to_str_or_err(val, label)?; + let delay_rate = v.split(',').collect::>(); const ERRMSG: &str = "Invalid value for linux-x11-repeat-delay-rate.\nExpected two numbers 0-65535 separated by a comma, e.g. 200,25"; if delay_rate.len() != 2 { bail_expr!(val, "{}", ERRMSG) @@ -179,31 +189,33 @@ pub fn parse_defcfg(expr: &[SExpr]) -> Result { }); } } - _k @ "windows-altgr" => { + "windows-altgr" => { #[cfg(any(target_os = "windows", target_os = "unknown"))] { const CANCEL: &str = "cancel-lctl-press"; const ADD: &str = "add-lctl-release"; - cfg.windows_altgr = match v.t.trim_matches('"') { + let v = sexpr_to_str_or_err(val, label)?; + cfg.windows_altgr = match v { CANCEL => AltGrBehaviour::CancelLctlPress, ADD => AltGrBehaviour::AddLctlRelease, _ => bail_expr!( val, - "Invalid value for {_k}: {}. Valid values are {},{}", - v.t, + "Invalid value for {label}: {}. Valid values are {},{}", + v, CANCEL, ADD ), } } } - _k @ "windows-interception-mouse-hwid" => { + "windows-interception-mouse-hwid" => { #[cfg(any( all(feature = "interception_driver", target_os = "windows"), target_os = "unknown" ))] { - let hwid = v.t.trim_matches('"'); + let v = sexpr_to_str_or_err(val, label)?; + let hwid = v; log::trace!("win hwid: {hwid}"); let hwid_vec = hwid .split(',') @@ -212,12 +224,12 @@ pub fn parse_defcfg(expr: &[SExpr]) -> Result { hwid_bytes.push(b); hwid_bytes }) - }).map_err(|_| anyhow_expr!(val, "{_k} format is invalid. It should consist of integers separated by commas"))?; + }).map_err(|_| anyhow_expr!(val, "{label} format is invalid. It should consist of integers separated by commas"))?; let hwid_slice = hwid_vec.iter().copied().enumerate() .try_fold([0u8; HWID_ARR_SZ], |mut hwid, idx_byte| { let (i, b) = idx_byte; if i > HWID_ARR_SZ { - bail_expr!(val, "{_k} is too long; it should be up to {HWID_ARR_SZ} 8-bit unsigned integers") + bail_expr!(val, "{label} is too long; it should be up to {HWID_ARR_SZ} 8-bit unsigned integers") } hwid[i] = b; Ok(hwid) @@ -227,17 +239,17 @@ pub fn parse_defcfg(expr: &[SExpr]) -> Result { } "process-unmapped-keys" => { - cfg.process_unmapped_keys = parse_defcfg_val_bool(val, &k.t)? + cfg.process_unmapped_keys = parse_defcfg_val_bool(val, label)? } - "danger-enable-cmd" => cfg.enable_cmd = parse_defcfg_val_bool(val, &k.t)?, + "danger-enable-cmd" => cfg.enable_cmd = parse_defcfg_val_bool(val, label)?, "sequence-backtrack-modcancel" => { - cfg.sequence_backtrack_modcancel = parse_defcfg_val_bool(val, &k.t)? + cfg.sequence_backtrack_modcancel = parse_defcfg_val_bool(val, label)? } "log-layer-changes" => { - cfg.log_layer_changes = parse_defcfg_val_bool(val, &k.t)? + cfg.log_layer_changes = parse_defcfg_val_bool(val, label)? } "delegate-to-first-layer" => { - cfg.delegate_to_first_layer = parse_defcfg_val_bool(val, &k.t)?; + cfg.delegate_to_first_layer = parse_defcfg_val_bool(val, label)?; if cfg.delegate_to_first_layer { log::info!("delegating transparent keys on other layers to first defined layer"); } @@ -245,23 +257,20 @@ pub fn parse_defcfg(expr: &[SExpr]) -> Result { "linux-continue-if-no-devs-found" => { #[cfg(any(target_os = "linux", target_os = "unknown"))] { - cfg.linux_continue_if_no_devs_found = parse_defcfg_val_bool(val, &k.t)? + cfg.linux_continue_if_no_devs_found = parse_defcfg_val_bool(val, label)? } } "movemouse-smooth-diagonals" => { - cfg.movemouse_smooth_diagonals = parse_defcfg_val_bool(val, &k.t)? + cfg.movemouse_smooth_diagonals = parse_defcfg_val_bool(val, label)? } "movemouse-inherit-accel-state" => { - cfg.movemouse_inherit_accel_state = parse_defcfg_val_bool(val, &k.t)? + cfg.movemouse_inherit_accel_state = parse_defcfg_val_bool(val, label)? } - _ => bail_expr!(key, "Unknown defcfg option {}", &k.t), + _ => bail_expr!(key, "Unknown defcfg option {}", label), }; } - (SExpr::List(_), _) => { - bail_expr!(key, "Lists are not allowed in defcfg"); - } - (_, SExpr::List(_)) => { - bail_expr!(val, "Lists are not allowed in defcfg"); + SExpr::List(_) => { + bail_expr!(key, "Lists are not allowed in as keys in defcfg"); } } } @@ -338,6 +347,48 @@ pub fn parse_colon_separated_text(paths: &str) -> Vec { all_paths } +#[cfg(any(target_os = "linux", target_os = "unknown"))] +pub fn parse_linux_dev(val: &SExpr) -> Result> { + Ok(match val { + SExpr::Atom(a) => { + let devs = parse_colon_separated_text(a.t.trim_matches('"')); + if devs.len() == 1 && devs[0].is_empty() { + bail_expr!(val, "an empty string is not a valid device name or path") + } + devs + } + SExpr::List(l) => { + let r: Result> = + l.t.iter() + .try_fold(Vec::with_capacity(l.t.len()), |mut acc, expr| match expr { + SExpr::Atom(path) => { + let trimmed_path = path.t.trim_matches('"').to_string(); + if trimmed_path.is_empty() { + bail_span!( + &path, + "an empty string is not a valid device name or path" + ) + } + acc.push(trimmed_path); + Ok(acc) + } + SExpr::List(inner_list) => { + bail_span!(&inner_list, "expected strings, found a list") + } + }); + + r? + } + }) +} + +fn sexpr_to_str_or_err<'a>(expr: &'a SExpr, label: &str) -> Result<&'a str> { + match expr { + SExpr::Atom(a) => Ok(a.t.trim_matches('"')), + SExpr::List(_) => bail_expr!(expr, "The value for {label} can't be a list"), + } +} + #[cfg(any(target_os = "linux", target_os = "unknown"))] #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct KeyRepeatSettings { diff --git a/parser/src/cfg/tests.rs b/parser/src/cfg/tests.rs index 627a1fd14..14a5787a5 100644 --- a/parser/src/cfg/tests.rs +++ b/parser/src/cfg/tests.rs @@ -1,5 +1,6 @@ use super::*; -use crate::cfg::sexpr::parse; +#[allow(unused_imports)] +use crate::cfg::sexpr::{parse, Span}; use kanata_keyberon::action::BooleanOperator::*; use std::sync::Mutex; @@ -1201,11 +1202,83 @@ fn list_action_not_in_list_error_message_is_good() { #[test] fn parse_device_paths() { + assert_eq!(parse_colon_separated_text(""), [""]); + assert_eq!(parse_colon_separated_text("device1"), ["device1"]); assert_eq!(parse_colon_separated_text("h:w"), ["h", "w"]); assert_eq!(parse_colon_separated_text("h\\:w"), ["h:w"]); assert_eq!(parse_colon_separated_text("h\\:w\\"), ["h:w\\"]); } +#[test] +#[cfg(any(target_os = "linux", target_os = "unknown"))] +fn test_parse_linux_dev() { + // The old colon separated devices format + assert_eq!( + parse_linux_dev(&SExpr::Atom(Spanned { + t: "\"Keyboard2:Input Device 1:pci-0000\\:00\\:14.0-usb-0\\:1\\:1.0-event\"" + .to_string(), + span: Span::default(), + })) + .expect("succeeds"), + [ + "Keyboard2", + "Input Device 1", + "pci-0000:00:14.0-usb-0:1:1.0-event" + ] + ); + parse_linux_dev(&SExpr::Atom(Spanned { + t: "\"\"".to_string(), + span: Span::default(), + })) + .expect_err("'' is not a valid device name/path, this should fail"); + + // The new device list format + assert_eq!( + parse_linux_dev(&SExpr::List(Spanned { + t: vec![ + SExpr::Atom(Spanned { + t: "Keyboard2".to_string(), + span: Span::default(), + }), + SExpr::Atom(Spanned { + t: "\"Input Device 1\"".to_string(), + span: Span::default(), + }), + SExpr::Atom(Spanned { + t: "pci-0000:00:14.0-usb-0:1:1.0-event".to_string(), + span: Span::default(), + }), + SExpr::Atom(Spanned { + t: r"backslashes\do\not\escape\:\anything".to_string(), + span: Span::default(), + }), + ], + span: Span::default(), + })) + .expect("succeeds"), + [ + "Keyboard2", + "Input Device 1", + "pci-0000:00:14.0-usb-0:1:1.0-event", + r"backslashes\do\not\escape\:\anything" + ] + ); + parse_linux_dev(&SExpr::List(Spanned { + t: vec![ + SExpr::Atom(Spanned { + t: "Device1".to_string(), + span: Span::default(), + }), + SExpr::List(Spanned { + t: vec![], + span: Span::default(), + }), + ], + span: Span::default(), + })) + .expect_err("nested lists in path list shouldn't be allowed"); +} + #[test] fn parse_all_defcfg() { let _lk = match CFG_PARSE_LOCK.lock() { From 9a635295699e01d4bb52dadee16e01ee10269d8e Mon Sep 17 00:00:00 2001 From: Jan Tache Date: Fri, 1 Dec 2023 21:45:57 -0800 Subject: [PATCH 092/819] fix: use proper value for names-include check --- parser/src/cfg/defcfg.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/parser/src/cfg/defcfg.rs b/parser/src/cfg/defcfg.rs index 071d01246..24465786c 100644 --- a/parser/src/cfg/defcfg.rs +++ b/parser/src/cfg/defcfg.rs @@ -129,10 +129,11 @@ pub fn parse_defcfg(expr: &[SExpr]) -> Result { "linux-dev-names-include" => { #[cfg(any(target_os = "linux", target_os = "unknown"))] { - cfg.linux_dev_names_include = Some(parse_linux_dev(val)?); - if cfg.linux_dev.is_empty() { + let dev_names = parse_linux_dev(val)?; + if dev_names.is_empty() { log::warn!("linux-dev-names-include is empty"); } + cfg.linux_dev_names_include = Some(dev_names); } } "linux-dev-names-exclude" => { From 089ad03e8f225f810bbcc15d189efcbb3c16b85d Mon Sep 17 00:00:00 2001 From: jtroo Date: Fri, 8 Dec 2023 22:55:34 -0800 Subject: [PATCH 093/819] chore: use better variable name --- parser/src/layers.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/parser/src/layers.rs b/parser/src/layers.rs index 1e3bca738..aa93383a6 100644 --- a/parser/src/layers.rs +++ b/parser/src/layers.rs @@ -6,14 +6,14 @@ use crate::keys::OsCode; // OsCode::KEY_MAX is the biggest OsCode pub const KEYS_IN_ROW: usize = OsCode::KEY_MAX as usize; -pub const LAYER_COLUMNS: usize = 2; +pub const LAYER_ROWS: usize = 2; pub const MAX_LAYERS: usize = 25; pub const ACTUAL_NUM_LAYERS: usize = MAX_LAYERS * 2; pub type KanataLayers = Layers< 'static, KEYS_IN_ROW, - LAYER_COLUMNS, + LAYER_ROWS, ACTUAL_NUM_LAYERS, &'static &'static [&'static CustomAction], >; @@ -22,7 +22,7 @@ type Row = [kanata_keyberon::action::Action<'static, &'static &'static [&'static KEYS_IN_ROW]; pub fn new_layers() -> Box { - let boxed_slice: Box<[[Row; LAYER_COLUMNS]]> = { + let boxed_slice: Box<[[Row; LAYER_ROWS]]> = { let mut layers = Vec::with_capacity(ACTUAL_NUM_LAYERS); for _ in 0..ACTUAL_NUM_LAYERS { layers.push([ From 20906651f6ccbddace23856b7d226590185e7c85 Mon Sep 17 00:00:00 2001 From: Psych3r Date: Sun, 10 Dec 2023 01:52:56 +0200 Subject: [PATCH 094/819] feat(macos): add limited macOS support (#652) Some notable bits of missing functionality are: - missing mouse functionality (input and output) - missing unicode functionality --- Cargo.lock | 20 + Cargo.toml | 5 +- README.md | 20 +- docs/config.adoc | 31 +- example_tcp_client/src/main.rs | 4 +- parser/src/cfg/defcfg.rs | 24 +- parser/src/cfg/mod.rs | 4 + parser/src/cfg/tests.rs | 10 +- parser/src/keys/linux.rs | 2 +- parser/src/keys/macos.rs | 2018 ++++++++++++++++++++++++++++++++ parser/src/keys/mod.rs | 15 + src/kanata/macos.rs | 75 ++ src/kanata/mod.rs | 11 +- src/main.rs | 10 + src/oskbd/macos.rs | 228 ++++ src/oskbd/mod.rs | 6 + 16 files changed, 2463 insertions(+), 20 deletions(-) create mode 100644 parser/src/keys/macos.rs create mode 100644 src/kanata/macos.rs create mode 100644 src/oskbd/macos.rs diff --git a/Cargo.lock b/Cargo.lock index bdad7e9e9..39a53ddcd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -104,6 +104,7 @@ version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ + "jobserver", "libc", ] @@ -363,6 +364,15 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +[[package]] +name = "jobserver" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" +dependencies = [ + "libc", +] + [[package]] name = "kanata" version = "1.5.0-prerelease-3" @@ -377,6 +387,7 @@ dependencies = [ "kanata-interception", "kanata-keyberon", "kanata-parser", + "karabiner-driverkit", "log", "miette", "mio", @@ -441,6 +452,15 @@ dependencies = [ "thiserror", ] +[[package]] +name = "karabiner-driverkit" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f1032243b23de4466cf25488e0f6082d9f8f6092e71641a2baefaffbf7b6768" +dependencies = [ + "cc", +] + [[package]] name = "lazy_static" version = "1.4.0" diff --git a/Cargo.toml b/Cargo.toml index ab43ba051..d7840ff45 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,10 @@ is-terminal = "=0.4.7" # Otherwise any changes to the local files will not reflect in the compiled # binary. kanata-keyberon = { path = "keyberon" } -kanata-parser = { path = "parser" } +kanata-parser = { path = "parser" } + +[target.'cfg(target_os = "macos")'.dependencies] +karabiner-driverkit = "0.1.0" [target.'cfg(target_os = "linux")'.dependencies] evdev = "=0.12.0" diff --git a/README.md b/README.md index 5e607153a..567dbba6a 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ ## What does this do? -This is a software keyboard remapper for Linux and Windows. A short summary of +This is a cross-platform software keyboard remapper for Linux, macOS and Windows. A short summary of the features: - multiple layers of key functionality @@ -76,7 +76,7 @@ Using `cargo install`: cargo install kanata - # On Linux, this may not work without `sudo`, see below + # On Linux and macOS, this may not work without `sudo`, see below kanata --cfg Build and run yourself in Linux: @@ -96,6 +96,22 @@ Build and run yourself in Windows. cargo build # --release optional, not really perf sensitive target\debug\kanata --cfg +Build and run yourself in macOS: + +First, install the [Karabiner VirtualHiDDevice Driver](https://github.com/pqrs-org/Karabiner-DriverKit-VirtualHIDDevice/blob/main/dist/Karabiner-DriverKit-VirtualHIDDevice-3.1.0.pkg). + +To activate it: + +`/Applications/.Karabiner-VirtualHIDDevice-Manager.app/Contents/MacOS/Karabiner-VirtualHIDDevice-Manager activate` + + + git clone https://github.com/jtroo/kanata && cd kanata + cargo build # --release optional, not really perf sensitive + + # sudo is needed to gain permission to intercept the keyboard + + sudo target/debug/kanata --cfg + The full configuration guide is [found here](./docs/config.adoc). Sample configuration files are found in [cfg_samples](./cfg_samples). The diff --git a/docs/config.adoc b/docs/config.adoc index 6736ea9f2..c38c5e7ec 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -170,11 +170,12 @@ that are not allowed in `defsrc` by default, at least according to the symbol shown. You can use `deflocalkeys` to define additional key names that can be used in `defsrc`, `deflayer` and anywhere else in the configuration. -There are three variants of deflocalkeys: +There are four variants of deflocalkeys: - `deflocalkeys-win` - `deflocalkeys-wintercept` - `deflocalkeys-linux` +- `deflocalkeys-macos` Only one of each deflocalkeys-* variant is allowed. The variants that are not @@ -201,6 +202,10 @@ Please contribute to the document if you are able! ì 13 ) +(deflocalkeys-macos + ì 13 +) + (defsrc grv 1 2 3 4 5 6 7 8 9 0 - ì bspc ) @@ -637,6 +642,30 @@ This configuration item does not affect Wayland or no-desktop environments. ) ---- +[[macos-only-macos-dev-names-include]] +=== macOS only: macos-dev-names-include +<> + +This option defines a list of device names that should be included. +By default, kanata will try to detect which input devices are keyboards and try +to intercept them all. However, you may specify exact keyboard devices to intercept +using the `macos-dev-names-include` configuration. +Device names that do not exist in the list will be ignored. +This option is parsed identically to `linux-dev`. + +Use `kanata -l` or `kanata --list` to list the available keyboards. + +.Example: +[source] +---- +(defcfg + macos-dev-names-include ( + "Device name 1" + "Device name 2" + ) +) +---- + [[windows-only-windows-altgr]] === Windows only: windows-altgr <> diff --git a/example_tcp_client/src/main.rs b/example_tcp_client/src/main.rs index 0090d24a8..6b98438ec 100644 --- a/example_tcp_client/src/main.rs +++ b/example_tcp_client/src/main.rs @@ -33,9 +33,7 @@ fn main() { ) .expect("connect to kanata"); log::info!("successfully connected"); - let writer_stream = kanata_conn - .try_clone() - .expect("clone writer"); + let writer_stream = kanata_conn.try_clone().expect("clone writer"); let reader_stream = kanata_conn; std::thread::spawn(move || write_to_kanata(writer_stream)); read_from_kanata(reader_stream); diff --git a/parser/src/cfg/defcfg.rs b/parser/src/cfg/defcfg.rs index 24465786c..09a02b3e9 100644 --- a/parser/src/cfg/defcfg.rs +++ b/parser/src/cfg/defcfg.rs @@ -39,6 +39,8 @@ pub struct CfgOptions { target_os = "unknown" ))] pub windows_interception_mouse_hwid: Option<[u8; HWID_ARR_SZ]>, + #[cfg(any(target_os = "macos", target_os = "unknown"))] + pub macos_dev_names_include: Option>, } impl Default for CfgOptions { @@ -77,6 +79,8 @@ impl Default for CfgOptions { target_os = "unknown" ))] windows_interception_mouse_hwid: None, + #[cfg(any(target_os = "macos", target_os = "unknown"))] + macos_dev_names_include: None, } } } @@ -117,7 +121,7 @@ pub fn parse_defcfg(expr: &[SExpr]) -> Result { "linux-dev" => { #[cfg(any(target_os = "linux", target_os = "unknown"))] { - cfg.linux_dev = parse_linux_dev(val)?; + cfg.linux_dev = parse_dev(val)?; if cfg.linux_dev.is_empty() { bail_expr!( val, @@ -129,7 +133,7 @@ pub fn parse_defcfg(expr: &[SExpr]) -> Result { "linux-dev-names-include" => { #[cfg(any(target_os = "linux", target_os = "unknown"))] { - let dev_names = parse_linux_dev(val)?; + let dev_names = parse_dev(val)?; if dev_names.is_empty() { log::warn!("linux-dev-names-include is empty"); } @@ -139,7 +143,7 @@ pub fn parse_defcfg(expr: &[SExpr]) -> Result { "linux-dev-names-exclude" => { #[cfg(any(target_os = "linux", target_os = "unknown"))] { - cfg.linux_dev_names_exclude = Some(parse_linux_dev(val)?); + cfg.linux_dev_names_exclude = Some(parse_dev(val)?); } } "linux-unicode-u-code" => { @@ -238,6 +242,16 @@ pub fn parse_defcfg(expr: &[SExpr]) -> Result { cfg.windows_interception_mouse_hwid = Some(hwid_slice?); } } + "macos-dev-names-include" => { + #[cfg(any(target_os = "macos", target_os = "unknown"))] + { + let dev_names = parse_dev(val)?; + if dev_names.is_empty() { + log::warn!("macos-dev-names-include is empty"); + } + cfg.macos_dev_names_include = Some(dev_names); + } + } "process-unmapped-keys" => { cfg.process_unmapped_keys = parse_defcfg_val_bool(val, label)? @@ -348,8 +362,8 @@ pub fn parse_colon_separated_text(paths: &str) -> Vec { all_paths } -#[cfg(any(target_os = "linux", target_os = "unknown"))] -pub fn parse_linux_dev(val: &SExpr) -> Result> { +#[cfg(any(target_os = "linux", target_os = "macos", target_os = "unknown"))] +pub fn parse_dev(val: &SExpr) -> Result> { Ok(match val { SExpr::Atom(a) => { let devs = parse_colon_separated_text(a.t.trim_matches('"')); diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index a57c6edf6..7850f877d 100644 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -263,6 +263,8 @@ fn parse_cfg( const DEF_LOCAL_KEYS: &str = "deflocalkeys-win"; #[cfg(all(feature = "interception_driver", target_os = "windows"))] const DEF_LOCAL_KEYS: &str = "deflocalkeys-wintercept"; +#[cfg(target_os = "macos")] +const DEF_LOCAL_KEYS: &str = "deflocalkeys-macos"; #[cfg(any(target_os = "linux", target_os = "unknown"))] const DEF_LOCAL_KEYS: &str = "deflocalkeys-linux"; @@ -402,6 +404,7 @@ pub fn parse_cfg_raw_string( "deflocalkeys-win", "deflocalkeys-wintercept", "deflocalkeys-linux", + "deflocalkeys-macos", ] { if let Some(result) = root_exprs .iter() @@ -633,6 +636,7 @@ fn error_on_unknown_top_level_atoms(exprs: &[Spanned>]) -> Result<()> | "defsrc" | "deflayer" | "defoverrides" + | "deflocalkeys-macos" | "deflocalkeys-linux" | "deflocalkeys-win" | "deflocalkeys-wintercept" diff --git a/parser/src/cfg/tests.rs b/parser/src/cfg/tests.rs index 14a5787a5..7f01be181 100644 --- a/parser/src/cfg/tests.rs +++ b/parser/src/cfg/tests.rs @@ -1211,10 +1211,10 @@ fn parse_device_paths() { #[test] #[cfg(any(target_os = "linux", target_os = "unknown"))] -fn test_parse_linux_dev() { +fn test_parse_dev() { // The old colon separated devices format assert_eq!( - parse_linux_dev(&SExpr::Atom(Spanned { + parse_dev(&SExpr::Atom(Spanned { t: "\"Keyboard2:Input Device 1:pci-0000\\:00\\:14.0-usb-0\\:1\\:1.0-event\"" .to_string(), span: Span::default(), @@ -1226,7 +1226,7 @@ fn test_parse_linux_dev() { "pci-0000:00:14.0-usb-0:1:1.0-event" ] ); - parse_linux_dev(&SExpr::Atom(Spanned { + parse_dev(&SExpr::Atom(Spanned { t: "\"\"".to_string(), span: Span::default(), })) @@ -1234,7 +1234,7 @@ fn test_parse_linux_dev() { // The new device list format assert_eq!( - parse_linux_dev(&SExpr::List(Spanned { + parse_dev(&SExpr::List(Spanned { t: vec![ SExpr::Atom(Spanned { t: "Keyboard2".to_string(), @@ -1263,7 +1263,7 @@ fn test_parse_linux_dev() { r"backslashes\do\not\escape\:\anything" ] ); - parse_linux_dev(&SExpr::List(Spanned { + parse_dev(&SExpr::List(Spanned { t: vec![ SExpr::Atom(Spanned { t: "Device1".to_string(), diff --git a/parser/src/keys/linux.rs b/parser/src/keys/linux.rs index 117b8aa30..43ae238a0 100644 --- a/parser/src/keys/linux.rs +++ b/parser/src/keys/linux.rs @@ -1,11 +1,11 @@ // This file is taken from the original ktrl project's keys.rs file with modifications. use super::OsCode; + impl OsCode { pub(super) const fn as_u16_linux(self) -> u16 { self as u16 } - pub(super) const fn from_u16_linux(code: u16) -> Option { match code { 0 => Some(OsCode::KEY_RESERVED), diff --git a/parser/src/keys/macos.rs b/parser/src/keys/macos.rs new file mode 100644 index 000000000..b747dc321 --- /dev/null +++ b/parser/src/keys/macos.rs @@ -0,0 +1,2018 @@ +use super::OsCode; + +// because the parser can't handle oscode u16 values > 767 +// and macos has fucked up key coding (page and code) +pub struct PageCode { + pub page: u32, + pub code: u32, +} + +impl TryFrom for PageCode { + type Error = &'static str; + fn try_from(item: OsCode) -> Result { + match item { + OsCode::KEY_RESERVED => Ok(PageCode { + page: 0xFF, + code: 0xFF, + }), + OsCode::KEY_A => Ok(PageCode { + page: 0x07, + code: 0x04, + }), + OsCode::KEY_B => Ok(PageCode { + page: 0x07, + code: 0x05, + }), + OsCode::KEY_C => Ok(PageCode { + page: 0x07, + code: 0x06, + }), + OsCode::KEY_D => Ok(PageCode { + page: 0x07, + code: 0x07, + }), + OsCode::KEY_E => Ok(PageCode { + page: 0x07, + code: 0x08, + }), + OsCode::KEY_F => Ok(PageCode { + page: 0x07, + code: 0x09, + }), + OsCode::KEY_G => Ok(PageCode { + page: 0x07, + code: 0x0A, + }), + OsCode::KEY_H => Ok(PageCode { + page: 0x07, + code: 0x0B, + }), + OsCode::KEY_I => Ok(PageCode { + page: 0x07, + code: 0x0C, + }), + OsCode::KEY_J => Ok(PageCode { + page: 0x07, + code: 0x0D, + }), + OsCode::KEY_K => Ok(PageCode { + page: 0x07, + code: 0x0E, + }), + OsCode::KEY_L => Ok(PageCode { + page: 0x07, + code: 0x0F, + }), + OsCode::KEY_M => Ok(PageCode { + page: 0x07, + code: 0x10, + }), + OsCode::KEY_N => Ok(PageCode { + page: 0x07, + code: 0x11, + }), + OsCode::KEY_O => Ok(PageCode { + page: 0x07, + code: 0x12, + }), + OsCode::KEY_P => Ok(PageCode { + page: 0x07, + code: 0x13, + }), + OsCode::KEY_Q => Ok(PageCode { + page: 0x07, + code: 0x14, + }), + OsCode::KEY_R => Ok(PageCode { + page: 0x07, + code: 0x15, + }), + OsCode::KEY_S => Ok(PageCode { + page: 0x07, + code: 0x16, + }), + OsCode::KEY_T => Ok(PageCode { + page: 0x07, + code: 0x17, + }), + OsCode::KEY_U => Ok(PageCode { + page: 0x07, + code: 0x18, + }), + OsCode::KEY_V => Ok(PageCode { + page: 0x07, + code: 0x19, + }), + OsCode::KEY_W => Ok(PageCode { + page: 0x07, + code: 0x1A, + }), + OsCode::KEY_X => Ok(PageCode { + page: 0x07, + code: 0x1B, + }), + OsCode::KEY_Y => Ok(PageCode { + page: 0x07, + code: 0x1C, + }), + OsCode::KEY_Z => Ok(PageCode { + page: 0x07, + code: 0x1D, + }), + OsCode::KEY_1 => Ok(PageCode { + page: 0x07, + code: 0x1E, + }), + OsCode::KEY_2 => Ok(PageCode { + page: 0x07, + code: 0x1F, + }), + OsCode::KEY_3 => Ok(PageCode { + page: 0x07, + code: 0x20, + }), + OsCode::KEY_4 => Ok(PageCode { + page: 0x07, + code: 0x21, + }), + OsCode::KEY_5 => Ok(PageCode { + page: 0x07, + code: 0x22, + }), + OsCode::KEY_6 => Ok(PageCode { + page: 0x07, + code: 0x23, + }), + OsCode::KEY_7 => Ok(PageCode { + page: 0x07, + code: 0x24, + }), + OsCode::KEY_8 => Ok(PageCode { + page: 0x07, + code: 0x25, + }), + OsCode::KEY_9 => Ok(PageCode { + page: 0x07, + code: 0x26, + }), + OsCode::KEY_0 => Ok(PageCode { + page: 0x07, + code: 0x27, + }), + OsCode::KEY_ENTER => Ok(PageCode { + page: 0x07, + code: 0x28, + }), + OsCode::KEY_ESC => Ok(PageCode { + page: 0x07, + code: 0x29, + }), + OsCode::KEY_BACKSPACE => Ok(PageCode { + page: 0x07, + code: 0x2A, + }), + OsCode::KEY_TAB => Ok(PageCode { + page: 0x07, + code: 0x2B, + }), + OsCode::KEY_SPACE => Ok(PageCode { + page: 0x07, + code: 0x2C, + }), + OsCode::KEY_MINUS => Ok(PageCode { + page: 0x07, + code: 0x2D, + }), + OsCode::KEY_EQUAL => Ok(PageCode { + page: 0x07, + code: 0x2E, + }), + OsCode::KEY_LEFTBRACE => Ok(PageCode { + page: 0x07, + code: 0x2F, + }), + OsCode::KEY_RIGHTBRACE => Ok(PageCode { + page: 0x07, + code: 0x30, + }), + OsCode::KEY_BACKSLASH => Ok(PageCode { + page: 0x07, + code: 0x31, + }), + // KeyboardNonUSPound => 0x0732, todo + OsCode::KEY_SEMICOLON => Ok(PageCode { + page: 0x07, + code: 0x33, + }), + OsCode::KEY_APOSTROPHE => Ok(PageCode { + page: 0x07, + code: 0x34, + }), + OsCode::KEY_GRAVE => Ok(PageCode { + page: 0x07, + code: 0x35, + }), + OsCode::KEY_COMMA => Ok(PageCode { + page: 0x07, + code: 0x36, + }), + OsCode::KEY_DOT => Ok(PageCode { + page: 0x07, + code: 0x37, + }), + OsCode::KEY_SLASH => Ok(PageCode { + page: 0x07, + code: 0x38, + }), + OsCode::KEY_CAPSLOCK => Ok(PageCode { + page: 0x07, + code: 0x39, + }), + OsCode::KEY_F1 => Ok(PageCode { + page: 0x07, + code: 0x3A, + }), + OsCode::KEY_F2 => Ok(PageCode { + page: 0x07, + code: 0x3B, + }), + OsCode::KEY_F3 => Ok(PageCode { + page: 0x07, + code: 0x3C, + }), + OsCode::KEY_F4 => Ok(PageCode { + page: 0x07, + code: 0x3D, + }), + OsCode::KEY_F5 => Ok(PageCode { + page: 0x07, + code: 0x3E, + }), + OsCode::KEY_F6 => Ok(PageCode { + page: 0x07, + code: 0x3F, + }), + OsCode::KEY_F7 => Ok(PageCode { + page: 0x07, + code: 0x40, + }), + OsCode::KEY_F8 => Ok(PageCode { + page: 0x07, + code: 0x41, + }), + OsCode::KEY_F9 => Ok(PageCode { + page: 0x07, + code: 0x42, + }), + OsCode::KEY_F10 => Ok(PageCode { + page: 0x07, + code: 0x43, + }), + OsCode::KEY_F11 => Ok(PageCode { + page: 0x07, + code: 0x44, + }), + OsCode::KEY_F12 => Ok(PageCode { + page: 0x07, + code: 0x45, + }), + OsCode::KEY_PRINT => Ok(PageCode { + page: 0x07, + code: 0x46, + }), + OsCode::KEY_SCROLLLOCK => Ok(PageCode { + page: 0x07, + code: 0x47, + }), + OsCode::KEY_PAUSE => Ok(PageCode { + page: 0x07, + code: 0x48, + }), + OsCode::KEY_INSERT => Ok(PageCode { + page: 0x07, + code: 0x49, + }), + OsCode::KEY_HOME => Ok(PageCode { + page: 0x07, + code: 0x4A, + }), + OsCode::KEY_PAGEUP => Ok(PageCode { + page: 0x07, + code: 0x4B, + }), + OsCode::KEY_DELETE => Ok(PageCode { + page: 0x07, + code: 0x4C, + }), + OsCode::KEY_END => Ok(PageCode { + page: 0x07, + code: 0x4D, + }), + OsCode::KEY_PAGEDOWN => Ok(PageCode { + page: 0x07, + code: 0x4E, + }), + OsCode::KEY_RIGHT => Ok(PageCode { + page: 0x07, + code: 0x4F, + }), + OsCode::KEY_LEFT => Ok(PageCode { + page: 0x07, + code: 0x50, + }), + OsCode::KEY_DOWN => Ok(PageCode { + page: 0x07, + code: 0x51, + }), + OsCode::KEY_UP => Ok(PageCode { + page: 0x07, + code: 0x52, + }), + OsCode::KEY_NUMLOCK => Ok(PageCode { + page: 0x07, + code: 0x53, + }), + OsCode::KEY_KPSLASH => Ok(PageCode { + page: 0x07, + code: 0x54, + }), + OsCode::KEY_KPASTERISK => Ok(PageCode { + page: 0x07, + code: 0x55, + }), + OsCode::KEY_KPMINUS => Ok(PageCode { + page: 0x07, + code: 0x56, + }), + OsCode::KEY_KPPLUS => Ok(PageCode { + page: 0x07, + code: 0x57, + }), + OsCode::KEY_KPENTER => Ok(PageCode { + page: 0x07, + code: 0x58, + }), + OsCode::KEY_KP1 => Ok(PageCode { + page: 0x07, + code: 0x59, + }), + OsCode::KEY_KP2 => Ok(PageCode { + page: 0x07, + code: 0x5A, + }), + OsCode::KEY_KP3 => Ok(PageCode { + page: 0x07, + code: 0x5B, + }), + OsCode::KEY_KP4 => Ok(PageCode { + page: 0x07, + code: 0x5C, + }), + OsCode::KEY_KP5 => Ok(PageCode { + page: 0x07, + code: 0x5D, + }), + OsCode::KEY_KP6 => Ok(PageCode { + page: 0x07, + code: 0x5E, + }), + OsCode::KEY_KP7 => Ok(PageCode { + page: 0x07, + code: 0x5F, + }), + OsCode::KEY_KP8 => Ok(PageCode { + page: 0x07, + code: 0x60, + }), + OsCode::KEY_KP9 => Ok(PageCode { + page: 0x07, + code: 0x61, + }), + OsCode::KEY_KP0 => Ok(PageCode { + page: 0x07, + code: 0x62, + }), + OsCode::KEY_KPDOT => Ok(PageCode { + page: 0x07, + code: 0x63, + }), + OsCode::KEY_102ND => Ok(PageCode { + page: 0x07, + code: 0x64, + }), //KeyboardNonUSBackslash + // KeyboardApplication => 0x0765, todo + OsCode::KEY_POWER => Ok(PageCode { + page: 0x07, + code: 0x66, + }), + OsCode::KEY_KPEQUAL => Ok(PageCode { + page: 0x07, + code: 0x67, + }), + OsCode::KEY_F13 => Ok(PageCode { + page: 0x07, + code: 0x68, + }), + OsCode::KEY_F14 => Ok(PageCode { + page: 0x07, + code: 0x69, + }), + OsCode::KEY_F15 => Ok(PageCode { + page: 0x07, + code: 0x6A, + }), + OsCode::KEY_F16 => Ok(PageCode { + page: 0x07, + code: 0x6B, + }), + OsCode::KEY_F17 => Ok(PageCode { + page: 0x07, + code: 0x6C, + }), + OsCode::KEY_F18 => Ok(PageCode { + page: 0x07, + code: 0x6D, + }), + OsCode::KEY_F19 => Ok(PageCode { + page: 0x07, + code: 0x6E, + }), + OsCode::KEY_F20 => Ok(PageCode { + page: 0x07, + code: 0x6F, + }), + OsCode::KEY_F21 => Ok(PageCode { + page: 0x07, + code: 0x70, + }), + OsCode::KEY_F22 => Ok(PageCode { + page: 0x07, + code: 0x71, + }), + OsCode::KEY_F23 => Ok(PageCode { + page: 0x07, + code: 0x72, + }), + OsCode::KEY_F24 => Ok(PageCode { + page: 0x07, + code: 0x73, + }), + // KeyboardExecute => 0x0774, todo + OsCode::KEY_HELP => Ok(PageCode { + page: 0x07, + code: 0x75, + }), + OsCode::KEY_MENU => Ok(PageCode { + page: 0x07, + code: 0x76, + }), + OsCode::KEY_SELECT => Ok(PageCode { + page: 0x07, + code: 0x77, + }), + OsCode::KEY_STOP => Ok(PageCode { + page: 0x07, + code: 0x78, + }), + OsCode::KEY_AGAIN => Ok(PageCode { + page: 0x07, + code: 0x79, + }), + OsCode::KEY_UNDO => Ok(PageCode { + page: 0x07, + code: 0x7A, + }), + OsCode::KEY_CUT => Ok(PageCode { + page: 0x07, + code: 0x7B, + }), + OsCode::KEY_COPY => Ok(PageCode { + page: 0x07, + code: 0x7C, + }), + OsCode::KEY_PASTE => Ok(PageCode { + page: 0x07, + code: 0x7D, + }), + OsCode::KEY_FIND => Ok(PageCode { + page: 0x07, + code: 0x7E, + }), + OsCode::KEY_MUTE => Ok(PageCode { + page: 0x0C, + code: 0xE2, + }), // 0x077f + OsCode::KEY_VOLUMEUP => Ok(PageCode { + page: 0x0C, + code: 0xE9, + }), // 0x0780 + OsCode::KEY_VOLUMEDOWN => Ok(PageCode { + page: 0x0C, + code: 0xEA, + }), // 0x0781 + //KeyboardLockingCapsLock => 82, todo + //KeyboardLockingNumLock => 83, todo + //KeyboardLockingScrollLock => 84, todo + OsCode::KEY_KPCOMMA => Ok(PageCode { + page: 0x07, + code: 0x85, + }), + OsCode::KEY_ALTERASE => Ok(PageCode { + page: 0x07, + code: 0x99, + }), + OsCode::KEY_CANCEL => Ok(PageCode { + page: 0x07, + code: 0x9B, + }), + OsCode::KEY_CLEAR => Ok(PageCode { + page: 0x07, + code: 0x9C, + }), + OsCode::KEY_LEFTCTRL => Ok(PageCode { + page: 0x07, + code: 0xE0, + }), + OsCode::KEY_LEFTSHIFT => Ok(PageCode { + page: 0x07, + code: 0xE1, + }), + OsCode::KEY_LEFTALT => Ok(PageCode { + page: 0x07, + code: 0xE2, + }), + OsCode::KEY_LEFTMETA => Ok(PageCode { + page: 0x07, + code: 0xE3, + }), + OsCode::KEY_RIGHTCTRL => Ok(PageCode { + page: 0x07, + code: 0xE4, + }), + OsCode::KEY_RIGHTSHIFT => Ok(PageCode { + page: 0x07, + code: 0xE5, + }), + OsCode::KEY_RIGHTALT => Ok(PageCode { + page: 0x07, + code: 0xE6, + }), + OsCode::KEY_RIGHTMETA => Ok(PageCode { + page: 0x07, + code: 0xE7, + }), + // ?? + //OsCode::KEY_POWER => Ok( PageCode { page: 0x0C, code: 0x30 } ), + OsCode::KEY_SLEEP => Ok(PageCode { + page: 0x0C, + code: 0x32, + }), + OsCode::KEY_FORWARD => Ok(PageCode { + page: 0x0C, + code: 0xB3, + }), + OsCode::KEY_REWIND => Ok(PageCode { + page: 0x0C, + code: 0xB4, + }), + OsCode::KEY_NEXTSONG => Ok(PageCode { + page: 0x0C, + code: 0xB5, + }), + OsCode::KEY_PREVIOUSSONG => Ok(PageCode { + page: 0x0C, + code: 0xB6, + }), + OsCode::KEY_PLAYPAUSE => Ok(PageCode { + page: 0x0C, + code: 0xCD, + }), + OsCode::KEY_FN => Ok(PageCode { + page: 0xFF, + code: 0x03, + }), + OsCode::KEY_BRIGHTNESSUP => Ok(PageCode { + page: 0x0C, + code: 0x6F, + }), + OsCode::KEY_BRIGHTNESSDOWN => Ok(PageCode { + page: 0x0C, + code: 0x70, + }), + // OsCode::KEY_BRIGHTNESSUP => Ok( PageCode { page: 0x0C, code: 0x04 } ), + // OsCode::KEY_BRIGHTNESSDOWN => Ok( PageCode { page: 0x0C, code: 0x05 } ), + // OsCode::KEY_KBDILLUMTOGGLE => Ok( PageCode { page: 0x0C, code: 0x07 } ), + // OsCode::KEY_KBDILLUMUP => Ok( PageCode { page: 0x0C, code: 0x08 } ), + // OsCode::KEY_KBDILLUMDOWN => Ok( PageCode { page: 0x0C, code: 0x09 } ), + OsCode::KEY_KBDILLUMTOGGLE => Ok(PageCode { + page: 0xFF, + code: 0x07, + }), + OsCode::KEY_KBDILLUMUP => Ok(PageCode { + page: 0xFF, + code: 0x08, + }), + OsCode::KEY_KBDILLUMDOWN => Ok(PageCode { + page: 0xFF, + code: 0x09, + }), + // ?? + OsCode::KEY_DASHBOARD => Ok(PageCode { + page: 0xFF, + code: 0x02, + }), + OsCode::KEY_SEARCH => Ok(PageCode { + page: 0xFF, + code: 0x01, + }), + // OsCode::KEY_FN_ESC => 0x07, + // OsCode::KEY_FN_F1 => 0x07, + // OsCode::KEY_FN_F2 => 0x07, + // OsCode::KEY_FN_F3 => 0x07, + // OsCode::KEY_FN_F4 => 0x07, + // OsCode::KEY_FN_F5 => 0x07, + // OsCode::KEY_FN_F6 => 0x07, + // OsCode::KEY_FN_F7 => 0x07, + // OsCode::KEY_FN_F8 => 0x07, + // OsCode::KEY_FN_F9 => 0x07, + // OsCode::KEY_FN_F10 => 0x07, + // OsCode::KEY_FN_F11 => 0x07, + // OsCode::KEY_FN_F12 => 0x07, + // OsCode::KEY_FN_1 => 0x07, + // OsCode::KEY_FN_2 => 0x07, + // OsCode::KEY_FN_D => 0x07, + // OsCode::KEY_FN_E => 0x07, + // OsCode::KEY_FN_F => 0x07, + // OsCode::KEY_FN_S => 0x07, + // OsCode::KEY_FN_B => 0x07, + // osc => Err(&format!("OsCode {} not mapped!", osc.as_u16())), + _ => Err("OsCode unrecognized!"), + } + } +} + +impl TryFrom for OsCode { + type Error = &'static str; + fn try_from(item: PageCode) -> Result { + match item { + PageCode { + page: 0xFF, + code: 0xFF, + } => Ok(OsCode::KEY_RESERVED), + PageCode { + page: 0x07, + code: 0x04, + } => Ok(OsCode::KEY_A), + PageCode { + page: 0x07, + code: 0x05, + } => Ok(OsCode::KEY_B), + PageCode { + page: 0x07, + code: 0x06, + } => Ok(OsCode::KEY_C), + PageCode { + page: 0x07, + code: 0x07, + } => Ok(OsCode::KEY_D), + PageCode { + page: 0x07, + code: 0x08, + } => Ok(OsCode::KEY_E), + PageCode { + page: 0x07, + code: 0x09, + } => Ok(OsCode::KEY_F), + PageCode { + page: 0x07, + code: 0x0A, + } => Ok(OsCode::KEY_G), + PageCode { + page: 0x07, + code: 0x0B, + } => Ok(OsCode::KEY_H), + PageCode { + page: 0x07, + code: 0x0C, + } => Ok(OsCode::KEY_I), + PageCode { + page: 0x07, + code: 0x0D, + } => Ok(OsCode::KEY_J), + PageCode { + page: 0x07, + code: 0x0E, + } => Ok(OsCode::KEY_K), + PageCode { + page: 0x07, + code: 0x0F, + } => Ok(OsCode::KEY_L), + PageCode { + page: 0x07, + code: 0x10, + } => Ok(OsCode::KEY_M), + PageCode { + page: 0x07, + code: 0x11, + } => Ok(OsCode::KEY_N), + PageCode { + page: 0x07, + code: 0x12, + } => Ok(OsCode::KEY_O), + PageCode { + page: 0x07, + code: 0x13, + } => Ok(OsCode::KEY_P), + PageCode { + page: 0x07, + code: 0x14, + } => Ok(OsCode::KEY_Q), + PageCode { + page: 0x07, + code: 0x15, + } => Ok(OsCode::KEY_R), + PageCode { + page: 0x07, + code: 0x16, + } => Ok(OsCode::KEY_S), + PageCode { + page: 0x07, + code: 0x17, + } => Ok(OsCode::KEY_T), + PageCode { + page: 0x07, + code: 0x18, + } => Ok(OsCode::KEY_U), + PageCode { + page: 0x07, + code: 0x19, + } => Ok(OsCode::KEY_V), + PageCode { + page: 0x07, + code: 0x1A, + } => Ok(OsCode::KEY_W), + PageCode { + page: 0x07, + code: 0x1B, + } => Ok(OsCode::KEY_X), + PageCode { + page: 0x07, + code: 0x1C, + } => Ok(OsCode::KEY_Y), + PageCode { + page: 0x07, + code: 0x1D, + } => Ok(OsCode::KEY_Z), + PageCode { + page: 0x07, + code: 0x1E, + } => Ok(OsCode::KEY_1), + PageCode { + page: 0x07, + code: 0x1F, + } => Ok(OsCode::KEY_2), + PageCode { + page: 0x07, + code: 0x20, + } => Ok(OsCode::KEY_3), + PageCode { + page: 0x07, + code: 0x21, + } => Ok(OsCode::KEY_4), + PageCode { + page: 0x07, + code: 0x22, + } => Ok(OsCode::KEY_5), + PageCode { + page: 0x07, + code: 0x23, + } => Ok(OsCode::KEY_6), + PageCode { + page: 0x07, + code: 0x24, + } => Ok(OsCode::KEY_7), + PageCode { + page: 0x07, + code: 0x25, + } => Ok(OsCode::KEY_8), + PageCode { + page: 0x07, + code: 0x26, + } => Ok(OsCode::KEY_9), + PageCode { + page: 0x07, + code: 0x27, + } => Ok(OsCode::KEY_0), + PageCode { + page: 0x07, + code: 0x28, + } => Ok(OsCode::KEY_ENTER), + PageCode { + page: 0x07, + code: 0x29, + } => Ok(OsCode::KEY_ESC), + PageCode { + page: 0x07, + code: 0x2A, + } => Ok(OsCode::KEY_BACKSPACE), + PageCode { + page: 0x07, + code: 0x2B, + } => Ok(OsCode::KEY_TAB), + PageCode { + page: 0x07, + code: 0x2C, + } => Ok(OsCode::KEY_SPACE), + PageCode { + page: 0x07, + code: 0x2D, + } => Ok(OsCode::KEY_MINUS), + PageCode { + page: 0x07, + code: 0x2E, + } => Ok(OsCode::KEY_EQUAL), + PageCode { + page: 0x07, + code: 0x2F, + } => Ok(OsCode::KEY_LEFTBRACE), + PageCode { + page: 0x07, + code: 0x30, + } => Ok(OsCode::KEY_RIGHTBRACE), + PageCode { + page: 0x07, + code: 0x31, + } => Ok(OsCode::KEY_BACKSLASH), + PageCode { + page: 0x07, + code: 0x33, + } => Ok(OsCode::KEY_SEMICOLON), + PageCode { + page: 0x07, + code: 0x34, + } => Ok(OsCode::KEY_APOSTROPHE), + PageCode { + page: 0x07, + code: 0x35, + } => Ok(OsCode::KEY_GRAVE), + PageCode { + page: 0x07, + code: 0x36, + } => Ok(OsCode::KEY_COMMA), + PageCode { + page: 0x07, + code: 0x37, + } => Ok(OsCode::KEY_DOT), + PageCode { + page: 0x07, + code: 0x38, + } => Ok(OsCode::KEY_SLASH), + PageCode { + page: 0x07, + code: 0x39, + } => Ok(OsCode::KEY_CAPSLOCK), + PageCode { + page: 0x07, + code: 0x3A, + } => Ok(OsCode::KEY_F1), + PageCode { + page: 0x07, + code: 0x3B, + } => Ok(OsCode::KEY_F2), + PageCode { + page: 0x07, + code: 0x3C, + } => Ok(OsCode::KEY_F3), + PageCode { + page: 0x07, + code: 0x3D, + } => Ok(OsCode::KEY_F4), + PageCode { + page: 0x07, + code: 0x3E, + } => Ok(OsCode::KEY_F5), + PageCode { + page: 0x07, + code: 0x3F, + } => Ok(OsCode::KEY_F6), + PageCode { + page: 0x07, + code: 0x40, + } => Ok(OsCode::KEY_F7), + PageCode { + page: 0x07, + code: 0x41, + } => Ok(OsCode::KEY_F8), + PageCode { + page: 0x07, + code: 0x42, + } => Ok(OsCode::KEY_F9), + PageCode { + page: 0x07, + code: 0x43, + } => Ok(OsCode::KEY_F10), + PageCode { + page: 0x07, + code: 0x44, + } => Ok(OsCode::KEY_F11), + PageCode { + page: 0x07, + code: 0x45, + } => Ok(OsCode::KEY_F12), + PageCode { + page: 0x07, + code: 0x46, + } => Ok(OsCode::KEY_PRINT), + PageCode { + page: 0x07, + code: 0x47, + } => Ok(OsCode::KEY_SCROLLLOCK), + PageCode { + page: 0x07, + code: 0x48, + } => Ok(OsCode::KEY_PAUSE), + PageCode { + page: 0x07, + code: 0x49, + } => Ok(OsCode::KEY_INSERT), + PageCode { + page: 0x07, + code: 0x4A, + } => Ok(OsCode::KEY_HOME), + PageCode { + page: 0x07, + code: 0x4B, + } => Ok(OsCode::KEY_PAGEUP), + PageCode { + page: 0x07, + code: 0x4C, + } => Ok(OsCode::KEY_DELETE), + PageCode { + page: 0x07, + code: 0x4D, + } => Ok(OsCode::KEY_END), + PageCode { + page: 0x07, + code: 0x4E, + } => Ok(OsCode::KEY_PAGEDOWN), + PageCode { + page: 0x07, + code: 0x4F, + } => Ok(OsCode::KEY_RIGHT), + PageCode { + page: 0x07, + code: 0x50, + } => Ok(OsCode::KEY_LEFT), + PageCode { + page: 0x07, + code: 0x51, + } => Ok(OsCode::KEY_DOWN), + PageCode { + page: 0x07, + code: 0x52, + } => Ok(OsCode::KEY_UP), + PageCode { + page: 0x07, + code: 0x53, + } => Ok(OsCode::KEY_NUMLOCK), + PageCode { + page: 0x07, + code: 0x54, + } => Ok(OsCode::KEY_KPSLASH), + PageCode { + page: 0x07, + code: 0x55, + } => Ok(OsCode::KEY_KPASTERISK), + PageCode { + page: 0x07, + code: 0x56, + } => Ok(OsCode::KEY_KPMINUS), + PageCode { + page: 0x07, + code: 0x57, + } => Ok(OsCode::KEY_KPPLUS), + PageCode { + page: 0x07, + code: 0x58, + } => Ok(OsCode::KEY_KPENTER), + PageCode { + page: 0x07, + code: 0x59, + } => Ok(OsCode::KEY_KP1), + PageCode { + page: 0x07, + code: 0x5A, + } => Ok(OsCode::KEY_KP2), + PageCode { + page: 0x07, + code: 0x5B, + } => Ok(OsCode::KEY_KP3), + PageCode { + page: 0x07, + code: 0x5C, + } => Ok(OsCode::KEY_KP4), + PageCode { + page: 0x07, + code: 0x5D, + } => Ok(OsCode::KEY_KP5), + PageCode { + page: 0x07, + code: 0x5E, + } => Ok(OsCode::KEY_KP6), + PageCode { + page: 0x07, + code: 0x5F, + } => Ok(OsCode::KEY_KP7), + PageCode { + page: 0x07, + code: 0x60, + } => Ok(OsCode::KEY_KP8), + PageCode { + page: 0x07, + code: 0x61, + } => Ok(OsCode::KEY_KP9), + PageCode { + page: 0x07, + code: 0x62, + } => Ok(OsCode::KEY_KP0), + PageCode { + page: 0x07, + code: 0x63, + } => Ok(OsCode::KEY_KPDOT), + PageCode { + page: 0x07, + code: 0x64, + } => Ok(OsCode::KEY_102ND), + PageCode { + page: 0x07, + code: 0x66, + } => Ok(OsCode::KEY_POWER), + PageCode { + page: 0x07, + code: 0x67, + } => Ok(OsCode::KEY_KPEQUAL), + PageCode { + page: 0x07, + code: 0x68, + } => Ok(OsCode::KEY_F13), + PageCode { + page: 0x07, + code: 0x69, + } => Ok(OsCode::KEY_F14), + PageCode { + page: 0x07, + code: 0x6A, + } => Ok(OsCode::KEY_F15), + PageCode { + page: 0x07, + code: 0x6B, + } => Ok(OsCode::KEY_F16), + PageCode { + page: 0x07, + code: 0x6C, + } => Ok(OsCode::KEY_F17), + PageCode { + page: 0x07, + code: 0x6D, + } => Ok(OsCode::KEY_F18), + PageCode { + page: 0x07, + code: 0x6E, + } => Ok(OsCode::KEY_F19), + PageCode { + page: 0x07, + code: 0x6F, + } => Ok(OsCode::KEY_F20), + PageCode { + page: 0x07, + code: 0x70, + } => Ok(OsCode::KEY_F21), + PageCode { + page: 0x07, + code: 0x71, + } => Ok(OsCode::KEY_F22), + PageCode { + page: 0x07, + code: 0x72, + } => Ok(OsCode::KEY_F23), + PageCode { + page: 0x07, + code: 0x73, + } => Ok(OsCode::KEY_F24), + PageCode { + page: 0x07, + code: 0x75, + } => Ok(OsCode::KEY_HELP), + PageCode { + page: 0x07, + code: 0x76, + } => Ok(OsCode::KEY_MENU), + PageCode { + page: 0x07, + code: 0x77, + } => Ok(OsCode::KEY_SELECT), + PageCode { + page: 0x07, + code: 0x78, + } => Ok(OsCode::KEY_STOP), + PageCode { + page: 0x07, + code: 0x79, + } => Ok(OsCode::KEY_AGAIN), + PageCode { + page: 0x07, + code: 0x7A, + } => Ok(OsCode::KEY_UNDO), + PageCode { + page: 0x07, + code: 0x7B, + } => Ok(OsCode::KEY_CUT), + PageCode { + page: 0x07, + code: 0x7C, + } => Ok(OsCode::KEY_COPY), + PageCode { + page: 0x07, + code: 0x7D, + } => Ok(OsCode::KEY_PASTE), + PageCode { + page: 0x07, + code: 0x7E, + } => Ok(OsCode::KEY_FIND), + PageCode { + page: 0x0C, + code: 0xE2, + } => Ok(OsCode::KEY_MUTE), + PageCode { + page: 0x0C, + code: 0xE9, + } => Ok(OsCode::KEY_VOLUMEUP), + PageCode { + page: 0x0C, + code: 0xEA, + } => Ok(OsCode::KEY_VOLUMEDOWN), + PageCode { + page: 0x07, + code: 0x85, + } => Ok(OsCode::KEY_KPCOMMA), + PageCode { + page: 0x07, + code: 0x99, + } => Ok(OsCode::KEY_ALTERASE), + PageCode { + page: 0x07, + code: 0x9B, + } => Ok(OsCode::KEY_CANCEL), + PageCode { + page: 0x07, + code: 0x9C, + } => Ok(OsCode::KEY_CLEAR), + PageCode { + page: 0x07, + code: 0xE0, + } => Ok(OsCode::KEY_LEFTCTRL), + PageCode { + page: 0x07, + code: 0xE1, + } => Ok(OsCode::KEY_LEFTSHIFT), + PageCode { + page: 0x07, + code: 0xE2, + } => Ok(OsCode::KEY_LEFTALT), + PageCode { + page: 0x07, + code: 0xE3, + } => Ok(OsCode::KEY_LEFTMETA), + PageCode { + page: 0x07, + code: 0xE4, + } => Ok(OsCode::KEY_RIGHTCTRL), + PageCode { + page: 0x07, + code: 0xE5, + } => Ok(OsCode::KEY_RIGHTSHIFT), + PageCode { + page: 0x07, + code: 0xE6, + } => Ok(OsCode::KEY_RIGHTALT), + PageCode { + page: 0x07, + code: 0xE7, + } => Ok(OsCode::KEY_RIGHTMETA), + PageCode { + page: 0x0C, + code: 0x32, + } => Ok(OsCode::KEY_SLEEP), + PageCode { + page: 0x0C, + code: 0xB3, + } => Ok(OsCode::KEY_FORWARD), + PageCode { + page: 0x0C, + code: 0xB4, + } => Ok(OsCode::KEY_REWIND), + PageCode { + page: 0x0C, + code: 0xB5, + } => Ok(OsCode::KEY_NEXTSONG), + PageCode { + page: 0x0C, + code: 0xB6, + } => Ok(OsCode::KEY_PREVIOUSSONG), + PageCode { + page: 0x0C, + code: 0xCD, + } => Ok(OsCode::KEY_PLAYPAUSE), + PageCode { + page: 0xFF, + code: 0x03, + } => Ok(OsCode::KEY_FN), + PageCode { + page: 0x0C, + code: 0x6F, + } => Ok(OsCode::KEY_BRIGHTNESSUP), + PageCode { + page: 0x0C, + code: 0x70, + } => Ok(OsCode::KEY_BRIGHTNESSDOWN), + PageCode { + page: 0xFF, + code: 0x07, + } => Ok(OsCode::KEY_KBDILLUMTOGGLE), + PageCode { + page: 0xFF, + code: 0x08, + } => Ok(OsCode::KEY_KBDILLUMUP), + PageCode { + page: 0xFF, + code: 0x09, + } => Ok(OsCode::KEY_KBDILLUMDOWN), + PageCode { + page: 0xFF, + code: 0x02, + } => Ok(OsCode::KEY_DASHBOARD), + PageCode { + page: 0xFF, + code: 0x01, + } => Ok(OsCode::KEY_SEARCH), + _ => Err("PageCode unrecognized!"), + } + } +} + +impl OsCode { + pub(super) const fn as_u16_macos(self) -> u16 { + self as u16 + } + pub(super) const fn from_u16_macos(code: u16) -> Option { + match code { + 0 => Some(OsCode::KEY_RESERVED), + 1 => Some(OsCode::KEY_ESC), + 2 => Some(OsCode::KEY_1), + 3 => Some(OsCode::KEY_2), + 4 => Some(OsCode::KEY_3), + 5 => Some(OsCode::KEY_4), + 6 => Some(OsCode::KEY_5), + 7 => Some(OsCode::KEY_6), + 8 => Some(OsCode::KEY_7), + 9 => Some(OsCode::KEY_8), + 10 => Some(OsCode::KEY_9), + 11 => Some(OsCode::KEY_0), + 12 => Some(OsCode::KEY_MINUS), + 13 => Some(OsCode::KEY_EQUAL), + 14 => Some(OsCode::KEY_BACKSPACE), + 15 => Some(OsCode::KEY_TAB), + 16 => Some(OsCode::KEY_Q), + 17 => Some(OsCode::KEY_W), + 18 => Some(OsCode::KEY_E), + 19 => Some(OsCode::KEY_R), + 20 => Some(OsCode::KEY_T), + 21 => Some(OsCode::KEY_Y), + 22 => Some(OsCode::KEY_U), + 23 => Some(OsCode::KEY_I), + 24 => Some(OsCode::KEY_O), + 25 => Some(OsCode::KEY_P), + 26 => Some(OsCode::KEY_LEFTBRACE), + 27 => Some(OsCode::KEY_RIGHTBRACE), + 28 => Some(OsCode::KEY_ENTER), + 29 => Some(OsCode::KEY_LEFTCTRL), + 30 => Some(OsCode::KEY_A), + 31 => Some(OsCode::KEY_S), + 32 => Some(OsCode::KEY_D), + 33 => Some(OsCode::KEY_F), + 34 => Some(OsCode::KEY_G), + 35 => Some(OsCode::KEY_H), + 36 => Some(OsCode::KEY_J), + 37 => Some(OsCode::KEY_K), + 38 => Some(OsCode::KEY_L), + 39 => Some(OsCode::KEY_SEMICOLON), + 40 => Some(OsCode::KEY_APOSTROPHE), + 41 => Some(OsCode::KEY_GRAVE), + 42 => Some(OsCode::KEY_LEFTSHIFT), + 43 => Some(OsCode::KEY_BACKSLASH), + 44 => Some(OsCode::KEY_Z), + 45 => Some(OsCode::KEY_X), + 46 => Some(OsCode::KEY_C), + 47 => Some(OsCode::KEY_V), + 48 => Some(OsCode::KEY_B), + 49 => Some(OsCode::KEY_N), + 50 => Some(OsCode::KEY_M), + 51 => Some(OsCode::KEY_COMMA), + 52 => Some(OsCode::KEY_DOT), + 53 => Some(OsCode::KEY_SLASH), + 54 => Some(OsCode::KEY_RIGHTSHIFT), + 55 => Some(OsCode::KEY_KPASTERISK), + 56 => Some(OsCode::KEY_LEFTALT), + 57 => Some(OsCode::KEY_SPACE), + 58 => Some(OsCode::KEY_CAPSLOCK), + 59 => Some(OsCode::KEY_F1), + 60 => Some(OsCode::KEY_F2), + 61 => Some(OsCode::KEY_F3), + 62 => Some(OsCode::KEY_F4), + 63 => Some(OsCode::KEY_F5), + 64 => Some(OsCode::KEY_F6), + 65 => Some(OsCode::KEY_F7), + 66 => Some(OsCode::KEY_F8), + 67 => Some(OsCode::KEY_F9), + 68 => Some(OsCode::KEY_F10), + 69 => Some(OsCode::KEY_NUMLOCK), + 70 => Some(OsCode::KEY_SCROLLLOCK), + 71 => Some(OsCode::KEY_KP7), + 72 => Some(OsCode::KEY_KP8), + 73 => Some(OsCode::KEY_KP9), + 74 => Some(OsCode::KEY_KPMINUS), + 75 => Some(OsCode::KEY_KP4), + 76 => Some(OsCode::KEY_KP5), + 77 => Some(OsCode::KEY_KP6), + 78 => Some(OsCode::KEY_KPPLUS), + 79 => Some(OsCode::KEY_KP1), + 80 => Some(OsCode::KEY_KP2), + 81 => Some(OsCode::KEY_KP3), + 82 => Some(OsCode::KEY_KP0), + 83 => Some(OsCode::KEY_KPDOT), + 84 => Some(OsCode::KEY_84), + 85 => Some(OsCode::KEY_ZENKAKUHANKAKU), + 86 => Some(OsCode::KEY_102ND), + 87 => Some(OsCode::KEY_F11), + 88 => Some(OsCode::KEY_F12), + 89 => Some(OsCode::KEY_RO), + 90 => Some(OsCode::KEY_KATAKANA), + 91 => Some(OsCode::KEY_HIRAGANA), + 92 => Some(OsCode::KEY_HENKAN), + 93 => Some(OsCode::KEY_KATAKANAHIRAGANA), + 94 => Some(OsCode::KEY_MUHENKAN), + 95 => Some(OsCode::KEY_KPJPCOMMA), + 96 => Some(OsCode::KEY_KPENTER), + 97 => Some(OsCode::KEY_RIGHTCTRL), + 98 => Some(OsCode::KEY_KPSLASH), + 99 => Some(OsCode::KEY_SYSRQ), + 100 => Some(OsCode::KEY_RIGHTALT), + 101 => Some(OsCode::KEY_LINEFEED), + 102 => Some(OsCode::KEY_HOME), + 103 => Some(OsCode::KEY_UP), + 104 => Some(OsCode::KEY_PAGEUP), + 105 => Some(OsCode::KEY_LEFT), + 106 => Some(OsCode::KEY_RIGHT), + 107 => Some(OsCode::KEY_END), + 108 => Some(OsCode::KEY_DOWN), + 109 => Some(OsCode::KEY_PAGEDOWN), + 110 => Some(OsCode::KEY_INSERT), + 111 => Some(OsCode::KEY_DELETE), + 112 => Some(OsCode::KEY_MACRO), + 113 => Some(OsCode::KEY_MUTE), + 114 => Some(OsCode::KEY_VOLUMEDOWN), + 115 => Some(OsCode::KEY_VOLUMEUP), + 116 => Some(OsCode::KEY_POWER), + 117 => Some(OsCode::KEY_KPEQUAL), + 118 => Some(OsCode::KEY_KPPLUSMINUS), + 119 => Some(OsCode::KEY_PAUSE), + 120 => Some(OsCode::KEY_SCALE), + 121 => Some(OsCode::KEY_KPCOMMA), + 122 => Some(OsCode::KEY_HANGEUL), + 123 => Some(OsCode::KEY_HANJA), + 124 => Some(OsCode::KEY_YEN), + 125 => Some(OsCode::KEY_LEFTMETA), + 126 => Some(OsCode::KEY_RIGHTMETA), + 127 => Some(OsCode::KEY_COMPOSE), + 128 => Some(OsCode::KEY_STOP), + 129 => Some(OsCode::KEY_AGAIN), + 130 => Some(OsCode::KEY_PROPS), + 131 => Some(OsCode::KEY_UNDO), + 132 => Some(OsCode::KEY_FRONT), + 133 => Some(OsCode::KEY_COPY), + 134 => Some(OsCode::KEY_OPEN), + 135 => Some(OsCode::KEY_PASTE), + 136 => Some(OsCode::KEY_FIND), + 137 => Some(OsCode::KEY_CUT), + 138 => Some(OsCode::KEY_HELP), + 139 => Some(OsCode::KEY_MENU), + 140 => Some(OsCode::KEY_CALC), + 141 => Some(OsCode::KEY_SETUP), + 142 => Some(OsCode::KEY_SLEEP), + 143 => Some(OsCode::KEY_WAKEUP), + 144 => Some(OsCode::KEY_FILE), + 145 => Some(OsCode::KEY_SENDFILE), + 146 => Some(OsCode::KEY_DELETEFILE), + 147 => Some(OsCode::KEY_XFER), + 148 => Some(OsCode::KEY_PROG1), + 149 => Some(OsCode::KEY_PROG2), + 150 => Some(OsCode::KEY_WWW), + 151 => Some(OsCode::KEY_MSDOS), + 152 => Some(OsCode::KEY_COFFEE), + 153 => Some(OsCode::KEY_ROTATE_DISPLAY), + 154 => Some(OsCode::KEY_CYCLEWINDOWS), + 155 => Some(OsCode::KEY_MAIL), + 156 => Some(OsCode::KEY_BOOKMARKS), + 157 => Some(OsCode::KEY_COMPUTER), + 158 => Some(OsCode::KEY_BACK), + 159 => Some(OsCode::KEY_FORWARD), + 160 => Some(OsCode::KEY_CLOSECD), + 161 => Some(OsCode::KEY_EJECTCD), + 162 => Some(OsCode::KEY_EJECTCLOSECD), + 163 => Some(OsCode::KEY_NEXTSONG), + 164 => Some(OsCode::KEY_PLAYPAUSE), + 165 => Some(OsCode::KEY_PREVIOUSSONG), + 166 => Some(OsCode::KEY_STOPCD), + 167 => Some(OsCode::KEY_RECORD), + 168 => Some(OsCode::KEY_REWIND), + 169 => Some(OsCode::KEY_PHONE), + 170 => Some(OsCode::KEY_ISO), + 171 => Some(OsCode::KEY_CONFIG), + 172 => Some(OsCode::KEY_HOMEPAGE), + 173 => Some(OsCode::KEY_REFRESH), + 174 => Some(OsCode::KEY_EXIT), + 175 => Some(OsCode::KEY_MOVE), + 176 => Some(OsCode::KEY_EDIT), + 177 => Some(OsCode::KEY_SCROLLUP), + 178 => Some(OsCode::KEY_SCROLLDOWN), + 179 => Some(OsCode::KEY_KPLEFTPAREN), + 180 => Some(OsCode::KEY_KPRIGHTPAREN), + 181 => Some(OsCode::KEY_NEW), + 182 => Some(OsCode::KEY_REDO), + 183 => Some(OsCode::KEY_F13), + 184 => Some(OsCode::KEY_F14), + 185 => Some(OsCode::KEY_F15), + 186 => Some(OsCode::KEY_F16), + 187 => Some(OsCode::KEY_F17), + 188 => Some(OsCode::KEY_F18), + 189 => Some(OsCode::KEY_F19), + 190 => Some(OsCode::KEY_F20), + 191 => Some(OsCode::KEY_F21), + 192 => Some(OsCode::KEY_F22), + 193 => Some(OsCode::KEY_F23), + 194 => Some(OsCode::KEY_F24), + 195 => Some(OsCode::KEY_195), + 196 => Some(OsCode::KEY_196), + 197 => Some(OsCode::KEY_197), + 198 => Some(OsCode::KEY_198), + 199 => Some(OsCode::KEY_199), + 200 => Some(OsCode::KEY_PLAYCD), + 201 => Some(OsCode::KEY_PAUSECD), + 202 => Some(OsCode::KEY_PROG3), + 203 => Some(OsCode::KEY_PROG4), + 204 => Some(OsCode::KEY_DASHBOARD), + 205 => Some(OsCode::KEY_SUSPEND), + 206 => Some(OsCode::KEY_CLOSE), + 207 => Some(OsCode::KEY_PLAY), + 208 => Some(OsCode::KEY_FASTFORWARD), + 209 => Some(OsCode::KEY_BASSBOOST), + 210 => Some(OsCode::KEY_PRINT), + 211 => Some(OsCode::KEY_HP), + 212 => Some(OsCode::KEY_CAMERA), + 213 => Some(OsCode::KEY_SOUND), + 214 => Some(OsCode::KEY_QUESTION), + 215 => Some(OsCode::KEY_EMAIL), + 216 => Some(OsCode::KEY_CHAT), + 217 => Some(OsCode::KEY_SEARCH), + 218 => Some(OsCode::KEY_CONNECT), + 219 => Some(OsCode::KEY_FINANCE), + 220 => Some(OsCode::KEY_SPORT), + 221 => Some(OsCode::KEY_SHOP), + 222 => Some(OsCode::KEY_ALTERASE), + 223 => Some(OsCode::KEY_CANCEL), + 224 => Some(OsCode::KEY_BRIGHTNESSDOWN), + 225 => Some(OsCode::KEY_BRIGHTNESSUP), + 226 => Some(OsCode::KEY_MEDIA), + 227 => Some(OsCode::KEY_SWITCHVIDEOMODE), + 228 => Some(OsCode::KEY_KBDILLUMTOGGLE), + 229 => Some(OsCode::KEY_KBDILLUMDOWN), + 230 => Some(OsCode::KEY_KBDILLUMUP), + 231 => Some(OsCode::KEY_SEND), + 232 => Some(OsCode::KEY_REPLY), + 233 => Some(OsCode::KEY_FORWARDMAIL), + 234 => Some(OsCode::KEY_SAVE), + 235 => Some(OsCode::KEY_DOCUMENTS), + 236 => Some(OsCode::KEY_BATTERY), + 237 => Some(OsCode::KEY_BLUETOOTH), + 238 => Some(OsCode::KEY_WLAN), + 239 => Some(OsCode::KEY_UWB), + 240 => Some(OsCode::KEY_UNKNOWN), + 241 => Some(OsCode::KEY_VIDEO_NEXT), + 242 => Some(OsCode::KEY_VIDEO_PREV), + 243 => Some(OsCode::KEY_BRIGHTNESS_CYCLE), + 244 => Some(OsCode::KEY_BRIGHTNESS_AUTO), + 245 => Some(OsCode::KEY_DISPLAY_OFF), + 246 => Some(OsCode::KEY_WWAN), + 247 => Some(OsCode::KEY_RFKILL), + 248 => Some(OsCode::KEY_MICMUTE), + 249 => Some(OsCode::KEY_249), + 250 => Some(OsCode::KEY_250), + 251 => Some(OsCode::KEY_251), + 252 => Some(OsCode::KEY_252), + 253 => Some(OsCode::KEY_253), + 254 => Some(OsCode::KEY_254), + 255 => Some(OsCode::KEY_255), + 256 => Some(OsCode::BTN_0), + 257 => Some(OsCode::BTN_1), + 258 => Some(OsCode::BTN_2), + 259 => Some(OsCode::BTN_3), + 260 => Some(OsCode::BTN_4), + 261 => Some(OsCode::BTN_5), + 262 => Some(OsCode::BTN_6), + 263 => Some(OsCode::BTN_7), + 264 => Some(OsCode::BTN_8), + 265 => Some(OsCode::BTN_9), + 266 => Some(OsCode::KEY_266), + 267 => Some(OsCode::KEY_267), + 268 => Some(OsCode::KEY_268), + 269 => Some(OsCode::KEY_269), + 270 => Some(OsCode::KEY_270), + 271 => Some(OsCode::KEY_271), + 272 => Some(OsCode::BTN_LEFT), + 273 => Some(OsCode::BTN_RIGHT), + 274 => Some(OsCode::BTN_MIDDLE), + 275 => Some(OsCode::BTN_SIDE), + 276 => Some(OsCode::BTN_EXTRA), + 277 => Some(OsCode::BTN_FORWARD), + 278 => Some(OsCode::BTN_BACK), + 279 => Some(OsCode::BTN_TASK), + 280 => Some(OsCode::KEY_280), + 281 => Some(OsCode::KEY_281), + 282 => Some(OsCode::KEY_282), + 283 => Some(OsCode::KEY_283), + 284 => Some(OsCode::KEY_284), + 285 => Some(OsCode::KEY_285), + 286 => Some(OsCode::KEY_286), + 287 => Some(OsCode::KEY_287), + 288 => Some(OsCode::BTN_TRIGGER), + 289 => Some(OsCode::BTN_THUMB), + 290 => Some(OsCode::BTN_THUMB2), + 291 => Some(OsCode::BTN_TOP), + 292 => Some(OsCode::BTN_TOP2), + 293 => Some(OsCode::BTN_PINKIE), + 294 => Some(OsCode::BTN_BASE), + 295 => Some(OsCode::BTN_BASE2), + 296 => Some(OsCode::BTN_BASE3), + 297 => Some(OsCode::BTN_BASE4), + 298 => Some(OsCode::BTN_BASE5), + 299 => Some(OsCode::BTN_BASE6), + 300 => Some(OsCode::KEY_300), + 301 => Some(OsCode::KEY_301), + 302 => Some(OsCode::KEY_302), + 303 => Some(OsCode::BTN_DEAD), + 304 => Some(OsCode::BTN_SOUTH), + 305 => Some(OsCode::BTN_EAST), + 306 => Some(OsCode::BTN_C), + 307 => Some(OsCode::BTN_NORTH), + 308 => Some(OsCode::BTN_WEST), + 309 => Some(OsCode::BTN_Z), + 310 => Some(OsCode::BTN_TL), + 311 => Some(OsCode::BTN_TR), + 312 => Some(OsCode::BTN_TL2), + 313 => Some(OsCode::BTN_TR2), + 314 => Some(OsCode::BTN_SELECT), + 315 => Some(OsCode::BTN_START), + 316 => Some(OsCode::BTN_MODE), + 317 => Some(OsCode::BTN_THUMBL), + 318 => Some(OsCode::BTN_THUMBR), + 319 => Some(OsCode::KEY_319), + 320 => Some(OsCode::BTN_TOOL_PEN), + 321 => Some(OsCode::BTN_TOOL_RUBBER), + 322 => Some(OsCode::BTN_TOOL_BRUSH), + 323 => Some(OsCode::BTN_TOOL_PENCIL), + 324 => Some(OsCode::BTN_TOOL_AIRBRUSH), + 325 => Some(OsCode::BTN_TOOL_FINGER), + 326 => Some(OsCode::BTN_TOOL_MOUSE), + 327 => Some(OsCode::BTN_TOOL_LENS), + 328 => Some(OsCode::BTN_TOOL_QUINTTAP), + 329 => Some(OsCode::BTN_STYLUS3), + 330 => Some(OsCode::BTN_TOUCH), + 331 => Some(OsCode::BTN_STYLUS), + 332 => Some(OsCode::BTN_STYLUS2), + 333 => Some(OsCode::BTN_TOOL_DOUBLETAP), + 334 => Some(OsCode::BTN_TOOL_TRIPLETAP), + 335 => Some(OsCode::BTN_TOOL_QUADTAP), + 336 => Some(OsCode::BTN_GEAR_DOWN), + 337 => Some(OsCode::BTN_GEAR_UP), + 338 => Some(OsCode::KEY_338), + 339 => Some(OsCode::KEY_339), + 340 => Some(OsCode::KEY_340), + 341 => Some(OsCode::KEY_341), + 342 => Some(OsCode::KEY_342), + 343 => Some(OsCode::KEY_343), + 344 => Some(OsCode::KEY_344), + 345 => Some(OsCode::KEY_345), + 346 => Some(OsCode::KEY_346), + 347 => Some(OsCode::KEY_347), + 348 => Some(OsCode::KEY_348), + 349 => Some(OsCode::KEY_349), + 350 => Some(OsCode::KEY_350), + 351 => Some(OsCode::KEY_351), + 352 => Some(OsCode::KEY_OK), + 353 => Some(OsCode::KEY_SELECT), + 354 => Some(OsCode::KEY_GOTO), + 355 => Some(OsCode::KEY_CLEAR), + 356 => Some(OsCode::KEY_POWER2), + 357 => Some(OsCode::KEY_OPTION), + 358 => Some(OsCode::KEY_INFO), + 359 => Some(OsCode::KEY_TIME), + 360 => Some(OsCode::KEY_VENDOR), + 361 => Some(OsCode::KEY_ARCHIVE), + 362 => Some(OsCode::KEY_PROGRAM), + 363 => Some(OsCode::KEY_CHANNEL), + 364 => Some(OsCode::KEY_FAVORITES), + 365 => Some(OsCode::KEY_EPG), + 366 => Some(OsCode::KEY_PVR), + 367 => Some(OsCode::KEY_MHP), + 368 => Some(OsCode::KEY_LANGUAGE), + 369 => Some(OsCode::KEY_TITLE), + 370 => Some(OsCode::KEY_SUBTITLE), + 371 => Some(OsCode::KEY_ANGLE), + 372 => Some(OsCode::KEY_FULL_SCREEN), + 373 => Some(OsCode::KEY_MODE), + 374 => Some(OsCode::KEY_KEYBOARD), + 375 => Some(OsCode::KEY_ASPECT_RATIO), + 376 => Some(OsCode::KEY_PC), + 377 => Some(OsCode::KEY_TV), + 378 => Some(OsCode::KEY_TV2), + 379 => Some(OsCode::KEY_VCR), + 380 => Some(OsCode::KEY_VCR2), + 381 => Some(OsCode::KEY_SAT), + 382 => Some(OsCode::KEY_SAT2), + 383 => Some(OsCode::KEY_CD), + 384 => Some(OsCode::KEY_TAPE), + 385 => Some(OsCode::KEY_RADIO), + 386 => Some(OsCode::KEY_TUNER), + 387 => Some(OsCode::KEY_PLAYER), + 388 => Some(OsCode::KEY_TEXT), + 389 => Some(OsCode::KEY_DVD), + 390 => Some(OsCode::KEY_AUX), + 391 => Some(OsCode::KEY_MP3), + 392 => Some(OsCode::KEY_AUDIO), + 393 => Some(OsCode::KEY_VIDEO), + 394 => Some(OsCode::KEY_DIRECTORY), + 395 => Some(OsCode::KEY_LIST), + 396 => Some(OsCode::KEY_MEMO), + 397 => Some(OsCode::KEY_CALENDAR), + 398 => Some(OsCode::KEY_RED), + 399 => Some(OsCode::KEY_GREEN), + 400 => Some(OsCode::KEY_YELLOW), + 401 => Some(OsCode::KEY_BLUE), + 402 => Some(OsCode::KEY_CHANNELUP), + 403 => Some(OsCode::KEY_CHANNELDOWN), + 404 => Some(OsCode::KEY_FIRST), + 405 => Some(OsCode::KEY_LAST), + 406 => Some(OsCode::KEY_AB), + 407 => Some(OsCode::KEY_NEXT), + 408 => Some(OsCode::KEY_RESTART), + 409 => Some(OsCode::KEY_SLOW), + 410 => Some(OsCode::KEY_SHUFFLE), + 411 => Some(OsCode::KEY_BREAK), + 412 => Some(OsCode::KEY_PREVIOUS), + 413 => Some(OsCode::KEY_DIGITS), + 414 => Some(OsCode::KEY_TEEN), + 415 => Some(OsCode::KEY_TWEN), + 416 => Some(OsCode::KEY_VIDEOPHONE), + 417 => Some(OsCode::KEY_GAMES), + 418 => Some(OsCode::KEY_ZOOMIN), + 419 => Some(OsCode::KEY_ZOOMOUT), + 420 => Some(OsCode::KEY_ZOOMRESET), + 421 => Some(OsCode::KEY_WORDPROCESSOR), + 422 => Some(OsCode::KEY_EDITOR), + 423 => Some(OsCode::KEY_SPREADSHEET), + 424 => Some(OsCode::KEY_GRAPHICSEDITOR), + 425 => Some(OsCode::KEY_PRESENTATION), + 426 => Some(OsCode::KEY_DATABASE), + 427 => Some(OsCode::KEY_NEWS), + 428 => Some(OsCode::KEY_VOICEMAIL), + 429 => Some(OsCode::KEY_ADDRESSBOOK), + 430 => Some(OsCode::KEY_MESSENGER), + 431 => Some(OsCode::KEY_DISPLAYTOGGLE), + 432 => Some(OsCode::KEY_SPELLCHECK), + 433 => Some(OsCode::KEY_LOGOFF), + 434 => Some(OsCode::KEY_DOLLAR), + 435 => Some(OsCode::KEY_EURO), + 436 => Some(OsCode::KEY_FRAMEBACK), + 437 => Some(OsCode::KEY_FRAMEFORWARD), + 438 => Some(OsCode::KEY_CONTEXT_MENU), + 439 => Some(OsCode::KEY_MEDIA_REPEAT), + 440 => Some(OsCode::KEY_10CHANNELSUP), + 441 => Some(OsCode::KEY_10CHANNELSDOWN), + 442 => Some(OsCode::KEY_IMAGES), + 443 => Some(OsCode::KEY_443), + 444 => Some(OsCode::KEY_444), + 445 => Some(OsCode::KEY_445), + 446 => Some(OsCode::KEY_446), + 447 => Some(OsCode::KEY_447), + 448 => Some(OsCode::KEY_DEL_EOL), + 449 => Some(OsCode::KEY_DEL_EOS), + 450 => Some(OsCode::KEY_INS_LINE), + 451 => Some(OsCode::KEY_DEL_LINE), + 452 => Some(OsCode::KEY_452), + 453 => Some(OsCode::KEY_453), + 454 => Some(OsCode::KEY_454), + 455 => Some(OsCode::KEY_455), + 456 => Some(OsCode::KEY_456), + 457 => Some(OsCode::KEY_457), + 458 => Some(OsCode::KEY_458), + 459 => Some(OsCode::KEY_459), + 460 => Some(OsCode::KEY_460), + 461 => Some(OsCode::KEY_461), + 462 => Some(OsCode::KEY_462), + 463 => Some(OsCode::KEY_463), + 464 => Some(OsCode::KEY_FN), + 465 => Some(OsCode::KEY_FN_ESC), + 466 => Some(OsCode::KEY_FN_F1), + 467 => Some(OsCode::KEY_FN_F2), + 468 => Some(OsCode::KEY_FN_F3), + 469 => Some(OsCode::KEY_FN_F4), + 470 => Some(OsCode::KEY_FN_F5), + 471 => Some(OsCode::KEY_FN_F6), + 472 => Some(OsCode::KEY_FN_F7), + 473 => Some(OsCode::KEY_FN_F8), + 474 => Some(OsCode::KEY_FN_F9), + 475 => Some(OsCode::KEY_FN_F10), + 476 => Some(OsCode::KEY_FN_F11), + 477 => Some(OsCode::KEY_FN_F12), + 478 => Some(OsCode::KEY_FN_1), + 479 => Some(OsCode::KEY_FN_2), + 480 => Some(OsCode::KEY_FN_D), + 481 => Some(OsCode::KEY_FN_E), + 482 => Some(OsCode::KEY_FN_F), + 483 => Some(OsCode::KEY_FN_S), + 484 => Some(OsCode::KEY_FN_B), + 485 => Some(OsCode::KEY_485), + 486 => Some(OsCode::KEY_486), + 487 => Some(OsCode::KEY_487), + 488 => Some(OsCode::KEY_488), + 489 => Some(OsCode::KEY_489), + 490 => Some(OsCode::KEY_490), + 491 => Some(OsCode::KEY_491), + 492 => Some(OsCode::KEY_492), + 493 => Some(OsCode::KEY_493), + 494 => Some(OsCode::KEY_494), + 495 => Some(OsCode::KEY_495), + 496 => Some(OsCode::KEY_496), + 497 => Some(OsCode::KEY_BRL_DOT1), + 498 => Some(OsCode::KEY_BRL_DOT2), + 499 => Some(OsCode::KEY_BRL_DOT3), + 500 => Some(OsCode::KEY_BRL_DOT4), + 501 => Some(OsCode::KEY_BRL_DOT5), + 502 => Some(OsCode::KEY_BRL_DOT6), + 503 => Some(OsCode::KEY_BRL_DOT7), + 504 => Some(OsCode::KEY_BRL_DOT8), + 505 => Some(OsCode::KEY_BRL_DOT9), + 506 => Some(OsCode::KEY_BRL_DOT10), + 507 => Some(OsCode::KEY_507), + 508 => Some(OsCode::KEY_508), + 509 => Some(OsCode::KEY_509), + 510 => Some(OsCode::KEY_510), + 511 => Some(OsCode::KEY_511), + 512 => Some(OsCode::KEY_NUMERIC_0), + 513 => Some(OsCode::KEY_NUMERIC_1), + 514 => Some(OsCode::KEY_NUMERIC_2), + 515 => Some(OsCode::KEY_NUMERIC_3), + 516 => Some(OsCode::KEY_NUMERIC_4), + 517 => Some(OsCode::KEY_NUMERIC_5), + 518 => Some(OsCode::KEY_NUMERIC_6), + 519 => Some(OsCode::KEY_NUMERIC_7), + 520 => Some(OsCode::KEY_NUMERIC_8), + 521 => Some(OsCode::KEY_NUMERIC_9), + 522 => Some(OsCode::KEY_NUMERIC_STAR), + 523 => Some(OsCode::KEY_NUMERIC_POUND), + 524 => Some(OsCode::KEY_NUMERIC_A), + 525 => Some(OsCode::KEY_NUMERIC_B), + 526 => Some(OsCode::KEY_NUMERIC_C), + 527 => Some(OsCode::KEY_NUMERIC_D), + 528 => Some(OsCode::KEY_CAMERA_FOCUS), + 529 => Some(OsCode::KEY_WPS_BUTTON), + 530 => Some(OsCode::KEY_TOUCHPAD_TOGGLE), + 531 => Some(OsCode::KEY_TOUCHPAD_ON), + 532 => Some(OsCode::KEY_TOUCHPAD_OFF), + 533 => Some(OsCode::KEY_CAMERA_ZOOMIN), + 534 => Some(OsCode::KEY_CAMERA_ZOOMOUT), + 535 => Some(OsCode::KEY_CAMERA_UP), + 536 => Some(OsCode::KEY_CAMERA_DOWN), + 537 => Some(OsCode::KEY_CAMERA_LEFT), + 538 => Some(OsCode::KEY_CAMERA_RIGHT), + 539 => Some(OsCode::KEY_ATTENDANT_ON), + 540 => Some(OsCode::KEY_ATTENDANT_OFF), + 541 => Some(OsCode::KEY_ATTENDANT_TOGGLE), + 542 => Some(OsCode::KEY_LIGHTS_TOGGLE), + 543 => Some(OsCode::KEY_543), + 544 => Some(OsCode::BTN_DPAD_UP), + 545 => Some(OsCode::BTN_DPAD_DOWN), + 546 => Some(OsCode::BTN_DPAD_LEFT), + 547 => Some(OsCode::BTN_DPAD_RIGHT), + 548 => Some(OsCode::KEY_548), + 549 => Some(OsCode::KEY_549), + 550 => Some(OsCode::KEY_550), + 551 => Some(OsCode::KEY_551), + 552 => Some(OsCode::KEY_552), + 553 => Some(OsCode::KEY_553), + 554 => Some(OsCode::KEY_554), + 555 => Some(OsCode::KEY_555), + 556 => Some(OsCode::KEY_556), + 557 => Some(OsCode::KEY_557), + 558 => Some(OsCode::KEY_558), + 559 => Some(OsCode::KEY_559), + 560 => Some(OsCode::KEY_ALS_TOGGLE), + 561 => Some(OsCode::KEY_ROTATE_LOCK_TOGGLE), + 562 => Some(OsCode::KEY_562), + 563 => Some(OsCode::KEY_563), + 564 => Some(OsCode::KEY_564), + 565 => Some(OsCode::KEY_565), + 566 => Some(OsCode::KEY_566), + 567 => Some(OsCode::KEY_567), + 568 => Some(OsCode::KEY_568), + 569 => Some(OsCode::KEY_569), + 570 => Some(OsCode::KEY_570), + 571 => Some(OsCode::KEY_571), + 572 => Some(OsCode::KEY_572), + 573 => Some(OsCode::KEY_573), + 574 => Some(OsCode::KEY_574), + 575 => Some(OsCode::KEY_575), + 576 => Some(OsCode::KEY_BUTTONCONFIG), + 577 => Some(OsCode::KEY_TASKMANAGER), + 578 => Some(OsCode::KEY_JOURNAL), + 579 => Some(OsCode::KEY_CONTROLPANEL), + 580 => Some(OsCode::KEY_APPSELECT), + 581 => Some(OsCode::KEY_SCREENSAVER), + 582 => Some(OsCode::KEY_VOICECOMMAND), + 583 => Some(OsCode::KEY_ASSISTANT), + 584 => Some(OsCode::KEY_KBD_LAYOUT_NEXT), + 585 => Some(OsCode::KEY_585), + 586 => Some(OsCode::KEY_586), + 587 => Some(OsCode::KEY_587), + 588 => Some(OsCode::KEY_588), + 589 => Some(OsCode::KEY_589), + 590 => Some(OsCode::KEY_590), + 591 => Some(OsCode::KEY_591), + 592 => Some(OsCode::KEY_BRIGHTNESS_MIN), + 593 => Some(OsCode::KEY_BRIGHTNESS_MAX), + 594 => Some(OsCode::KEY_594), + 595 => Some(OsCode::KEY_595), + 596 => Some(OsCode::KEY_596), + 597 => Some(OsCode::KEY_597), + 598 => Some(OsCode::KEY_598), + 599 => Some(OsCode::KEY_599), + 600 => Some(OsCode::KEY_600), + 601 => Some(OsCode::KEY_601), + 602 => Some(OsCode::KEY_602), + 603 => Some(OsCode::KEY_603), + 604 => Some(OsCode::KEY_604), + 605 => Some(OsCode::KEY_605), + 606 => Some(OsCode::KEY_606), + 607 => Some(OsCode::KEY_607), + 608 => Some(OsCode::KEY_KBDINPUTASSIST_PREV), + 609 => Some(OsCode::KEY_KBDINPUTASSIST_NEXT), + 610 => Some(OsCode::KEY_KBDINPUTASSIST_PREVGROUP), + 611 => Some(OsCode::KEY_KBDINPUTASSIST_NEXTGROUP), + 612 => Some(OsCode::KEY_KBDINPUTASSIST_ACCEPT), + 613 => Some(OsCode::KEY_KBDINPUTASSIST_CANCEL), + 614 => Some(OsCode::KEY_RIGHT_UP), + 615 => Some(OsCode::KEY_RIGHT_DOWN), + 616 => Some(OsCode::KEY_LEFT_UP), + 617 => Some(OsCode::KEY_LEFT_DOWN), + 618 => Some(OsCode::KEY_ROOT_MENU), + 619 => Some(OsCode::KEY_MEDIA_TOP_MENU), + 620 => Some(OsCode::KEY_NUMERIC_11), + 621 => Some(OsCode::KEY_NUMERIC_12), + 622 => Some(OsCode::KEY_AUDIO_DESC), + 623 => Some(OsCode::KEY_3D_MODE), + 624 => Some(OsCode::KEY_NEXT_FAVORITE), + 625 => Some(OsCode::KEY_STOP_RECORD), + 626 => Some(OsCode::KEY_PAUSE_RECORD), + 627 => Some(OsCode::KEY_VOD), + 628 => Some(OsCode::KEY_UNMUTE), + 629 => Some(OsCode::KEY_FASTREVERSE), + 630 => Some(OsCode::KEY_SLOWREVERSE), + 631 => Some(OsCode::KEY_DATA), + 632 => Some(OsCode::KEY_ONSCREEN_KEYBOARD), + 633 => Some(OsCode::KEY_633), + 634 => Some(OsCode::KEY_634), + 635 => Some(OsCode::KEY_635), + 636 => Some(OsCode::KEY_636), + 637 => Some(OsCode::KEY_637), + 638 => Some(OsCode::KEY_638), + 639 => Some(OsCode::KEY_639), + 640 => Some(OsCode::KEY_640), + 641 => Some(OsCode::KEY_641), + 642 => Some(OsCode::KEY_642), + 643 => Some(OsCode::KEY_643), + 644 => Some(OsCode::KEY_644), + 645 => Some(OsCode::KEY_645), + 646 => Some(OsCode::KEY_646), + 647 => Some(OsCode::KEY_647), + 648 => Some(OsCode::KEY_648), + 649 => Some(OsCode::KEY_649), + 650 => Some(OsCode::KEY_650), + 651 => Some(OsCode::KEY_651), + 652 => Some(OsCode::KEY_652), + 653 => Some(OsCode::KEY_653), + 654 => Some(OsCode::KEY_654), + 655 => Some(OsCode::KEY_655), + 656 => Some(OsCode::KEY_656), + 657 => Some(OsCode::KEY_657), + 658 => Some(OsCode::KEY_658), + 659 => Some(OsCode::KEY_659), + 660 => Some(OsCode::KEY_660), + 661 => Some(OsCode::KEY_661), + 662 => Some(OsCode::KEY_662), + 663 => Some(OsCode::KEY_663), + 664 => Some(OsCode::KEY_664), + 665 => Some(OsCode::KEY_665), + 666 => Some(OsCode::KEY_666), + 667 => Some(OsCode::KEY_667), + 668 => Some(OsCode::KEY_668), + 669 => Some(OsCode::KEY_669), + 670 => Some(OsCode::KEY_670), + 671 => Some(OsCode::KEY_671), + 672 => Some(OsCode::KEY_672), + 673 => Some(OsCode::KEY_673), + 674 => Some(OsCode::KEY_674), + 675 => Some(OsCode::KEY_675), + 676 => Some(OsCode::KEY_676), + 677 => Some(OsCode::KEY_677), + 678 => Some(OsCode::KEY_678), + 679 => Some(OsCode::KEY_679), + 680 => Some(OsCode::KEY_680), + 681 => Some(OsCode::KEY_681), + 682 => Some(OsCode::KEY_682), + 683 => Some(OsCode::KEY_683), + 684 => Some(OsCode::KEY_684), + 685 => Some(OsCode::KEY_685), + 686 => Some(OsCode::KEY_686), + 687 => Some(OsCode::KEY_687), + 688 => Some(OsCode::KEY_688), + 689 => Some(OsCode::KEY_689), + 690 => Some(OsCode::KEY_690), + 691 => Some(OsCode::KEY_691), + 692 => Some(OsCode::KEY_692), + 693 => Some(OsCode::KEY_693), + 694 => Some(OsCode::KEY_694), + 695 => Some(OsCode::KEY_695), + 696 => Some(OsCode::KEY_696), + 697 => Some(OsCode::KEY_697), + 698 => Some(OsCode::KEY_698), + 699 => Some(OsCode::KEY_699), + 700 => Some(OsCode::KEY_700), + 701 => Some(OsCode::KEY_701), + 702 => Some(OsCode::KEY_702), + 703 => Some(OsCode::KEY_703), + 704 => Some(OsCode::BTN_TRIGGER_HAPPY1), + 705 => Some(OsCode::BTN_TRIGGER_HAPPY2), + 706 => Some(OsCode::BTN_TRIGGER_HAPPY3), + 707 => Some(OsCode::BTN_TRIGGER_HAPPY4), + 708 => Some(OsCode::BTN_TRIGGER_HAPPY5), + 709 => Some(OsCode::BTN_TRIGGER_HAPPY6), + 710 => Some(OsCode::BTN_TRIGGER_HAPPY7), + 711 => Some(OsCode::BTN_TRIGGER_HAPPY8), + 712 => Some(OsCode::BTN_TRIGGER_HAPPY9), + 713 => Some(OsCode::BTN_TRIGGER_HAPPY10), + 714 => Some(OsCode::BTN_TRIGGER_HAPPY11), + 715 => Some(OsCode::BTN_TRIGGER_HAPPY12), + 716 => Some(OsCode::BTN_TRIGGER_HAPPY13), + 717 => Some(OsCode::BTN_TRIGGER_HAPPY14), + 718 => Some(OsCode::BTN_TRIGGER_HAPPY15), + 719 => Some(OsCode::BTN_TRIGGER_HAPPY16), + 720 => Some(OsCode::BTN_TRIGGER_HAPPY17), + 721 => Some(OsCode::BTN_TRIGGER_HAPPY18), + 722 => Some(OsCode::BTN_TRIGGER_HAPPY19), + 723 => Some(OsCode::BTN_TRIGGER_HAPPY20), + 724 => Some(OsCode::BTN_TRIGGER_HAPPY21), + 725 => Some(OsCode::BTN_TRIGGER_HAPPY22), + 726 => Some(OsCode::BTN_TRIGGER_HAPPY23), + 727 => Some(OsCode::BTN_TRIGGER_HAPPY24), + 728 => Some(OsCode::BTN_TRIGGER_HAPPY25), + 729 => Some(OsCode::BTN_TRIGGER_HAPPY26), + 730 => Some(OsCode::BTN_TRIGGER_HAPPY27), + 731 => Some(OsCode::BTN_TRIGGER_HAPPY28), + 732 => Some(OsCode::BTN_TRIGGER_HAPPY29), + 733 => Some(OsCode::BTN_TRIGGER_HAPPY30), + 734 => Some(OsCode::BTN_TRIGGER_HAPPY31), + 735 => Some(OsCode::BTN_TRIGGER_HAPPY32), + 736 => Some(OsCode::BTN_TRIGGER_HAPPY33), + 737 => Some(OsCode::BTN_TRIGGER_HAPPY34), + 738 => Some(OsCode::BTN_TRIGGER_HAPPY35), + 739 => Some(OsCode::BTN_TRIGGER_HAPPY36), + 740 => Some(OsCode::BTN_TRIGGER_HAPPY37), + 741 => Some(OsCode::BTN_TRIGGER_HAPPY38), + 742 => Some(OsCode::BTN_TRIGGER_HAPPY39), + 743 => Some(OsCode::BTN_TRIGGER_HAPPY40), + 744 => Some(OsCode::BTN_MAX), + 767 => Some(OsCode::KEY_MAX), + _ => None, + } + } +} diff --git a/parser/src/keys/mod.rs b/parser/src/keys/mod.rs index 351f32352..97f66986e 100644 --- a/parser/src/keys/mod.rs +++ b/parser/src/keys/mod.rs @@ -7,8 +7,12 @@ use rustc_hash::FxHashMap as HashMap; #[cfg(any(target_os = "linux", target_os = "unknown"))] mod linux; +#[cfg(any(target_os = "macos", target_os = "unknown"))] +mod macos; #[cfg(any(target_os = "windows", target_os = "unknown"))] mod windows; +#[cfg(any(target_os = "macos", target_os = "unknown"))] +pub use macos::PageCode; mod mappings; pub use mappings::*; @@ -18,6 +22,7 @@ pub use mappings::*; pub enum Platform { Win, Linux, + Macos, } #[cfg(target_os = "unknown")] @@ -29,6 +34,7 @@ impl OsCode { return match *OSCODE_MAPPING_VARIANT.lock() { Platform::Win => self.as_u16_windows(), Platform::Linux => self.as_u16_linux(), + Platform::Macos => self.as_u16_macos(), }; #[cfg(target_os = "linux")] @@ -36,6 +42,9 @@ impl OsCode { #[cfg(target_os = "windows")] return self.as_u16_windows(); + + #[cfg(target_os = "macos")] + return self.as_u16_macos(); } pub fn from_u16(code: u16) -> Option { @@ -43,6 +52,7 @@ impl OsCode { return match *OSCODE_MAPPING_VARIANT.lock() { Platform::Win => OsCode::from_u16_windows(code), Platform::Linux => OsCode::from_u16_linux(code), + Platform::Macos => OsCode::from_u16_macos(code), }; #[cfg(target_os = "linux")] @@ -50,6 +60,9 @@ impl OsCode { #[cfg(target_os = "windows")] return OsCode::from_u16_windows(code); + + #[cfg(target_os = "macos")] + return OsCode::from_u16_macos(code); } } @@ -254,6 +267,8 @@ pub fn str_to_oscode(s: &str) -> Option { "f22" => OsCode::KEY_F22, "f23" => OsCode::KEY_F23, "f24" => OsCode::KEY_F24, + #[cfg(any(target_os = "macos", target_os = "unknown"))] + "fn" => OsCode::KEY_FN, #[cfg(target_os = "windows")] "kana" | "katakana" | "katakanahiragana" => OsCode::KEY_HANGEUL, #[cfg(any(target_os = "linux", target_os = "unknown"))] diff --git a/src/kanata/macos.rs b/src/kanata/macos.rs new file mode 100644 index 000000000..a9e97051f --- /dev/null +++ b/src/kanata/macos.rs @@ -0,0 +1,75 @@ +use super::*; +use anyhow::{anyhow, bail, Result}; +use log::info; +use parking_lot::Mutex; +use std::convert::TryFrom; +use std::sync::mpsc::SyncSender as Sender; +use std::sync::Arc; + +static PRESSED_KEYS: Lazy>> = Lazy::new(|| Mutex::new(HashSet::default())); + +impl Kanata { + /// Enter an infinite loop that listens for OS key events and sends them to the processing thread. + pub fn event_loop(kanata: Arc>, tx: Sender) -> Result<()> { + info!("entering the event loop"); + + let k = kanata.lock(); + let mut kb = match KbdIn::new(k.include_names.clone()) { + Ok(kbd_in) => kbd_in, + Err(e) => bail!("failed to open keyboard device(s): {}", e), + }; + drop(k); + + loop { + let event = kb.read().map_err(|e| anyhow!("failed read: {}", e))?; + + let mut key_event = match KeyEvent::try_from(event) { + Ok(ev) => ev, + _ => { + // Pass-through unrecognized keys + log::debug!("{event:?} is unrecognized!"); + let mut kanata = kanata.lock(); + kanata + .kbd_out + .write(event.clone()) + .map_err(|e| anyhow!("failed write: {}", e))?; + continue; + } + }; + + check_for_exit(&key_event); + + if !MAPPED_KEYS.lock().contains(&key_event.code) { + log::debug!("{key_event:?} is not mapped"); + let mut kanata = kanata.lock(); + kanata + .kbd_out + .write(event.clone()) + .map_err(|e| anyhow!("failed write: {}", e))?; + continue; + } + + log::debug!("sending {key_event:?} to processing loop"); + + match key_event.value { + KeyValue::Release => { + PRESSED_KEYS.lock().remove(&key_event.code); + } + KeyValue::Press => { + let mut pressed_keys = PRESSED_KEYS.lock(); + if pressed_keys.contains(&key_event.code) { + key_event.value = KeyValue::Repeat; + } else { + pressed_keys.insert(key_event.code); + } + } + _ => {} + } + tx.try_send(key_event)?; + } + } + + pub fn check_release_non_physical_shift(&mut self) -> Result<()> { + Ok(()) + } +} diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index 2c157f7b5..ff56cdc02 100644 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -39,6 +39,11 @@ mod linux; #[cfg(target_os = "linux")] pub use linux::*; +#[cfg(target_os = "macos")] +mod macos; +#[cfg(target_os = "macos")] +pub use macos::*; + mod caps_word; pub use caps_word::*; @@ -121,8 +126,8 @@ pub struct Kanata { #[cfg(target_os = "linux")] /// Tracks the Linux user configuration to continue or abort if no devices are found. continue_if_no_devices: bool, - #[cfg(target_os = "linux")] - /// Tracks the Linux user configuration for device names (instead of paths) that should be + #[cfg(any(target_os = "linux", target_os = "macos"))] + /// Tracks the Linux/Macos user configuration for device names (instead of paths) that should be /// included for interception and processing by kanata. pub include_names: Option>, #[cfg(target_os = "linux")] @@ -319,6 +324,8 @@ impl Kanata { live_reload_requested: false, overrides: cfg.overrides, override_states: OverrideStates::new(), + #[cfg(target_os = "macos")] + include_names: cfg.items.macos_dev_names_include, #[cfg(target_os = "linux")] kbd_in_paths: cfg.items.linux_dev, #[cfg(target_os = "linux")] diff --git a/src/main.rs b/src/main.rs index d986d2977..3eac3dd5d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -90,6 +90,10 @@ kanata.kbd in the current working directory and #[arg(short, long, verbatim_doc_comment)] symlink_path: Option, + #[cfg_attr(target_os = "macos", doc = "List the keyboards available for grabbing")] + #[arg(short, long)] + list: bool, + /// Enable debug logging. #[arg(short, long)] debug: bool, @@ -108,6 +112,12 @@ kanata.kbd in the current working directory and fn cli_init() -> Result { let args = Args::parse(); + #[cfg(target_os = "macos")] + if args.list { + karabiner_driverkit::list_keyboards(); + std::process::exit(0); + } + let cfg_paths = args.cfg.unwrap_or_else(default_cfg); let log_lvl = match (args.debug, args.trace) { diff --git a/src/oskbd/macos.rs b/src/oskbd/macos.rs new file mode 100644 index 000000000..40962e521 --- /dev/null +++ b/src/oskbd/macos.rs @@ -0,0 +1,228 @@ +//! Contains the input/output code for keyboards on Macos. +use super::*; +use crate::kanata::CalculatedMouseMove; +use crate::oskbd::KeyEvent; +use anyhow::anyhow; +use kanata_parser::custom_action::*; +use kanata_parser::keys::*; +use karabiner_driverkit::*; +use std::convert::TryFrom; +use std::io; + +pub const HI_RES_SCROLL_UNITS_IN_LO_RES: u16 = 120; + +#[derive(Debug, Clone, Copy)] +pub struct InputEvent { + value: u64, + page: u32, + code: u32, +} + +impl InputEvent { + pub fn new(event: DKEvent) -> Self { + InputEvent { + value: event.value, + page: event.page, + code: event.code, + } + } +} + +impl From for DKEvent { + fn from(event: InputEvent) -> Self { + Self { + value: event.value, + page: event.page, + code: event.code, + } + } +} + +pub struct KbdIn {} + +impl Drop for KbdIn { + fn drop(&mut self) { + release(); + } +} + +impl KbdIn { + pub fn new(include_names: Option>) -> Result { + if !driver_activated() { + return Err(anyhow!( + "Karabiner-VirtualHIDDevice driver is not activated." + )); + } + + let device_names = if include_names.is_some() { + validate_and_register_devices(include_names.unwrap()) + } else { + vec![] + }; + + if !device_names.is_empty() || register_device("") { + if grab() { + Ok(Self {}) + } else { + Err(anyhow!("grab failed")) + } + } else { + Err(anyhow!("Couldn't register any device")) + } + } + + pub fn read(&mut self) -> Result { + let mut event = DKEvent { + value: 0, + page: 0, + code: 0, + }; + + wait_key(&mut event); + + Ok(InputEvent::new(event)) + } +} + +fn validate_and_register_devices(include_names: Vec) -> Vec { + include_names + .iter() + .filter_map(|dev| match device_matches(dev) { + true => Some(dev.to_string()), + false => { + log::warn!("Not a valid device name '{dev}'"); + None + } + }) + .filter_map(|dev| { + if register_device(&dev) { + Some(dev.to_string()) + } else { + log::warn!("Couldn't register device '{dev}'"); + None + } + }) + .collect() +} + +impl TryFrom for KeyEvent { + type Error = (); + + fn try_from(item: InputEvent) -> Result { + if let Ok(oscode) = OsCode::try_from(PageCode { + page: item.page, + code: item.code, + }) { + Ok(KeyEvent { + code: oscode, + value: if item.value == 1 { + KeyValue::Press + } else { + KeyValue::Release + }, + }) + } else { + Err(()) + } + } +} + +impl TryFrom for InputEvent { + type Error = (); + + fn try_from(item: KeyEvent) -> Result { + if let Ok(pagecode) = PageCode::try_from(item.code) { + let val = match item.value { + KeyValue::Press => 1, + _ => 0, + }; + Ok(InputEvent { + value: val, + page: pagecode.page, + code: pagecode.code, + }) + } else { + Err(()) + } + } +} + +pub struct KbdOut {} + +impl KbdOut { + pub fn new() -> Result { + Ok(KbdOut {}) + } + + pub fn write(&mut self, event: InputEvent) -> Result<(), io::Error> { + let mut devent = event.into(); + log::debug!("Attempting to write {event:?} {devent:?}"); + let _sent = send_key(&mut devent); + Ok(()) + } + + pub fn write_key(&mut self, key: OsCode, value: KeyValue) -> Result<(), io::Error> { + if let Ok(event) = InputEvent::try_from(KeyEvent { value, code: key }) { + self.write(event) + } else { + log::debug!("couldn't write unrecognized {key:?}"); + Err(io::Error::new( + io::ErrorKind::NotFound, + "OsCode not recognized!", + )) + } + } + + pub fn write_code(&mut self, code: u32, value: KeyValue) -> Result<(), io::Error> { + if let Ok(event) = InputEvent::try_from(KeyEvent { + value, + code: OsCode::from_u16(code as u16).unwrap(), + }) { + self.write(event) + } else { + log::debug!("couldn't write unrecognized OsCode {code}"); + Err(io::Error::new( + io::ErrorKind::NotFound, + "OsCode not recognized!", + )) + } + } + + pub fn press_key(&mut self, key: OsCode) -> Result<(), io::Error> { + self.write_key(key, KeyValue::Press) + } + + pub fn release_key(&mut self, key: OsCode) -> Result<(), io::Error> { + self.write_key(key, KeyValue::Release) + } + + /// Send using C-S-u + + spc + pub fn send_unicode(&mut self, c: char) -> Result<(), io::Error> { + log::error!("Unable to send unicode {c}, unsupported functionality"); + todo!() + } + + pub fn scroll(&mut self, _direction: MWheelDirection, _distance: u16) -> Result<(), io::Error> { + panic!("Mouse is not supported yet on Macos") + } + + pub fn click_btn(&mut self, _btn: Btn) -> Result<(), io::Error> { + panic!("Mouse is not supported yet on Macos") + } + + pub fn release_btn(&mut self, _btn: Btn) -> Result<(), io::Error> { + panic!("Mouse is not supported yet on Macos") + } + + pub fn move_mouse(&mut self, _mv: CalculatedMouseMove) -> Result<(), io::Error> { + panic!("Mouse is not supported yet on Macos") + } + + pub fn move_mouse_many(&mut self, _moves: &[CalculatedMouseMove]) -> Result<(), io::Error> { + panic!("Mouse is not supported yet on Macos") + } + + pub fn set_mouse(&mut self, _x: u16, _y: u16) -> Result<(), io::Error> { + panic!("Mouse is not supported yet on Macos") + } +} diff --git a/src/oskbd/mod.rs b/src/oskbd/mod.rs index 05664ac1c..406a25a67 100644 --- a/src/oskbd/mod.rs +++ b/src/oskbd/mod.rs @@ -10,6 +10,11 @@ mod windows; #[cfg(target_os = "windows")] pub use windows::*; +#[cfg(target_os = "macos")] +mod macos; +#[cfg(target_os = "macos")] +pub use macos::*; + // ------------------ KeyValue -------------------- #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -55,6 +60,7 @@ pub struct KeyEvent { } #[cfg(not(all(feature = "interception_driver", target_os = "windows")))] +#[cfg(not(target_os = "macos"))] impl KeyEvent { pub fn new(code: OsCode, value: KeyValue) -> Self { Self { code, value } From fdc0df2e14704e2818149fc20c60748f79ed69ab Mon Sep 17 00:00:00 2001 From: jtroo Date: Sat, 9 Dec 2023 15:55:34 -0800 Subject: [PATCH 095/819] feat(linux): add configurable delay for registering devices (#653) --- src/main.rs | 31 ++++++++++++++++++++++++------- src/oskbd/linux.rs | 9 +++++++-- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/main.rs b/src/main.rs index 3eac3dd5d..c94a85df6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,20 @@ use anyhow::{bail, Result}; +use clap::Parser; use log::info; use simplelog::*; + use std::path::PathBuf; mod kanata; mod oskbd; mod tcp_server; -#[cfg(test)] -mod tests; - -use clap::Parser; use kanata::Kanata; use tcp_server::TcpServer; +#[cfg(test)] +mod tests; + type CfgPath = PathBuf; pub struct ValidatedArgs { @@ -102,10 +103,19 @@ kanata.kbd in the current working directory and #[arg(short, long)] trace: bool, - /// Remove the startup delay on kanata. In some cases, removing the delay may cause keyboard - /// issues on startup. - #[arg(short, long)] + /// Remove the startup delay on kanata. + /// In some cases, removing the delay may cause keyboard issues on startup. + #[arg(short, long, verbatim_doc_comment)] nodelay: bool, + + /// Milliseconds to wait before attempting to register a newly connected + /// device. The default is 200. + /// + /// You may wish to increase this if you have a device that is failing + /// to register - the device may be taking too long to become ready. + #[cfg(target_os = "linux")] + #[arg(short, long, verbatim_doc_comment)] + wait_device_ms: Option, } /// Parse CLI arguments and initialize logging. @@ -159,6 +169,13 @@ fn cli_init() -> Result { bail!("No config files provided\nFor more info, pass the `-h` or `--help` flags."); } + #[cfg(target_os = "linux")] + if let Some(wait) = args.wait_device_ms { + use std::sync::atomic::Ordering; + log::info!("Setting device registration wait time to {wait} ms."); + oskbd::WAIT_DEVICE_MS.store(wait, Ordering::SeqCst); + } + Ok(ValidatedArgs { paths: cfg_paths, port: args.port, diff --git a/src/oskbd/linux.rs b/src/oskbd/linux.rs index 7ecc245dd..d6e8319ae 100644 --- a/src/oskbd/linux.rs +++ b/src/oskbd/linux.rs @@ -15,6 +15,7 @@ use std::fs; use std::io; use std::os::unix::io::AsRawFd; use std::path::PathBuf; +use std::sync::atomic::{AtomicU64, Ordering}; use std::thread; use super::*; @@ -38,6 +39,8 @@ pub struct KbdIn { const INOTIFY_TOKEN_VALUE: usize = 0; const INOTIFY_TOKEN: Token = Token(INOTIFY_TOKEN_VALUE); +pub static WAIT_DEVICE_MS: AtomicU64 = AtomicU64::new(200); + impl KbdIn { pub fn new( dev_paths: &[String], @@ -184,7 +187,7 @@ impl KbdIn { let discovered_devices = missing .iter() .filter_map(|dev_path| { - for _ in 0..10 { + for _ in 0..(WAIT_DEVICE_MS.load(Ordering::SeqCst) / 10) { // try a few times with waits in between; device might not be ready if let Ok(device) = Device::open(dev_path) { return Some((device, dev_path.clone())); @@ -206,7 +209,9 @@ impl KbdIn { missing.retain(|path| !paths_registered.contains(path)); } else { log::info!("sleeping for a moment to let devices become ready"); - std::thread::sleep(std::time::Duration::from_millis(200)); + std::thread::sleep(std::time::Duration::from_millis( + WAIT_DEVICE_MS.load(Ordering::SeqCst), + )); discover_devices(self.include_names.as_deref(), self.exclude_names.as_deref()) .into_iter() .try_for_each(|(dev, path)| { From f44cd90e91d5249371533df7ee0b7a2bf5ebdcd8 Mon Sep 17 00:00:00 2001 From: Jan Tache Date: Sat, 9 Dec 2023 16:05:18 -0800 Subject: [PATCH 096/819] refactor: adjust fake key action error messages --- parser/src/cfg/mod.rs | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index 7850f877d..81856b0e6 100644 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -1260,7 +1260,7 @@ fn parse_action_list(ac: &[SExpr], s: &ParsedState) -> Result<&'static KanataAct CHORD => parse_chord(&ac[1..], s), RELEASE_KEY => parse_release_key(&ac[1..], s), RELEASE_LAYER => parse_release_layer(&ac[1..], s), - ON_PRESS_FAKEKEY => parse_fake_key_op(&ac[1..], s), + ON_PRESS_FAKEKEY => parse_on_press_fake_key_op(&ac[1..], s), ON_RELEASE_FAKEKEY => parse_on_release_fake_key_op(&ac[1..], s), ON_PRESS_FAKEKEY_DELAY => parse_fake_key_delay(&ac[1..], s), ON_RELEASE_FAKEKEY_DELAY => parse_on_release_fake_key_delay(&ac[1..], s), @@ -2274,8 +2274,11 @@ fn parse_fake_keys(exprs: &[&Vec], s: &mut ParsedState) -> Result<()> { Ok(()) } -fn parse_fake_key_op(ac_params: &[SExpr], s: &ParsedState) -> Result<&'static KanataAction> { - let (coord, action) = parse_fake_key_op_coord_action(ac_params, s)?; +fn parse_on_press_fake_key_op( + ac_params: &[SExpr], + s: &ParsedState, +) -> Result<&'static KanataAction> { + let (coord, action) = parse_fake_key_op_coord_action(ac_params, s, ON_PRESS_FAKEKEY)?; Ok(s.a.sref(Action::Custom( s.a.sref(s.a.sref_slice(CustomAction::FakeKey { coord, action })), ))) @@ -2285,7 +2288,7 @@ fn parse_on_release_fake_key_op( ac_params: &[SExpr], s: &ParsedState, ) -> Result<&'static KanataAction> { - let (coord, action) = parse_fake_key_op_coord_action(ac_params, s)?; + let (coord, action) = parse_fake_key_op_coord_action(ac_params, s, ON_RELEASE_FAKEKEY)?; Ok(s.a.sref(Action::Custom(s.a.sref( s.a.sref_slice(CustomAction::FakeKeyOnRelease { coord, action }), )))) @@ -2294,22 +2297,22 @@ fn parse_on_release_fake_key_op( fn parse_fake_key_op_coord_action( ac_params: &[SExpr], s: &ParsedState, + ac_name: &str, ) -> Result<(Coord, FakeKeyAction)> { - const ERR_MSG: &str = - "on-(press|release)-fakekey expects two parameters: <(tap|press|release|toggle)>"; + const ERR_MSG: &str = "expects two parameters: <(tap|press|release|toggle)>"; if ac_params.len() != 2 { - bail!("{ERR_MSG}"); + bail!("{ac_name} {ERR_MSG}"); } let y = match s.fake_keys.get(ac_params[0].atom(s.vars()).ok_or_else(|| { anyhow_expr!( &ac_params[0], - "{ERR_MSG}\nInvalid first parameter: a fake key name cannot be a list", + "{ac_name} {ERR_MSG}\nInvalid first parameter: a fake key name cannot be a list", ) })?) { Some((y, _)) => *y as u16, // cast should be safe; checked in `parse_fake_keys` None => bail_expr!( &ac_params[0], - "{ERR_MSG}\nInvalid first parameter: unknown fake key name {:?}", + "{ac_name} {ERR_MSG}\nInvalid first parameter: unknown fake key name {:?}", &ac_params[0] ), }; From 5eddba413c33bb4427859ca1708acf14803486ae Mon Sep 17 00:00:00 2001 From: Jan Tache Date: Sat, 9 Dec 2023 16:09:33 -0800 Subject: [PATCH 097/819] doc(macos): document unimplemented functionality --- docs/platform-known-issues.adoc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/platform-known-issues.adoc b/docs/platform-known-issues.adoc index de8de31cc..0d405118a 100644 --- a/docs/platform-known-issues.adoc +++ b/docs/platform-known-issues.adoc @@ -52,3 +52,9 @@ which map to the binaries * Key repeats can occur when they normally wouldn't in some cases ** https://github.com/jtroo/kanata/discussions/422 ** https://github.com/jtroo/kanata/issues/450 + +== MacOS + +* Mouse output actions is not implemented +* Mouse input processing is not implemented +* Unicode output action is not implemented From 568a0c8a6a5650d950328db7d41799acbb0d5a8e Mon Sep 17 00:00:00 2001 From: Jan Tache Date: Sat, 9 Dec 2023 16:12:15 -0800 Subject: [PATCH 098/819] doc: fix bad grammar --- docs/platform-known-issues.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/platform-known-issues.adoc b/docs/platform-known-issues.adoc index 0d405118a..bad337477 100644 --- a/docs/platform-known-issues.adoc +++ b/docs/platform-known-issues.adoc @@ -55,6 +55,6 @@ which map to the binaries == MacOS -* Mouse output actions is not implemented +* Mouse output actions are not implemented * Mouse input processing is not implemented * Unicode output action is not implemented From 905b038838acc56fc780736bd8be13f9e6b9467f Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 10 Dec 2023 15:05:54 -0800 Subject: [PATCH 099/819] ci: add test+clippy for macos --- .github/workflows/rust.yml | 38 ++++++++++++++++---------------------- cfg_samples/kanata.kbd | 4 ++++ parser/src/cfg/tests.rs | 19 +++++++++++++++++++ src/kanata/macos.rs | 4 ++-- src/oskbd/macos.rs | 4 ++-- 5 files changed, 43 insertions(+), 26 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index a5cd53475..3455a9eb3 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -35,30 +35,24 @@ jobs: - name: Check fmt run: cargo fmt --all --check - build-test-clippy-linux: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: Swatinem/rust-cache@v2 - with: - shared-key: "persist-cross-job" - workspaces: ./ - - run: rustup component add clippy - - name: Run tests - run: cargo test --all - - name: Run tests all features - run: cargo test --all --all-features - - name: Run clippy no features - run: cargo clippy --all -- -D warnings - - name: Run clippy all features - run: cargo clippy --all --all-features -- -D warnings - - build-test-clippy-windows: - runs-on: windows-latest + build-test-clippy: + runs-on: ${{ matrix.os }} strategy: matrix: - target: - - x86_64-pc-windows-msvc + # You can add more, for any target you'd like! + include: + - build: linux + os: ubuntu-latest + target: x86_64-unknown-linux-musl + + - build: windows + os: windows-latest + target: x86_64-pc-windows-msvc + + - build: macos + os: macos-latest + target: x86_64-apple-darwin + steps: - uses: actions/checkout@v3 - uses: Swatinem/rust-cache@v2 diff --git a/cfg_samples/kanata.kbd b/cfg_samples/kanata.kbd index 246d4f844..48b27967d 100644 --- a/cfg_samples/kanata.kbd +++ b/cfg_samples/kanata.kbd @@ -232,6 +232,10 @@ If you need help, please feel welcome to ask in the GitHub discussions. ì 13 ) +(deflocalkeys-macos + ì 13 +) + ;; Only one defsrc is allowed. ;; ;; defsrc defines the keys that will be intercepted by kanata. The order of the diff --git a/parser/src/cfg/tests.rs b/parser/src/cfg/tests.rs index 7f01be181..f23d0f138 100644 --- a/parser/src/cfg/tests.rs +++ b/parser/src/cfg/tests.rs @@ -1129,6 +1129,25 @@ yen 314 ¥ 315 new 316 ) +(deflocalkeys-macos ++ 300 +[ 301 +] 302 +{ 303 +} 304 +/ 305 +; 306 +` 307 += 308 +- 309 +' 310 +, 311 +. 312 +\ 313 +yen 314 +¥ 315 +new 316 +) (defsrc + [ ] { } / ; ` = - ' , . \ yen ¥ new) (deflayer base + [ ] { } / ; ` = - ' , . \ yen ¥ new) "#; diff --git a/src/kanata/macos.rs b/src/kanata/macos.rs index a9e97051f..ae42046db 100644 --- a/src/kanata/macos.rs +++ b/src/kanata/macos.rs @@ -31,7 +31,7 @@ impl Kanata { let mut kanata = kanata.lock(); kanata .kbd_out - .write(event.clone()) + .write(event) .map_err(|e| anyhow!("failed write: {}", e))?; continue; } @@ -44,7 +44,7 @@ impl Kanata { let mut kanata = kanata.lock(); kanata .kbd_out - .write(event.clone()) + .write(event) .map_err(|e| anyhow!("failed write: {}", e))?; continue; } diff --git a/src/oskbd/macos.rs b/src/oskbd/macos.rs index 40962e521..c14fa1c9d 100644 --- a/src/oskbd/macos.rs +++ b/src/oskbd/macos.rs @@ -54,8 +54,8 @@ impl KbdIn { )); } - let device_names = if include_names.is_some() { - validate_and_register_devices(include_names.unwrap()) + let device_names = if let Some(names) = include_names { + validate_and_register_devices(names) } else { vec![] }; From 38bbabd4792cff6b4948b8bd8337b72b48e70cd2 Mon Sep 17 00:00:00 2001 From: Jan Tache Date: Sun, 10 Dec 2023 15:33:19 -0800 Subject: [PATCH 100/819] ci: minor edits to rust.yml, add macos build action --- .github/workflows/macos-build.yml | 44 +++++++++++++++++++++++++++++++ .github/workflows/rust.yml | 10 +++---- 2 files changed, 48 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/macos-build.yml diff --git a/.github/workflows/macos-build.yml b/.github/workflows/macos-build.yml new file mode 100644 index 000000000..8a4e8ca4a --- /dev/null +++ b/.github/workflows/macos-build.yml @@ -0,0 +1,44 @@ +name: macos-build + +on: + workflow_dispatch: + branches: [ "main" ] + +env: + CARGO_TERM_COLOR: always + RUSTFLAGS: "-Dwarnings" + +jobs: + + build-macos: + runs-on: macos-latest + + steps: + - uses: actions/checkout@v3 + - uses: Swatinem/rust-cache@v2 + with: + shared-key: "persist-cross-job" + workspaces: ./ + - name: Build release + run: cargo build --release + - name: Move build artifact + shell: bash + run: | + mkdir -p artifacts + mv ./target/aarch64-apple-darwin/release/kanata artifacts/kanata_macos + - name: Build release with cmd feature + run: cargo build --release --features cmd + - name: Move build artifact with cmd feature + shell: bash + run: | + mv ./target/aarch64-apple-darwin/release/kanata artifacts/kanata_macos_cmd_allowed + - name: Strip binaries + shell: bash + run: | + strip artifacts/* + - uses: actions/upload-artifact@v3 + with: + name: macos-binaries + path: | + artifacts/kanata_macos + artifacts/kanata_macos_cmd_allowed diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 3455a9eb3..87e6199ed 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -9,8 +9,7 @@ on: - keyberon/**/* - cfg_samples/**/* - parser/**/* - - test_cfgs/**/* - - .github/workflows/**/* + - .github/workflows/rust.yml pull_request: branches: [ "main" ] paths: @@ -19,8 +18,7 @@ on: - keyberon/**/* - parser/**/* - cfg_samples/**/* - - test_cfgs/**/* - - .github/workflows/**/* + - .github/workflows/rust.yml env: CARGO_TERM_COLOR: always @@ -39,7 +37,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - # You can add more, for any target you'd like! + include: - build: linux os: ubuntu-latest @@ -52,7 +50,7 @@ jobs: - build: macos os: macos-latest target: x86_64-apple-darwin - + steps: - uses: actions/checkout@v3 - uses: Swatinem/rust-cache@v2 From 6fbffdd0c281d25f5e4c6f30352aa74691fdeaea Mon Sep 17 00:00:00 2001 From: Jan Tache Date: Sun, 10 Dec 2023 16:01:26 -0800 Subject: [PATCH 101/819] ci: edit still non-functional macos build action --- .github/workflows/macos-build.yml | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/.github/workflows/macos-build.yml b/.github/workflows/macos-build.yml index 8a4e8ca4a..e7beccc90 100644 --- a/.github/workflows/macos-build.yml +++ b/.github/workflows/macos-build.yml @@ -1,5 +1,11 @@ name: macos-build +# This currently doesn't *seem* to work. +# For some reason the binary is much tinier than Linux/Windows. (70 KB, is that real??) +# Also standard and the cmd_allowed variant have the same checksum, +# meaning they are the same binary. Which makes no sense. +# Unfortunately I can't test the binary itself since I don't own any macOS devices. + on: workflow_dispatch: branches: [ "main" ] @@ -18,24 +24,14 @@ jobs: - uses: Swatinem/rust-cache@v2 with: shared-key: "persist-cross-job" - workspaces: ./ - - name: Build release - run: cargo build --release - - name: Move build artifact + - name: Do the stuff shell: bash run: | mkdir -p artifacts - mv ./target/aarch64-apple-darwin/release/kanata artifacts/kanata_macos - - name: Build release with cmd feature - run: cargo build --release --features cmd - - name: Move build artifact with cmd feature - shell: bash - run: | - mv ./target/aarch64-apple-darwin/release/kanata artifacts/kanata_macos_cmd_allowed - - name: Strip binaries - shell: bash - run: | - strip artifacts/* + cargo build --release + mv target/release/kanata artifacts/kanata_macos + cargo build --release --features cmd + mv target/release/kanata artifacts/kanata_macos_cmd_allowed - uses: actions/upload-artifact@v3 with: name: macos-binaries From 375b6248bfe8e7c4c1b696caf4c36c89c78d3d2f Mon Sep 17 00:00:00 2001 From: cyxae Date: Tue, 12 Dec 2023 02:03:14 +0100 Subject: [PATCH 102/819] feat: add tap-hold-except-keys (#656) --- cfg_samples/kanata.kbd | 3 ++ docs/config.adoc | 27 +++++++++++++++ keyberon/src/action.rs | 5 ++- keyberon/src/layout.rs | 26 ++++++++------ parser/src/cfg/custom_tap_hold.rs | 56 ++++++++++++++++++++++--------- parser/src/cfg/list_actions.rs | 4 ++- parser/src/cfg/mod.rs | 20 ++++++++--- parser/src/cfg/tests.rs | 1 + 8 files changed, 110 insertions(+), 32 deletions(-) diff --git a/cfg_samples/kanata.kbd b/cfg_samples/kanata.kbd index 48b27967d..f1a8c56ed 100644 --- a/cfg_samples/kanata.kbd +++ b/cfg_samples/kanata.kbd @@ -374,6 +374,9 @@ If you need help, please feel welcome to ask in the GitHub discussions. ;; tap: u hold: misc layer early tap if any of: (a o e) are pressed umk (tap-hold-release-keys 200 200 u @msc (a o e)) + ;; tap: u hold: misc layer always tap if any of: (a o e) are pressed + uek (tap-hold-except-keys 200 200 u @msc (a o e)) + ;; tap for capslk, hold for lctl cap (tap-hold 200 200 caps lctl) diff --git a/docs/config.adoc b/docs/config.adoc index c38c5e7ec..a5be3d122 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -1553,6 +1553,33 @@ that are in `deflayer`, `defalias`, etc. for the corresponding `defsrc` key. ) ---- +- `tap-hold-except-keys` + +This variant takes a 5th parameter which is a list of keys +that always trigger a tap +when they are pressed while the `tap-hold-except-keys` action is waiting. +No key is ever output until there is either a release of the key or any other +key is pressed. This differs from `tap-hold` behaviour. + +The keys in the 5th parameter correspond to the physical input keys, +or in other words the key that corresponds to `defsrc`. +This is in contrast to the `fork` and `switch` actions +which operates on outputted keys, or in other words the outputs +that are in `deflayer`, `defalias`, etc. for the corresponding `defsrc` key. + +.Example: +[source] +---- +(defalias + ;; tap: o hold: arrows layer timeout: backspace + oat (tap-hold-press-timeout 200 200 o @arr bspc) + ;; tap: e hold: chords layer timeout: esc + ect (tap-hold-release-timeout 200 200 e @chr esc) + ;; tap: u hold: misc layer always tap if any of: (a o e) are pressed + umk (tap-hold-except-keys 200 200 u @msc (a o e)) +) +---- + [[macro]] === macro <> diff --git a/keyberon/src/action.rs b/keyberon/src/action.rs index e23539203..6e48e9f2b 100644 --- a/keyberon/src/action.rs +++ b/keyberon/src/action.rs @@ -84,7 +84,10 @@ pub enum HoldTapConfig<'a> { /// value will cause a fallback to the timeout-based approach. If the /// timeout is not triggered, the next tick will call the custom handler /// again. - Custom(&'a (dyn Fn(QueuedIter) -> Option + Send + Sync)), + /// The bool value defines if the timeout check should be skipped at the + /// next tick. This should generally be false. This is used by `tap-hold- + /// except-keys` to handle presses even when the timeout has been reached. + Custom(&'a (dyn Fn(QueuedIter) -> (Option, bool) + Send + Sync)), } impl<'a> Debug for HoldTapConfig<'a> { diff --git a/keyberon/src/layout.rs b/keyberon/src/layout.rs index ca49afbe1..54b9d72e9 100644 --- a/keyberon/src/layout.rs +++ b/keyberon/src/layout.rs @@ -400,6 +400,7 @@ impl<'a, T: std::fmt::Debug> WaitingState<'a, T> { } fn handle_hold_tap(&mut self, cfg: HoldTapConfig, queued: &Queue) -> Option { + let mut skip_timeout = false; match cfg { HoldTapConfig::Default => (), HoldTapConfig::HoldOnOtherKeyPress => { @@ -420,8 +421,11 @@ impl<'a, T: std::fmt::Debug> WaitingState<'a, T> { } } HoldTapConfig::Custom(func) => { - if let waiting_action @ Some(_) = (func)(QueuedIter(queued.iter())) { + let (waiting_action, local_skip) = (func)(QueuedIter(queued.iter())); + if waiting_action.is_some() { return waiting_action; + } else { + skip_timeout = local_skip; } } } @@ -429,12 +433,12 @@ impl<'a, T: std::fmt::Debug> WaitingState<'a, T> { .iter() .find(|s| self.is_corresponding_release(&s.event)) { - if self.timeout >= self.delay.saturating_sub(since) { + if self.timeout > self.delay.saturating_sub(since) { Some(WaitingAction::Tap) } else { Some(WaitingAction::Timeout) } - } else if self.timeout == 0 { + } else if (self.timeout == 0) && (!skip_timeout) { Some(WaitingAction::Timeout) } else { None @@ -1979,17 +1983,17 @@ mod test { #[test] fn custom_handler() { - fn always_tap(_: QueuedIter) -> Option { - Some(WaitingAction::Tap) + fn always_tap(_: QueuedIter) -> (Option, bool) { + (Some(WaitingAction::Tap), false) } - fn always_hold(_: QueuedIter) -> Option { - Some(WaitingAction::Hold) + fn always_hold(_: QueuedIter) -> (Option, bool) { + (Some(WaitingAction::Hold), false) } - fn always_nop(_: QueuedIter) -> Option { - Some(WaitingAction::NoOp) + fn always_nop(_: QueuedIter) -> (Option, bool) { + (Some(WaitingAction::NoOp), false) } - fn always_none(_: QueuedIter) -> Option { - None + fn always_none(_: QueuedIter) -> (Option, bool) { + (None, false) } static LAYERS: Layers<4, 1, 1> = [[[ HoldTap(&HoldTapAction { diff --git a/parser/src/cfg/custom_tap_hold.rs b/parser/src/cfg/custom_tap_hold.rs index c46603523..c4ffe875d 100644 --- a/parser/src/cfg/custom_tap_hold.rs +++ b/parser/src/cfg/custom_tap_hold.rs @@ -10,23 +10,49 @@ use super::alloc::Allocations; pub(crate) fn custom_tap_hold_release( keys: &[OsCode], a: &Allocations, -) -> &'static (dyn Fn(QueuedIter) -> Option + Send + Sync) { +) -> &'static (dyn Fn(QueuedIter) -> (Option, bool) + Send + Sync) { let keys = a.sref_vec(Vec::from_iter(keys.iter().copied())); - a.sref(move |mut queued: QueuedIter| -> Option { - while let Some(q) = queued.next() { - if q.event().is_press() { - let (i, j) = q.event().coord(); - // If any key matches the input, do a tap right away. - if keys.iter().copied().map(u16::from).any(|j2| j2 == j) { - return Some(WaitingAction::Tap); + a.sref( + move |mut queued: QueuedIter| -> (Option, bool) { + while let Some(q) = queued.next() { + if q.event().is_press() { + let (i, j) = q.event().coord(); + // If any key matches the input, do a tap right away. + if keys.iter().copied().map(u16::from).any(|j2| j2 == j) { + return (Some(WaitingAction::Tap), false); + } + // Otherwise do the PermissiveHold algorithm. + let target = Event::Release(i, j); + if queued.clone().copied().any(|q| q.event() == target) { + return (Some(WaitingAction::Hold), false); + } } - // Otherwise do the PermissiveHold algorithm. - let target = Event::Release(i, j); - if queued.clone().copied().any(|q| q.event() == target) { - return Some(WaitingAction::Hold); + } + (None, false) + }, + ) +} + +pub(crate) fn custom_tap_hold_except( + keys: &[OsCode], + a: &Allocations, +) -> &'static (dyn Fn(QueuedIter) -> (Option, bool) + Send + Sync) { + let keys = a.sref_vec(Vec::from_iter(keys.iter().copied())); + a.sref( + move |mut queued: QueuedIter| -> (Option, bool) { + for q in queued.by_ref() { + if q.event().is_press() { + let (_i, j) = q.event().coord(); + // If any key matches the input, do a tap. + if keys.iter().copied().map(u16::from).any(|j2| j2 == j) { + return (Some(WaitingAction::Tap), false); + } + // Otherwise continue with default behavior + return (None, false); } } - } - None - }) + // Otherwise skip timeout + (None, true) + }, + ) } diff --git a/parser/src/cfg/list_actions.rs b/parser/src/cfg/list_actions.rs index 9577dd5a7..23072cb42 100644 --- a/parser/src/cfg/list_actions.rs +++ b/parser/src/cfg/list_actions.rs @@ -11,6 +11,7 @@ pub const TAP_HOLD_RELEASE: &str = "tap-hold-release"; pub const TAP_HOLD_PRESS_TIMEOUT: &str = "tap-hold-press-timeout"; pub const TAP_HOLD_RELEASE_TIMEOUT: &str = "tap-hold-release-timeout"; pub const TAP_HOLD_RELEASE_KEYS: &str = "tap-hold-release-keys"; +pub const TAP_HOLD_EXCEPT_KEYS: &str = "tap-hold-except-keys"; pub const MULTI: &str = "multi"; pub const MACRO: &str = "macro"; pub const MACRO_REPEAT: &str = "macro-repeat"; @@ -61,7 +62,7 @@ pub const UNMOD: &str = "unmod"; pub const UNSHIFT: &str = "unshift"; pub fn is_list_action(ac: &str) -> bool { - const LIST_ACTIONS: [&str; 57] = [ + const LIST_ACTIONS: [&str; 58] = [ LAYER_SWITCH, LAYER_TOGGLE, LAYER_WHILE_HELD, @@ -71,6 +72,7 @@ pub fn is_list_action(ac: &str) -> bool { TAP_HOLD_PRESS_TIMEOUT, TAP_HOLD_RELEASE_TIMEOUT, TAP_HOLD_RELEASE_KEYS, + TAP_HOLD_EXCEPT_KEYS, MULTI, MACRO, MACRO_REPEAT, diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index 81856b0e6..25650b4b8 100644 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -157,6 +157,12 @@ pub type KanataAction = Action<'static, &'static &'static [&'static CustomAction type KLayout = Layout<'static, KEYS_IN_ROW, 2, ACTUAL_NUM_LAYERS, &'static &'static [&'static CustomAction]>; +type TapHoldCustomFunc = + fn( + &[OsCode], + &Allocations, + ) -> &'static (dyn Fn(QueuedIter) -> (Option, bool) + Send + Sync); + pub type BorrowedKLayout<'a> = Layout<'a, KEYS_IN_ROW, 2, ACTUAL_NUM_LAYERS, &'a &'a [&'a CustomAction]>; pub type KeySeqsToFKeys = Trie; @@ -1240,7 +1246,10 @@ fn parse_action_list(ac: &[SExpr], s: &ParsedState) -> Result<&'static KanataAct TAP_HOLD_RELEASE_TIMEOUT => { parse_tap_hold_timeout(&ac[1..], s, HoldTapConfig::PermissiveHold) } - TAP_HOLD_RELEASE_KEYS => parse_tap_hold_release_keys(&ac[1..], s), + TAP_HOLD_RELEASE_KEYS => { + parse_tap_hold_keys(&ac[1..], s, "release", custom_tap_hold_release) + } + TAP_HOLD_EXCEPT_KEYS => parse_tap_hold_keys(&ac[1..], s, "except", custom_tap_hold_except), MULTI => parse_multi(&ac[1..], s), MACRO => parse_macro(&ac[1..], s, RepeatMacro::No), MACRO_REPEAT => parse_macro(&ac[1..], s, RepeatMacro::Yes), @@ -1387,15 +1396,18 @@ Params in order: })))) } -fn parse_tap_hold_release_keys( +fn parse_tap_hold_keys( ac_params: &[SExpr], s: &ParsedState, + custom_name: &str, + custom_func: TapHoldCustomFunc, ) -> Result<&'static KanataAction> { if ac_params.len() != 5 { bail!( - r"tap-hold-release-keys expects 5 items after it, got {}. + r"tap-hold-{}-keys expects 5 items after it, got {}. Params in order: ", + custom_name, ac_params.len(), ) } @@ -1408,7 +1420,7 @@ Params in order: bail!("tap-hold does not work in the tap-action of tap-hold") } Ok(s.a.sref(Action::HoldTap(s.a.sref(HoldTapAction { - config: HoldTapConfig::Custom(custom_tap_hold_release(&tap_trigger_keys, &s.a)), + config: HoldTapConfig::Custom(custom_func(&tap_trigger_keys, &s.a)), tap_hold_interval: tap_timeout, timeout: hold_timeout, tap: *tap_action, diff --git a/parser/src/cfg/tests.rs b/parser/src/cfg/tests.rs index f23d0f138..f65c11d1a 100644 --- a/parser/src/cfg/tests.rs +++ b/parser/src/cfg/tests.rs @@ -170,6 +170,7 @@ fn parse_action_vars() { thd (tap-hold $one $two $chr $two) tht (tap-hold-release-timeout $one $two $chr $two $one) thk (tap-hold-release-keys $one $two $chr $two $three) + the (tap-hold-except-keys $one $two $chr $two $three) mac (macro $one $two $one $two $chr C-S-$three $one) rmc (macro-repeat $one $two $one $two $chr C-S-$three $one) dr1 (dynamic-macro-record $one) From 5ad5116de42ddfd585d2027523e967b2f5e2eced Mon Sep 17 00:00:00 2001 From: rszyma Date: Tue, 12 Dec 2023 02:04:27 +0100 Subject: [PATCH 103/819] fix(linux): libinput "disable while typing" while kanata is running (#661) Change virtual output device's bus_type from BUS_USB to BUS_I8042. This fixes the dwt issues for me on Hyprland and I haven't noticed any regression caused by this change. Tested manually in Hyprland with `disable_while_typing=1` Closes #548 Closes #659 --- src/oskbd/linux.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/oskbd/linux.rs b/src/oskbd/linux.rs index d6e8319ae..0faa304d0 100644 --- a/src/oskbd/linux.rs +++ b/src/oskbd/linux.rs @@ -347,7 +347,9 @@ impl KbdOut { let mut device = uinput::VirtualDeviceBuilder::new()? .name("kanata") - .input_id(evdev::InputId::new(evdev::BusType::BUS_USB, 1, 1, 1)) + // libinput's "disable while typing" feature don't work when bus_type + // is set to BUS_USB, but appears to work when it's set to BUS_I8042. + .input_id(evdev::InputId::new(evdev::BusType::BUS_I8042, 1, 1, 1)) .with_keys(&keys)? .with_relative_axes(&relative_axes)? .build()?; From 3bcc40af9b94da9a8caf6734a4c3bf45a95b1c3a Mon Sep 17 00:00:00 2001 From: rszyma Date: Wed, 20 Dec 2023 23:36:41 +0100 Subject: [PATCH 104/819] fix(macos): bump driverkit to v0.1.1 (#672) --- .github/workflows/macos-build.yml | 6 ------ Cargo.lock | 4 ++-- Cargo.toml | 2 +- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/.github/workflows/macos-build.yml b/.github/workflows/macos-build.yml index e7beccc90..d193ec743 100644 --- a/.github/workflows/macos-build.yml +++ b/.github/workflows/macos-build.yml @@ -1,11 +1,5 @@ name: macos-build -# This currently doesn't *seem* to work. -# For some reason the binary is much tinier than Linux/Windows. (70 KB, is that real??) -# Also standard and the cmd_allowed variant have the same checksum, -# meaning they are the same binary. Which makes no sense. -# Unfortunately I can't test the binary itself since I don't own any macOS devices. - on: workflow_dispatch: branches: [ "main" ] diff --git a/Cargo.lock b/Cargo.lock index 39a53ddcd..a2ecb4d70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -454,9 +454,9 @@ dependencies = [ [[package]] name = "karabiner-driverkit" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f1032243b23de4466cf25488e0f6082d9f8f6092e71641a2baefaffbf7b6768" +checksum = "8dc92890fc2bb14a485e777968e7c18355386a30467cc0bf24f7744c9e44dde5" dependencies = [ "cc", ] diff --git a/Cargo.toml b/Cargo.toml index d7840ff45..4019871e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,7 +46,7 @@ kanata-keyberon = { path = "keyberon" } kanata-parser = { path = "parser" } [target.'cfg(target_os = "macos")'.dependencies] -karabiner-driverkit = "0.1.0" +karabiner-driverkit = "0.1.1" [target.'cfg(target_os = "linux")'.dependencies] evdev = "=0.12.0" From fe2e41af5746394ee9dc19f3072402f781f2d981 Mon Sep 17 00:00:00 2001 From: jtroo Date: Thu, 21 Dec 2023 21:53:59 -0800 Subject: [PATCH 105/819] ver: 1.5.0 --- Cargo.lock | 40 ++++++++++++++++++++++++++++++++++------ Cargo.toml | 10 +++++----- keyberon/Cargo.toml | 2 +- parser/Cargo.toml | 6 +++--- 4 files changed, 43 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a2ecb4d70..c19cd7f57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -375,7 +375,7 @@ dependencies = [ [[package]] name = "kanata" -version = "1.5.0-prerelease-3" +version = "1.5.0" dependencies = [ "anyhow", "clap", @@ -385,8 +385,8 @@ dependencies = [ "inotify", "is-terminal", "kanata-interception", - "kanata-keyberon", - "kanata-parser", + "kanata-keyberon 0.150.4 (registry+https://github.com/rust-lang/crates.io-index)", + "kanata-parser 0.150.4 (registry+https://github.com/rust-lang/crates.io-index)", "karabiner-driverkit", "log", "miette", @@ -420,7 +420,18 @@ dependencies = [ [[package]] name = "kanata-keyberon" -version = "0.150.3" +version = "0.150.4" +dependencies = [ + "arraydeque", + "heapless", + "kanata-keyberon-macros", +] + +[[package]] +name = "kanata-keyberon" +version = "0.150.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d843a10ea56d4b2ace524a36021592c9e5f1b99db0401836fa9d401cb4f2e746" dependencies = [ "arraydeque", "heapless", @@ -439,10 +450,27 @@ dependencies = [ [[package]] name = "kanata-parser" -version = "0.150.3" +version = "0.150.4" +dependencies = [ + "anyhow", + "kanata-keyberon 0.150.4 (registry+https://github.com/rust-lang/crates.io-index)", + "log", + "miette", + "once_cell", + "parking_lot", + "radix_trie", + "rustc-hash", + "thiserror", +] + +[[package]] +name = "kanata-parser" +version = "0.150.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01989b1457cc24d8838196a50900a894fab5c957c58890a316a8cb115d20a34c" dependencies = [ "anyhow", - "kanata-keyberon", + "kanata-keyberon 0.150.4 (registry+https://github.com/rust-lang/crates.io-index)", "log", "miette", "once_cell", diff --git a/Cargo.toml b/Cargo.toml index 4019871e8..685aad216 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ members = [ [package] name = "kanata" -version = "1.5.0-prerelease-3" +version = "1.5.0" authors = ["jtroo "] description = "Multi-layer keyboard customization" keywords = ["cli", "linux", "windows", "keyboard", "layout"] @@ -37,13 +37,13 @@ dirs = "5.0.1" # Pinned to avoid including multiple versions of a dependency is-terminal = "=0.4.7" -# kanata-keyberon = "0.150.3" -# kanata-parser = "0.150.3" +kanata-keyberon = "0.150.4" +kanata-parser = "0.150.4" # Uncomment below and comment out above for testing local changes. # Otherwise any changes to the local files will not reflect in the compiled # binary. -kanata-keyberon = { path = "keyberon" } -kanata-parser = { path = "parser" } +# kanata-keyberon = { path = "keyberon" } +# kanata-parser = { path = "parser" } [target.'cfg(target_os = "macos")'.dependencies] karabiner-driverkit = "0.1.1" diff --git a/keyberon/Cargo.toml b/keyberon/Cargo.toml index 25b4417ca..743178a9e 100644 --- a/keyberon/Cargo.toml +++ b/keyberon/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kanata-keyberon" -version = "0.150.3" +version = "0.150.4" authors = ["Guillaume Pinot ", "Robin Krahl ", "jtroo "] edition = "2018" description = "Pure Rust keyboard firmware. Fork intended for use with kanata." diff --git a/parser/Cargo.toml b/parser/Cargo.toml index d072649a7..41d478ed6 100644 --- a/parser/Cargo.toml +++ b/parser/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kanata-parser" -version = "0.150.3" +version = "0.150.4" authors = ["jtroo "] description = "A parser for configuration language of kanata, a keyboard remapper." keywords = ["kanata", "parser"] @@ -20,11 +20,11 @@ rustc-hash = "1.1.0" miette = { version = "5.7.0", features = ["fancy"] } thiserror = "1.0.38" -# kanata-keyberon = "0.150.3" +kanata-keyberon = "0.150.4" # Uncomment below and comment out above for testing local changes. # Otherwise any changes to the local files will not reflect in the compiled # binary. -kanata-keyberon = { path = "../keyberon" } +# kanata-keyberon = { path = "../keyberon" } [features] cmd = [] From b80766633d58b47fccc626446e5e04758cc620bc Mon Sep 17 00:00:00 2001 From: jtroo Date: Thu, 21 Dec 2023 22:36:49 -0800 Subject: [PATCH 106/819] doc: update release template for macOS --- docs/release-template.md | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/docs/release-template.md b/docs/release-template.md index f9866390a..4e4cfc102 100644 --- a/docs/release-template.md +++ b/docs/release-template.md @@ -31,7 +31,7 @@ You need to run `kanata.exe` via `cmd` or `powershell` to use a different config Download `kanata`. -Run it in a terminal and point it to a valid configuration file. Kanata does not start a background process, so the window needs to stay open after startup. See [this discussion](https://github.com/jtroo/kanata/discussions/130) for how to set up kanata with systemd. +Run it in a terminal and point it to a valid configuration file. Kanata does not start a background process, so the window needs to stay open after startup. See [this discussion](https://github.com/jtroo/kanata/discussions/130) for how to set up kanata with systemd. ``` chmod +x kanata # may be downloaded without executable permissions sudo ./kanata --cfg ` @@ -41,6 +41,32 @@ To avoid requiring `sudo`, [follow the instructions here](https://github.com/jtr +## macOS + +
+Instructions + +**WARNING**: feature support on macOS [is limited](https://github.com/jtroo/kanata/blob/main/docs/platform-known-issues.adoc#macos). + +First, install the [Karabiner VirtualHiDDevice Driver](https://github.com/pqrs-org/Karabiner-DriverKit-VirtualHIDDevice/blob/main/dist/Karabiner-DriverKit-VirtualHIDDevice-3.1.0.pkg). + +To activate it: + +``` +/Applications/.Karabiner-VirtualHIDDevice-Manager.app/Contents/MacOS/Karabiner-VirtualHIDDevice-Manager activate +``` + +Download `kanata_macos`. + +Run it in a terminal and point it to a valid configuration file. Kanata does not start a background process, so the window needs to stay open after startup. + +``` +chmod +x kanata_macos # may be downloaded without executable permissions +sudo ./kanata_macos --cfg ` +``` + +
+ ## cmd_allowed variants
From 73e7c0c9ede97fe920df57a10f73eb860e9dbd51 Mon Sep 17 00:00:00 2001 From: jtroo Date: Thu, 21 Dec 2023 22:39:49 -0800 Subject: [PATCH 107/819] dep: switch to local dependencies --- Cargo.lock | 34 +++------------------------------- Cargo.toml | 8 ++++---- parser/Cargo.toml | 4 ++-- 3 files changed, 9 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c19cd7f57..2e25a20f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -385,8 +385,8 @@ dependencies = [ "inotify", "is-terminal", "kanata-interception", - "kanata-keyberon 0.150.4 (registry+https://github.com/rust-lang/crates.io-index)", - "kanata-parser 0.150.4 (registry+https://github.com/rust-lang/crates.io-index)", + "kanata-keyberon", + "kanata-parser", "karabiner-driverkit", "log", "miette", @@ -427,17 +427,6 @@ dependencies = [ "kanata-keyberon-macros", ] -[[package]] -name = "kanata-keyberon" -version = "0.150.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d843a10ea56d4b2ace524a36021592c9e5f1b99db0401836fa9d401cb4f2e746" -dependencies = [ - "arraydeque", - "heapless", - "kanata-keyberon-macros", -] - [[package]] name = "kanata-keyberon-macros" version = "0.2.0" @@ -453,24 +442,7 @@ name = "kanata-parser" version = "0.150.4" dependencies = [ "anyhow", - "kanata-keyberon 0.150.4 (registry+https://github.com/rust-lang/crates.io-index)", - "log", - "miette", - "once_cell", - "parking_lot", - "radix_trie", - "rustc-hash", - "thiserror", -] - -[[package]] -name = "kanata-parser" -version = "0.150.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01989b1457cc24d8838196a50900a894fab5c957c58890a316a8cb115d20a34c" -dependencies = [ - "anyhow", - "kanata-keyberon 0.150.4 (registry+https://github.com/rust-lang/crates.io-index)", + "kanata-keyberon", "log", "miette", "once_cell", diff --git a/Cargo.toml b/Cargo.toml index 685aad216..8d956a50e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,13 +37,13 @@ dirs = "5.0.1" # Pinned to avoid including multiple versions of a dependency is-terminal = "=0.4.7" -kanata-keyberon = "0.150.4" -kanata-parser = "0.150.4" +# kanata-keyberon = "0.150.4" +# kanata-parser = "0.150.4" # Uncomment below and comment out above for testing local changes. # Otherwise any changes to the local files will not reflect in the compiled # binary. -# kanata-keyberon = { path = "keyberon" } -# kanata-parser = { path = "parser" } +kanata-keyberon = { path = "keyberon" } +kanata-parser = { path = "parser" } [target.'cfg(target_os = "macos")'.dependencies] karabiner-driverkit = "0.1.1" diff --git a/parser/Cargo.toml b/parser/Cargo.toml index 41d478ed6..0972bede9 100644 --- a/parser/Cargo.toml +++ b/parser/Cargo.toml @@ -20,11 +20,11 @@ rustc-hash = "1.1.0" miette = { version = "5.7.0", features = ["fancy"] } thiserror = "1.0.38" -kanata-keyberon = "0.150.4" +# kanata-keyberon = "0.150.4" # Uncomment below and comment out above for testing local changes. # Otherwise any changes to the local files will not reflect in the compiled # binary. -# kanata-keyberon = { path = "../keyberon" } +kanata-keyberon = { path = "../keyberon" } [features] cmd = [] From 3d02b2b05311bad24054d5e3b8c2735e11b30662 Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 24 Dec 2023 15:18:25 -0800 Subject: [PATCH 108/819] feat(defcfg): add concurrent-tap-hold (#664) --- cfg_samples/kanata.kbd | 6 ++++ docs/config.adoc | 16 +++++++-- keyberon/src/layout.rs | 70 ++++++++++++++++++++++++++++++++++++++-- parser/src/cfg/defcfg.rs | 9 ++++-- parser/src/cfg/mod.rs | 13 +++----- parser/src/cfg/tests.rs | 1 + 6 files changed, 100 insertions(+), 15 deletions(-) diff --git a/cfg_samples/kanata.kbd b/cfg_samples/kanata.kbd index f1a8c56ed..83b3031e0 100644 --- a/cfg_samples/kanata.kbd +++ b/cfg_samples/kanata.kbd @@ -189,6 +189,12 @@ If you need help, please feel welcome to ask in the GitHub discussions. ;; The default limit is 128 keys. ;; ;; dynamic-macro-max-presses 1000 + + ;; This configuration makes multiple tap-hold actions that are activated near + ;; in time expire their timeout quicker. Without this, the timeout for the 2nd + ;; tap-hold onwards will start from 0ms after the previous tap-hold expires. + ;; + ;; concurrent-tap-hold yes ) ;; deflocalkeys-* enables you to define and use key names that match your locale diff --git a/docs/config.adoc b/docs/config.adoc index a5be3d122..73030e6d5 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -463,6 +463,18 @@ The default length limit is 128 keys. ) ---- +=== concurrent-tap-hold [[concurrent-tap-hold]] +This configuration makes multiple tap-hold actions +that are activated near in time expire their timeout quicker. +By default this is disabled. +When disabled, the timeout for a following tap-hold +will start from 0ms **after** the previous tap-hold expires. +When enabled, the timeout will start +as soon as the tap-hold action is pressed +even if a previous tap-hold action is still held and has not expired. + +concurrent-tap-hold yes + [[linux-only-linux-dev]] === Linux only: linux-dev <> @@ -734,8 +746,8 @@ The `defcfg` entry is treated as a list with pairs of strings. For example: This will be treated as configuration `a` having value `1` and configuration `b` having value `2`. -An example defcfg containing all of the options is shown below. It should be -noted options that are Linux-only or Windows-only will be ignored when used on +An example defcfg containing many of the options is shown below. It should be +noted options that are Linux-only, Windows-only, or macOS-only will be ignored when used on a non-applicable operating system. [source] diff --git a/keyberon/src/layout.rs b/keyberon/src/layout.rs index 54b9d72e9..44fc4fc15 100644 --- a/keyberon/src/layout.rs +++ b/keyberon/src/layout.rs @@ -84,6 +84,7 @@ where pub action_queue: ActionQueue<'a, T>, pub rpt_action: Option<&'a Action<'a, T>>, pub historical_keys: ArrayDeque<[KeyCode; 8], arraydeque::behavior::Wrapping>, + pub quick_tap_hold_timeout: bool, rpt_multikey_key_buffer: MultiKeyBuffer<'a, T>, } @@ -438,7 +439,7 @@ impl<'a, T: std::fmt::Debug> WaitingState<'a, T> { } else { Some(WaitingAction::Timeout) } - } else if (self.timeout == 0) && (!skip_timeout) { + } else if self.timeout == 0 && (!skip_timeout) { Some(WaitingAction::Timeout) } else { None @@ -881,6 +882,7 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt rpt_action: None, historical_keys: ArrayDeque::new(), rpt_multikey_key_buffer: unsafe { MultiKeyBuffer::new() }, + quick_tap_hold_timeout: false, } } /// Iterates on the key codes of the current state. @@ -1254,7 +1256,11 @@ impl<'a, const C: usize, const R: usize, const L: usize, T: 'a + Copy + std::fmt { let waiting: WaitingState = WaitingState { coord, - timeout: *timeout, + timeout: if self.quick_tap_hold_timeout { + timeout.saturating_sub(delay) + } else { + *timeout + }, delay, ticks: 0, hold, @@ -1873,6 +1879,66 @@ mod test { assert_keys(&[], layout.keycodes()); } + #[test] + fn simultaneous_hold() { + static LAYERS: Layers<3, 1, 1> = [[[ + HoldTap(&HoldTapAction { + timeout: 200, + hold: k(LAlt), + timeout_action: k(LAlt), + tap: k(Space), + config: HoldTapConfig::Default, + tap_hold_interval: 0, + }), + HoldTap(&HoldTapAction { + timeout: 200, + hold: k(RAlt), + timeout_action: k(RAlt), + tap: k(A), + config: HoldTapConfig::Default, + tap_hold_interval: 0, + }), + HoldTap(&HoldTapAction { + timeout: 200, + hold: k(LCtrl), + timeout_action: k(LCtrl), + tap: k(A), + config: HoldTapConfig::Default, + tap_hold_interval: 0, + }), + ]]]; + let mut layout = Layout::new(&LAYERS); + layout.quick_tap_hold_timeout = true; + + // Press and release another key before timeout + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + layout.event(Press(0, 0)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + layout.event(Press(0, 1)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + layout.event(Press(0, 2)); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + + for _ in 0..196 { + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[], layout.keycodes()); + } + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[LAlt], layout.keycodes()); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[LAlt], layout.keycodes()); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[LAlt, RAlt], layout.keycodes()); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[LAlt, RAlt], layout.keycodes()); + assert_eq!(CustomEvent::NoEvent, layout.tick()); + assert_keys(&[LAlt, RAlt, LCtrl], layout.keycodes()); + } + #[test] fn multiple_actions() { static LAYERS: Layers<2, 1, 2> = [ diff --git a/parser/src/cfg/defcfg.rs b/parser/src/cfg/defcfg.rs index 09a02b3e9..ef5d3886d 100644 --- a/parser/src/cfg/defcfg.rs +++ b/parser/src/cfg/defcfg.rs @@ -18,6 +18,7 @@ pub struct CfgOptions { pub movemouse_inherit_accel_state: bool, pub movemouse_smooth_diagonals: bool, pub dynamic_macro_max_presses: u16, + pub concurrent_tap_hold: bool, #[cfg(any(target_os = "linux", target_os = "unknown"))] pub linux_dev: Vec, #[cfg(any(target_os = "linux", target_os = "unknown"))] @@ -56,6 +57,7 @@ impl Default for CfgOptions { movemouse_inherit_accel_state: false, movemouse_smooth_diagonals: false, dynamic_macro_max_presses: 128, + concurrent_tap_hold: false, #[cfg(any(target_os = "linux", target_os = "unknown"))] linux_dev: vec![], #[cfg(any(target_os = "linux", target_os = "unknown"))] @@ -281,6 +283,9 @@ pub fn parse_defcfg(expr: &[SExpr]) -> Result { "movemouse-inherit-accel-state" => { cfg.movemouse_inherit_accel_state = parse_defcfg_val_bool(val, label)? } + "concurrent-tap-hold" => { + cfg.concurrent_tap_hold = parse_defcfg_val_bool(val, label)? + } _ => bail_expr!(key, "Unknown defcfg option {}", label), }; } @@ -380,7 +385,7 @@ pub fn parse_dev(val: &SExpr) -> Result> { let trimmed_path = path.t.trim_matches('"').to_string(); if trimmed_path.is_empty() { bail_span!( - &path, + path, "an empty string is not a valid device name or path" ) } @@ -388,7 +393,7 @@ pub fn parse_dev(val: &SExpr) -> Result> { Ok(acc) } SExpr::List(inner_list) => { - bail_span!(&inner_list, "expected strings, found a list") + bail_span!(inner_list, "expected strings, found a list") } }); diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index 25650b4b8..75c7bb10d 100644 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -254,15 +254,10 @@ fn parse_cfg( )> { let mut s = ParsedState::default(); let (cfg, src, layer_info, klayers, seqs, overrides) = parse_cfg_raw(p, &mut s)?; - Ok(( - cfg, - src, - layer_info, - create_key_outputs(&klayers, &overrides), - create_layout(klayers, s.a), - seqs, - overrides, - )) + let key_outputs = create_key_outputs(&klayers, &overrides); + let mut layout = create_layout(klayers, s.a); + layout.bm().quick_tap_hold_timeout = cfg.concurrent_tap_hold; + Ok((cfg, src, layer_info, key_outputs, layout, seqs, overrides)) } #[cfg(all(not(feature = "interception_driver"), target_os = "windows"))] diff --git a/parser/src/cfg/tests.rs b/parser/src/cfg/tests.rs index f65c11d1a..27e6b0dee 100644 --- a/parser/src/cfg/tests.rs +++ b/parser/src/cfg/tests.rs @@ -1317,6 +1317,7 @@ fn parse_all_defcfg() { movemouse-inherit-accel-state yes movemouse-smooth-diagonals yes dynamic-macro-max-presses 1000 + concurrent-tap-hold yes linux-dev /dev/input/dev1:/dev/input/dev2 linux-dev-names-include "Name 1:Name 2" linux-dev-names-exclude "Name 3:Name 4" From 1632a0eead02f74c4e26d112c034fe00a54384ab Mon Sep 17 00:00:00 2001 From: Martin Mauch Date: Tue, 26 Dec 2023 22:19:51 +0100 Subject: [PATCH 109/819] doc: Mac OS support is not an advantage of kmonad anymore (#679) --- docs/kmonad_comparison.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/kmonad_comparison.md b/docs/kmonad_comparison.md index 39c414952..1017354a9 100644 --- a/docs/kmonad_comparison.md +++ b/docs/kmonad_comparison.md @@ -4,7 +4,7 @@ The kmonad project is the closest alternative for this project. ## Benefits of kmonad over kanata -- MacOS support +- ~MacOS support~ (this is implemented now) - Different features ## Why I built and use kanata From 2fafe6852c70449d2829b55e69b16cd0edb45a66 Mon Sep 17 00:00:00 2001 From: Psych3r Date: Tue, 26 Dec 2023 23:20:44 +0200 Subject: [PATCH 110/819] Add kext support for macOS version 10 (#678) Bump driverkit to v0.1.2 and update the readme. Closes #676 --- Cargo.toml | 2 +- README.md | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8d956a50e..1b4bfe0fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,7 +46,7 @@ kanata-keyberon = { path = "keyberon" } kanata-parser = { path = "parser" } [target.'cfg(target_os = "macos")'.dependencies] -karabiner-driverkit = "0.1.1" +karabiner-driverkit = "0.1.2" [target.'cfg(target_os = "linux")'.dependencies] evdev = "=0.12.0" diff --git a/README.md b/README.md index 567dbba6a..d72ba9b40 100644 --- a/README.md +++ b/README.md @@ -98,12 +98,14 @@ Build and run yourself in Windows. Build and run yourself in macOS: -First, install the [Karabiner VirtualHiDDevice Driver](https://github.com/pqrs-org/Karabiner-DriverKit-VirtualHIDDevice/blob/main/dist/Karabiner-DriverKit-VirtualHIDDevice-3.1.0.pkg). +For macOS version 11 and newer: Install the [Karabiner VirtualHiDDevice Driver](https://github.com/pqrs-org/Karabiner-DriverKit-VirtualHIDDevice/blob/main/dist/Karabiner-DriverKit-VirtualHIDDevice-3.1.0.pkg). To activate it: `/Applications/.Karabiner-VirtualHIDDevice-Manager.app/Contents/MacOS/Karabiner-VirtualHIDDevice-Manager activate` +For macOS version 10 and older: +Install the [Karabiner kernel extension](https://github.com/pqrs-org/Karabiner-VirtualHIDDevice). git clone https://github.com/jtroo/kanata && cd kanata cargo build # --release optional, not really perf sensitive From 90e6173fa9aa183d99221bef15a8dc78f5849f5c Mon Sep 17 00:00:00 2001 From: jtroo Date: Wed, 27 Dec 2023 23:41:57 -0800 Subject: [PATCH 111/819] feat!: conditionally compile tcp server code (#680) The motivation for this commit is to enable savings on binary size if desired. This commit turns the TCP server code into an optional feature. The feature is enabled by default for backwards compatibility. Turning off default features saves 81920 bytes from the stripped binary when compiling with release profile when testing on my system. --- Cargo.lock | 16 ++++++++++++++-- Cargo.toml | 4 +++- src/kanata/mod.rs | 16 +++++++++++++--- src/main.rs | 15 ++++++++++++++- src/tcp_server.rs | 32 +++++++++++++++++++++++++++++--- 5 files changed, 73 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2e25a20f1..c8c0c2951 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -454,11 +454,12 @@ dependencies = [ [[package]] name = "karabiner-driverkit" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dc92890fc2bb14a485e777968e7c18355386a30467cc0bf24f7744c9e44dde5" +checksum = "37c0f0d450e1a3e33739108102ec7e23e56b870b09b367ffb54a82a95682f999" dependencies = [ "cc", + "os_info", ] [[package]] @@ -670,6 +671,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "os_info" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "006e42d5b888366f1880eda20371fedde764ed2213dc8496f49622fa0c99cd5e" +dependencies = [ + "log", + "serde", + "winapi", +] + [[package]] name = "owo-colors" version = "3.5.0" diff --git a/Cargo.toml b/Cargo.toml index 1b4bfe0fd..5e925d264 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,8 +67,10 @@ native-windows-gui = { version = "1.0.12", default_features = false } kanata-interception = { version = "0.2.0", optional = true } [features] -cmd = ["kanata-parser/cmd"] +default = ["tcp_server"] perf_logging = [] +tcp_server = [] +cmd = ["kanata-parser/cmd"] interception_driver = ["kanata-interception", "kanata-parser/interception_driver"] [profile.release] diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index ff56cdc02..3d7d78f27 100644 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -9,8 +9,6 @@ use kanata_keyberon::key_code::*; use kanata_keyberon::layout::*; use std::collections::VecDeque; -use std::io::Write; -use std::net::TcpStream; use std::path::PathBuf; use std::sync::atomic::{AtomicBool, Ordering::SeqCst}; use std::sync::Arc; @@ -1507,6 +1505,7 @@ impl Kanata { Ok(()) } + #[cfg(feature = "tcp_server")] pub fn change_layer(&mut self, layer_name: String) { for (i, l) in self.layer_info.iter().enumerate() { if l.name == layer_name { @@ -1516,6 +1515,7 @@ impl Kanata { } } + #[allow(unused_variables)] /// Prints the layer. If the TCP server is enabled, then this will also send a notification to /// all connected clients. fn check_handle_layer_change(&mut self, tx: &Option>) { @@ -1525,6 +1525,7 @@ impl Kanata { self.prev_layer = cur_layer; self.print_layer(cur_layer); + #[cfg(feature = "tcp_server")] if let Some(tx) = tx { match tx.try_send(ServerMessage::LayerChange { new }) { Ok(_) => {} @@ -1542,10 +1543,12 @@ impl Kanata { } } + #[cfg(feature = "tcp_server")] pub fn start_notification_loop( rx: Receiver, - clients: Arc>>, + clients: crate::tcp_server::Connections, ) { + use std::io::Write; info!("listening for event notifications to relay to connected clients"); std::thread::spawn(move || { loop { @@ -1579,6 +1582,13 @@ impl Kanata { }); } + #[cfg(not(feature = "tcp_server"))] + pub fn start_notification_loop( + _rx: Receiver, + _clients: crate::tcp_server::Connections, + ) { + } + /// Starts a new thread that processes OS key events and advances the keyberon layout's state. pub fn start_processing_loop( kanata: Arc>, diff --git a/src/main.rs b/src/main.rs index c94a85df6..7bab90920 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,6 +19,7 @@ type CfgPath = PathBuf; pub struct ValidatedArgs { paths: Vec, + #[cfg(feature = "tcp_server")] port: Option, #[cfg(target_os = "linux")] symlink_path: Option, @@ -82,6 +83,7 @@ kanata.kbd in the current working directory and /// Port to run the optional TCP server on. If blank, no TCP port will be /// listened on. + #[cfg(feature = "tcp_server")] #[arg(short, long, verbatim_doc_comment)] port: Option, @@ -178,6 +180,7 @@ fn cli_init() -> Result { Ok(ValidatedArgs { paths: cfg_paths, + #[cfg(feature = "tcp_server")] port: args.port, #[cfg(target_os = "linux")] symlink_path: args.symlink_path, @@ -200,7 +203,16 @@ fn main_impl() -> Result<()> { // events, which it sends to the "processing loop". The processing loop handles keyboard events // while also maintaining `tick()` calls to keyberon. - let (server, ntx, nrx) = if let Some(port) = args.port { + let (server, ntx, nrx) = if let Some(port) = { + #[cfg(feature = "tcp_server")] + { + args.port + } + #[cfg(not(feature = "tcp_server"))] + { + None + } + } { let mut server = TcpServer::new(port); server.start(kanata_arc.clone()); let (ntx, nrx) = std::sync::mpsc::sync_channel(100); @@ -213,6 +225,7 @@ fn main_impl() -> Result<()> { Kanata::start_processing_loop(kanata_arc.clone(), rx, ntx, args.nodelay); if let (Some(server), Some(nrx)) = (server, nrx) { + #[allow(clippy::unit_arg)] Kanata::start_notification_loop(nrx, server.connections); } diff --git a/src/tcp_server.rs b/src/tcp_server.rs index 8d81c2446..fe57aa4a9 100644 --- a/src/tcp_server.rs +++ b/src/tcp_server.rs @@ -1,12 +1,15 @@ use crate::Kanata; use parking_lot::Mutex; use serde::{Deserialize, Serialize}; -use std::io::{Read, Write}; -use std::net::{TcpListener, TcpStream}; use std::str::FromStr; use std::sync::Arc; +#[cfg(feature = "tcp_server")] type HashMap = rustc_hash::FxHashMap; +#[cfg(feature = "tcp_server")] +use std::io::{Read, Write}; +#[cfg(feature = "tcp_server")] +use std::net::{TcpListener, TcpStream}; #[derive(Debug, Serialize, Deserialize)] pub enum ServerMessage { @@ -26,6 +29,7 @@ pub enum ClientMessage { ChangeLayer { new: String }, } +#[cfg(feature = "tcp_server")] impl ServerMessage { pub fn as_bytes(&self) -> Vec { serde_json::to_string(self) @@ -43,12 +47,25 @@ impl FromStr for ClientMessage { } } +#[cfg(feature = "tcp_server")] +pub type Connections = Arc>>; + +#[cfg(not(feature = "tcp_server"))] +pub type Connections = (); + +#[cfg(feature = "tcp_server")] pub struct TcpServer { pub port: i32, - pub connections: Arc>>, + pub connections: Connections, +} + +#[cfg(not(feature = "tcp_server"))] +pub struct TcpServer { + pub connections: Connections, } impl TcpServer { + #[cfg(feature = "tcp_server")] pub fn new(port: i32) -> Self { Self { port, @@ -56,6 +73,12 @@ impl TcpServer { } } + #[cfg(not(feature = "tcp_server"))] + pub fn new(_port: i32) -> Self { + Self { connections: () } + } + + #[cfg(feature = "tcp_server")] pub fn start(&mut self, kanata: Arc>) { let listener = TcpListener::bind(format!("0.0.0.0:{}", self.port)).expect("TCP server starts"); @@ -135,4 +158,7 @@ impl TcpServer { } }); } + + #[cfg(not(feature = "tcp_server"))] + pub fn start(&mut self, _kanata: Arc>) {} } From 1c4f585211af587b6c32d99c370f4245154afd0f Mon Sep 17 00:00:00 2001 From: jtroo Date: Thu, 28 Dec 2023 00:06:05 -0800 Subject: [PATCH 112/819] fix(linux): exit on SIGTSTP (#681) This commit adds code to exit on SIGTSTP to avoid hanging onto keyboards while paused. Without this code, pausing kanata can prevent the user from doing anything until they forcefully reboot their system. --- src/oskbd/linux.rs | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/src/oskbd/linux.rs b/src/oskbd/linux.rs index 0faa304d0..d6116bbe7 100644 --- a/src/oskbd/linux.rs +++ b/src/oskbd/linux.rs @@ -6,7 +6,7 @@ use mio::{unix::SourceFd, Events, Interest, Poll, Token}; use nix::ioctl_read_buf; use rustc_hash::FxHashMap as HashMap; use signal_hook::{ - consts::{SIGINT, SIGTERM}, + consts::{SIGINT, SIGTERM, SIGTSTP}, iterator::Signals, }; @@ -314,8 +314,6 @@ pub struct KbdOut { device: uinput::VirtualDevice, accumulated_scroll: u16, accumulated_hscroll: u16, - #[allow(dead_code)] // stored here for persistence+cleanup on exit - symlink: Option, raw_buf: Vec, pub unicode_termination: Cell, pub unicode_u_code: Cell, @@ -361,17 +359,16 @@ impl KbdOut { let symlink = if let Some(symlink_path) = symlink_path { let dest = PathBuf::from(symlink_path); let symlink = Symlink::new(devnode, dest)?; - Symlink::clean_when_killed(symlink.clone()); Some(symlink) } else { None }; + handle_signals(symlink); Ok(KbdOut { device, accumulated_scroll: 0, accumulated_hscroll: 0, - symlink, raw_buf: vec![], // historically was the only option, so make Enter the default @@ -688,23 +685,28 @@ impl Symlink { log::info!("Created symlink {:#?} -> {:#?}", dest, source); Ok(Self { dest }) } +} - fn clean_when_killed(symlink: Self) { - thread::spawn(|| { - let mut signals = Signals::new([SIGINT, SIGTERM]).expect("signals register"); - if let Some(signal) = (&mut signals).into_iter().next() { - match signal { - SIGINT | SIGTERM => { - drop(symlink); - signal_hook::low_level::emulate_default_handler(signal) - .expect("run original sighandlers"); - unreachable!(); - } - _ => unreachable!(), +fn handle_signals(symlink: Option) { + thread::spawn(|| { + let mut signals = Signals::new([SIGINT, SIGTERM, SIGTSTP]).expect("signals register"); + if let Some(signal) = (&mut signals).into_iter().next() { + match signal { + SIGINT | SIGTERM => { + drop(symlink); + signal_hook::low_level::emulate_default_handler(signal) + .expect("run original sighandlers"); + unreachable!(); + } + SIGTSTP => { + drop(symlink); + log::warn!("got SIGTSTP, exiting instead of pausing so keyboards don't hang"); + std::process::exit(SIGTSTP); } + _ => unreachable!(), } - }); - } + } + }); } // Note for allow: the ioctl_read_buf triggers this clippy lint. From b5c1a6c31f882e6767351555661549ded0b25d70 Mon Sep 17 00:00:00 2001 From: KAGEYAM4 <75798544+KAGEYAM4@users.noreply.github.com> Date: Thu, 28 Dec 2023 23:20:43 +0530 Subject: [PATCH 113/819] doc: change kmonad links to org repo (#682) --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d72ba9b40..3405af904 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ the features: To see all of the features, see the [configuration guide](./docs/config.adoc). -The most similar project is [kmonad](https://github.com/david-janssen/kmonad), +The most similar project is [kmonad](https://github.com/kmonad/kmonad), which served as the inspiration for kanata. [Here's a comparison document](./docs/kmonad_comparison.md). You can see a [list of known issues here](./docs/platform-known-issues.adoc). @@ -268,7 +268,7 @@ hardware, instead of having to purchase an enthusiast mechanical keyboard (which are admittedly very nice — I own a few — but can be costly). The best alternative solution that I found for keyboards that don't run QMK was -[kmonad](https://github.com/david-janssen/kmonad). This is an excellent project +[kmonad](https://github.com/kmonad/kmonad). This is an excellent project and I recommend it if you want to try something similar. The reason for this project's existence is that kmonad is written in Haskell @@ -289,7 +289,7 @@ exists. ## Similar Projects -- [kmonad](https://github.com/david-janssen/kmonad): The inspiration for kanata (Linux, Windows, Mac) +- [kmonad](https://github.com/kmonad/kmonad): The inspiration for kanata (Linux, Windows, Mac) - [QMK](https://docs.qmk.fm/#/): Open source keyboard firmware - [keyberon](https://github.com/TeXitoi/keyberon): Rust `#[no_std]` library intended for keyboard firmware - [ktrl](https://github.com/ItayGarin/ktrl): Linux-only keyboard customizer with layers, a TCP server, and audio support From 17d42abe87010134ac041bebc61f83cbf7d55a65 Mon Sep 17 00:00:00 2001 From: jtroo Date: Fri, 29 Dec 2023 03:49:10 -0800 Subject: [PATCH 114/819] refactor: move dynamic macro code to new file (#684) --- src/kanata/dynamic_macro.rs | 217 ++++++++++++++++++++++++++++++++++++ src/kanata/mod.rs | 210 +++++----------------------------- 2 files changed, 245 insertions(+), 182 deletions(-) create mode 100644 src/kanata/dynamic_macro.rs diff --git a/src/kanata/dynamic_macro.rs b/src/kanata/dynamic_macro.rs new file mode 100644 index 000000000..147711182 --- /dev/null +++ b/src/kanata/dynamic_macro.rs @@ -0,0 +1,217 @@ +use std::collections::VecDeque; + +use kanata_keyberon::layout::Event; +use kanata_parser::keys::OsCode; +use rustc_hash::FxHashMap as HashMap; +use rustc_hash::FxHashSet as HashSet; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum DynamicMacroItem { + Press(OsCode), + Release(OsCode), + EndMacro(u16), +} + +pub struct DynamicMacroReplayState { + pub active_macros: HashSet, + pub delay_remaining: u16, + pub macro_items: VecDeque, +} + +pub struct DynamicMacroRecordState { + pub starting_macro_id: u16, + pub macro_items: Vec, +} + +impl DynamicMacroRecordState { + pub fn add_release_for_all_unreleased_presses(&mut self) { + let mut pressed_oscs = HashSet::default(); + for item in self.macro_items.iter() { + match item { + DynamicMacroItem::Press(osc) => pressed_oscs.insert(*osc), + DynamicMacroItem::Release(osc) => pressed_oscs.remove(osc), + DynamicMacroItem::EndMacro(_) => false, + }; + } + // Hopefully release order doesn't matter here since a HashSet is being used + for osc in pressed_oscs.into_iter() { + self.macro_items.push(DynamicMacroItem::Release(osc)); + } + } +} + +pub fn tick_replay_state(state: &mut Option) -> Option { + let mut ret = None; + let mut clear_replaying_macro = false; + if let Some(state) = state { + state.delay_remaining = state.delay_remaining.saturating_sub(1); + if state.delay_remaining == 0 { + match state.macro_items.pop_front() { + None => clear_replaying_macro = true, + Some(i) => match i { + DynamicMacroItem::Press(k) => { + ret = Some(Event::Press(0, k.into())); + } + DynamicMacroItem::Release(k) => { + ret = Some(Event::Release(0, k.into())); + } + DynamicMacroItem::EndMacro(macro_id) => { + state.active_macros.remove(¯o_id); + } + }, + } + state.delay_remaining = 5; + } + } + if clear_replaying_macro { + log::debug!("finished macro replay"); + *state = None; + } + ret +} + +pub fn record_macro( + macro_id: u16, + record_state: &mut Option, +) -> Option<(u16, Vec)> { + let mut stop_record = false; + let mut new_recording = None; + let mut ret = None; + match record_state.take() { + None => { + log::info!("starting dynamic macro {macro_id} recording"); + *record_state = Some(DynamicMacroRecordState { + starting_macro_id: macro_id, + macro_items: vec![], + }) + } + Some(mut state) => { + // remove the last item, since it's almost certainly a "macro + // record" key press action which we don't want to keep. + state.macro_items.remove(state.macro_items.len() - 1); + state.add_release_for_all_unreleased_presses(); + + ret = Some((state.starting_macro_id, state.macro_items)); + if state.starting_macro_id == macro_id { + log::info!( + "same macro id pressed. saving and stopping dynamic macro {} recording", + state.starting_macro_id + ); + stop_record = true; + } else { + log::info!( + "saving dynamic macro {} recording then starting new macro recording {macro_id}", + state.starting_macro_id, + ); + new_recording = Some(macro_id); + } + } + } + if stop_record { + *record_state = None; + } else if let Some(macro_id) = new_recording { + log::info!("starting new dynamic macro {macro_id} recording"); + *record_state = Some(DynamicMacroRecordState { + starting_macro_id: macro_id, + macro_items: vec![], + }); + } + ret +} + +pub fn stop_macro( + record_state: &mut Option, + num_actions_to_remove: u16, +) -> Option<(u16, Vec)> { + let mut ret = None; + if let Some(mut state) = record_state.take() { + // remove the last item independently of `num_actions_to_remove` + // since it's almost certainly a "macro record stop" key press + // action which we don't want to keep. + state.macro_items.remove(state.macro_items.len() - 1); + log::info!( + "saving and stopping dynamic macro {} recording with {num_actions_to_remove} actions at the end removed", + state.starting_macro_id, + ); + state.macro_items.truncate( + state + .macro_items + .len() + .saturating_sub(usize::from(num_actions_to_remove)), + ); + state.add_release_for_all_unreleased_presses(); + ret = Some((state.starting_macro_id, state.macro_items)); + } + *record_state = None; + ret +} + +pub fn play_macro( + macro_id: u16, + replay_state: &mut Option, + recorded_macros: &HashMap>, +) { + match replay_state { + None => { + log::info!("replaying macro {macro_id}"); + *replay_state = recorded_macros.get(¯o_id).map(|macro_items| { + let mut active_macros = HashSet::default(); + active_macros.insert(macro_id); + DynamicMacroReplayState { + active_macros, + delay_remaining: 0, + macro_items: macro_items.clone().into(), + } + }); + } + Some(state) => { + if state.active_macros.contains(¯o_id) { + log::warn!("refusing to recurse into macro {macro_id}"); + } else if let Some(items) = recorded_macros.get(¯o_id) { + log::debug!("prepending macro {macro_id} items to current replay"); + state.active_macros.insert(macro_id); + state + .macro_items + .push_front(DynamicMacroItem::EndMacro(macro_id)); + for item in items.iter().copied().rev() { + state.macro_items.push_front(item); + } + } + } + } +} + +pub fn record_press( + record_state: &mut Option, + osc: OsCode, + max_presses: u16, +) -> Option<(u16, Vec)> { + let mut ret = None; + if let Some(state) = record_state { + // This is not 100% accurate since there may be multiple presses before any of + // their relesease are received. But it's probably good enough in practice. + // + // The presses are defined so that a user cares about the number of keys rather + // than events. So rather than the user multiplying by 2 in their config after + // considering the number of keys they want, kanata does the multiplication + // instead. + if state.macro_items.len() > usize::from(max_presses) * 2 { + log::warn!( + "saving and stopping dynamic macro {} recording due to exceeding limit", + state.starting_macro_id, + ); + state.add_release_for_all_unreleased_presses(); + let state = record_state.take().unwrap(); + ret = Some((state.starting_macro_id, state.macro_items)); + } else { + state.macro_items.push(DynamicMacroItem::Press(osc)); + } + } + ret +} + +pub fn record_release(record_state: &mut Option, osc: OsCode) { + if let Some(state) = record_state { + state.macro_items.push(DynamicMacroItem::Release(osc)); + } +} diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index 3d7d78f27..351ec7287 100644 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -8,7 +8,6 @@ use std::sync::mpsc::{Receiver, SyncSender as Sender, TryRecvError}; use kanata_keyberon::key_code::*; use kanata_keyberon::layout::*; -use std::collections::VecDeque; use std::path::PathBuf; use std::sync::atomic::{AtomicBool, Ordering::SeqCst}; use std::sync::Arc; @@ -22,6 +21,9 @@ use kanata_parser::cfg::*; use kanata_parser::custom_action::*; use kanata_parser::keys::*; +mod dynamic_macro; +use dynamic_macro::*; + #[cfg(feature = "cmd")] mod cmd; #[cfg(feature = "cmd")] @@ -48,13 +50,6 @@ pub use caps_word::*; type HashSet = rustc_hash::FxHashSet; type HashMap = rustc_hash::FxHashMap; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum DynamicMacroItem { - Press(OsCode), - Release(OsCode), - EndMacro(u16), -} - pub struct Kanata { /// Handle to some OS keyboard output mechanism. pub kbd_out: KbdOut, @@ -220,34 +215,6 @@ pub struct SequenceState { pub sequence_timeout: u16, } -pub struct DynamicMacroReplayState { - pub active_macros: HashSet, - pub delay_remaining: u16, - pub macro_items: VecDeque, -} - -pub struct DynamicMacroRecordState { - pub starting_macro_id: u16, - pub macro_items: Vec, -} - -impl DynamicMacroRecordState { - fn add_release_for_all_unreleased_presses(&mut self) { - let mut pressed_oscs = HashSet::default(); - for item in self.macro_items.iter() { - match item { - DynamicMacroItem::Press(osc) => pressed_oscs.insert(*osc), - DynamicMacroItem::Release(osc) => pressed_oscs.remove(osc), - DynamicMacroItem::EndMacro(_) => false, - }; - } - // Hopefully release order doesn't matter here since a HashSet is being used - for osc in pressed_oscs.into_iter() { - self.macro_items.push(DynamicMacroItem::Release(osc)); - } - } -} - use once_cell::sync::Lazy; static MAPPED_KEYS: Lazy> = @@ -394,35 +361,17 @@ impl Kanata { self.ticks_since_idle = 0; let kbrn_ev = match event.value { KeyValue::Press => { - if let Some(state) = &mut self.dynamic_macro_record_state { - // This is not 100% accurate since there may be multiple presses before any of - // their relesease are received. But it's probably good enough in practice. - // - // The presses are defined so that a user cares about the number of keys rather - // than events. So rather than the user multiplying by 2 in their config after - // considering the number of keys they want, kanata does the multiplication - // instead. - if state.macro_items.len() > usize::from(self.dynamic_macro_max_presses) * 2 { - log::warn!( - "saving and stopping dynamic macro {} recording due to exceeding limit", - state.starting_macro_id, - ); - state.add_release_for_all_unreleased_presses(); - self.dynamic_macros - .insert(state.starting_macro_id, state.macro_items.clone()); - self.dynamic_macro_record_state = None; - } else { - state.macro_items.push(DynamicMacroItem::Press(event.code)); - } + if let Some((macro_id, recorded_macro)) = record_press( + &mut self.dynamic_macro_record_state, + event.code, + self.dynamic_macro_max_presses, + ) { + self.dynamic_macros.insert(macro_id, recorded_macro); } Event::Press(0, evc) } KeyValue::Release => { - if let Some(state) = &mut self.dynamic_macro_record_state { - state - .macro_items - .push(DynamicMacroItem::Release(event.code)); - } + record_release(&mut self.dynamic_macro_record_state, event.code); Event::Release(0, evc) } KeyValue::Repeat => { @@ -454,9 +403,12 @@ impl Kanata { self.handle_scrolling()?; self.handle_move_mouse()?; self.tick_sequence_state()?; - self.tick_dynamic_macro_state()?; self.tick_idle_timeout(); + if let Some(event) = tick_replay_state(&mut self.dynamic_macro_replay_state) { + self.layout.bm().event(event); + } + self.prev_keys.clear(); self.prev_keys.append(&mut self.cur_keys); } @@ -638,35 +590,6 @@ impl Kanata { Ok(()) } - fn tick_dynamic_macro_state(&mut self) -> Result<()> { - let mut clear_replaying_macro = false; - if let Some(state) = &mut self.dynamic_macro_replay_state { - state.delay_remaining = state.delay_remaining.saturating_sub(1); - if state.delay_remaining == 0 { - match state.macro_items.pop_front() { - None => clear_replaying_macro = true, - Some(i) => match i { - DynamicMacroItem::Press(k) => { - self.layout.bm().event(Event::Press(0, k.into())) - } - DynamicMacroItem::Release(k) => { - self.layout.bm().event(Event::Release(0, k.into())) - } - DynamicMacroItem::EndMacro(macro_id) => { - state.active_macros.remove(¯o_id); - } - }, - } - state.delay_remaining = 5; - } - } - if clear_replaying_macro { - log::debug!("finished macro replay"); - self.dynamic_macro_replay_state = None; - } - Ok(()) - } - fn tick_idle_timeout(&mut self) { if self.waiting_for_idle.is_empty() { return; @@ -1200,103 +1123,26 @@ impl Kanata { } } CustomAction::DynamicMacroRecord(macro_id) => { - let mut stop_record = false; - let mut new_recording = None; - match &mut self.dynamic_macro_record_state { - None => { - log::info!("starting dynamic macro {macro_id} recording"); - self.dynamic_macro_record_state = - Some(DynamicMacroRecordState { - starting_macro_id: *macro_id, - macro_items: vec![], - }) - } - Some(ref mut state) => { - // remove the last item, since it's almost certainly a "macro - // record" key press action which we don't want to keep. - state.macro_items.remove(state.macro_items.len() - 1); - state.add_release_for_all_unreleased_presses(); - self.dynamic_macros - .insert(state.starting_macro_id, state.macro_items.clone()); - if state.starting_macro_id == *macro_id { - log::info!( - "same macro id pressed. saving and stopping dynamic macro {} recording", - state.starting_macro_id - ); - stop_record = true; - } else { - log::info!( - "saving dynamic macro {} recording then starting new macro recording {macro_id}", - state.starting_macro_id, - ); - new_recording = Some(macro_id); - } - } - } - if stop_record { - self.dynamic_macro_record_state = None; - } else if let Some(macro_id) = new_recording { - log::info!("starting new dynamic macro {macro_id} recording"); - self.dynamic_macro_record_state = Some(DynamicMacroRecordState { - starting_macro_id: *macro_id, - macro_items: vec![], - }); + if let Some((macro_id, prev_recorded_macro)) = + record_macro(*macro_id, &mut self.dynamic_macro_record_state) + { + self.dynamic_macros.insert(macro_id, prev_recorded_macro); } } CustomAction::DynamicMacroRecordStop(num_actions_to_remove) => { - if let Some(state) = &mut self.dynamic_macro_record_state { - // remove the last item independently of `num_actions_to_remove` - // since it's almost certainly a "macro record stop" key press - // action which we don't want to keep. - state.macro_items.remove(state.macro_items.len() - 1); - log::info!( - "saving and stopping dynamic macro {} recording with {num_actions_to_remove} actions at the end removed", - state.starting_macro_id, - ); - state.macro_items.truncate( - state - .macro_items - .len() - .saturating_sub(usize::from(*num_actions_to_remove)), - ); - state.add_release_for_all_unreleased_presses(); - self.dynamic_macros - .insert(state.starting_macro_id, state.macro_items.clone()); + if let Some((macro_id, prev_recorded_macro)) = stop_macro( + &mut self.dynamic_macro_record_state, + *num_actions_to_remove, + ) { + self.dynamic_macros.insert(macro_id, prev_recorded_macro); } - self.dynamic_macro_record_state = None; } CustomAction::DynamicMacroPlay(macro_id) => { - match &mut self.dynamic_macro_replay_state { - None => { - log::info!("replaying macro {macro_id}"); - self.dynamic_macro_replay_state = - self.dynamic_macros.get(macro_id).map(|macro_items| { - let mut active_macros = HashSet::default(); - active_macros.insert(*macro_id); - DynamicMacroReplayState { - active_macros, - delay_remaining: 0, - macro_items: macro_items.clone().into(), - } - }); - } - Some(state) => { - if state.active_macros.contains(macro_id) { - log::warn!("refusing to recurse into macro {macro_id}"); - } else if let Some(items) = self.dynamic_macros.get(macro_id) { - log::debug!( - "prepending macro {macro_id} items to current replay" - ); - state.active_macros.insert(*macro_id); - state - .macro_items - .push_front(DynamicMacroItem::EndMacro(*macro_id)); - for item in items.iter().copied().rev() { - state.macro_items.push_front(item); - } - } - } - } + play_macro( + *macro_id, + &mut self.dynamic_macro_replay_state, + &self.dynamic_macros, + ); } CustomAction::SendArbitraryCode(code) => { self.kbd_out.write_code(*code as u32, KeyValue::Press)?; From ff7c9c84f54df633749eb17d221192abfab52bc1 Mon Sep 17 00:00:00 2001 From: Jan Tache Date: Fri, 29 Dec 2023 04:00:34 -0800 Subject: [PATCH 115/819] refactor: remove mutable return --- src/kanata/dynamic_macro.rs | 72 ++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 41 deletions(-) diff --git a/src/kanata/dynamic_macro.rs b/src/kanata/dynamic_macro.rs index 147711182..367dd1eae 100644 --- a/src/kanata/dynamic_macro.rs +++ b/src/kanata/dynamic_macro.rs @@ -40,50 +40,46 @@ impl DynamicMacroRecordState { } } -pub fn tick_replay_state(state: &mut Option) -> Option { - let mut ret = None; - let mut clear_replaying_macro = false; - if let Some(state) = state { +pub fn tick_replay_state(record_state: &mut Option) -> Option { + if let Some(state) = record_state { state.delay_remaining = state.delay_remaining.saturating_sub(1); if state.delay_remaining == 0 { + state.delay_remaining = 5; match state.macro_items.pop_front() { - None => clear_replaying_macro = true, + None => { + *record_state = None; + log::debug!("finished macro replay"); + None + } Some(i) => match i { - DynamicMacroItem::Press(k) => { - ret = Some(Event::Press(0, k.into())); - } - DynamicMacroItem::Release(k) => { - ret = Some(Event::Release(0, k.into())); - } + DynamicMacroItem::Press(k) => Some(Event::Press(0, k.into())), + DynamicMacroItem::Release(k) => Some(Event::Release(0, k.into())), DynamicMacroItem::EndMacro(macro_id) => { state.active_macros.remove(¯o_id); + None } }, } - state.delay_remaining = 5; + } else { + None } + } else { + None } - if clear_replaying_macro { - log::debug!("finished macro replay"); - *state = None; - } - ret } pub fn record_macro( macro_id: u16, record_state: &mut Option, ) -> Option<(u16, Vec)> { - let mut stop_record = false; - let mut new_recording = None; - let mut ret = None; match record_state.take() { None => { log::info!("starting dynamic macro {macro_id} recording"); *record_state = Some(DynamicMacroRecordState { starting_macro_id: macro_id, macro_items: vec![], - }) + }); + None } Some(mut state) => { // remove the last item, since it's almost certainly a "macro @@ -91,39 +87,31 @@ pub fn record_macro( state.macro_items.remove(state.macro_items.len() - 1); state.add_release_for_all_unreleased_presses(); - ret = Some((state.starting_macro_id, state.macro_items)); if state.starting_macro_id == macro_id { log::info!( "same macro id pressed. saving and stopping dynamic macro {} recording", state.starting_macro_id ); - stop_record = true; + *record_state = None; } else { log::info!( "saving dynamic macro {} recording then starting new macro recording {macro_id}", state.starting_macro_id, ); - new_recording = Some(macro_id); + *record_state = Some(DynamicMacroRecordState { + starting_macro_id: macro_id, + macro_items: vec![], + }); } + Some((state.starting_macro_id, state.macro_items)) } } - if stop_record { - *record_state = None; - } else if let Some(macro_id) = new_recording { - log::info!("starting new dynamic macro {macro_id} recording"); - *record_state = Some(DynamicMacroRecordState { - starting_macro_id: macro_id, - macro_items: vec![], - }); - } - ret } pub fn stop_macro( record_state: &mut Option, num_actions_to_remove: u16, ) -> Option<(u16, Vec)> { - let mut ret = None; if let Some(mut state) = record_state.take() { // remove the last item independently of `num_actions_to_remove` // since it's almost certainly a "macro record stop" key press @@ -140,10 +128,11 @@ pub fn stop_macro( .saturating_sub(usize::from(num_actions_to_remove)), ); state.add_release_for_all_unreleased_presses(); - ret = Some((state.starting_macro_id, state.macro_items)); + Some((state.starting_macro_id, state.macro_items)) + } else { + *record_state = None; + None } - *record_state = None; - ret } pub fn play_macro( @@ -186,7 +175,6 @@ pub fn record_press( osc: OsCode, max_presses: u16, ) -> Option<(u16, Vec)> { - let mut ret = None; if let Some(state) = record_state { // This is not 100% accurate since there may be multiple presses before any of // their relesease are received. But it's probably good enough in practice. @@ -202,12 +190,14 @@ pub fn record_press( ); state.add_release_for_all_unreleased_presses(); let state = record_state.take().unwrap(); - ret = Some((state.starting_macro_id, state.macro_items)); + Some((state.starting_macro_id, state.macro_items)) } else { state.macro_items.push(DynamicMacroItem::Press(osc)); + None } + } else { + None } - ret } pub fn record_release(record_state: &mut Option, osc: OsCode) { From f132b0fd905961395a87f71c5eb197dea5957789 Mon Sep 17 00:00:00 2001 From: Jan Tache Date: Mon, 1 Jan 2024 20:15:02 -0800 Subject: [PATCH 116/819] chore: fix clippy warnings --- parser/src/keys/mod.rs | 1 - src/kanata/mod.rs | 2 -- 2 files changed, 3 deletions(-) diff --git a/parser/src/keys/mod.rs b/parser/src/keys/mod.rs index 97f66986e..f0e68c0b4 100644 --- a/parser/src/keys/mod.rs +++ b/parser/src/keys/mod.rs @@ -15,7 +15,6 @@ mod windows; pub use macos::PageCode; mod mappings; -pub use mappings::*; #[cfg(target_os = "unknown")] #[derive(Clone, Copy)] diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index 351ec7287..ebd302182 100644 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -36,8 +36,6 @@ pub use windows::*; #[cfg(target_os = "linux")] mod linux; -#[cfg(target_os = "linux")] -pub use linux::*; #[cfg(target_os = "macos")] mod macos; From 080f1f9bd56466324bd7b4a6c3db88f735e22f55 Mon Sep 17 00:00:00 2001 From: jtroo Date: Mon, 1 Jan 2024 20:17:15 -0800 Subject: [PATCH 117/819] chore: fix more clippy warnings --- src/kanata/windows/mod.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/kanata/windows/mod.rs b/src/kanata/windows/mod.rs index 609ecc72b..9deb70bb4 100644 --- a/src/kanata/windows/mod.rs +++ b/src/kanata/windows/mod.rs @@ -6,13 +6,9 @@ use crate::kanata::*; #[cfg(not(feature = "interception_driver"))] mod llhook; -#[cfg(not(feature = "interception_driver"))] -pub use llhook::*; #[cfg(feature = "interception_driver")] mod interception; -#[cfg(feature = "interception_driver")] -pub use self::interception::*; static PRESSED_KEYS: Lazy>> = Lazy::new(|| Mutex::new(HashSet::default())); From 916a3cfb91163a10ecb05f24c50df5e0c22bf0d4 Mon Sep 17 00:00:00 2001 From: jtroo Date: Tue, 2 Jan 2024 21:19:06 -0800 Subject: [PATCH 118/819] feat: add grave and right overridable key names --- parser/src/keys/mod.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/parser/src/keys/mod.rs b/parser/src/keys/mod.rs index f0e68c0b4..0750e8b72 100644 --- a/parser/src/keys/mod.rs +++ b/parser/src/keys/mod.rs @@ -99,7 +99,7 @@ pub fn clear_custom_str_oscode_mapping() { /// be useful to remap via `defcustomkeys`, then it should be moved into here. This is so that the /// key name can be remapped while also working for older configurations that already use it. fn add_default_str_osc_mappings(mapping: &mut HashMap) { - let default_mappings = [ + const DEFAULT_MAPPINGS: &[(&str, OsCode)] = &[ ("+", OsCode::KEY_KPPLUS), ("[", OsCode::KEY_LEFTBRACE), ("]", OsCode::KEY_RIGHTBRACE), @@ -118,8 +118,10 @@ fn add_default_str_osc_mappings(mapping: &mut HashMap) { ("yen", OsCode::KEY_BACKSLASH), // Unicode yen is probably the yen key, so map this to a separate oscode by default. ("¥", OsCode::KEY_YEN), + ("right", OsCode::KEY_RIGHT), + ("grave", OsCode::KEY_GRAVE), ]; - for dm in default_mappings { + for dm in DEFAULT_MAPPINGS { mapping.entry(dm.0.into()).or_insert(dm.1); } } From 4f47ef34b6b616fe3c79035389723f1a11190175 Mon Sep 17 00:00:00 2001 From: jtroo Date: Wed, 3 Jan 2024 23:02:50 -0800 Subject: [PATCH 119/819] refactor: remove specified array size for action names --- parser/src/cfg/list_actions.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parser/src/cfg/list_actions.rs b/parser/src/cfg/list_actions.rs index 23072cb42..57d463730 100644 --- a/parser/src/cfg/list_actions.rs +++ b/parser/src/cfg/list_actions.rs @@ -62,7 +62,7 @@ pub const UNMOD: &str = "unmod"; pub const UNSHIFT: &str = "unshift"; pub fn is_list_action(ac: &str) -> bool { - const LIST_ACTIONS: [&str; 58] = [ + const LIST_ACTIONS: &[&str] = &[ LAYER_SWITCH, LAYER_TOGGLE, LAYER_WHILE_HELD, From e5395c94fa7a545242411c444d01c8c16c1c6346 Mon Sep 17 00:00:00 2001 From: jtroo Date: Wed, 3 Jan 2024 23:11:32 -0800 Subject: [PATCH 120/819] feat!(dynamic-macro): record and simulate delays by default (#685) This is a breaking change in behaviour of dynamic macros. I'm not sure that the anyone would really want the old behaviour though. It seems odd that someone might be dependent on it. Kanata defaults to the new behaviour but can be reverted if desired. I may choose to remove the `defcfg` option though, and **only** have the new behaviour. For now the option is left undocumented, so that I can argue that it is not a breaking change if I choose to remove the option in the future, since the option was never documented ;) --- parser/src/cfg/defcfg.rs | 28 +++++ src/kanata/dynamic_macro.rs | 234 ++++++++++++++++++++++++++---------- src/kanata/mod.rs | 57 +++++++-- 3 files changed, 244 insertions(+), 75 deletions(-) diff --git a/parser/src/cfg/defcfg.rs b/parser/src/cfg/defcfg.rs index ef5d3886d..ccde86065 100644 --- a/parser/src/cfg/defcfg.rs +++ b/parser/src/cfg/defcfg.rs @@ -18,6 +18,7 @@ pub struct CfgOptions { pub movemouse_inherit_accel_state: bool, pub movemouse_smooth_diagonals: bool, pub dynamic_macro_max_presses: u16, + pub dynamic_macro_replay_delay_behaviour: ReplayDelayBehaviour, pub concurrent_tap_hold: bool, #[cfg(any(target_os = "linux", target_os = "unknown"))] pub linux_dev: Vec, @@ -57,6 +58,7 @@ impl Default for CfgOptions { movemouse_inherit_accel_state: false, movemouse_smooth_diagonals: false, dynamic_macro_max_presses: 128, + dynamic_macro_replay_delay_behaviour: ReplayDelayBehaviour::Recorded, concurrent_tap_hold: false, #[cfg(any(target_os = "linux", target_os = "unknown"))] linux_dev: vec![], @@ -120,6 +122,21 @@ pub fn parse_defcfg(expr: &[SExpr]) -> Result { "dynamic-macro-max-presses" => { cfg.dynamic_macro_max_presses = parse_cfg_val_u16(val, label, false)?; } + "dynamic-macro-replay-delay-behaviour" => { + cfg.dynamic_macro_replay_delay_behaviour = val + .atom(None) + .map(|v| match v { + "constant" => Ok(ReplayDelayBehaviour::Constant), + "recorded" => Ok(ReplayDelayBehaviour::Recorded), + _ => bail_expr!( + val, + "this option must be one of: constant | recorded" + ), + }) + .ok_or_else(|| { + anyhow_expr!(val, "this option must be one of: constant | recorded") + })??; + } "linux-dev" => { #[cfg(any(target_os = "linux", target_os = "unknown"))] { @@ -445,3 +462,14 @@ impl Default for AltGrBehaviour { target_os = "unknown" ))] pub const HWID_ARR_SZ: usize = 128; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ReplayDelayBehaviour { + /// Always use a fixed number of ticks between presses and releases. + /// This is the original kanata behaviour. + /// This means that held action activations like in tap-hold do not behave as intended. + Constant, + /// Use the recorded number of ticks between presses and releases. + /// This is newer behaviour. + Recorded, +} diff --git a/src/kanata/dynamic_macro.rs b/src/kanata/dynamic_macro.rs index 367dd1eae..cfd237c73 100644 --- a/src/kanata/dynamic_macro.rs +++ b/src/kanata/dynamic_macro.rs @@ -1,59 +1,147 @@ use std::collections::VecDeque; use kanata_keyberon::layout::Event; +use kanata_parser::cfg::ReplayDelayBehaviour; use kanata_parser::keys::OsCode; use rustc_hash::FxHashMap as HashMap; use rustc_hash::FxHashSet as HashSet; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum DynamicMacroItem { - Press(OsCode), - Release(OsCode), + Press((OsCode, u16)), + Release((OsCode, u16)), EndMacro(u16), } pub struct DynamicMacroReplayState { - pub active_macros: HashSet, - pub delay_remaining: u16, - pub macro_items: VecDeque, + active_macros: HashSet, + delay_remaining: u16, + macro_items: VecDeque, } pub struct DynamicMacroRecordState { - pub starting_macro_id: u16, - pub macro_items: Vec, + starting_macro_id: u16, + waiting_event: Option<(OsCode, WaitingEventType)>, + macro_items: Vec, + current_delay: u16, +} + +enum WaitingEventType { + Press, + Release, } impl DynamicMacroRecordState { - pub fn add_release_for_all_unreleased_presses(&mut self) { + fn new(macro_id: u16) -> Self { + Self { + starting_macro_id: macro_id, + waiting_event: None, + macro_items: vec![], + current_delay: 0, + } + } + + fn add_release_for_all_unreleased_presses(&mut self) { let mut pressed_oscs = HashSet::default(); for item in self.macro_items.iter() { match item { - DynamicMacroItem::Press(osc) => pressed_oscs.insert(*osc), - DynamicMacroItem::Release(osc) => pressed_oscs.remove(osc), - DynamicMacroItem::EndMacro(_) => false, + DynamicMacroItem::Press((osc, _)) => { + pressed_oscs.insert(*osc); + } + DynamicMacroItem::Release((osc, _)) => { + pressed_oscs.remove(osc); + } + DynamicMacroItem::EndMacro(_) => {} }; } - // Hopefully release order doesn't matter here since a HashSet is being used + // Hopefully release order doesn't matter here. A HashSet is being used, meaning release order is arbitrary. for osc in pressed_oscs.into_iter() { - self.macro_items.push(DynamicMacroItem::Release(osc)); + self.macro_items.push(DynamicMacroItem::Release((osc, 0))); + } + } + + fn add_event(&mut self, osc: OsCode, evtype: WaitingEventType) { + if let Some(pending_event) = self.waiting_event.take() { + match pending_event.1 { + WaitingEventType::Press => self.macro_items.push(DynamicMacroItem::Press(( + pending_event.0, + self.current_delay, + ))), + WaitingEventType::Release => self.macro_items.push(DynamicMacroItem::Release(( + pending_event.0, + self.current_delay, + ))), + }; } + self.current_delay = 0; + self.waiting_event = Some((osc, evtype)); + } +} + +/// A replay event for a dynamically recorded macro. +/// Note that the key event and the subsequent delay must be processed together. +/// Otherwise there will be real-world time gap between event and the delay, +/// which results in an inaccurate simulation of the keyberon state machine. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct ReplayEvent(Event, u16); + +impl ReplayEvent { + pub fn key_event(self) -> Event { + self.0 + } + pub fn delay(self) -> u16 { + self.1 } } -pub fn tick_replay_state(record_state: &mut Option) -> Option { +pub fn tick_record_state(record_state: &mut Option) { if let Some(state) = record_state { + state.current_delay = state.current_delay.saturating_add(1); + } +} + +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct ReplayBehaviour { + pub delay: ReplayDelayBehaviour, +} + +pub fn tick_replay_state( + replay_state: &mut Option, + replay_behaviour: ReplayBehaviour, +) -> Option { + if let Some(state) = replay_state { state.delay_remaining = state.delay_remaining.saturating_sub(1); if state.delay_remaining == 0 { state.delay_remaining = 5; match state.macro_items.pop_front() { None => { - *record_state = None; + *replay_state = None; log::debug!("finished macro replay"); None } Some(i) => match i { - DynamicMacroItem::Press(k) => Some(Event::Press(0, k.into())), - DynamicMacroItem::Release(k) => Some(Event::Release(0, k.into())), + DynamicMacroItem::Press((key, delay)) => { + let event = Event::Press(0, key.into()); + let delay = match replay_behaviour.delay { + ReplayDelayBehaviour::Constant => 0, + ReplayDelayBehaviour::Recorded => { + state.delay_remaining = delay; + delay + } + }; + Some(ReplayEvent(event, delay)) + } + DynamicMacroItem::Release((key, delay)) => { + let event = Event::Release(0, key.into()); + let delay = match replay_behaviour.delay { + ReplayDelayBehaviour::Constant => 0, + ReplayDelayBehaviour::Recorded => { + state.delay_remaining = delay; + delay + } + }; + Some(ReplayEvent(event, delay)) + } DynamicMacroItem::EndMacro(macro_id) => { state.active_macros.remove(¯o_id); None @@ -68,20 +156,28 @@ pub fn tick_replay_state(record_state: &mut Option) -> } } -pub fn record_macro( +pub fn begin_record_macro( macro_id: u16, record_state: &mut Option, ) -> Option<(u16, Vec)> { match record_state.take() { None => { log::info!("starting dynamic macro {macro_id} recording"); - *record_state = Some(DynamicMacroRecordState { - starting_macro_id: macro_id, - macro_items: vec![], - }); + *record_state = Some(DynamicMacroRecordState::new(macro_id)); None } Some(mut state) => { + if let Some(pending_event) = state.waiting_event.take() { + match pending_event.1 { + WaitingEventType::Press => state.macro_items.push(DynamicMacroItem::Press(( + pending_event.0, + state.current_delay, + ))), + WaitingEventType::Release => state.macro_items.push(DynamicMacroItem::Release( + (pending_event.0, state.current_delay), + )), + }; + } // remove the last item, since it's almost certainly a "macro // record" key press action which we don't want to keep. state.macro_items.remove(state.macro_items.len() - 1); @@ -98,21 +194,68 @@ pub fn record_macro( "saving dynamic macro {} recording then starting new macro recording {macro_id}", state.starting_macro_id, ); - *record_state = Some(DynamicMacroRecordState { - starting_macro_id: macro_id, - macro_items: vec![], - }); + *record_state = Some(DynamicMacroRecordState::new(macro_id)); } Some((state.starting_macro_id, state.macro_items)) } } } +pub fn record_press( + record_state: &mut Option, + osc: OsCode, + max_presses: u16, +) -> Option<(u16, Vec)> { + if let Some(state) = record_state { + // This is not 100% accurate since there may be multiple presses before any of + // their relesease are received. But it's probably good enough in practice. + // + // The presses are defined so that a user cares about the number of keys rather + // than events. So rather than the user multiplying by 2 in their config after + // considering the number of keys they want, kanata does the multiplication + // instead. + if state.macro_items.len() > usize::from(max_presses) * 2 { + log::warn!( + "saving and stopping dynamic macro {} recording due to exceeding limit", + state.starting_macro_id, + ); + state.add_release_for_all_unreleased_presses(); + let state = record_state.take().unwrap(); + Some((state.starting_macro_id, state.macro_items)) + } else { + log::debug!("delay to press: {}", state.current_delay); + state.add_event(osc, WaitingEventType::Press); + None + } + } else { + None + } +} + +pub fn record_release(record_state: &mut Option, osc: OsCode) { + if let Some(state) = record_state { + log::debug!("delay to release: {}", state.current_delay); + state.add_event(osc, WaitingEventType::Release); + } +} + pub fn stop_macro( record_state: &mut Option, num_actions_to_remove: u16, ) -> Option<(u16, Vec)> { if let Some(mut state) = record_state.take() { + if let Some(pending_event) = state.waiting_event.take() { + match pending_event.1 { + WaitingEventType::Press => state.macro_items.push(DynamicMacroItem::Press(( + pending_event.0, + state.current_delay, + ))), + WaitingEventType::Release => state.macro_items.push(DynamicMacroItem::Release(( + pending_event.0, + state.current_delay, + ))), + }; + } // remove the last item independently of `num_actions_to_remove` // since it's almost certainly a "macro record stop" key press // action which we don't want to keep. @@ -130,7 +273,6 @@ pub fn stop_macro( state.add_release_for_all_unreleased_presses(); Some((state.starting_macro_id, state.macro_items)) } else { - *record_state = None; None } } @@ -146,6 +288,7 @@ pub fn play_macro( *replay_state = recorded_macros.get(¯o_id).map(|macro_items| { let mut active_macros = HashSet::default(); active_macros.insert(macro_id); + log::debug!("playing macro {macro_items:?}"); DynamicMacroReplayState { active_macros, delay_remaining: 0, @@ -158,6 +301,7 @@ pub fn play_macro( log::warn!("refusing to recurse into macro {macro_id}"); } else if let Some(items) = recorded_macros.get(¯o_id) { log::debug!("prepending macro {macro_id} items to current replay"); + log::debug!("playing macro {items:?}"); state.active_macros.insert(macro_id); state .macro_items @@ -169,39 +313,3 @@ pub fn play_macro( } } } - -pub fn record_press( - record_state: &mut Option, - osc: OsCode, - max_presses: u16, -) -> Option<(u16, Vec)> { - if let Some(state) = record_state { - // This is not 100% accurate since there may be multiple presses before any of - // their relesease are received. But it's probably good enough in practice. - // - // The presses are defined so that a user cares about the number of keys rather - // than events. So rather than the user multiplying by 2 in their config after - // considering the number of keys they want, kanata does the multiplication - // instead. - if state.macro_items.len() > usize::from(max_presses) * 2 { - log::warn!( - "saving and stopping dynamic macro {} recording due to exceeding limit", - state.starting_macro_id, - ); - state.add_release_for_all_unreleased_presses(); - let state = record_state.take().unwrap(); - Some((state.starting_macro_id, state.macro_items)) - } else { - state.macro_items.push(DynamicMacroItem::Press(osc)); - None - } - } else { - None - } -} - -pub fn record_release(record_state: &mut Option, osc: OsCode) { - if let Some(state) = record_state { - state.macro_items.push(DynamicMacroItem::Release(osc)); - } -} diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index ebd302182..8ce253157 100644 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -153,6 +153,8 @@ pub struct Kanata { /// Configured maximum for dynamic macro recording, to protect users from themselves if they /// have accidentally left it on. dynamic_macro_max_presses: u16, + /// Determines behaviour of replayed dynamic macros. + dynamic_macro_replay_behaviour: ReplayBehaviour, /// Keys that should be unmodded. If non-empty, any modifier should be cleared. unmodded_keys: Vec, /// Keys that should be unshifted. If non-empty, left+right shift keys should be cleared. @@ -307,6 +309,9 @@ impl Kanata { movemouse_smooth_diagonals: cfg.items.movemouse_smooth_diagonals, movemouse_inherit_accel_state: cfg.items.movemouse_inherit_accel_state, dynamic_macro_max_presses: cfg.items.dynamic_macro_max_presses, + dynamic_macro_replay_behaviour: ReplayBehaviour { + delay: cfg.items.dynamic_macro_replay_delay_behaviour, + }, #[cfg(target_os = "linux")] x11_repeat_rate: cfg.items.linux_x11_repeat_delay_rate, waiting_for_idle: HashSet::default(), @@ -344,6 +349,9 @@ impl Kanata { self.movemouse_smooth_diagonals = cfg.items.movemouse_smooth_diagonals; self.movemouse_inherit_accel_state = cfg.items.movemouse_inherit_accel_state; self.dynamic_macro_max_presses = cfg.items.dynamic_macro_max_presses; + self.dynamic_macro_replay_behaviour = ReplayBehaviour { + delay: cfg.items.dynamic_macro_replay_delay_behaviour, + }; *MAPPED_KEYS.lock() = cfg.mapped_keys; #[cfg(target_os = "linux")] @@ -396,22 +404,33 @@ impl Kanata { let ms_elapsed = ns_elapsed_with_rem / NS_IN_MS; self.time_remainder = ns_elapsed_with_rem % NS_IN_MS; + let mut extra_ticks: u16 = 0; for _ in 0..ms_elapsed { - self.live_reload_requested |= self.handle_keystate_changes()?; - self.handle_scrolling()?; - self.handle_move_mouse()?; - self.tick_sequence_state()?; - self.tick_idle_timeout(); - - if let Some(event) = tick_replay_state(&mut self.dynamic_macro_replay_state) { - self.layout.bm().event(event); + self.tick_states()?; + if let Some(event) = tick_replay_state( + &mut self.dynamic_macro_replay_state, + self.dynamic_macro_replay_behaviour, + ) { + self.layout.bm().event(event.key_event()); + extra_ticks = extra_ticks.saturating_add(event.delay()); + log::debug!("dyn macro extra ticks: {extra_ticks}, ms_elapsed: {ms_elapsed}"); } - - self.prev_keys.clear(); - self.prev_keys.append(&mut self.cur_keys); } if ms_elapsed > 0 { + for i in 0..(extra_ticks.saturating_sub(ms_elapsed as u16)) { + self.tick_states()?; + if tick_replay_state( + &mut self.dynamic_macro_replay_state, + self.dynamic_macro_replay_behaviour, + ) + .is_some() + { + log::error!("overshot to next event at iteration #{i}, the code is broken!"); + break; + } + } + self.last_tick = match ms_elapsed { 0..=10 => now, // If too many ms elapsed, probably doing a tight loop of something that's quite @@ -456,6 +475,18 @@ impl Kanata { Ok(ms_elapsed as u16) } + fn tick_states(&mut self) -> Result<()> { + self.live_reload_requested |= self.handle_keystate_changes()?; + self.handle_scrolling()?; + self.handle_move_mouse()?; + self.tick_sequence_state()?; + self.tick_idle_timeout(); + tick_record_state(&mut self.dynamic_macro_record_state); + self.prev_keys.clear(); + self.prev_keys.append(&mut self.cur_keys); + Ok(()) + } + fn handle_scrolling(&mut self) -> Result<()> { if let Some(scroll_state) = &mut self.scroll_state { if scroll_state.ticks_until_scroll == 0 { @@ -1122,8 +1153,9 @@ impl Kanata { } CustomAction::DynamicMacroRecord(macro_id) => { if let Some((macro_id, prev_recorded_macro)) = - record_macro(*macro_id, &mut self.dynamic_macro_record_state) + begin_record_macro(*macro_id, &mut self.dynamic_macro_record_state) { + log::debug!("saving macro {prev_recorded_macro:?}"); self.dynamic_macros.insert(macro_id, prev_recorded_macro); } } @@ -1132,6 +1164,7 @@ impl Kanata { &mut self.dynamic_macro_record_state, *num_actions_to_remove, ) { + log::debug!("saving macro {prev_recorded_macro:?}"); self.dynamic_macros.insert(macro_id, prev_recorded_macro); } } From 7650270c4c37a41dccf7e8ede33ddb0fbdaeec83 Mon Sep 17 00:00:00 2001 From: rszyma Date: Fri, 5 Jan 2024 07:23:04 +0100 Subject: [PATCH 121/819] feat: add --check flag (#693) Add `--check` flag that validates the config file then exits. Additionally fixed bug with `list` argument showing up despite of binary not being compiled for macOS. --- parser/src/cfg/mod.rs | 2 +- src/kanata/mod.rs | 6 ++++++ src/main.rs | 24 +++++++++++++++++++----- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index 75c7bb10d..04aa7e04e 100644 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -216,7 +216,7 @@ pub struct Cfg { /// Parse a new configuration from a file. pub fn new_from_file(p: &Path) -> MResult { let (items, mapped_keys, layer_info, key_outputs, layout, sequences, overrides) = parse_cfg(p)?; - log::info!("config parsed"); + log::info!("config file is valid"); Ok(Cfg { items, mapped_keys, diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index 8ce253157..22c983948 100644 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -1491,6 +1491,12 @@ impl Kanata { let mut ms_elapsed = 0; info!("Starting kanata proper"); + + info!( + "You may forcefully exit kanata by pressing lctl+spc+esc at any time. \ + These keys refer to defsrc input, meaning BEFORE kanata remaps keys." + ); + let err = loop { let can_block = { let mut k = kanata.lock(); diff --git a/src/main.rs b/src/main.rs index 7bab90920..f9a4460f0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ use anyhow::{bail, Result}; use clap::Parser; +use kanata_parser::cfg; use log::info; use simplelog::*; @@ -93,7 +94,8 @@ kanata.kbd in the current working directory and #[arg(short, long, verbatim_doc_comment)] symlink_path: Option, - #[cfg_attr(target_os = "macos", doc = "List the keyboards available for grabbing")] + /// List the keyboards available for grabbing and exit. + #[cfg(target_os = "macos")] #[arg(short, long)] list: bool, @@ -118,6 +120,10 @@ kanata.kbd in the current working directory and #[cfg(target_os = "linux")] #[arg(short, long, verbatim_doc_comment)] wait_device_ms: Option, + + /// Validate configuration file and exit + #[arg(long, verbatim_doc_comment)] + check: bool, } /// Parse CLI arguments and initialize logging. @@ -155,10 +161,6 @@ fn cli_init() -> Result { log::info!("using LLHOOK+SendInput for keyboard IO"); #[cfg(all(feature = "interception_driver", target_os = "windows"))] log::info!("using the Interception driver for keyboard IO"); - log::info!( - "You may forcefully exit kanata by pressing lctl+spc+esc at any time. \ - These keys refer to defsrc input, meaning BEFORE kanata remaps keys." - ); if let Some(config_file) = cfg_paths.first() { if !config_file.exists() { @@ -171,6 +173,18 @@ fn cli_init() -> Result { bail!("No config files provided\nFor more info, pass the `-h` or `--help` flags."); } + if args.check { + log::info!("validating config only and exiting"); + let status = match cfg::new_from_file(&cfg_paths[0]) { + Ok(_) => 0, + Err(e) => { + log::error!("{e:?}"); + 1 + } + }; + std::process::exit(status); + } + #[cfg(target_os = "linux")] if let Some(wait) = args.wait_device_ms { use std::sync::atomic::Ordering; From bf6a5017a4d86643dd32f65fc4af83654921d077 Mon Sep 17 00:00:00 2001 From: Pheon Dev <93865776+Pheon-Dev@users.noreply.github.com> Date: Mon, 8 Jan 2024 02:57:35 +0300 Subject: [PATCH 122/819] doc: update linux setup (#692) --- docs/avoid-sudo-linux.md | 38 ------------------ docs/dropping-root.md | 27 ------------- docs/setup-linux.md | 87 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 65 deletions(-) delete mode 100644 docs/avoid-sudo-linux.md delete mode 100644 docs/dropping-root.md create mode 100644 docs/setup-linux.md diff --git a/docs/avoid-sudo-linux.md b/docs/avoid-sudo-linux.md deleted file mode 100644 index 63dd41205..000000000 --- a/docs/avoid-sudo-linux.md +++ /dev/null @@ -1,38 +0,0 @@ -# Instructions - -In Linux, kanata needs to be able to access the input and uinput subsystem to inject events. To do this, your user needs to have permissions. Follow the steps in this page to obtain user permissions. - -### 1. If the uinput group does not exist, create a new group - -```bash -sudo groupadd uinput -``` - -### 2. Add your user to the input and the uinput group - -```bash -sudo usermod -aG input $USER -sudo usermod -aG uinput $USER -``` - -Make sure that it's effective by running `groups`. You might have to logout and login. - -### 3. Make sure the uinput device file has the right permissions. - -Add a udev rule (in either `/etc/udev/rules.d` or `/lib/udev/rules.d`) with the following content: - -``` -KERNEL=="uinput", MODE="0660", GROUP="uinput", OPTIONS+="static_node=uinput" -``` - -### 4. Make sure the uinput drivers are loaded - -You may need to run this command whenever you start kanata for the first time: - -``` -sudo modprobe uinput -``` - -# Credits - -The original text was taken and adapted from: https://github.com/kmonad/kmonad/blob/master/doc/faq.md#linux diff --git a/docs/dropping-root.md b/docs/dropping-root.md deleted file mode 100644 index 4517d629c..000000000 --- a/docs/dropping-root.md +++ /dev/null @@ -1,27 +0,0 @@ -Create a user for kanata and add it to the input groups - -``` -sudo useradd -r -s /bin/false kanata -sudo groupadd uinput -sudo usermod -aG input kanata -sudo usermod -aG uinput kanata -``` - -Add a new udev rule - -``` -sudo touch /etc/udev/rules.d/99-uinput.rules -``` - -Add the following line to it - -``` -KERNEL=="uinput", MODE="0660", GROUP="uinput", OPTIONS+="static_node=uinput" -``` - -If you are using the default `/opt/kanata` directory - - -``` -sudo chown -R kanata:$USER /opt/kanata -sudo chmod -R 0770 /opt/kanata -``` diff --git a/docs/setup-linux.md b/docs/setup-linux.md new file mode 100644 index 000000000..caa886af1 --- /dev/null +++ b/docs/setup-linux.md @@ -0,0 +1,87 @@ +# Instructions + +In Linux, kanata needs to be able to access the input and uinput subsystem to inject events. To do this, your user needs to have permissions. Follow the steps in this page to obtain user permissions. + +### 1. If the uinput group does not exist, create a new group + +```bash +sudo groupadd uinput +``` + +### 2. Add your user to the input and the uinput group + +```bash +sudo usermod -aG input $USER +sudo usermod -aG uinput $USER +``` + +Make sure that it's effective by running `groups`. You might have to logout and login. + +### 3. Make sure the uinput device file has the right permissions. + +#### Create a new file: +`/etc/udev/rules.d/99-input.rules` + +#### Insert the following in the code +```bash +KERNEL=="uinput", MODE="0660", GROUP="uinput", OPTIONS+="static_node=uinput" +``` + +#### Machine reboot or run this to reload +```bash +sudo sh -c 'udevadm control –reload; udevadm trigger -v –name-match uinput' +``` + +#### Verify settings by following command: +```bash +ls -l /dev/uinput +``` + +#### Output: +```bash +crw-rw---- 1 root date uinput /dev/uinput +``` + +### 4. Make sure the uinput drivers are loaded + +You may need to run this command whenever you start kanata for the first time: + +``` +sudo modprobe uinput +``` +### 5. To create and enable a daemon service + +Run this command first: +```bash +mkdir -p ~/.config/systemd/user +``` + +Then add this to: `~/.config/systemd/user/kanata.service` +```bash +[Unit] +Description=Kanata keyboard remapper +Documentation=https://github.com/jtroo/kanata + +[Service] +Environment=PATH=/usr/local/bin:/usr/local/sbin:/usr/bin:/bin +Environment=DISPLAY=:0 +Environment=HOME=/$HOME +Type=simple +ExecStart=$(which kanata) --cfg $HOME/.config/kanata/config.kbd +Restart=no + +[Install] +WantedBy=default.target + +``` + +Then run: +```bash +systemctl --user daemon-reload +systemctl --user enable kanata.service +systemctl --user start kanata.service +systemctl --user status kanata.service # check whether the service is running +``` +# Credits + +The original text was taken and adapted from: https://github.com/kmonad/kmonad/blob/master/doc/faq.md#linux From 9c1fa3d75e161dfc23078b66d9efea41006e64d6 Mon Sep 17 00:00:00 2001 From: rszyma Date: Sat, 13 Jan 2024 09:45:48 +0100 Subject: [PATCH 123/819] parser: small adjustments (#696) - add `span()` to `SExprMetaData` - add `pub` to some methods - remove `PartialOrd` and `Ord` for types, where comparison doesn't make sense - move unterminated block comment error catching from `parse` to `parse_` I need this merged, especially the last thing from the bullet list above to get https://github.com/rszyma/vscode-kanata/pull/13 working correctly. --- parser/src/cfg/sexpr.rs | 53 ++++++++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/parser/src/cfg/sexpr.rs b/parser/src/cfg/sexpr.rs index aee983286..5d8b69c14 100644 --- a/parser/src/cfg/sexpr.rs +++ b/parser/src/cfg/sexpr.rs @@ -7,7 +7,7 @@ type HashMap = rustc_hash::FxHashMap; use super::{ParseError, Result}; -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] pub struct Position { /// The position (since the beginning of the file), in bytes. pub absolute: usize, @@ -18,7 +18,7 @@ pub struct Position { } impl Position { - fn new(absolute: usize, line: usize, line_beginning: usize) -> Self { + pub fn new(absolute: usize, line: usize, line_beginning: usize) -> Self { assert!(line <= absolute); assert!(line_beginning <= absolute); Self { @@ -29,7 +29,7 @@ impl Position { } } -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Span { pub start: Position, pub end: Position, @@ -49,7 +49,7 @@ impl Default for Span { } impl Span { - fn new(start: Position, end: Position, file_name: Rc, file_content: Rc) -> Span { + pub fn new(start: Position, end: Position, file_name: Rc, file_content: Rc) -> Span { assert!(start.absolute <= end.absolute); assert!(start.line <= end.line); Span { @@ -114,7 +114,7 @@ impl Index for String { } } -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Spanned { pub t: T, pub span: Span, @@ -126,7 +126,7 @@ impl Spanned { } } -#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Clone, PartialEq, Eq, Hash)] /// I know this isn't the classic definition of an S-Expression which uses cons cell and atom, but /// this is more convenient to work with (I find). pub enum SExpr { @@ -191,7 +191,7 @@ impl std::fmt::Debug for SExpr { } } -#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Clone, PartialEq, Eq, Debug)] /// Complementary to SExpr metadata items. pub enum SExprMetaData { LineComment(Spanned), @@ -199,6 +199,16 @@ pub enum SExprMetaData { Whitespace(Spanned), } +impl SExprMetaData { + pub fn span(&self) -> Span { + match self { + Self::LineComment(x) => x.span.clone(), + Self::BlockComment(x) => x.span.clone(), + Self::Whitespace(x) => x.span.clone(), + } + } +} + #[derive(Debug)] enum Token { Open, @@ -387,21 +397,7 @@ pub type TopLevel = Spanned>; pub fn parse(cfg: &str, file_name: &str) -> std::result::Result, ParseError> { let ignore_whitespace_and_comments = true; - parse_(cfg, file_name, ignore_whitespace_and_comments) - .map_err(|e| { - if e.msg.contains("Unterminated multiline comment") { - if let Some(mut span) = e.span { - span.end = span.start; - span.end.absolute += 2; - ParseError::new(span, e.msg) - } else { - e - } - } else { - e - } - }) - .map(|(x, _)| x) + parse_(cfg, file_name, ignore_whitespace_and_comments).map(|(x, _)| x) } pub fn parse_( @@ -413,6 +409,19 @@ pub fn parse_( cfg, Lexer::new(cfg, file_name, ignore_whitespace_and_comments), ) + .map_err(|e| { + if e.msg.contains("Unterminated multiline comment") { + if let Some(mut span) = e.span { + span.end = span.start; + span.end.absolute += 2; + ParseError::new(span, e.msg) + } else { + e + } + } else { + e + } + }) } fn parse_with( From ae855e7d4955acebb8471c5619b7f188b3289947 Mon Sep 17 00:00:00 2001 From: jtroo Date: Sat, 13 Jan 2024 16:35:11 -0800 Subject: [PATCH 124/819] feat: implement lrld-num (#701) Implements a new live reload mechanism, which specifies a config file by its position in the kanata command arguments. --- cfg_samples/kanata.kbd | 6 ++++-- docs/config.adoc | 10 +++++++++- parser/src/cfg/list_actions.rs | 2 ++ parser/src/cfg/mod.rs | 15 +++++++++++++++ parser/src/custom_action.rs | 4 ++++ src/kanata/mod.rs | 15 +++++++++++++-- 6 files changed, 47 insertions(+), 5 deletions(-) diff --git a/cfg_samples/kanata.kbd b/cfg_samples/kanata.kbd index 83b3031e0..1cb9cc201 100644 --- a/cfg_samples/kanata.kbd +++ b/cfg_samples/kanata.kbd @@ -569,13 +569,15 @@ If you need help, please feel welcome to ask in the GitHub discussions. ;; except for linux-dev. So in Linux, you cannot live reload and switch keyboard ;; devices at the time of writing. The variants `lrpv` and `lrnx` will cycle ;; between multiple configuration files, if they are specified in the startup. -;; arguments. +;; arguments. The list action variant `lrld-num` takes a number parameter and +;; reloads the configuration file specified by the number, according to the +;; order passed into the arguments on kanata startup. ;; ;; Upon a successful reload, the kanata state will begin on the default base layer ;; in the configuration. E.g. in this example configuration, you would start on ;; the qwerty layer. (deflayer layers - _ @qwr @dvk lrld lrpv lrnx _ _ _ _ _ _ _ _ + _ @qwr @dvk lrld lrpv lrnx (lrld-num 1) _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ diff --git a/docs/config.adoc b/docs/config.adoc index 73030e6d5..9332daa6d 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -921,11 +921,16 @@ The first configuration file specified will be the one loaded on startup. The prev/next variants can be used with shortened names of `lrpv` and `lrnx` as well. +Another variant is the list action `lrld-num`. +This reloads the configuration file specified by the number, +according to the order that the configuration file arguments +are passed into kanata's startup command. + .Example: [source] ---- (deflayer has-live-reloads - lrld lrpv lrnx + lrld lrpv lrnx (lrld-num 3) ) ---- @@ -936,6 +941,9 @@ Example specifying multiple config files in the command line: kanata -c startup.cfg -c 2nd.cfg -c 3rd.cfg ---- +Given the above startup command, +activating `(lrld-num 2)` would reload the `2nd.cfg` file. + [[layer-switch]] === layer-switch <> diff --git a/parser/src/cfg/list_actions.rs b/parser/src/cfg/list_actions.rs index 57d463730..4af71c7a4 100644 --- a/parser/src/cfg/list_actions.rs +++ b/parser/src/cfg/list_actions.rs @@ -60,6 +60,7 @@ pub const SWITCH: &str = "switch"; pub const SEQUENCE: &str = "sequence"; pub const UNMOD: &str = "unmod"; pub const UNSHIFT: &str = "unshift"; +pub const LIVE_RELOAD_NUM: &str = "lrld-num"; pub fn is_list_action(ac: &str) -> bool { const LIST_ACTIONS: &[&str] = &[ @@ -121,6 +122,7 @@ pub fn is_list_action(ac: &str) -> bool { SEQUENCE, UNMOD, UNSHIFT, + LIVE_RELOAD_NUM, ]; LIST_ACTIONS.contains(&ac) } diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index 04aa7e04e..a47e4b059 100644 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -1296,6 +1296,7 @@ fn parse_action_list(ac: &[SExpr], s: &ParsedState) -> Result<&'static KanataAct SEQUENCE => parse_sequence_start(&ac[1..], s), UNMOD => parse_unmod(UNMOD, &ac[1..], s), UNSHIFT => parse_unmod(UNSHIFT, &ac[1..], s), + LIVE_RELOAD_NUM => parse_live_reload_num(&ac[1..], s), _ => unreachable!(), } } @@ -2508,6 +2509,20 @@ fn parse_dynamic_macro_play(ac_params: &[SExpr], s: &ParsedState) -> Result<&'st ))) } +fn parse_live_reload_num(ac_params: &[SExpr], s: &ParsedState) -> Result<&'static KanataAction> { + const ERR_MSG: &str = + "{LIVE_RELOAD_NUM} expects 1 parameter: "; + if ac_params.len() != 1 { + bail!("{ERR_MSG}, found {}", ac_params.len()); + } + let num = parse_non_zero_u16(&ac_params[0], s, "config argument position")?; + Ok(s.a.sref(Action::Custom( + // Note: for user-friendliness (hopefully), begin at 1 for parsing. + // But for use as an index when stored as data, subtract 1 for 0-based indexing. + s.a.sref(s.a.sref_slice(CustomAction::LiveReloadNum(num - 1))), + ))) +} + fn parse_layers(s: &mut ParsedState) -> Result> { // There are two copies/versions of each layer. One is used as the target of "layer-switch" and // the other is the target of "layer-while-held". diff --git a/parser/src/custom_action.rs b/parser/src/custom_action.rs index dffe40ee6..0ef21d0f7 100644 --- a/parser/src/custom_action.rs +++ b/parser/src/custom_action.rs @@ -54,6 +54,10 @@ pub enum CustomAction { LiveReload, LiveReloadNext, LiveReloadPrev, + /// Live-reload the n'th configuration file provided on the CLI. This should begin with 0 as + /// the first configuration file provided. The rest of the parser code is free to choose 0 or 1 + /// as the user-facing value though. + LiveReloadNum(u16), Repeat, CancelMacroOnRelease, DynamicMacroRecord(u16), diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index 22c983948..e36e41f9f 100644 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -39,8 +39,6 @@ mod linux; #[cfg(target_os = "macos")] mod macos; -#[cfg(target_os = "macos")] -pub use macos::*; mod caps_word; pub use caps_word::*; @@ -939,6 +937,19 @@ impl Kanata { self.cfg_paths[self.cur_cfg_idx].display() ); } + CustomAction::LiveReloadNum(n) => { + let n = usize::from(*n); + live_reload_requested = true; + match self.cfg_paths.get(n) { + Some(path) => { + self.cur_cfg_idx = n; + log::info!("Requested live reload of file: {}", path.display(),); + } + None => { + log::error!("Requested live reload of config file number {}, but only {} config files were passed", n+1, self.cfg_paths.len()); + } + } + } CustomAction::Mouse(btn) => { log::debug!("click {:?}", btn); if let Some(pbtn) = prev_mouse_btn { From 40df28e963324dc464105b2dfc6411c8444f531c Mon Sep 17 00:00:00 2001 From: jtroo Date: Tue, 23 Jan 2024 02:06:59 -0800 Subject: [PATCH 125/819] doc: add more tap timeout details --- docs/config.adoc | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/docs/config.adoc b/docs/config.adoc index 9332daa6d..f7c139cdf 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -1512,6 +1512,50 @@ The tap timeout is the number of milliseconds within which a rapid press+release+press of a key will result in the tap action being held instead of the hold action activating. +.Tap timeout in more detail +[%collapsible] +==== +The way a `tap-hold` action works with respect to the tap timeout +is often unclear to newcomers. +To make it concrete, the output event sequence of the `tap-hold` action +`(tap-hold $tap-timeout 200 a lctl)` +for varying values of `$tap-timeout` +with a fixed input event sequence will be described. + +The input event sequence is: + +- press +- 50 ms elapses +- release +- 50 ms elapses +- press +- 300 ms elapses +- release + +With `(defvar $tap-timeout 0)`, the output event sequence is: + +- 50 ms elapses +- press `a` +- release `a` +- 250 ms elapses +- press `lctl` +- 100 ms elapses +- release `lctl` + +The above output sequence apply for all values `0` to `99` inclusive. + +For a value of `100` or greater for `$tap-timeout`, +the output event sequence is instead: + +- 50 ms elapses +- press `a` +- release `a` +- 50 ms elapses +- press `a` +- 300 ms elapses +- release `a` +==== + The hold timeout is the number of milliseconds after which the hold action will activate. From 41214665947364e877def5a374881621ed18e94a Mon Sep 17 00:00:00 2001 From: jtroo Date: Tue, 23 Jan 2024 02:10:23 -0800 Subject: [PATCH 126/819] doc: experiment with indent, minor changes --- docs/config.adoc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/config.adoc b/docs/config.adoc index f7c139cdf..f287c460f 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -1513,7 +1513,7 @@ press+release+press of a key will result in the tap action being held instead of the hold action activating. .Tap timeout in more detail -[%collapsible] +[%collapsible,indent=4] ==== The way a `tap-hold` action works with respect to the tap timeout is often unclear to newcomers. @@ -1542,7 +1542,8 @@ With `(defvar $tap-timeout 0)`, the output event sequence is: - 100 ms elapses - release `lctl` -The above output sequence apply for all values `0` to `99` inclusive. +The above output sequence is the same for all `$tap-timeout` values +between and including `0` and `99`. For a value of `100` or greater for `$tap-timeout`, the output event sequence is instead: From d4287a34a77c00672f8753d0f4e9c47598c597c3 Mon Sep 17 00:00:00 2001 From: rszyma Date: Mon, 29 Jan 2024 10:04:33 +0100 Subject: [PATCH 127/819] chore: bump driverkit to 0.1.3 (#709) fixes #699 --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c8c0c2951..19cc41b51 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -454,9 +454,9 @@ dependencies = [ [[package]] name = "karabiner-driverkit" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c0f0d450e1a3e33739108102ec7e23e56b870b09b367ffb54a82a95682f999" +checksum = "7cb3342c9a82d919c5ee0915ed8f7a0870fb14ad254e7984c0d174a5e4972932" dependencies = [ "cc", "os_info", diff --git a/Cargo.toml b/Cargo.toml index 5e925d264..54638bc42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,7 +46,7 @@ kanata-keyberon = { path = "keyberon" } kanata-parser = { path = "parser" } [target.'cfg(target_os = "macos")'.dependencies] -karabiner-driverkit = "0.1.2" +karabiner-driverkit = "0.1.3" [target.'cfg(target_os = "linux")'.dependencies] evdev = "=0.12.0" From dd6f0de55b86f476915009d1486123d510895a8a Mon Sep 17 00:00:00 2001 From: jtroo Date: Wed, 31 Jan 2024 14:36:05 -0800 Subject: [PATCH 128/819] doc: fix concurrent-tap-hold example --- docs/config.adoc | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/config.adoc b/docs/config.adoc index f287c460f..a78f50356 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -473,7 +473,13 @@ When enabled, the timeout will start as soon as the tap-hold action is pressed even if a previous tap-hold action is still held and has not expired. -concurrent-tap-hold yes +.Example: +[source] +---- +(defcfg + concurrent-tap-hold yes +) +---- [[linux-only-linux-dev]] === Linux only: linux-dev From b3690ad6f82da4242f3da458df3bc8ad8a080b62 Mon Sep 17 00:00:00 2001 From: jtroo Date: Thu, 1 Feb 2024 23:55:21 -0800 Subject: [PATCH 129/819] doc: reorder defcfg in config guide (#713) --- docs/config.adoc | 2452 +++++++++++++++++++++++----------------------- 1 file changed, 1232 insertions(+), 1220 deletions(-) diff --git a/docs/config.adoc b/docs/config.adoc index a78f50356..55efb70e8 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -229,19 +229,18 @@ mentioned before, please ask for help in an issue or discussion if needed, and help with https://github.com/jtroo/kanata/blob/main/docs/locales.adoc[this document] is very welcome so that future users can have an easier time 🙂. -[[optional-defcfg-entries]] -== Optional defcfg entries - -[[defcfg]] -=== defcfg +[[introduction-defcfg]] +== Introduction to defcfg <> Your configuration file may include a single `defcfg` entry. +The `defcfg` can be empty or omitted. +There are options that change kanata's behaviour, +but this introduction will introduce +only the most prevalent entry: `process-unmapped-keys`. +All other options can be found later in the <> section. -It can be empty but there are options that can change kanata's behaviour that -will be described in this section. - -.Example: +.Example of an empty defcfg: [source] ---- (defcfg) @@ -251,16 +250,21 @@ will be described in this section. === process-unmapped-keys <> -Enabling this configuration makes kanata process keys that are not in defsrc. -This is useful if you are only mapping a few keys in defsrc instead of most of -the keys on your keyboard. +The `process-unmapped-keys` option in `defcfg` is probably the most +generally impactful option. +Enabling this configuration makes kanata process keys +that are not defined in `defsrc'. +This might be useful +if you are only mapping a few keys in defsrc +instead of most of the keys on your keyboard. Without this, some actions like `+rpt+`, `+tap-hold-release+`, `+one-shot+`, will not work correctly for subsequent key presses that are not in defsrc. -This is disabled by default. The reason this is not enabled by default is -because some keys may not work correctly if they are intercepted. For example, -see the <> option below. +This option is disabled by default. +The reason this is not enabled by default is +because some keys may not work correctly if they are intercepted. +For example, see <>. .Example: [source] @@ -270,1743 +274,1751 @@ see the <> option below. ) ---- -[[danger-enable-cmd]] -=== danger-enable-cmd +== Aliases and variables[[aliases-and-vars]] <> -This option can be used to enable the `cmd` action in your configuration. The -`+cmd+` action allows kanata to execute programs with arguments passed to them. +Before learning about actions, +it will be useful to first learn about aliases and variables. -This requires using a kanata program that is compiled with the `cmd` action -enabled. The reason for this is so that if you choose to, there is no way for -kanata to execute arbitrary programs even if you download some random -configuration from the internet. +[[aliases]] +=== Aliases +<> -This configuration is disabled by default and can be enabled by giving it the -value `yes`. +Using the `defalias` configuration entry, you can introduce a shortcut label +for an action. + +Similar to how `defcfg` works, `defalias` reads pairs of items in a sequence +where the first item in the pair is the alias name and the second item is the +action it can be substituted for. However, unlike `+defcfg+`, the second item +in `defalias` may be a "list" as opposed to a single string like it was in +`defcfg`. + +A list is a sequence of strings separated by whitespace, surrounded by +parentheses. All of the configuration entries we've looked at so far are lists; +`defalias` is where we'll first see nested lists in this guide. .Example: [source] ---- -(defcfg - danger-enable-cmd yes +(defalias + ;; tap for caps lock, hold for left control + cap (tap-hold 200 200 caps lctl) ) ---- -[[sequence-timeout]] -=== sequence-timeout -<> - -This option customizes the key sequence timeout (unit: ms). Its default value -is 1000. The purpose of this item is explained in <>. +This alias can be used in `deflayer` as a substitute for the long action. The +alias name is prefixed with `@` to signify that it's an alias as opposed to a +normal key. -.Example: [source] ---- -(defcfg - sequence-timeout 2000 +(deflayer example + @cap a s d f ) ---- -[[sequence-input-mode]] -=== sequence-input-mode -<> - -This option customizes the key sequence input mode. Its default value when not -configured is `hidden-suppressed`. - -The options are: - -- `visible-backspaced`: types sequence characters as they are inputted. The - typed characters will be erased with backspaces for a valid sequence termination. -- `hidden-suppressed`: hides sequence characters as they are typed. Does not - output the hidden characters for an invalid sequence termination. -- `hidden-delay-type`: hides sequence characters as they are typed. Outputs the - hidden characters for an invalid sequence termination either after a - timeout or after a non-sequence key is typed. +You may have multiple `defalias` entries and multiple aliases within a single +`defalias`. Aliases may also refer to other aliases that were defined earlier +in the configuration file. -For `visible-backspaced` and `hidden-delay-type`, a sequence leader input will -be ignored if a sequence is already active. For historical reasons, and in case -it is desired behaviour, a sequence leader input using `hidden-suppressed` will -reset the key sequence. +.Example: +[source] +---- +(defalias one (tap-hold 200 200 caps lctl)) +(defalias two (tap-hold 200 200 esc lctl)) +(defalias + three C-A-del ;; Ctrl+Alt+Del + four (tap-hold 200 200 @three ralt) +) +---- -See <> for more about sequences. +You can choose to put actions without aliasing them right into `deflayer`. +However, for long actions it is recommended not to do so to keep a nice visual +alignment. Visually aligning your `deflayer` entries will hopefully make your +configuration file easier to read. .Example: [source] ---- -(defcfg - sequence-input-mode visible-backspaced +(deflayer example + ;; this is equivalent to the previous deflayer example + (tap-hold 200 200 caps lctl) a s d f ) ---- - -[[sequence-backtrack-modcancel]] -=== sequence-backtrack-modcancel +[[variables]] +=== Variables <> -This option customizes the behaviour of key sequences -when modifiers are used. -The default is `yes` and can be overridden to `no` if desired. +Using the `defvar` configuration entry, +you can introduce a shortcut label for an arbitrary string or list. +Unlike an alias, a variable does not need to be a valid standalone action. +In other words, +a variable can be used as components of actions. -Setting it to `yes` allows both `fk1` and `fk2` to be activated -in the following configuration, but with `no`, -`fk1` will be impossible to activate +The most common use case is to define common number strings +for actions such as `tap-hold`, `tap-dance`, and `one-shot`. ----- -(defseq - fk1 (lsft a b) - fk2 (S-(c d)) -) ----- +Similar to how `defalias` works, +`defvar` reads pairs of items in a sequence +where the first item in the pair is the variable name +and the second item is a string or list. +Variables are allowed to refer to previously defined variables. -See <> for more about sequences and -https://github.com/jtroo/kanata/blob/main/docs/sequence-adding-chords-ideas.md[this document] -for more context about this specific configuration. +Variables can be used to substitute most values. +Some notable exceptions are: + +- variables cannot be used in `defcfg`, `defsrc`, or `deflocalkeys` +- variables cannot be used to substitute a layer name +- variables cannot be used to substitute an action name + +Variables are referred to by prefixing their name with `$`. .Example: [source] ---- -(defcfg - sequence-backtrack-modcancel no +(defvar + tap-timeout 100 + hold-timeout 200 + tt $tap-timeout + ht $hold-timeout +) + +(defalias + th1 (tap-hold $tt $ht caps lctl) + th2 (tap-hold $tt $ht spc lsft) ) ---- -[[log-layer-changes]] -=== log-layer-changes +[[actions]] +== Actions + +The actions kanata provides are what make it truly customizable. This section +explains the available actions. + +[[live-reload]] +=== Live reload <> -By default, kanata will log layer changes. However, logging has some processing -overhead. If you do not care for the logging, you can choose to disable it. +You can put the `+lrld+` action onto a key to live-reload your configuration +file. If kanata can't parse the file, it will continue using the previous +configuration. .Example: [source] ---- -(defcfg - log-layer-changes no +(deflayer has-live-reload + lrld a s d f ) ---- -[[delegate-to-first-layer]] -=== delegate-to-first-layer -<> - - -By default, transparent keys on layers -will delegate to the corresponding defsrc key -when found on a layer activated by `layer-switch`. - -This config entry changes the behaviour -to delegate to the action in the same position on the first layer defined -in the configuration, which is the active layer on startup. +There are variants of `lrld`: `lrld-prev` and `lrld-next`. These will cycle +through different configuration files that you specify on kanata's startup. +The first configuration file specified will be the one loaded on startup. +The prev/next variants can be used with shortened names of `lrpv` and `lrnx` as +well. -For more context, see https://github.com/jtroo/kanata/issues/435. +Another variant is the list action `lrld-num`. +This reloads the configuration file specified by the number, +according to the order that the configuration file arguments +are passed into kanata's startup command. .Example: [source] ---- -(defcfg - delegate-to-first-layer yes +(deflayer has-live-reloads + lrld lrpv lrnx (lrld-num 3) ) ---- +Example specifying multiple config files in the command line: -[[movemouse-inherit-accel-state]] -=== movemouse-inherit-accel-state +[source] +---- +kanata -c startup.cfg -c 2nd.cfg -c 3rd.cfg +---- + +Given the above startup command, +activating `(lrld-num 2)` would reload the `2nd.cfg` file. + +[[layer-switch]] +=== layer-switch <> -By default `movemouse-accel` actions will track the acceleration -state for vertical and horizontal axes separately. +This action allows you to switch to another "base" layer. This is permanent +until a `layer-switch` to another layer is activated. The concept of a base +layer makes more sense when looking at the next action: `layer-while-held`. -When this setting is enabled, `movemouse-accel` will behave exactly like mouse movements in https://qmk.fm[QMK], -i.e. the acceleration state of new mouse -movement actions will be inherited if others are already being pressed. +This action accepts a single subsequent string which must be a layer name +defined in a `deflayer` entry. .Example: [source] ---- -(defcfg - movemouse-inherit-accel-state yes -) +(defalias dvk (layer-switch dvorak)) ---- -[[movemouse-smooth-diagonals]] -=== movemouse-smooth-diagonals +[[layer-while-held]] +=== layer-while-held <> -By default, mouse movements move one direction at a time -and vertical/horizontal movements are on independent timers. +This action allows you to temporarily change to another layer while the key +remains held. When the key is released, you go back to the currently active +"base" layer. -This can result in non-smooth diagonals when drawing a line in some app. -This option adds a small imperceptible amount of latency to -synchronize the mouse movements. +This action accepts a single subsequent string which must be a layer name +defined in a `deflayer` entry. .Example: [source] ---- -(defcfg - movemouse-smooth-diagonals yes -) +(defalias nav (layer-while-held navigation)) ---- -=== dynamic-macro-max-presses [[dynamic-macro-max-presses]] +You may also use `layer-toggle` in place of `layer-while-held`; they behave +exactly the same. The `layer-toggle` name is slightly shorter but is a bit +inaccurate with regards to its meaning. + +[[transparent-key]] +=== Transparent key <> -This configuration allows you to customize the length limit on dynamic macros. -The default length limit is 128 keys. +If you use a single underscore for a key `+_+` then it acts as a "transparent" +key in a `+deflayer+`. The behaviour depends if `+_+` is on a base layer or a +while-held layer. When `+_+` is pressed on the active base layer, the key will +default to the corresponding `defsrc` key. If `+_+` is pressed on the active +while-held layer, the base layer's behaviour will activate. .Example: [source] ---- -(defcfg - dynamic-macro-max-presses 1000 +(defsrc + a b c ) ----- -=== concurrent-tap-hold [[concurrent-tap-hold]] -This configuration makes multiple tap-hold actions -that are activated near in time expire their timeout quicker. -By default this is disabled. -When disabled, the timeout for a following tap-hold -will start from 0ms **after** the previous tap-hold expires. -When enabled, the timeout will start -as soon as the tap-hold action is pressed -even if a previous tap-hold action is still held and has not expired. - -.Example: -[source] ----- -(defcfg - concurrent-tap-hold yes +(deflayer remap-only-c-to-d + _ _ d ) ---- -[[linux-only-linux-dev]] -=== Linux only: linux-dev +[[no-op]] +=== No-op <> -By default, kanata will try to detect which input devices are keyboards and try -to intercept them all. However, you may specify exact keyboard devices from the -`/dev/input` directories using the `linux-dev` configuration. + +You may use the action `+XX+` as a "no operation" key, meaning pressing the key +will do nothing. This might be desirable in place of a transparent key on a +layer that is not fully mapped so that a key that is intentionally not mapped +will do nothing as opposed to typing a letter. .Example: [source] ---- -(defcfg - linux-dev /dev/input/by-path/platform-i8042-serio-0-event-kbd +(deflayer contains-no-op + XX a s d f ) ---- -If you want to specify multiple keyboards, you can separate the paths with a -colon `+:+`. +[[unicode]] +=== Unicode +<> -.Example: -[source] ----- -(defcfg - linux-dev /dev/input/dev1:/dev/input/dev2 -) ----- +The `+unicode+` action accepts a single unicode character. The character will +not be repeatedly typed if you hold the key down. -Due to using the colon to separate devices, if you have a device with colons in -its file name, you must escape those colons with backslashes: +You may use a unicode character as an alias if desired. -[source] ----- -(defcfg - linux-dev /dev/input/path-to\:device -) ----- +NOTE: The unicode action may not be correctly accepted by the active +application. -Alternatively, you can use list syntax, where both backslashes and colons -are parsed literally. List items are separated by spaces or newlines. -Using quotation marks for each item is optional, and only required if an -item contains spaces. +NOTE: If using Linux, make sure to look at the +<> in defcfg. [source] ---- -(defcfg - linux-dev ( - /dev/input/path:to:device - "/dev/input/path to device" - ) +(defalias + sml (unicode 😀) + 🙁 (unicode 🙁) +) +(deflayer has-happy-sad + @sml @🙁 a s d f ) ---- -[[linux-only-linux-dev-names-include]] -=== Linux only: linux-dev-names-include +[[output-chordscombos]] +=== Output chords/combos <> -In the case that `linux-dev` is omitted, -this option defines a list of device names that should be included. -Device names that do not exist in the list will be ignored. -This option is parsed identically to `linux-dev`. +You may want to remap a key to automatically be pressed in combination with +modifiers such as Control or Shift. You can achieve this by prefixing the +normal key name with one or more of: -Kanata will print device names on startup with log lines that look like below: +* `+C-+`: Left Control +* `+A-+`: Left Alt +* `+S-+`: Left Shift +* `+M-+`: Left Meta, a.k.a. Windows, GUI, Command, Super +* `+RA-+` or `+AG+`: Right Alt, a.k.a. AltGr ----- -registering /dev/input/eventX: "Name goes here" ----- +These modifiers may be combined together if desired. .Example: [source] ---- -(defcfg - linux-dev-names-include ( - "Device name 1" - "Device name 2" - ) +(defalias + ;; Type exclamation mark (US layout) + ex! S-1 + ;; Ctrl+C: send SIGINT to a Linux terminal program + int C-c + ;; Win+Tab: open Windows' Task View + tsk M-tab + ;; Ctrl+Shift+(C|V): copy or paste from certain terminal programs + cpy C-S-c + pst C-S-v ) ---- -[[linux-only-linux-dev-names-exclude]] -=== Linux only: linux-dev-names-exclude -<> - -In the case that `linux-dev` is omitted, -this option defines a list of device names that should be excluded. -This option is parsed identically to `linux-dev`. +A special behaviour of output chords is that if another key is pressed, +all of the chord keys will be released +before the newly pressed key action activates. +Output chords are typically used do one-off actions such as: -The `linux-dev-names-include` and `linux-dev-names-exclude` options -are not mutually exclusive -but in practice it probably only makes sense to use one and not both. +- type a symbol, e.g. `S-1` +- type a special/accented character, e.g. `RA-a` +- do a special action like `C-c` to send `SIGTERM` in the terminal -.Example: -[source] ----- -(defcfg - linux-dev-names-exclude ( - "Device Name 1" - "Device Name 2" - ) -) ----- +The modifier pressed with these one-off action +is usually not desired for subsequent actions. -[[linux-only-linux-continue-if-no-devs-found]] -=== Linux only: linux-continue-if-no-devs-found +[[repeat-key]] +=== Repeat key <> -By default, kanata will crash if no input devices are found. You can change -this behaviour by setting `linux-continue-if-no-devs-found`. +The action `+rpt+` repeats the most recently typed key. Holding down this key +will not repeatedly send the key. The intended use case is to be able to use a +different finger or even thumb key to repeat a typed key, as opposed to +double-tapping a key. .Example: [source] ---- -(defcfg - linux-continue-if-no-devs-found yes +(deflayer has-repeat + rpt a s d f ) ---- -[[linux-only-linux-unicode-u-code]] -=== Linux only: linux-unicode-u-code -<> - -Unicode on Linux works by pressing Ctrl+Shift+U, typing the unicode hex value, -then pressing Enter. However, if you do remapping in userspace, e.g. via -xmodmap/xkb, the keycode "U" that kanata outputs may not become a keysym "u" -after the userspace remapping. This will be likely if you use non-US, -non-European keyboards on top of kanata. For unicode to work, kanata needs to -use the keycode that outputs the keysym "u", which might not be the keycode -"U". +The `rpt` action only repeats the last key output. +For example, it won't output a chord like `ctrl+c` +if the previous key pressed was `C-c`. +The `rpt` action will only output `c` in this case. -You can use `evtest` or `kanata --debug`, set your userspace key remapping, -then press the key that outputs the keysym "u" to see which underlying keycode -is sent. Then you can use this configuration to change kanata's behaviour. +There is a variant `rpt-any` +which will repeat any previous action +and would output `ctrl+c` in the example case. -.Example: -[source] ---- -(defcfg - linux-unicode-u-code v +(deflayer has-repeat-any + rpt-any a s d f ) ---- -[[linux-only-linux-unicode-termination]] -=== Linux only: linux-unicode-termination +[[release-a-key-or-layer]] +=== Release a key or layer <> -Unicode on Linux terminates with the Enter key by default. This may not work in -some applications. The termination is configurable with the following options: +You can release a held key or layer via these actions: -- `enter` -- `space` -- `enter-space` -- `space-enter` +* `release-key`: release a key, accepts `defsrc` compatible names +* `release-layer`: release a while-held layer -.Example: -[source] ----- -(defcfg - linux-unicode-termination space -) ----- +An example practical use case for `release-key` is seen in the `multi` section +directly below. -=== Linux only: linux-x11-repeat-delay-rate[[linux-only-x11-repeat-rate]] +There is currently no known practical use case for +`release-layer`, but it exists nonetheless. + +[[multi]] +=== multi <> -On Linux, you can tell kanata to run `xset r rate ` -on startup and on live reload -via the configuration item `linux-only-x11-repeat-rate`. -This takes two numbers separated by a comma. -The first number is the delay in ms -and the second number is the repeat rate in repeats/second. +The `+multi+` action executes multiple keys or actions in order but also +simultaneously. It accepts one or more actions. -This configuration item does not affect Wayland or no-desktop environments. +An example use case is to press the "Alt" key while also activating another +layer. + +In the example below, holding the physical "Alt" key will result in a held +layer being activated while also holding "Alt" itself. The held layer operates +nearly the same as the standard keyboard, so for example the sequence (hold +Alt)+(Tab+Tab+Tab) will work as expected. This is in contrast to having a layer +where `tab` is mapped to `A-tab`, which results in repeated press+release of +the two keys and has different behaviour than expected. Some special keys will +release the "Alt" key and do some other action that requires "Alt" to be +released. In other words, the "Alt" key serves a dual purpose of still +fulfilling the "Alt" key role for some button presses (e.g. Tab), but also as a +new layer for keys that aren't typically used with "Alt" to have added useful +functionality. -.Example: [source] ---- -(defcfg - linux-x11-repeat-delay-rate 400,50 +(defalias + atl (multi alt (layer-while-held alted-with-exceptions)) + lft (multi (release-key alt) left) ;; release alt if held and also press left + rgt (multi (release-key alt) rght) ;; release alt if held and also press rght ) ----- - -[[macos-only-macos-dev-names-include]] -=== macOS only: macos-dev-names-include -<> -This option defines a list of device names that should be included. -By default, kanata will try to detect which input devices are keyboards and try -to intercept them all. However, you may specify exact keyboard devices to intercept -using the `macos-dev-names-include` configuration. -Device names that do not exist in the list will be ignored. -This option is parsed identically to `linux-dev`. +(defsrc + alt a s d f +) -Use `kanata -l` or `kanata --list` to list the available keyboards. +(deflayer base + @atl _ _ _ _ +) -.Example: -[source] ----- -(defcfg - macos-dev-names-include ( - "Device name 1" - "Device name 2" - ) +(deflayer alted-with-exceptions + _ _ _ @lft @rgt ) ---- -[[windows-only-windows-altgr]] -=== Windows only: windows-altgr +WARNING: This action can sometimes behave in surprising ways +with regards to simultaneity and order of actions. +For example, an action like `(multi sldr ')` will not behave as expected. +Due to implementation details, `sldr` will activate after the `'` +even though it is listed before. +This example could instead be written as `(macro sldr 10 ')`, +and that would work as intended. +It is recommended to avoid `multi` if it can be replaced +with a different action like `macro` or an output chord. + +[[mouse-actions]] +=== Mouse actions <> -There is an option for Windows to help mitigate the strange behaviour of AltGr -(ralt) if you're using that key in your defsrc. This is applicable for many -non-US layouts. You can use one of the listed values to change what kanata does -with the key: +You can click the left, middle, and right buttons using kanata actions, do +vertical/horizontal scrolling, and move the mouse. -* `cancel-lctl-press` -** This will remove the `lctl` press that is generated alonside `ralt` -* `add-lctl-release` -** This adds an `lctl` release when `ralt` is released +[[mouse-buttons]] +==== Mouse buttons +<> -.Example: -[source] ----- -(defcfg - windows-altgr add-lctl-release -) ----- +The mouse button actions are: -For more context, see: https://github.com/jtroo/kanata/issues/55. +* `mlft`: left mouse button +* `mmid`: middle mouse button +* `mrgt`: right mouse button +* `mfwd`: forward mouse button +* `mbck`: backward mouse button -NOTE: Even with these workarounds, putting `+lctl+`+`+ralt+` in your defsrc may not -work properly with other applications that also use keyboard interception. -Known application with issues: GWSL/VcXsrv +The mouse button will be held while the key mapped to it is held. -=== Windows only: windows-interception-mouse-hwid[[windows-only-windows-interception-mouse-hwid]] -<> +If there are multiple mouse click actions within a single multi action, e.g. -This defcfg item allows you to intercept mouse buttons for a specific mouse -device. This only works with the Interception driver (the -wintercept variants -of the binary). +`+(multi mrgt mlft)+` -The intended use case for this is for laptops such as a Thinkpad, which have -mouse buttons that may be desirable to activate kanata actions with. +then all the buttons except the last will be clicked then unclicked. The last +button will remain held until key release. In the example above, pressing then +releasing the key mapped to this action will result in the following event +sequence: -To know what numbers to put into the string, you can run the variant with this -defcfg item defined with any numbers. Then when a button is first pressed on -the mouse device, kanata will print its hwid in the log; you can then -copy-paste that into this configuration entry. If this defcfg item is not -defined, the log will not print. +. press key mapped to `+multi+` +. click right mouse button +. unclick right mouse button +. click left mouse button +. release key mapped to `+multi+` +. release left mouse button -https://github.com/jtroo/kanata/issues/108[Relevant issue]. +There are variants of the standard mouse buttons which "tap" the button. Rather +than holding the button while the key is held, a mouse click will be +immediately followed by the release. Nothing happens when the key is released. +The actions are as follows: -.Example: -[source] ----- -(defcfg - windows-interception-mouse-hwid "70, 0, 60, 0" -) ----- +* `mltp`: tap left mouse button +* `mmtp`: tap middle mouse button +* `mrtp`: tap right mouse button +* `mftp`: tap forward mouse button +* `mbtp`: tap bacward mouse button -[[using-multiple-defcfg-entries]] -=== Using multiple defcfg entries +[[mouse-wheel]] +==== Mouse wheel <> -The `defcfg` entry is treated as a list with pairs of strings. For example: +The mouse wheel actions are: -[source] ----- -(defcfg a 1 b 2) ----- +* `mwheel-up`: vertical scroll up +* `mwheel-down`: vertical scroll down +* `mwheel-left`: horizontal scroll left +* `mwheel-right`: horizontal scroll right -This will be treated as configuration `a` having value `1` and configuration -`b` having value `2`. +All of these actions accept two number strings. The first is the interval +(unit: ms) between scroll actions. The second number is the distance +(unit: arbitrary). In both Windows and Linux, 120 distance units is equivalent +to a notch movement on a physical wheel. You can play with the parameters to +see what feels correct to you. Both numbers must be in the range [1,65535]. -An example defcfg containing many of the options is shown below. It should be -noted options that are Linux-only, Windows-only, or macOS-only will be ignored when used on -a non-applicable operating system. +NOTE: In Linux, not all desktop environments support the `REL_WHEEL_HI_RES` event. +If this is the case for yours, +it will likely be a better experience to use a distance value that is a multiple of 120. -[source] ----- -;; Don't actually use this exact configuration, -;; it's almost certainly not what you want. -(defcfg - process-unmapped-keys yes - danger-enable-cmd yes - sequence-timeout 2000 - sequence-input-mode visible-backspaced - sequence-backtrack-modcancel no - log-layer-changes no - delegate-to-first-layer yes - movemouse-inherit-accel-state yes - movemouse-smooth-diagonals yes - dynamic-macro-max-presses 1000 - linux-dev (/dev/input/dev1 /dev/input/dev2) - linux-dev-names-include ("Name 1" "Name 2") - linux-dev-names-exclude ("Name 3" "Name 4") - linux-continue-if-no-devs-found yes - linux-unicode-u-code v - linux-unicode-termination space - linux-x11-repeat-delay-rate 400,50 - windows-altgr add-lctl-release - windows-interception-mouse-hwid "70, 0, 60, 0" -) ----- +On Linux and Interception, you can also choose to read from a mouse device. +When doing so, using the `mwu`, `mwd`, `mwl`, `mwr` key names in `defsrc` +allow you to remap the mouse scroll up/down/left/right actions like you would +with keyboard keys. -== Aliases and variables[[aliases-and-vars]] +NOTE: If you are using a high-resolution mouse in Linux, +only a full "notch" of the scroll wheel will activate the action. + +NOTE: If you are using a high-resolution mouse with Interception, +you will probably get way more events than you intended. + +[[mouse-movement]] +==== Mouse movement <> -Before learning about actions, -it will be useful to first learn about aliases and variables. +The mouse movement actions are: -[[aliases]] -=== Aliases +* `movemouse-up` +* `movemouse-down` +* `movemouse-left` +* `movemouse-right` + +Similar to the mouse wheel actions, all of these actions accept two number strings. +The first is the interval (unit: ms) between movement actions and the second number +is the distance (unit: pixels) of each movement. + +The following are variants of the above mouse movements that apply linear mouse +acceleration from the minimum distance to the maximum distance as the mapped key is held. + +* `movemouse-accel-up` +* `movemouse-accel-down` +* `movemouse-accel-left` +* `movemouse-accel-right` + +All these actions accept four number strings. The first number is the +interval (unit: ms) between movement actions. The second number is the time it +takes (unit: ms) to linearly ramp up from the minimum distance to the maximum +distance. The third and fourth numbers are the minimum and maximum distances +(unit: pixels) of each movement. + +There is a toggable defcfg option related to `movemouse-accel` - <>. You might want to enable it, especially if you're coming from QMK. + +[[set-mouse]] +==== Set absolute mouse position <> -Using the `defalias` configuration entry, you can introduce a shortcut label -for an action. +The action `setmouse` sets the absolute mouse position. -Similar to how `defcfg` works, `defalias` reads pairs of items in a sequence -where the first item in the pair is the alias name and the second item is the -action it can be substituted for. However, unlike `+defcfg+`, the second item -in `defalias` may be a "list" as opposed to a single string like it was in -`defcfg`. +WARNING: This is only supported in Windows right now. +For an interesting keyboard-centric mouse solution in Linux, +try looking at +https://github.com/rvaiya/warpd[warpd]. -A list is a sequence of strings separated by whitespace, surrounded by -parentheses. All of the configuration entries we've looked at so far are lists; -`defalias` is where we'll first see nested lists in this guide. +This list action takes two parameters which are `x` and `y` positions +of the absolute movement. +The values go from 0,0 which is the upper-left corner of the screen +to 65535,65535 which is the lower-right corner of the screen. +If you have multiple monitors, +`setmouse` treats them all as a single large screen. +This can make it a little confusing for how to set the `x, y` values +to get the positions that you want. +Experimentation will be needed. -.Example: -[source] ----- -(defalias - ;; tap for caps lock, hold for left control - cap (tap-hold 200 200 caps lctl) -) ----- +[[mouse-speed]] +==== Modify the speed of mouse movements +<> -This alias can be used in `deflayer` as a substitute for the long action. The -alias name is prefixed with `@` to signify that it's an alias as opposed to a -normal key. +The action `movemouse-speed` modifies the speed at which `movemouse` and +`movemouse-accel` function at runtime. It does this by expanding or shrinking +`min_distance` and `max_distance` while the action key is pressed. -[source] ----- -(deflayer example - @cap a s d f -) ----- +This action accepts one number (unit: percentage) by which the +mouse movements will be accelerated. -You may have multiple `defalias` entries and multiple aliases within a single -`defalias`. Aliases may also refer to other aliases that were defined earlier -in the configuration file. +WARNING: Due to the nature of pixels being whole numbers, some values such as +33 may not result in an exact third of the distance. .Example: [source] ---- -(defalias one (tap-hold 200 200 caps lctl)) -(defalias two (tap-hold 200 200 esc lctl)) (defalias - three C-A-del ;; Ctrl+Alt+Del - four (tap-hold 200 200 @three ralt) + fst (movemouse-speed 200) + slw (movemouse-speed 50) ) ---- -You can choose to put actions without aliasing them right into `deflayer`. -However, for long actions it is recommended not to do so to keep a nice visual -alignment. Visually aligning your `deflayer` entries will hopefully make your -configuration file easier to read. +[[mouse-all-actions-example]] +==== Mouse all actions example +<> -.Example: [source] ---- -(deflayer example - ;; this is equivalent to the previous deflayer example - (tap-hold 200 200 caps lctl) a s d f -) ----- - -[[variables]] -=== Variables -<> - -Using the `defvar` configuration entry, -you can introduce a shortcut label for an arbitrary string or list. -Unlike an alias, a variable does not need to be a valid standalone action. -In other words, -a variable can be used as components of actions. - -The most common use case is to define common number strings -for actions such as `tap-hold`, `tap-dance`, and `one-shot`. - -Similar to how `defalias` works, -`defvar` reads pairs of items in a sequence -where the first item in the pair is the variable name -and the second item is a string or list. -Variables are allowed to refer to previously defined variables. +(defalias + mwu (mwheel-up 50 120) + mwd (mwheel-down 50 120) + mwl (mwheel-left 50 120) + mwr (mwheel-right 50 120) -Variables can be used to substitute most values. -Some notable exceptions are: + ms↑ (movemouse-up 1 1) + ms← (movemouse-left 1 1) + ms↓ (movemouse-down 1 1) + ms→ (movemouse-right 1 1) -- variables cannot be used in `defcfg`, `defsrc`, or `deflocalkeys` -- variables cannot be used to substitute a layer name -- variables cannot be used to substitute an action name + ma↑ (movemouse-accel-up 1 1000 1 5) + ma← (movemouse-accel-left 1 1000 1 5) + ma↓ (movemouse-accel-down 1 1000 1 5) + ma→ (movemouse-accel-right 1 1000 1 5) -Variables are referred to by prefixing their name with `$`. + sm (setmouse 32228 32228) -.Example: -[source] ----- -(defvar - tap-timeout 100 - hold-timeout 200 - tt $tap-timeout - ht $hold-timeout + fst (movemouse-speed 200) ) -(defalias - th1 (tap-hold $tt $ht caps lctl) - th2 (tap-hold $tt $ht spc lsft) +(deflayer mouse + _ @mwu @mwd @mwl @mwr _ _ _ _ _ @ma↑ _ _ _ + _ pgup bck _ fwd _ _ _ _ @ma← @ma↓ @ma→ _ _ + _ pgdn mlft _ mrgt mmid _ mbck mfwd _ @ms↑ _ _ + @fst _ mltp _ mrtp mmtp _ mbtp mftp @ms← @ms↓ @ms→ + _ _ _ _ _ _ _ ) ---- -[[actions]] -== Actions +[[tap-dance]] +=== tap-dance +<> -The actions kanata provides are what make it truly customizable. This section -explains the available actions. +The `+tap-dance+` action allows repeated tapping of a key to result in +different actions. It is followed by a timeout (unit: ms) and a list +of keys or actions. Each time the key is pressed, its timeout will reset. The +action will be chosen if one of the following events occur: -[[live-reload]] -=== Live reload -<> +* the timeout expires +* a different key is pressed +* the key is repeated up to the final action -You can put the `+lrld+` action onto a key to live-reload your configuration -file. If kanata can't parse the file, it will continue using the previous -configuration. +You may put normal keys or other actions in `+tap-dance+`. .Example: [source] ---- -(deflayer has-live-reload - lrld a s d f +(defalias + ;; 1 tap : "A" key + ;; 2 taps: Control+C + ;; 3 taps: Switch to another layer + ;; 4 taps: Escape key + td (tap-dance 200 (a C-c (layer-switch l2) esc)) ) ---- -There are variants of `lrld`: `lrld-prev` and `lrld-next`. These will cycle -through different configuration files that you specify on kanata's startup. -The first configuration file specified will be the one loaded on startup. -The prev/next variants can be used with shortened names of `lrpv` and `lrnx` as -well. +There is a variant of `tap-dance` with the name `tap-dance-eager`. The variant +is parsed identically but the difference is that it will activate every +action in the sequence as the taps progress. -Another variant is the list action `lrld-num`. -This reloads the configuration file specified by the number, -according to the order that the configuration file arguments -are passed into kanata's startup command. +In the example below, repeated taps will, in order: + +1. type `a` +2. erase the `a` and type `bb` +3. erase the `bb` and type `ccc` -.Example: [source] ---- -(deflayer has-live-reloads - lrld lrpv lrnx (lrld-num 3) +(defalias + td2 (tap-dance-eager 500 ( + (macro a) ;; use macro to prevent auto-repeat of the key + (macro bspc b b) + (macro bspc bspc c c c) + )) ) ---- -Example specifying multiple config files in the command line: +[[one-shot]] +=== one-shot +<> -[source] ----- -kanata -c startup.cfg -c 2nd.cfg -c 3rd.cfg ----- +The `+one-shot+` action is similar to "sticky keys", if you know what that is. +This activates an action or key until either the timeout expires or a different +key is used. The `+one-shot+` action must be followed by a timeout (unit: +ms) and another key or action. -Given the above startup command, -activating `(lrld-num 2)` would reload the `2nd.cfg` file. +Some of the intended use cases are: -[[layer-switch]] -=== layer-switch -<> +* press a modifier for exactly one following key press +* switch to another layer for exactly one following key press -This action allows you to switch to another "base" layer. This is permanent -until a `layer-switch` to another layer is activated. The concept of a base -layer makes more sense when looking at the next action: `layer-while-held`. +If a `+one-shot+` key is held then it will act as the regular key. E.g. holding +a key assigned with `+@os1+` in the example below will keep Left Shift held for +every key, not just one, as long as it's still physically pressed. -This action accepts a single subsequent string which must be a layer name -defined in a `deflayer` entry. +Pressing multiple `+one-shot+` keys in a row within the timeout will combine +the actions of those keys and reset the timeout to the value of the most +recently pressed `+one-shot+` key. -.Example: -[source] ----- -(defalias dvk (layer-switch dvorak)) ----- +There are four variants of the `+one-shot+` action: -[[layer-while-held]] -=== layer-while-held -<> +- `+one-shot-press+`: + end on the first press of another key +- `+one-shot-release+`: + end on the first release of another key +- `+one-shot-press-pcancel+`: + end on the first press of another key + or on re-press of another active one-shot key +- `+one-shot-release-pcancel+`: + end on the first release of another key + or on re-press of another active one-shot key -This action allows you to temporarily change to another layer while the key -remains held. When the key is released, you go back to the currently active -"base" layer. +It is important to note that the first activation of a one-shot key +determines the behaviour with regards to the 4 variants +for all subsequent one-shot key activations, +even if a following one-shot key has a different configuration +than the initial key pressed. -This action accepts a single subsequent string which must be a layer name -defined in a `deflayer` entry. +The default name `+one-shot+` corresponds to `+one-shot-press+`. .Example: [source] ---- -(defalias nav (layer-while-held navigation)) +(defalias + os1 (one-shot 500 (layer-while-held another-layer)) + os2 (one-shot-press 2000 lsft) + os3 (one-shot-release 2000 lctl) + os4 (one-shot-press-pcancel 2000 lalt) + os5 (one-shot-release-pcancel 2000 lmet) +) ---- -You may also use `layer-toggle` in place of `layer-while-held`; they behave -exactly the same. The `layer-toggle` name is slightly shorter but is a bit -inaccurate with regards to its meaning. -[[transparent-key]] -=== Transparent key +[[tap-hold]] +=== tap-hold <> -If you use a single underscore for a key `+_+` then it acts as a "transparent" -key in a `+deflayer+`. The behaviour depends if `+_+` is on a base layer or a -while-held layer. When `+_+` is pressed on the active base layer, the key will -default to the corresponding `defsrc` key. If `+_+` is pressed on the active -while-held layer, the base layer's behaviour will activate. +WARNING: The `tap-hold` action and all variants can behave unexpectedly on Linux +with respect to repeat of antecedent key presses. +The full context is in https://github.com/jtroo/kanata/discussions/422[discussion #422]. +In brief, the workaround is to use `tap-hold` inside of <>, +combined with another key action that behaves as a no-op like `f24`. + +Example: `(multi f24 (tap-hold ...))` -.Example: -[source] ----- -(defsrc - a b c -) +The `+tap-hold+` action allows you to have one action/key for a "tap" and a +different action/key for a "hold". A tap is a rapid press then release of the +key whereas a hold is a long press. -(deflayer remap-only-c-to-d - _ _ d -) ----- +The action takes 4 parameters in the listed order: -[[no-op]] -=== No-op -<> +. tap timeout (unit: ms) +. hold timeout (unit: ms) +. tap action +. hold action -You may use the action `+XX+` as a "no operation" key, meaning pressing the key -will do nothing. This might be desirable in place of a transparent key on a -layer that is not fully mapped so that a key that is intentionally not mapped -will do nothing as opposed to typing a letter. +The tap timeout is the number of milliseconds within which a rapid +press+release+press of a key will result in the tap action being held instead +of the hold action activating. -.Example: -[source] ----- -(deflayer contains-no-op - XX a s d f -) ----- +.Tap timeout in more detail +[%collapsible,indent=4] +==== +The way a `tap-hold` action works with respect to the tap timeout +is often unclear to newcomers. +To make it concrete, the output event sequence of the `tap-hold` action +`(tap-hold $tap-timeout 200 a lctl)` +for varying values of `$tap-timeout` +with a fixed input event sequence will be described. -[[unicode]] -=== Unicode -<> +The input event sequence is: -The `+unicode+` action accepts a single unicode character. The character will -not be repeatedly typed if you hold the key down. +- press +- 50 ms elapses +- release +- 50 ms elapses +- press +- 300 ms elapses +- release -You may use a unicode character as an alias if desired. +With `(defvar $tap-timeout 0)`, the output event sequence is: -NOTE: The unicode action may not be correctly accepted by the active -application. +- 50 ms elapses +- press `a` +- release `a` +- 250 ms elapses +- press `lctl` +- 100 ms elapses +- release `lctl` -NOTE: If using Linux, make sure to look at the -<> in defcfg. +The above output sequence is the same for all `$tap-timeout` values +between and including `0` and `99`. -[source] ----- -(defalias - sml (unicode 😀) - 🙁 (unicode 🙁) -) -(deflayer has-happy-sad - @sml @🙁 a s d f -) ----- +For a value of `100` or greater for `$tap-timeout`, +the output event sequence is instead: -[[output-chordscombos]] -=== Output chords/combos -<> +- 50 ms elapses +- press `a` +- release `a` +- 50 ms elapses +- press `a` +- 300 ms elapses +- release `a` +==== -You may want to remap a key to automatically be pressed in combination with -modifiers such as Control or Shift. You can achieve this by prefixing the -normal key name with one or more of: +The hold timeout is the number of milliseconds after which the hold action will +activate. -* `+C-+`: Left Control -* `+A-+`: Left Alt -* `+S-+`: Left Shift -* `+M-+`: Left Meta, a.k.a. Windows, GUI, Command, Super -* `+RA-+` or `+AG+`: Right Alt, a.k.a. AltGr +There are two additional variants of `+tap-hold+`: -These modifiers may be combined together if desired. +* `+tap-hold-press+` +** If there is a press of a different key, the hold action is activated even if +the hold timeout hasn't expired yet +* `+tap-hold-release+` +** If there is a press+release of a different key, the hold action is activated +even if the hold timeout hasn't expired yet + +These variants may be useful if you want more responsive tap-hold keys, +but you should be wary of activating the hold action unintentionally. .Example: [source] ---- (defalias - ;; Type exclamation mark (US layout) - ex! S-1 - ;; Ctrl+C: send SIGINT to a Linux terminal program - int C-c - ;; Win+Tab: open Windows' Task View - tsk M-tab - ;; Ctrl+Shift+(C|V): copy or paste from certain terminal programs - cpy C-S-c - pst C-S-v + anm (tap-hold 200 200 a @num) ;; tap: a hold: numbers layer + oar (tap-hold-press 200 200 o @arr) ;; tap: o hold: arrows layer + ech (tap-hold-release 200 200 e @chr) ;; tap: e hold: chords layer ) ---- -A special behaviour of output chords is that if another key is pressed, -all of the chord keys will be released -before the newly pressed key action activates. -Output chords are typically used do one-off actions such as: +There are further additional variants of `tap-hold-press` and `tap-hold-release`: -- type a symbol, e.g. `S-1` -- type a special/accented character, e.g. `RA-a` -- do a special action like `C-c` to send `SIGTERM` in the terminal +- `tap-hold-press-timeout` +- `tap-hold-release-timeout` -The modifier pressed with these one-off action -is usually not desired for subsequent actions. +These variants take a 5th parameter, in addition to the same 4 as the other +variants. The 5th parameter is another action, which will activate if the hold +timeout expires as opposed to being triggered by other key actions, whereas the +non `-timeout` variants will activate the hold action in both cases. -[[repeat-key]] -=== Repeat key -<> +- `tap-hold-release-keys` -The action `+rpt+` repeats the most recently typed key. Holding down this key -will not repeatedly send the key. The intended use case is to be able to use a -different finger or even thumb key to repeat a typed key, as opposed to -double-tapping a key. +This variant takes a 5th parameter which is a list of keys +that trigger an early tap +when they are pressed while the `tap-hold-release-keys` action is waiting. +Otherwise this behaves as `tap-hold-release`. + +The keys in the 5th parameter correspond to the physical input keys, +or in other words the key that corresponds to `defsrc`. +This is in contrast to the `fork` and `switch` actions +which operates on outputted keys, or in other words the outputs +that are in `deflayer`, `defalias`, etc. for the corresponding `defsrc` key. .Example: [source] ---- -(deflayer has-repeat - rpt a s d f +(defalias + ;; tap: o hold: arrows layer timeout: backspace + oat (tap-hold-press-timeout 200 200 o @arr bspc) + ;; tap: e hold: chords layer timeout: esc + ect (tap-hold-release-timeout 200 200 e @chr esc) + ;; tap: u hold: misc layer early tap if any of: (a o e) are pressed + umk (tap-hold-release-keys 200 200 u @msc (a o e)) ) ---- -The `rpt` action only repeats the last key output. -For example, it won't output a chord like `ctrl+c` -if the previous key pressed was `C-c`. -The `rpt` action will only output `c` in this case. +- `tap-hold-except-keys` -There is a variant `rpt-any` -which will repeat any previous action -and would output `ctrl+c` in the example case. +This variant takes a 5th parameter which is a list of keys +that always trigger a tap +when they are pressed while the `tap-hold-except-keys` action is waiting. +No key is ever output until there is either a release of the key or any other +key is pressed. This differs from `tap-hold` behaviour. + +The keys in the 5th parameter correspond to the physical input keys, +or in other words the key that corresponds to `defsrc`. +This is in contrast to the `fork` and `switch` actions +which operates on outputted keys, or in other words the outputs +that are in `deflayer`, `defalias`, etc. for the corresponding `defsrc` key. +.Example: +[source] ---- -(deflayer has-repeat-any - rpt-any a s d f +(defalias + ;; tap: o hold: arrows layer timeout: backspace + oat (tap-hold-press-timeout 200 200 o @arr bspc) + ;; tap: e hold: chords layer timeout: esc + ect (tap-hold-release-timeout 200 200 e @chr esc) + ;; tap: u hold: misc layer always tap if any of: (a o e) are pressed + umk (tap-hold-except-keys 200 200 u @msc (a o e)) ) ---- -[[release-a-key-or-layer]] -=== Release a key or layer +[[macro]] +=== macro <> -You can release a held key or layer via these actions: - -* `release-key`: release a key, accepts `defsrc` compatible names -* `release-layer`: release a while-held layer +The `+macro+` action will tap a sequence of keys with optional +delays. This is different from `+multi+` because in the `+multi+` action, +all keys are held, whereas in `+macro+`, keys are pressed then released. -An example practical use case for `release-key` is seen in the `multi` section -directly below. +This means that with `+macro+` you can have some letters capitalized and others +not. This is not possible with `+multi+`. -There is currently no known practical use case for -`release-layer`, but it exists nonetheless. +The `+macro+` action accepts one or more keys, some actions, chords, and delays +(unit: ms). It also accepts a list prefixed with <> +modifiers where the list is subject to the aforementioned restrictions. The +number keys will be parsed as delays, so they must be aliased to be used in a macro. -[[multi]] -=== multi -<> +Up to 4 macros can be active at the same time. -The `+multi+` action executes multiple keys or actions in order but also -simultaneously. It accepts one or more actions. +The actions supported in `+macro+` are: -An example use case is to press the "Alt" key while also activating another -layer. +* <> +* <> +* <> +* <> +* <> +* <> +* <> +* <> +* <> +* <> -In the example below, holding the physical "Alt" key will result in a held -layer being activated while also holding "Alt" itself. The held layer operates -nearly the same as the standard keyboard, so for example the sequence (hold -Alt)+(Tab+Tab+Tab) will work as expected. This is in contrast to having a layer -where `tab` is mapped to `A-tab`, which results in repeated press+release of -the two keys and has different behaviour than expected. Some special keys will -release the "Alt" key and do some other action that requires "Alt" to be -released. In other words, the "Alt" key serves a dual purpose of still -fulfilling the "Alt" key role for some button presses (e.g. Tab), but also as a -new layer for keys that aren't typically used with "Alt" to have added useful -functionality. +NOTE: Some of these actions may need short delays between. +For example, `(macro a (unmod b) 5 (unmod c) d))` +needs the delay of `5` to work correctly. +.Example: [source] ---- (defalias - atl (multi alt (layer-while-held alted-with-exceptions)) - lft (multi (release-key alt) left) ;; release alt if held and also press left - rgt (multi (release-key alt) rght) ;; release alt if held and also press rght -) + : S-; + 8 8 + 0 0 + 🙃 (unicode 🙃) -(defsrc - alt a s d f -) + ;; Type "http://localhost:8080" + lch (macro h t t p @: / / 100 l o c a l h o s t @: @8 @0 @8 @0) -(deflayer base - @atl _ _ _ _ -) + ;; Type "I am HAPPY my FrIeNd 🙃" + hpy (macro S-i spc a m spc S-(h a p p y) spc m y S-f r S-i e S-n d spc @🙃) -(deflayer alted-with-exceptions - _ _ _ @lft @rgt + ;; alt-tab(x3) and alt-shift-tab(x3) with macro + tfd (macro A-(tab 200 tab 200 tab)) + tbk (macro A-S-(tab 200 tab 200 tab)) ) ---- -WARNING: This action can sometimes behave in surprising ways -with regards to simultaneity and order of actions. -For example, an action like `(multi sldr ')` will not behave as expected. -Due to implementation details, `sldr` will activate after the `'` -even though it is listed before. -This example could instead be written as `(macro sldr 10 ')`, -and that would work as intended. -It is recommended to avoid `multi` if it can be replaced -with a different action like `macro` or an output chord. +There is a variant of the `+macro+` action that will cancel all active macros +upon releasing the key: `+macro-release-cancel+`. It is parsed identically to +the non-cancelling version. An example use case for this action is holding down +a key to get different outputs, similar to tap-dance but one can see which keys +are being outputted. -[[mouse-actions]] -=== Mouse actions -<> +E.g. in the example below, when holding the key, first `1` is typed, then +replaced by `!` after 500ms, and finally that is replaced by `@` after another +500ms. However, if the key is released, the last character typed will remain +and the rest of the macro does not run. -You can click the left, middle, and right buttons using kanata actions, do -vertical/horizontal scrolling, and move the mouse. +[source] +---- +(defalias + 1 1 -[[mouse-buttons]] -==== Mouse buttons + ;; macro-release-cancel to output different characters with visual feedback + ;; after holding for different amounts of time. + 1!@ (macro-release-cancel @1 500 bspc S-1 500 bspc S-2) +) +---- + +There are further variants of the two `macro` actions which repeat while held. +The repeat will only occur once all macros have completed, +including the held macro key. +If multiple repeating macros are being held simulaneously, +only the most recently pressed macro will be repeated. + +[source] +---- +(defalias + mr1 (macro-repeat mltp) + mr2 (macro-repeat-release-cancel mltp) +) +---- + +[[dynamic-macro]] +=== dynamic-macro <> -The mouse button actions are: +The dynamic-macro actions allow for recording and playing key presses. The +dynamic macro records physical key presses, as opposed to kanata's outputs. +This allows the dynamic macro to replicate any action, but it means that if +the macro starts and ends on different layers, then the macro might not be +properly repeatable. -* `mlft`: left mouse button -* `mmid`: middle mouse button -* `mrgt`: right mouse button -* `mfwd`: forward mouse button -* `mbck`: backward mouse button +The action `dynamic-macro-record` accepts one number (0-65535), which represents +the macro ID. Activating this action will begin recording physical key inputs. +If `dynamic-macro-record` with the same ID is pressed again, the recording will +end and be saved. If `dynamic-macro-record` with a different ID is pressed then +the current recording will end and be saved, then a new recording with the new +ID will begin. -The mouse button will be held while the key mapped to it is held. +The action `dynamic-macro-record-stop` will stop and save any active recording. +There is a variant of this: +`dynamic-macro-record-stop-truncate` +This is a list action that takes a single parameter: +the number of key actions to remove at the end of a dynamic macro. +This variant is useful if the macro stop button is on a different layer. -If there are multiple mouse click actions within a single multi action, e.g. +The action `dynamic-macro-play` accepts one number (0-65535), which represents +the macro ID. Activating this action will play the saved recording of physical +keys from a previous `dynamic-macro-record` with the same macro ID, if it exists. -`+(multi mrgt mlft)+` +One can nest dynamic macros within each other, e.g. activate +`(dynamic-macro-play 1)` while recording with `(dynamic-macro-record 0)`. +However, dynamic macros cannot recurse; e.g. activating `(dynamic-macro-play 0)` +while recording with `(dynamic-macro-record 0)` will be ignored. -then all the buttons except the last will be clicked then unclicked. The last -button will remain held until key release. In the example above, pressing then -releasing the key mapped to this action will result in the following event -sequence: +.Example: +[source] +---- +(defalias + dr0 (dynamic-macro-record 0) + dr1 (dynamic-macro-record 1) + dr2 (dynamic-macro-record 2) + dp0 (dynamic-macro-play 0) + dp1 (dynamic-macro-play 1) + dp2 (dynamic-macro-play 2) + dms dynamic-macro-record-stop + dst (dynamic-macro-record-stop-truncate 1) +) +---- -. press key mapped to `+multi+` -. click right mouse button -. unclick right mouse button -. click left mouse button -. release key mapped to `+multi+` -. release left mouse button +[[fork]] +=== fork +<> -There are variants of the standard mouse buttons which "tap" the button. Rather -than holding the button while the key is held, a mouse click will be -immediately followed by the release. Nothing happens when the key is released. -The actions are as follows: +The fork action accepts two actions and a key list. +The first (left) action will activate by default. +The second (right) action will activate +if any of the keys in the third parameter (right-trigger-keys) are currently active. -* `mltp`: tap left mouse button -* `mmtp`: tap middle mouse button -* `mrtp`: tap right mouse button -* `mftp`: tap forward mouse button -* `mbtp`: tap bacward mouse button +.Example: +[source] +---- +(defalias + frk (fork k @special (lalt ralt)) +) +---- -[[mouse-wheel]] -==== Mouse wheel +[[caps-word]] +=== caps-word <> -The mouse wheel actions are: +The `caps-word` action triggers a state where the `lsft` key +will be added to the active key list +when a set of specific keys are active. +The keys are: `a-z` and `-`, which will be outputted as `A-Z` and `_` +respectively when using the US layout. -* `mwheel-up`: vertical scroll up -* `mwheel-down`: vertical scroll down -* `mwheel-left`: horizontal scroll left -* `mwheel-right`: horizontal scroll right +Examples where this is helpful +is capitalizing a single important word +like in `IMPORTANT!` +or defining a constant in code +like `const P99_99_VALUE: ...`. +This has an advantage over the regular caps lock +because it automatically ends +so it doesn't need to be toggled off manually, +and it also shifts `-` to `_` +which caps lock does not do. -All of these actions accept two number strings. The first is the interval -(unit: ms) between scroll actions. The second number is the distance -(unit: arbitrary). In both Windows and Linux, 120 distance units is equivalent -to a notch movement on a physical wheel. You can play with the parameters to -see what feels correct to you. Both numbers must be in the range [1,65535]. +The `caps-word` state ends when the keyboard is idle +for the duration of the defined timeout (1st parameter), +or a terminating key is pressed. +Every key is a terminating key +except the keys which get capitalized +and the extra keys in this list: -NOTE: In Linux, not all desktop environments support the `REL_WHEEL_HI_RES` event. -If this is the case for yours, -it will likely be a better experience to use a distance value that is a multiple of 120. +- `0-9` +- `kp0-kp9` +- `bspc del` +- `up down left rght` -On Linux and Interception, you can also choose to read from a mouse device. -When doing so, using the `mwu`, `mwd`, `mwl`, `mwr` key names in `defsrc` -allow you to remap the mouse scroll up/down/left/right actions like you would -with keyboard keys. +You can use `caps-word-custom` instead of `caps-word` +if you want to manually define which keys are capitalized (2nd parameter) +and what the extra non-terminal+non-capitalized keys should be (3rd parameter). -NOTE: If you are using a high-resolution mouse in Linux, -only a full "notch" of the scroll wheel will activate the action. +[source] +---- +(defalias + cw (caps-word 2000) -NOTE: If you are using a high-resolution mouse with Interception, -you will probably get way more events than you intended. + ;; This example is similar to the default caps-word behaviour but it moves the + ;; 0-9 keys to the capitalized key list from the extra non-terminating key list. + cwc (caps-word-custom + 2000 + (a b c d e f g h i j k l m n o p q r s t u v w x y z 0 1 2 3 4 5 6 7 8 9) + (kp0 kp1 kp2 kp3 kp4 kp5 kp6 kp7 kp8 kp9 bspc del up down left rght) + ) +) +---- -[[mouse-movement]] -==== Mouse movement +=== unmod[[unmod]] <> -The mouse movement actions are: +The `unmod` action will release all modifiers temporarily +and send one or more keys. +After the `unmod` key is released, the released modifiers are pressed again. +The modifiers affected are: `lsft,rsft,lctl,rctl,lmet,rmet,lalt,ralt`. -* `movemouse-up` -* `movemouse-down` -* `movemouse-left` -* `movemouse-right` +A variant of `unmod` is `unshift`. +This action only releases the `lsft,rsft` keys. +This can be useful for forcing unshifted keys while AltGr is still held. -Similar to the mouse wheel actions, all of these actions accept two number strings. -The first is the interval (unit: ms) between movement actions and the second number -is the distance (unit: pixels) of each movement. +.Example: +[source] +---- +(defalias + ;; holding shift and tapping a @um1 key will still output 1. + um1 (unmod 1) + ;; dead keys é (as opposed to using AltGr) that outputs É when shifted + dké (macro (unmod ') e) -The following are variants of the above mouse movements that apply linear mouse -acceleration from the minimum distance to the maximum distance as the mapped key is held. + ;; In ISO German QWERTZ, force unshifted symbols even if shift is held + { (unshift ralt 7) + [ (unshift ralt 8) +) +---- -* `movemouse-accel-up` -* `movemouse-accel-down` -* `movemouse-accel-left` -* `movemouse-accel-right` +[[cmd]] +=== cmd +<> -All these actions accept four number strings. The first number is the -interval (unit: ms) between movement actions. The second number is the time it -takes (unit: ms) to linearly ramp up from the minimum distance to the maximum -distance. The third and fourth numbers are the minimum and maximum distances -(unit: pixels) of each movement. +WARNING: +This action does not work unless you use the appropriate binary, +or if compiling yourself, the appropriate feature flag. +Additionally you must add the <> `defcfg` entry. -There is a toggable defcfg option related to `movemouse-accel` - <>. You might want to enable it, especially if you're coming from QMK. +The `+cmd+` action executes a program with arguments. It accepts one or more +strings. The first string is the program that will be run and the following +strings are arguments to that program. The arguments are provided to the +program in the order written in the config file. -[[set-mouse]] -==== Set absolute mouse position -<> +NOTE: The command is executed directly and not via a shell, so you cannot make +use of environment variables, e.g. `+~+` or `+$HOME+` in Linux will not be +substituted with your home directory. -The action `setmouse` sets the absolute mouse position. +.Example: +[source] +---- +(defalias + cm1 (cmd rm -fr /tmp/testing) -WARNING: This is only supported in Windows right now. -For an interesting keyboard-centric mouse solution in Linux, -try looking at -https://github.com/rvaiya/warpd[warpd]. + ;; You can use bash -c and then a quoted string to execute arbitrary text in + ;; bash. All text within double-quotes is treated as a single string. + cm2 (cmd bash -c "echo hello world") +) +---- -This list action takes two parameters which are `x` and `y` positions -of the absolute movement. -The values go from 0,0 which is the upper-left corner of the screen -to 65535,65535 which is the lower-right corner of the screen. -If you have multiple monitors, -`setmouse` treats them all as a single large screen. -This can make it a little confusing for how to set the `x, y` values -to get the positions that you want. -Experimentation will be needed. +There is a variant of `cmd`: `cmd-output-keys`. This variant reads the output +of the executed program and reads it as an S-expression, similarly to the +<>. However — unlike macro — only keys, chords, and +chorded lists are supported. Delays and other actions are not supported. -[[mouse-speed]] -==== Modify the speed of mouse movements +[source] +---- +(defalias + ;; bash: type date-time as YYYY-MM-DD HH:MM + pdb (cmd-output-keys bash -c "date +'%F %R' | sed 's/./& /g' | sed 's/:/S-;/g' | sed 's/\(.\{20\}\)\(.*\)/\(\1 spc \2\)/'") + + ;; powershell: type date-time as YYYY-MM-DD HH:MM + pdp (cmd-output-keys powershell.exe "echo '(' (((Get-Date -Format 'yyyy-MM-dd HH:mm').toCharArray() -join ' ').insert(20, ' spc ') -replace ':','S-;') ')'") +) +---- + +[[arbitrary-code]] +=== arbitrary-code <> -The action `movemouse-speed` modifies the speed at which `movemouse` and -`movemouse-accel` function at runtime. It does this by expanding or shrinking -`min_distance` and `max_distance` while the action key is pressed. +The `arbitrary-code` action allows sending an arbitrary number to kanata's +output mechanism. The press is sent when pressed, and the release sent when +released. This action can be useful for testing keys that are not yet named or +mapped in kanata. Please contribute findings with names and mappings, either in +a GitHub issue or as a pull request! -This action accepts one number (unit: percentage) by which the -mouse movements will be accelerated. +WARNING: This is not cross platform! -WARNING: Due to the nature of pixels being whole numbers, some values such as -33 may not result in an exact third of the distance. +WARNING: When using the Interception driver, this action is still sent over +SendInput. -.Example: [source] ---- (defalias - fst (movemouse-speed 200) - slw (movemouse-speed 50) + ab1 (arbitrary-code 700) ) ---- -[[mouse-all-actions-example]] -==== Mouse all actions example +[[global-overrides]] +== Global overrides <> +The `defoverrides` optional configuration item allows you to create global +key overrides, irrespective of what actions are used to generate those keys. +It accepts pairs of lists: + +1. the input key list that gets replaced +2. the output key list to replace the input keys with + +Both input and output lists accept 0 or more modifier keys (e.g. lctl, rsft) +and exactly 1 non-modifier key (e.g. 1, bspc). + +Only zero or one `defoverrides` is allowed in a configuration file. + +.Example: [source] ---- -(defalias - mwu (mwheel-up 50 120) - mwd (mwheel-down 50 120) - mwl (mwheel-left 50 120) - mwr (mwheel-right 50 120) +;; Swap numbers and their symbols with respect to shift +(defoverrides + (1) (lsft 1) + (2) (lsft 2) + ;; repeat for all remaining numbers - ms↑ (movemouse-up 1 1) - ms← (movemouse-left 1 1) - ms↓ (movemouse-down 1 1) - ms→ (movemouse-right 1 1) + (lsft 1) (1) + (lsft 2) (2) + ;; repeat for all remaining numbers +) +---- - ma↑ (movemouse-accel-up 1 1000 1 5) - ma← (movemouse-accel-left 1 1000 1 5) - ma↓ (movemouse-accel-down 1 1000 1 5) - ma→ (movemouse-accel-right 1 1000 1 5) +== Include other files[[include]] +<> - sm (setmouse 32228 32228) +The `include` optional configuration item +allows you to include other files into the configuration. +This configuration accepts a single string which is a file path. +The file path can be an absolute path or a relative path. +The path will be relative to the defined configuration file. - fst (movemouse-speed 200) +At the time of writing, includes can only be placed at the top level. +The included files also cannot contain includes themselves. + +.Example: +---- +;; This is in the file initially read by kanata, e.g. kanata.kbd +(include other-file.kbd) + +;; This is in the other file +(defalias + included-alias XX + ;; ... ) -(deflayer mouse - _ @mwu @mwd @mwl @mwr _ _ _ _ _ @ma↑ _ _ _ - _ pgup bck _ fwd _ _ _ _ @ma← @ma↓ @ma→ _ _ - _ pgdn mlft _ mrgt mmid _ mbck mfwd _ @ms↑ _ _ - @fst _ mltp _ mrtp mmtp _ mbtp mftp @ms← @ms↓ @ms→ - _ _ _ _ _ _ _ +;; This is in the other file +(deflayer included-layer + ;; ... ) ---- -[[tap-dance]] -=== tap-dance +[[optional-defcfg-options]] +== defcfg options + +[[danger-enable-cmd]] +=== danger-enable-cmd <> -The `+tap-dance+` action allows repeated tapping of a key to result in -different actions. It is followed by a timeout (unit: ms) and a list -of keys or actions. Each time the key is pressed, its timeout will reset. The -action will be chosen if one of the following events occur: +This option can be used to enable the `cmd` action in your configuration. The +`+cmd+` action allows kanata to execute programs with arguments passed to them. -* the timeout expires -* a different key is pressed -* the key is repeated up to the final action +This requires using a kanata program that is compiled with the `cmd` action +enabled. The reason for this is so that if you choose to, there is no way for +kanata to execute arbitrary programs even if you download some random +configuration from the internet. -You may put normal keys or other actions in `+tap-dance+`. +This configuration is disabled by default and can be enabled by giving it the +value `yes`. .Example: [source] ---- -(defalias - ;; 1 tap : "A" key - ;; 2 taps: Control+C - ;; 3 taps: Switch to another layer - ;; 4 taps: Escape key - td (tap-dance 200 (a C-c (layer-switch l2) esc)) +(defcfg + danger-enable-cmd yes ) ---- -There is a variant of `tap-dance` with the name `tap-dance-eager`. The variant -is parsed identically but the difference is that it will activate every -action in the sequence as the taps progress. - -In the example below, repeated taps will, in order: +[[sequence-timeout]] +=== sequence-timeout +<> -1. type `a` -2. erase the `a` and type `bb` -3. erase the `bb` and type `ccc` +This option customizes the key sequence timeout (unit: ms). Its default value +is 1000. The purpose of this item is explained in <>. +.Example: [source] ---- -(defalias - td2 (tap-dance-eager 500 ( - (macro a) ;; use macro to prevent auto-repeat of the key - (macro bspc b b) - (macro bspc bspc c c c) - )) +(defcfg + sequence-timeout 2000 ) ---- -[[one-shot]] -=== one-shot +[[sequence-input-mode]] +=== sequence-input-mode <> -The `+one-shot+` action is similar to "sticky keys", if you know what that is. -This activates an action or key until either the timeout expires or a different -key is used. The `+one-shot+` action must be followed by a timeout (unit: -ms) and another key or action. - -Some of the intended use cases are: - -* press a modifier for exactly one following key press -* switch to another layer for exactly one following key press - -If a `+one-shot+` key is held then it will act as the regular key. E.g. holding -a key assigned with `+@os1+` in the example below will keep Left Shift held for -every key, not just one, as long as it's still physically pressed. - -Pressing multiple `+one-shot+` keys in a row within the timeout will combine -the actions of those keys and reset the timeout to the value of the most -recently pressed `+one-shot+` key. +This option customizes the key sequence input mode. Its default value when not +configured is `hidden-suppressed`. -There are four variants of the `+one-shot+` action: +The options are: -- `+one-shot-press+`: - end on the first press of another key -- `+one-shot-release+`: - end on the first release of another key -- `+one-shot-press-pcancel+`: - end on the first press of another key - or on re-press of another active one-shot key -- `+one-shot-release-pcancel+`: - end on the first release of another key - or on re-press of another active one-shot key +- `visible-backspaced`: types sequence characters as they are inputted. The + typed characters will be erased with backspaces for a valid sequence termination. +- `hidden-suppressed`: hides sequence characters as they are typed. Does not + output the hidden characters for an invalid sequence termination. +- `hidden-delay-type`: hides sequence characters as they are typed. Outputs the + hidden characters for an invalid sequence termination either after a + timeout or after a non-sequence key is typed. -It is important to note that the first activation of a one-shot key -determines the behaviour with regards to the 4 variants -for all subsequent one-shot key activations, -even if a following one-shot key has a different configuration -than the initial key pressed. +For `visible-backspaced` and `hidden-delay-type`, a sequence leader input will +be ignored if a sequence is already active. For historical reasons, and in case +it is desired behaviour, a sequence leader input using `hidden-suppressed` will +reset the key sequence. -The default name `+one-shot+` corresponds to `+one-shot-press+`. +See <> for more about sequences. .Example: [source] ---- -(defalias - os1 (one-shot 500 (layer-while-held another-layer)) - os2 (one-shot-press 2000 lsft) - os3 (one-shot-release 2000 lctl) - os4 (one-shot-press-pcancel 2000 lalt) - os5 (one-shot-release-pcancel 2000 lmet) +(defcfg + sequence-input-mode visible-backspaced ) ---- -[[tap-hold]] -=== tap-hold +[[sequence-backtrack-modcancel]] +=== sequence-backtrack-modcancel <> -WARNING: The `tap-hold` action and all variants can behave unexpectedly on Linux -with respect to repeat of antecedent key presses. -The full context is in https://github.com/jtroo/kanata/discussions/422[discussion #422]. -In brief, the workaround is to use `tap-hold` inside of <>, -combined with another key action that behaves as a no-op like `f24`. + -Example: `(multi f24 (tap-hold ...))` - -The `+tap-hold+` action allows you to have one action/key for a "tap" and a -different action/key for a "hold". A tap is a rapid press then release of the -key whereas a hold is a long press. - -The action takes 4 parameters in the listed order: - -. tap timeout (unit: ms) -. hold timeout (unit: ms) -. tap action -. hold action - -The tap timeout is the number of milliseconds within which a rapid -press+release+press of a key will result in the tap action being held instead -of the hold action activating. +This option customizes the behaviour of key sequences +when modifiers are used. +The default is `yes` and can be overridden to `no` if desired. -.Tap timeout in more detail -[%collapsible,indent=4] -==== -The way a `tap-hold` action works with respect to the tap timeout -is often unclear to newcomers. -To make it concrete, the output event sequence of the `tap-hold` action -`(tap-hold $tap-timeout 200 a lctl)` -for varying values of `$tap-timeout` -with a fixed input event sequence will be described. +Setting it to `yes` allows both `fk1` and `fk2` to be activated +in the following configuration, but with `no`, +`fk1` will be impossible to activate -The input event sequence is: +---- +(defseq + fk1 (lsft a b) + fk2 (S-(c d)) +) +---- -- press -- 50 ms elapses -- release -- 50 ms elapses -- press -- 300 ms elapses -- release +See <> for more about sequences and +https://github.com/jtroo/kanata/blob/main/docs/sequence-adding-chords-ideas.md[this document] +for more context about this specific configuration. -With `(defvar $tap-timeout 0)`, the output event sequence is: +.Example: +[source] +---- +(defcfg + sequence-backtrack-modcancel no +) +---- -- 50 ms elapses -- press `a` -- release `a` -- 250 ms elapses -- press `lctl` -- 100 ms elapses -- release `lctl` +[[log-layer-changes]] +=== log-layer-changes +<> -The above output sequence is the same for all `$tap-timeout` values -between and including `0` and `99`. +By default, kanata will log layer changes. However, logging has some processing +overhead. If you do not care for the logging, you can choose to disable it. -For a value of `100` or greater for `$tap-timeout`, -the output event sequence is instead: +.Example: +[source] +---- +(defcfg + log-layer-changes no +) +---- -- 50 ms elapses -- press `a` -- release `a` -- 50 ms elapses -- press `a` -- 300 ms elapses -- release `a` -==== +[[delegate-to-first-layer]] +=== delegate-to-first-layer +<> -The hold timeout is the number of milliseconds after which the hold action will -activate. -There are two additional variants of `+tap-hold+`: +By default, transparent keys on layers +will delegate to the corresponding defsrc key +when found on a layer activated by `layer-switch`. -* `+tap-hold-press+` -** If there is a press of a different key, the hold action is activated even if -the hold timeout hasn't expired yet -* `+tap-hold-release+` -** If there is a press+release of a different key, the hold action is activated -even if the hold timeout hasn't expired yet +This config entry changes the behaviour +to delegate to the action in the same position on the first layer defined +in the configuration, which is the active layer on startup. -These variants may be useful if you want more responsive tap-hold keys, -but you should be wary of activating the hold action unintentionally. +For more context, see https://github.com/jtroo/kanata/issues/435. .Example: [source] ---- -(defalias - anm (tap-hold 200 200 a @num) ;; tap: a hold: numbers layer - oar (tap-hold-press 200 200 o @arr) ;; tap: o hold: arrows layer - ech (tap-hold-release 200 200 e @chr) ;; tap: e hold: chords layer +(defcfg + delegate-to-first-layer yes ) ---- -There are further additional variants of `tap-hold-press` and `tap-hold-release`: - -- `tap-hold-press-timeout` -- `tap-hold-release-timeout` - -These variants take a 5th parameter, in addition to the same 4 as the other -variants. The 5th parameter is another action, which will activate if the hold -timeout expires as opposed to being triggered by other key actions, whereas the -non `-timeout` variants will activate the hold action in both cases. - -- `tap-hold-release-keys` -This variant takes a 5th parameter which is a list of keys -that trigger an early tap -when they are pressed while the `tap-hold-release-keys` action is waiting. -Otherwise this behaves as `tap-hold-release`. +[[movemouse-inherit-accel-state]] +=== movemouse-inherit-accel-state +<> -The keys in the 5th parameter correspond to the physical input keys, -or in other words the key that corresponds to `defsrc`. -This is in contrast to the `fork` and `switch` actions -which operates on outputted keys, or in other words the outputs -that are in `deflayer`, `defalias`, etc. for the corresponding `defsrc` key. +By default `movemouse-accel` actions will track the acceleration +state for vertical and horizontal axes separately. + +When this setting is enabled, `movemouse-accel` will behave exactly like mouse movements in https://qmk.fm[QMK], +i.e. the acceleration state of new mouse +movement actions will be inherited if others are already being pressed. .Example: [source] ---- -(defalias - ;; tap: o hold: arrows layer timeout: backspace - oat (tap-hold-press-timeout 200 200 o @arr bspc) - ;; tap: e hold: chords layer timeout: esc - ect (tap-hold-release-timeout 200 200 e @chr esc) - ;; tap: u hold: misc layer early tap if any of: (a o e) are pressed - umk (tap-hold-release-keys 200 200 u @msc (a o e)) +(defcfg + movemouse-inherit-accel-state yes ) ---- -- `tap-hold-except-keys` +[[movemouse-smooth-diagonals]] +=== movemouse-smooth-diagonals +<> -This variant takes a 5th parameter which is a list of keys -that always trigger a tap -when they are pressed while the `tap-hold-except-keys` action is waiting. -No key is ever output until there is either a release of the key or any other -key is pressed. This differs from `tap-hold` behaviour. +By default, mouse movements move one direction at a time +and vertical/horizontal movements are on independent timers. -The keys in the 5th parameter correspond to the physical input keys, -or in other words the key that corresponds to `defsrc`. -This is in contrast to the `fork` and `switch` actions -which operates on outputted keys, or in other words the outputs -that are in `deflayer`, `defalias`, etc. for the corresponding `defsrc` key. +This can result in non-smooth diagonals when drawing a line in some app. +This option adds a small imperceptible amount of latency to +synchronize the mouse movements. .Example: [source] ---- -(defalias - ;; tap: o hold: arrows layer timeout: backspace - oat (tap-hold-press-timeout 200 200 o @arr bspc) - ;; tap: e hold: chords layer timeout: esc - ect (tap-hold-release-timeout 200 200 e @chr esc) - ;; tap: u hold: misc layer always tap if any of: (a o e) are pressed - umk (tap-hold-except-keys 200 200 u @msc (a o e)) +(defcfg + movemouse-smooth-diagonals yes ) ---- -[[macro]] -=== macro +=== dynamic-macro-max-presses [[dynamic-macro-max-presses]] <> -The `+macro+` action will tap a sequence of keys with optional -delays. This is different from `+multi+` because in the `+multi+` action, -all keys are held, whereas in `+macro+`, keys are pressed then released. +This configuration allows you to customize the length limit on dynamic macros. +The default length limit is 128 keys. -This means that with `+macro+` you can have some letters capitalized and others -not. This is not possible with `+multi+`. +.Example: +[source] +---- +(defcfg + dynamic-macro-max-presses 1000 +) +---- -The `+macro+` action accepts one or more keys, some actions, chords, and delays -(unit: ms). It also accepts a list prefixed with <> -modifiers where the list is subject to the aforementioned restrictions. The -number keys will be parsed as delays, so they must be aliased to be used in a macro. +=== concurrent-tap-hold [[concurrent-tap-hold]] +This configuration makes multiple tap-hold actions +that are activated near in time expire their timeout quicker. +By default this is disabled. +When disabled, the timeout for a following tap-hold +will start from 0ms **after** the previous tap-hold expires. +When enabled, the timeout will start +as soon as the tap-hold action is pressed +even if a previous tap-hold action is still held and has not expired. -Up to 4 macros can be active at the same time. +.Example: +[source] +---- +(defcfg + concurrent-tap-hold yes +) +---- -The actions supported in `+macro+` are: +[[linux-only-linux-dev]] +=== Linux only: linux-dev +<> +By default, kanata will try to detect which input devices are keyboards and try +to intercept them all. However, you may specify exact keyboard devices from the +`/dev/input` directories using the `linux-dev` configuration. -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> +.Example: +[source] +---- +(defcfg + linux-dev /dev/input/by-path/platform-i8042-serio-0-event-kbd +) +---- -NOTE: Some of these actions may need short delays between. -For example, `(macro a (unmod b) 5 (unmod c) d))` -needs the delay of `5` to work correctly. +If you want to specify multiple keyboards, you can separate the paths with a +colon `+:+`. .Example: [source] ---- -(defalias - : S-; - 8 8 - 0 0 - 🙃 (unicode 🙃) - - ;; Type "http://localhost:8080" - lch (macro h t t p @: / / 100 l o c a l h o s t @: @8 @0 @8 @0) +(defcfg + linux-dev /dev/input/dev1:/dev/input/dev2 +) +---- - ;; Type "I am HAPPY my FrIeNd 🙃" - hpy (macro S-i spc a m spc S-(h a p p y) spc m y S-f r S-i e S-n d spc @🙃) +Due to using the colon to separate devices, if you have a device with colons in +its file name, you must escape those colons with backslashes: - ;; alt-tab(x3) and alt-shift-tab(x3) with macro - tfd (macro A-(tab 200 tab 200 tab)) - tbk (macro A-S-(tab 200 tab 200 tab)) +[source] +---- +(defcfg + linux-dev /dev/input/path-to\:device ) ---- -There is a variant of the `+macro+` action that will cancel all active macros -upon releasing the key: `+macro-release-cancel+`. It is parsed identically to -the non-cancelling version. An example use case for this action is holding down -a key to get different outputs, similar to tap-dance but one can see which keys -are being outputted. - -E.g. in the example below, when holding the key, first `1` is typed, then -replaced by `!` after 500ms, and finally that is replaced by `@` after another -500ms. However, if the key is released, the last character typed will remain -and the rest of the macro does not run. +Alternatively, you can use list syntax, where both backslashes and colons +are parsed literally. List items are separated by spaces or newlines. +Using quotation marks for each item is optional, and only required if an +item contains spaces. [source] ---- -(defalias - 1 1 - - ;; macro-release-cancel to output different characters with visual feedback - ;; after holding for different amounts of time. - 1!@ (macro-release-cancel @1 500 bspc S-1 500 bspc S-2) +(defcfg + linux-dev ( + /dev/input/path:to:device + "/dev/input/path to device" + ) ) ---- -There are further variants of the two `macro` actions which repeat while held. -The repeat will only occur once all macros have completed, -including the held macro key. -If multiple repeating macros are being held simulaneously, -only the most recently pressed macro will be repeated. +[[linux-only-linux-dev-names-include]] +=== Linux only: linux-dev-names-include +<> + +In the case that `linux-dev` is omitted, +this option defines a list of device names that should be included. +Device names that do not exist in the list will be ignored. +This option is parsed identically to `linux-dev`. + +Kanata will print device names on startup with log lines that look like below: + +---- +registering /dev/input/eventX: "Name goes here" +---- +.Example: [source] ---- -(defalias - mr1 (macro-repeat mltp) - mr2 (macro-repeat-release-cancel mltp) +(defcfg + linux-dev-names-include ( + "Device name 1" + "Device name 2" + ) ) ---- -[[dynamic-macro]] -=== dynamic-macro +[[linux-only-linux-dev-names-exclude]] +=== Linux only: linux-dev-names-exclude <> -The dynamic-macro actions allow for recording and playing key presses. The -dynamic macro records physical key presses, as opposed to kanata's outputs. -This allows the dynamic macro to replicate any action, but it means that if -the macro starts and ends on different layers, then the macro might not be -properly repeatable. +In the case that `linux-dev` is omitted, +this option defines a list of device names that should be excluded. +This option is parsed identically to `linux-dev`. -The action `dynamic-macro-record` accepts one number (0-65535), which represents -the macro ID. Activating this action will begin recording physical key inputs. -If `dynamic-macro-record` with the same ID is pressed again, the recording will -end and be saved. If `dynamic-macro-record` with a different ID is pressed then -the current recording will end and be saved, then a new recording with the new -ID will begin. +The `linux-dev-names-include` and `linux-dev-names-exclude` options +are not mutually exclusive +but in practice it probably only makes sense to use one and not both. -The action `dynamic-macro-record-stop` will stop and save any active recording. -There is a variant of this: -`dynamic-macro-record-stop-truncate` -This is a list action that takes a single parameter: -the number of key actions to remove at the end of a dynamic macro. -This variant is useful if the macro stop button is on a different layer. +.Example: +[source] +---- +(defcfg + linux-dev-names-exclude ( + "Device Name 1" + "Device Name 2" + ) +) +---- -The action `dynamic-macro-play` accepts one number (0-65535), which represents -the macro ID. Activating this action will play the saved recording of physical -keys from a previous `dynamic-macro-record` with the same macro ID, if it exists. +[[linux-only-linux-continue-if-no-devs-found]] +=== Linux only: linux-continue-if-no-devs-found +<> -One can nest dynamic macros within each other, e.g. activate -`(dynamic-macro-play 1)` while recording with `(dynamic-macro-record 0)`. -However, dynamic macros cannot recurse; e.g. activating `(dynamic-macro-play 0)` -while recording with `(dynamic-macro-record 0)` will be ignored. +By default, kanata will crash if no input devices are found. You can change +this behaviour by setting `linux-continue-if-no-devs-found`. .Example: [source] ---- -(defalias - dr0 (dynamic-macro-record 0) - dr1 (dynamic-macro-record 1) - dr2 (dynamic-macro-record 2) - dp0 (dynamic-macro-play 0) - dp1 (dynamic-macro-play 1) - dp2 (dynamic-macro-play 2) - dms dynamic-macro-record-stop - dst (dynamic-macro-record-stop-truncate 1) +(defcfg + linux-continue-if-no-devs-found yes ) ---- -[[fork]] -=== fork +[[linux-only-linux-unicode-u-code]] +=== Linux only: linux-unicode-u-code <> -The fork action accepts two actions and a key list. -The first (left) action will activate by default. -The second (right) action will activate -if any of the keys in the third parameter (right-trigger-keys) are currently active. +Unicode on Linux works by pressing Ctrl+Shift+U, typing the unicode hex value, +then pressing Enter. However, if you do remapping in userspace, e.g. via +xmodmap/xkb, the keycode "U" that kanata outputs may not become a keysym "u" +after the userspace remapping. This will be likely if you use non-US, +non-European keyboards on top of kanata. For unicode to work, kanata needs to +use the keycode that outputs the keysym "u", which might not be the keycode +"U". + +You can use `evtest` or `kanata --debug`, set your userspace key remapping, +then press the key that outputs the keysym "u" to see which underlying keycode +is sent. Then you can use this configuration to change kanata's behaviour. .Example: [source] ---- -(defalias - frk (fork k @special (lalt ralt)) +(defcfg + linux-unicode-u-code v ) ---- -[[caps-word]] -=== caps-word +[[linux-only-linux-unicode-termination]] +=== Linux only: linux-unicode-termination <> -The `caps-word` action triggers a state where the `lsft` key -will be added to the active key list -when a set of specific keys are active. -The keys are: `a-z` and `-`, which will be outputted as `A-Z` and `_` -respectively when using the US layout. - -Examples where this is helpful -is capitalizing a single important word -like in `IMPORTANT!` -or defining a constant in code -like `const P99_99_VALUE: ...`. -This has an advantage over the regular caps lock -because it automatically ends -so it doesn't need to be toggled off manually, -and it also shifts `-` to `_` -which caps lock does not do. - -The `caps-word` state ends when the keyboard is idle -for the duration of the defined timeout (1st parameter), -or a terminating key is pressed. -Every key is a terminating key -except the keys which get capitalized -and the extra keys in this list: - -- `0-9` -- `kp0-kp9` -- `bspc del` -- `up down left rght` +Unicode on Linux terminates with the Enter key by default. This may not work in +some applications. The termination is configurable with the following options: -You can use `caps-word-custom` instead of `caps-word` -if you want to manually define which keys are capitalized (2nd parameter) -and what the extra non-terminal+non-capitalized keys should be (3rd parameter). +- `enter` +- `space` +- `enter-space` +- `space-enter` +.Example: [source] ---- -(defalias - cw (caps-word 2000) - - ;; This example is similar to the default caps-word behaviour but it moves the - ;; 0-9 keys to the capitalized key list from the extra non-terminating key list. - cwc (caps-word-custom - 2000 - (a b c d e f g h i j k l m n o p q r s t u v w x y z 0 1 2 3 4 5 6 7 8 9) - (kp0 kp1 kp2 kp3 kp4 kp5 kp6 kp7 kp8 kp9 bspc del up down left rght) - ) +(defcfg + linux-unicode-termination space ) ---- -=== unmod[[unmod]] +=== Linux only: linux-x11-repeat-delay-rate[[linux-only-x11-repeat-rate]] <> -The `unmod` action will release all modifiers temporarily -and send one or more keys. -After the `unmod` key is released, the released modifiers are pressed again. -The modifiers affected are: `lsft,rsft,lctl,rctl,lmet,rmet,lalt,ralt`. +On Linux, you can tell kanata to run `xset r rate ` +on startup and on live reload +via the configuration item `linux-only-x11-repeat-rate`. +This takes two numbers separated by a comma. +The first number is the delay in ms +and the second number is the repeat rate in repeats/second. -A variant of `unmod` is `unshift`. -This action only releases the `lsft,rsft` keys. -This can be useful for forcing unshifted keys while AltGr is still held. +This configuration item does not affect Wayland or no-desktop environments. .Example: [source] ---- -(defalias - ;; holding shift and tapping a @um1 key will still output 1. - um1 (unmod 1) - ;; dead keys é (as opposed to using AltGr) that outputs É when shifted - dké (macro (unmod ') e) - - ;; In ISO German QWERTZ, force unshifted symbols even if shift is held - { (unshift ralt 7) - [ (unshift ralt 8) +(defcfg + linux-x11-repeat-delay-rate 400,50 ) ---- -[[cmd]] -=== cmd +[[macos-only-macos-dev-names-include]] +=== macOS only: macos-dev-names-include <> -The `+cmd+` action executes a program with arguments. It accepts one or more -strings. The first string is the program that will be run and the following -strings are arguments to that program. The arguments are provided to the -program in the order written in the config file. +This option defines a list of device names that should be included. +By default, kanata will try to detect which input devices are keyboards and try +to intercept them all. However, you may specify exact keyboard devices to intercept +using the `macos-dev-names-include` configuration. +Device names that do not exist in the list will be ignored. +This option is parsed identically to `linux-dev`. -NOTE: The command is executed directly and not via a shell, so you cannot make -use of environment variables, e.g. `+~+` or `+$HOME+` in Linux will not be -substituted with your home directory. +Use `kanata -l` or `kanata --list` to list the available keyboards. .Example: [source] ---- -(defalias - cm1 (cmd rm -fr /tmp/testing) - - ;; You can use bash -c and then a quoted string to execute arbitrary text in - ;; bash. All text within double-quotes is treated as a single string. - cm2 (cmd bash -c "echo hello world") -) ----- - -There is a variant of `cmd`: `cmd-output-keys`. This variant reads the output -of the executed program and reads it as an S-expression, similarly to the -<>. However — unlike macro — only keys, chords, and -chorded lists are supported. Delays and other actions are not supported. - -[source] ----- -(defalias - ;; bash: type date-time as YYYY-MM-DD HH:MM - pdb (cmd-output-keys bash -c "date +'%F %R' | sed 's/./& /g' | sed 's/:/S-;/g' | sed 's/\(.\{20\}\)\(.*\)/\(\1 spc \2\)/'") - - ;; powershell: type date-time as YYYY-MM-DD HH:MM - pdp (cmd-output-keys powershell.exe "echo '(' (((Get-Date -Format 'yyyy-MM-dd HH:mm').toCharArray() -join ' ').insert(20, ' spc ') -replace ':','S-;') ')'") +(defcfg + macos-dev-names-include ( + "Device name 1" + "Device name 2" + ) ) ---- -[[arbitrary-code]] -=== arbitrary-code +[[windows-only-windows-altgr]] +=== Windows only: windows-altgr <> -The `arbitrary-code` action allows sending an arbitrary number to kanata's -output mechanism. The press is sent when pressed, and the release sent when -released. This action can be useful for testing keys that are not yet named or -mapped in kanata. Please contribute findings with names and mappings, either in -a GitHub issue or as a pull request! - -WARNING: This is not cross platform! +There is an option for Windows to help mitigate the strange behaviour of AltGr +(ralt) if you're using that key in your defsrc. This is applicable for many +non-US layouts. You can use one of the listed values to change what kanata does +with the key: -WARNING: When using the Interception driver, this action is still sent over -SendInput. +* `cancel-lctl-press` +** This will remove the `lctl` press that is generated alonside `ralt` +* `add-lctl-release` +** This adds an `lctl` release when `ralt` is released +.Example: [source] ---- -(defalias - ab1 (arbitrary-code 700) +(defcfg + windows-altgr add-lctl-release ) ---- -[[global-overrides]] -== Global overrides +For more context, see: https://github.com/jtroo/kanata/issues/55. + +NOTE: Even with these workarounds, putting `+lctl+`+`+ralt+` in your defsrc may not +work properly with other applications that also use keyboard interception. +Known application with issues: GWSL/VcXsrv + +=== Windows only: windows-interception-mouse-hwid[[windows-only-windows-interception-mouse-hwid]] <> -The `defoverrides` optional configuration item allows you to create global -key overrides, irrespective of what actions are used to generate those keys. -It accepts pairs of lists: +This defcfg item allows you to intercept mouse buttons for a specific mouse +device. This only works with the Interception driver (the -wintercept variants +of the binary). -1. the input key list that gets replaced -2. the output key list to replace the input keys with +The intended use case for this is for laptops such as a Thinkpad, which have +mouse buttons that may be desirable to activate kanata actions with. -Both input and output lists accept 0 or more modifier keys (e.g. lctl, rsft) -and exactly 1 non-modifier key (e.g. 1, bspc). +To know what numbers to put into the string, you can run the variant with this +defcfg item defined with any numbers. Then when a button is first pressed on +the mouse device, kanata will print its hwid in the log; you can then +copy-paste that into this configuration entry. If this defcfg item is not +defined, the log will not print. -Only zero or one `defoverrides` is allowed in a configuration file. +https://github.com/jtroo/kanata/issues/108[Relevant issue]. .Example: [source] ---- -;; Swap numbers and their symbols with respect to shift -(defoverrides - (1) (lsft 1) - (2) (lsft 2) - ;; repeat for all remaining numbers - - (lsft 1) (1) - (lsft 2) (2) - ;; repeat for all remaining numbers +(defcfg + windows-interception-mouse-hwid "70, 0, 60, 0" ) ---- -== Include other files[[include]] +[[using-multiple-defcfg-options]] +=== Using multiple defcfg options <> -The `include` optional configuration item -allows you to include other files into the configuration. -This configuration accepts a single string which is a file path. -The file path can be an absolute path or a relative path. -The path will be relative to the defined configuration file. - -At the time of writing, includes can only be placed at the top level. -The included files also cannot contain includes themselves. +The `defcfg` entry is treated as a list with pairs of strings. For example: -.Example: +[source] +---- +(defcfg a 1 b 2) ---- -;; This is in the file initially read by kanata, e.g. kanata.kbd -(include other-file.kbd) -;; This is in the other file -(defalias - included-alias XX - ;; ... -) +This will be treated as configuration `a` having value `1` and configuration +`b` having value `2`. -;; This is in the other file -(deflayer included-layer - ;; ... +An example defcfg containing many of the options is shown below. It should be +noted options that are Linux-only, Windows-only, or macOS-only will be ignored when used on +a non-applicable operating system. + +[source] +---- +;; Don't actually use this exact configuration, +;; it's almost certainly not what you want. +(defcfg + process-unmapped-keys yes + danger-enable-cmd yes + sequence-timeout 2000 + sequence-input-mode visible-backspaced + sequence-backtrack-modcancel no + log-layer-changes no + delegate-to-first-layer yes + movemouse-inherit-accel-state yes + movemouse-smooth-diagonals yes + dynamic-macro-max-presses 1000 + linux-dev (/dev/input/dev1 /dev/input/dev2) + linux-dev-names-include ("Name 1" "Name 2") + linux-dev-names-exclude ("Name 3" "Name 4") + linux-continue-if-no-devs-found yes + linux-unicode-u-code v + linux-unicode-termination space + linux-x11-repeat-delay-rate 400,50 + windows-altgr add-lctl-release + windows-interception-mouse-hwid "70, 0, 60, 0" ) ---- From 3f413a00e05476c5750a0a64ff9761fcd177e69b Mon Sep 17 00:00:00 2001 From: jtroo Date: Fri, 2 Feb 2024 19:27:10 -0800 Subject: [PATCH 130/819] feat!: add concat keyword for defvar (#714) Add the `concat` keyword to `defvar`. Potentially a breaking change in case someone used the string `concat` as the first string in a list variable at some point. I find the probability of this to be low, but worth noting anyway. --- cfg_samples/kanata.kbd | 8 +++ docs/config.adoc | 45 ++++++++++---- parser/src/cfg/mod.rs | 37 +++++++++++- parser/src/cfg/sexpr.rs | 16 +++++ parser/src/cfg/tests.rs | 129 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 223 insertions(+), 12 deletions(-) diff --git a/cfg_samples/kanata.kbd b/cfg_samples/kanata.kbd index 1cb9cc201..0a62c00fe 100644 --- a/cfg_samples/kanata.kbd +++ b/cfg_samples/kanata.kbd @@ -291,6 +291,14 @@ If you need help, please feel welcome to ask in the GitHub discussions. hold-timeout 200 tt $tap-timeout ht $hold-timeout + + ;; A list value in defvar that begins with concat behaves in a special manner + ;; where strings will be joined together. + ;; + ;; Below results in 100200 + a "hello" + b "world" + ct (concat $a " " $b) ) (defalias diff --git a/docs/config.adoc b/docs/config.adoc index 55efb70e8..da248e02f 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -253,7 +253,7 @@ All other options can be found later in the <> section. The `process-unmapped-keys` option in `defcfg` is probably the most generally impactful option. Enabling this configuration makes kanata process keys -that are not defined in `defsrc'. +that are not defined in `defsrc`. This might be useful if you are only mapping a few keys in defsrc instead of most of the keys on your keyboard. @@ -390,11 +390,29 @@ Variables are referred to by prefixing their name with `$`. ) ---- +[[concat-in-defvar]] +==== concat in defvar + +Within the second item of `defvar`, +a list that begins with the special keyword `concat` will concatenate all +subsequent items in the list together into a single string value. +Without using `concat`, lists are saved as-is. + +.Example: +[source] +---- +(defvar + rootpath "/home/myuser/mysubdir" + ;; $otherpath will be the string: /home/myuser/mysubdir/helloworld + otherpath (concat $rootpath "/helloworld") +) +---- + [[actions]] == Actions -The actions kanata provides are what make it truly customizable. This section -explains the available actions. +The actions kanata provides are what make it truly customizable. +This section explains the available actions. [[live-reload]] === Live reload @@ -1382,19 +1400,26 @@ This can be useful for forcing unshifted keys while AltGr is still held. === cmd <> -WARNING: -This action does not work unless you use the appropriate binary, -or if compiling yourself, the appropriate feature flag. -Additionally you must add the <> `defcfg` entry. +WARNING: This action does not work unless you use the appropriate binary +or - if compiling yourself - the appropriate feature flag. +Additionally you must add the <> `defcfg` option. The `+cmd+` action executes a program with arguments. It accepts one or more strings. The first string is the program that will be run and the following strings are arguments to that program. The arguments are provided to the program in the order written in the config file. - -NOTE: The command is executed directly and not via a shell, so you cannot make -use of environment variables, e.g. `+~+` or `+$HOME+` in Linux will not be +Lists may also be used within `cmd` +which you may desire to do for reuse via `defvar`. +Lists will be flattened such that arguments are provided to the program +in the order written in the config file, regardless of list nesting. +To be technical, it would be a depth-first flattening (similar to DFS). + +NOTE: commands are executed directly and not via a shell, so you cannot make +use of environment variables or symbols with special meaning. +For example `+~+` or `+$HOME+` in Linux will not be substituted with your home directory. +If you want to execute with a shell program +use the shell as the first parameter, e.g. `bash` or `powershell.exe`. .Example: [source] diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index a47e4b059..f6a83dbef 100644 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -936,13 +936,16 @@ fn parse_vars(exprs: &[&Vec], s: &mut ParsedState) -> Result<()> { _ => bail_expr!(var_name_expr, "variable name must not be a list"), }; let var_expr = match subexprs.next() { - Some(v) => v, + Some(v) => match v { + SExpr::Atom(_) => v.clone(), + SExpr::List(l) => parse_list_var(l, s)?, + }, None => bail_expr!( var_name_expr, "variable key name has no action - you should add an action." ), }; - if s.vars.insert(var_name.into(), var_expr.clone()).is_some() { + if s.vars.insert(var_name.into(), var_expr).is_some() { bail_expr!(var_name_expr, "duplicate variable name: {}", var_name); } } @@ -950,6 +953,36 @@ fn parse_vars(exprs: &[&Vec], s: &mut ParsedState) -> Result<()> { Ok(()) } +fn parse_list_var(expr: &Spanned>, s: &ParsedState) -> Result { + let ret = match expr.t.first() { + Some(SExpr::Atom(a)) => match a.t.as_str() { + "concat" => { + let mut concat_str = String::new(); + for expr in expr.t.iter().skip(1) { + if let Some(a) = expr.atom(s.vars()) { + concat_str.push_str(a.trim_matches('"')); + } else if let Some(l) = expr.span_list(s.vars()) { + match parse_list_var(l, s)? { + SExpr::Atom(a) => concat_str.push_str(a.t.trim_matches('"')), + SExpr::List(_) => bail_expr!( + expr, + "concat must contain only strings or nested concat lists" + ), + }; + } + } + SExpr::Atom(Spanned { + span: expr.span.clone(), + t: concat_str, + }) + } + _ => SExpr::List(expr.clone()), + }, + _ => SExpr::List(expr.clone()), + }; + Ok(ret) +} + /// Parse alias->action mappings from multiple exprs starting with defalias. /// Mutates the input `s` by storing aliases inside. fn parse_aliases(exprs: &[&Vec], s: &mut ParsedState) -> Result<()> { diff --git a/parser/src/cfg/sexpr.rs b/parser/src/cfg/sexpr.rs index 5d8b69c14..66abe9301 100644 --- a/parser/src/cfg/sexpr.rs +++ b/parser/src/cfg/sexpr.rs @@ -164,6 +164,22 @@ impl SExpr { } } + pub fn span_list<'a>( + &'a self, + vars: Option<&'a HashMap>, + ) -> Option<&'a Spanned>> { + match self { + SExpr::List(l) => Some(l), + SExpr::Atom(a) => match (a.t.strip_prefix('$'), vars) { + (Some(varname), Some(vars)) => match vars.get(varname) { + Some(var) => var.span_list(Some(vars)), + None => None, + }, + _ => None, + }, + } + } + pub fn span(&self) -> Span { match self { SExpr::Atom(a) => a.span.clone(), diff --git a/parser/src/cfg/tests.rs b/parser/src/cfg/tests.rs index 27e6b0dee..be23e74ae 100644 --- a/parser/src/cfg/tests.rs +++ b/parser/src/cfg/tests.rs @@ -1460,3 +1460,132 @@ fn parse_cmd() { ) .expect("succeeds"); } + +#[test] +fn parse_defvar_concat() { + let _lk = match CFG_PARSE_LOCK.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + }; + + let source = r#" +(defsrc a) +(deflayer base a) +(defvar + x (concat a b c) + y (concat d (concat e f)) + z (squish squash (splish splosh)) + xx (concat $x $y) + xy (concat $x (concat $y)) + xz (notconcat a b " " c " d") + yx (concat a b " " c " d" (concat "efg" " hij ") "kl") + yz (concat "abc"def"ghi""jkl") + + rootpath "/home/myuser/mysubdir" + ;; $otherpath will be the string: /home/myuser/mysubdir/helloworld + otherpath (concat $rootpath "/helloworld") +) +"#; + let mut s = ParsedState::default(); + parse_cfg_raw_string( + source, + &mut s, + &PathBuf::from("test"), + &mut FileContentProvider { + get_file_content_fn: &mut |_| unimplemented!(), + }, + DEF_LOCAL_KEYS, + ) + .expect("succeeds"); + match s.vars().unwrap().get("x").unwrap() { + SExpr::Atom(a) => assert_eq!(&a.t, "abc"), + SExpr::List(l) => panic!("expected string not list: {l:?}"), + } + match s.vars().unwrap().get("y").unwrap() { + SExpr::Atom(a) => assert_eq!(&a.t, "def"), + SExpr::List(l) => panic!("expected string not list: {l:?}"), + } + match s.vars().unwrap().get("z").unwrap() { + SExpr::Atom(a) => panic!("expected list not string: {a:?}"), + SExpr::List(_) => {} + } + match s.vars().unwrap().get("xx").unwrap() { + SExpr::Atom(a) => assert_eq!(&a.t, "abcdef"), + SExpr::List(l) => panic!("expected string not list: {l:?}"), + } + match s.vars().unwrap().get("xy").unwrap() { + SExpr::Atom(a) => assert_eq!(&a.t, "abcdef"), + SExpr::List(l) => panic!("expected string not list: {l:?}"), + } + match s.vars().unwrap().get("xz").unwrap() { + SExpr::Atom(a) => panic!("expected list not string {a:?}"), + SExpr::List(_) => {} + } + match s.vars().unwrap().get("yx").unwrap() { + SExpr::Atom(a) => assert_eq!(&a.t, "ab c defg hij kl"), + SExpr::List(l) => panic!("expected string not list: {l:?}"), + } + match s.vars().unwrap().get("yz").unwrap() { + SExpr::Atom(a) => assert_eq!(&a.t, "abcdefghijkl"), + SExpr::List(l) => panic!("expected string not list: {l:?}"), + } + match s.vars().unwrap().get("otherpath").unwrap() { + SExpr::Atom(a) => assert_eq!(&a.t, "/home/myuser/mysubdir/helloworld"), + SExpr::List(l) => panic!("expected string not list: {l:?}"), + } +} + +#[test] +fn parse_defvar_concat_err1() { + let _lk = match CFG_PARSE_LOCK.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + }; + + let source = r#" +(defsrc a) +(deflayer base a) +(defvar + x (concat a b c (not nested concat)) +) +"#; + let mut s = ParsedState::default(); + parse_cfg_raw_string( + source, + &mut s, + &PathBuf::from("test"), + &mut FileContentProvider { + get_file_content_fn: &mut |_| unimplemented!(), + }, + DEF_LOCAL_KEYS, + ) + .expect_err("fails"); +} + +#[test] +fn parse_defvar_concat_err2() { + let _lk = match CFG_PARSE_LOCK.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + }; + + let source = r#" +(defsrc a) +(deflayer base a) +(defvar + x (list var uh oh) + y (concat a b c $x) +) +"#; + let mut s = ParsedState::default(); + parse_cfg_raw_string( + source, + &mut s, + &PathBuf::from("test"), + &mut FileContentProvider { + get_file_content_fn: &mut |_| unimplemented!(), + }, + DEF_LOCAL_KEYS, + ) + .expect_err("fails"); +} From 1251e75692cad0e70ed873c45a0a6bcf282915fa Mon Sep 17 00:00:00 2001 From: jtroo Date: Fri, 2 Feb 2024 21:39:33 -0800 Subject: [PATCH 131/819] feat: add block-unmapped-keys to defcfg (#715) Add a new defcfg option to block all unmapped (meaning not in defsrc) keys from doing anything. --- cfg_samples/kanata.kbd | 4 ++++ docs/config.adoc | 31 +++++++++++++++++++++++++------ parser/src/cfg/defcfg.rs | 5 +++++ parser/src/cfg/mod.rs | 24 ++++++++++++++---------- 4 files changed, 48 insertions(+), 16 deletions(-) diff --git a/cfg_samples/kanata.kbd b/cfg_samples/kanata.kbd index 0a62c00fe..093f400af 100644 --- a/cfg_samples/kanata.kbd +++ b/cfg_samples/kanata.kbd @@ -150,6 +150,10 @@ If you need help, please feel welcome to ask in the GitHub discussions. ;; ;; process-unmapped-keys yes + ;; Disable all keys not mapped in defsrc. + ;; Only works if process-unmapped-keys is also yes. + ;; block-unmapped-keys yes + ;; Intercept mouse buttons for a specific mouse device. ;; The intended use case for this is for laptops such as a Thinkpad, which have ;; mouse buttons that may be useful to activate kanata actions with. This only diff --git a/docs/config.adoc b/docs/config.adoc index da248e02f..0692a3606 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -287,14 +287,14 @@ it will be useful to first learn about aliases and variables. Using the `defalias` configuration entry, you can introduce a shortcut label for an action. -Similar to how `defcfg` works, `defalias` reads pairs of items in a sequence +The `defalias` entry reads pairs of items in a sequence where the first item in the pair is the alias name and the second item is the -action it can be substituted for. However, unlike `+defcfg+`, the second item -in `defalias` may be a "list" as opposed to a single string like it was in -`defcfg`. +action it can be substituted for. -A list is a sequence of strings separated by whitespace, surrounded by -parentheses. All of the configuration entries we've looked at so far are lists; +A list is a sequence of strings +or nested lists separated by whitespace, +surrounded by parentheses. +All of the configuration entries we've looked at so far are lists; `defalias` is where we'll first see nested lists in this guide. .Example: @@ -1745,6 +1745,25 @@ even if a previous tap-hold action is still held and has not expired. ) ---- +[[block-unmapped-keys]] +=== block-unmapped-keys +<> + +If you desire to use only a subset of your keyboard +you can use `block-unmapped-keys` to make every key +other than those that exist in `defsrc` a no-op. + +NOTE: this only functions correctly if you also set +<> to yes. + +.Example: +[source] +---- +(defcfg + block-unmapped-keys yes +) +---- + [[linux-only-linux-dev]] === Linux only: linux-dev <> diff --git a/parser/src/cfg/defcfg.rs b/parser/src/cfg/defcfg.rs index ccde86065..1ca854875 100644 --- a/parser/src/cfg/defcfg.rs +++ b/parser/src/cfg/defcfg.rs @@ -9,6 +9,7 @@ use crate::{anyhow_expr, anyhow_span, bail, bail_expr, bail_span}; #[derive(Debug)] pub struct CfgOptions { pub process_unmapped_keys: bool, + pub block_unmapped_keys: bool, pub enable_cmd: bool, pub sequence_timeout: u16, pub sequence_input_mode: SequenceInputMode, @@ -49,6 +50,7 @@ impl Default for CfgOptions { fn default() -> Self { Self { process_unmapped_keys: false, + block_unmapped_keys: false, enable_cmd: false, sequence_timeout: 1000, sequence_input_mode: SequenceInputMode::HiddenSuppressed, @@ -275,6 +277,9 @@ pub fn parse_defcfg(expr: &[SExpr]) -> Result { "process-unmapped-keys" => { cfg.process_unmapped_keys = parse_defcfg_val_bool(val, label)? } + "block-unmapped-keys" => { + cfg.block_unmapped_keys = parse_defcfg_val_bool(val, label)? + } "danger-enable-cmd" => cfg.enable_cmd = parse_defcfg_val_bool(val, label)?, "sequence-backtrack-modcancel" => { cfg.sequence_backtrack_modcancel = parse_defcfg_val_bool(val, label)? diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index f6a83dbef..150b14e97 100644 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -451,7 +451,6 @@ pub fn parse_cfg_raw_string( "Only one defcfg is allowed, found more. Delete the extras." ) } - let src_expr = root_exprs .iter() .find(gen_first_atom_filter("defsrc")) @@ -559,6 +558,7 @@ pub fn parse_cfg_raw_string( delegate_to_first_layer: cfg.delegate_to_first_layer, default_sequence_timeout: cfg.sequence_timeout, default_sequence_input_mode: cfg.sequence_input_mode, + block_unmapped_keys: cfg.block_unmapped_keys, ..Default::default() }; @@ -886,6 +886,7 @@ pub struct ParsedState { delegate_to_first_layer: bool, default_sequence_timeout: u16, default_sequence_input_mode: SequenceInputMode, + block_unmapped_keys: bool, a: Arc, } @@ -911,6 +912,7 @@ impl Default for ParsedState { delegate_to_first_layer: default_cfg.delegate_to_first_layer, default_sequence_timeout: default_cfg.sequence_timeout, default_sequence_input_mode: default_cfg.sequence_input_mode, + block_unmapped_keys: default_cfg.block_unmapped_keys, a: unsafe { Allocations::new() }, } } @@ -2578,15 +2580,17 @@ fn parse_layers(s: &mut ParsedState) -> Result> { if *layer_action == Action::Trans { *layer_action = defsrc_action; } - // If there is no corresponding action in defsrc, default to the OsCode at the - // position. This is done so that `process-unmapped-keys` works correctly. - if *layer_action == Action::Trans { - *layer_action = OsCode::from_u16(i as u16) - .and_then(|osc| match KeyCode::from(osc) { - KeyCode::No => None, - kc => Some(Action::KeyCode(kc)), - }) - .unwrap_or(Action::Trans); + if !s.block_unmapped_keys { + // If there is no corresponding action in defsrc, default to the OsCode at the + // position. This is done so that `process-unmapped-keys` works correctly. + if *layer_action == Action::Trans { + *layer_action = OsCode::from_u16(i as u16) + .and_then(|osc| match KeyCode::from(osc) { + KeyCode::No => None, + kc => Some(Action::KeyCode(kc)), + }) + .unwrap_or(Action::Trans); + } } } // Set fake keys on the `layer-switch` version of each layer. From dae5cc5275ee199446aa28b04d9e9b8a6050af13 Mon Sep 17 00:00:00 2001 From: rszyma Date: Tue, 6 Feb 2024 15:39:26 +0100 Subject: [PATCH 132/819] fix: comment include in kanata.kbd sample (#721) --- cfg_samples/kanata.kbd | 3 ++- parser/src/cfg/mod.rs | 9 +++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/cfg_samples/kanata.kbd b/cfg_samples/kanata.kbd index 093f400af..6b8fafb5b 100644 --- a/cfg_samples/kanata.kbd +++ b/cfg_samples/kanata.kbd @@ -1030,4 +1030,5 @@ If you need help, please feel welcome to ask in the GitHub discussions. ;; The top-level action `include` will read a configuration from a new file. ;; At the time of writing, includes can only be placed at the top level. The ;; included files also cannot contain includes themselves. -(include included-file.kbd) +;; +;; (include included-file.kbd) diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index 150b14e97..a91c667a3 100644 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -294,18 +294,19 @@ fn parse_cfg_raw( relative_main_cfg_file_dir.join(filepath) }; - // Forbid loading the same file multiple times. - // This prevents a potential recursive infinite loop of includes - // (if includes within includes were to be allowed). let abs_filepath: PathBuf = filepath_relative_to_loaded_kanata_cfg .canonicalize() .map_err(|e| { format!( - "Failed to resolve absolute path: {}: {}", + "Failed to resolve relative path: {}: {}", filepath_relative_to_loaded_kanata_cfg.to_string_lossy(), e ) })?; + + // Forbid loading the same file multiple times. + // This prevents a potential recursive infinite loop of includes + // (if includes within includes were to be allowed). if !loaded_files.insert(abs_filepath.clone()) { return Err("The provided config file was already included before".to_string()); }; From 58e2481f99502cba94ac92a15fd885eeac62d307 Mon Sep 17 00:00:00 2001 From: jtroo Date: Wed, 7 Feb 2024 00:02:41 -0800 Subject: [PATCH 133/819] doc: add home row mods examples (#724) --- cfg_samples/home-row-mod-advanced.kbd | 57 +++++++++++++++++++++++++++ cfg_samples/home-row-mod-basic.kbd | 30 ++++++++++++++ src/tests.rs | 16 ++++++++ 3 files changed, 103 insertions(+) create mode 100644 cfg_samples/home-row-mod-advanced.kbd create mode 100644 cfg_samples/home-row-mod-basic.kbd diff --git a/cfg_samples/home-row-mod-advanced.kbd b/cfg_samples/home-row-mod-advanced.kbd new file mode 100644 index 000000000..11f2fb671 --- /dev/null +++ b/cfg_samples/home-row-mod-advanced.kbd @@ -0,0 +1,57 @@ +;; Home row mods QWERTY example with more complexity. +;; Some of the changes from the basic example: +;; - when a home row mod activates tap, the home row mods are disabled +;; while continuing to type rapidly +;; - tap-hold-release helps make the hold action more responsive +;; - pressing another key on the same half of the keyboard +;; as the home row mod will activate an early tap action + +(defcfg + process-unmapped-keys yes +) +(defsrc + a s d f j k l ; +) +(defvar + ;; Note: consider using different time values for your different fingers. + ;; For example, your pinkies might be slower to release keys and index + ;; fingers faster. + tap-time 200 + hold-time 150 + + left-hand-keys ( + q w e r t + a s d f g + z x c v b + ) + right-hand-keys ( + y u i o p + h j k l ; + n m , . / + ) +) +(deflayer base + @a @s @d @f @j @k @l @; +) + +(deflayer nomods + a s d f j k l ; +) +(deffakekeys + to-base (layer-switch base) +) +(defalias + tap (multi + (layer-switch nomods) + (on-idle-fakekey to-base tap 20) + ) + + a (tap-hold-release-keys $tap-time $hold-time (multi a @tap) lmet $left-hand-keys) + s (tap-hold-release-keys $tap-time $hold-time (multi s @tap) lalt $left-hand-keys) + d (tap-hold-release-keys $tap-time $hold-time (multi d @tap) lctl $left-hand-keys) + f (tap-hold-release-keys $tap-time $hold-time (multi f @tap) lsft $left-hand-keys) + j (tap-hold-release-keys $tap-time $hold-time (multi j @tap) rsft $right-hand-keys) + k (tap-hold-release-keys $tap-time $hold-time (multi k @tap) rctl $right-hand-keys) + l (tap-hold-release-keys $tap-time $hold-time (multi l @tap) ralt $right-hand-keys) + ; (tap-hold-release-keys $tap-time $hold-time (multi ; @tap) rmet $right-hand-keys) +) \ No newline at end of file diff --git a/cfg_samples/home-row-mod-basic.kbd b/cfg_samples/home-row-mod-basic.kbd new file mode 100644 index 000000000..44b9f9464 --- /dev/null +++ b/cfg_samples/home-row-mod-basic.kbd @@ -0,0 +1,30 @@ +;; Basic home row mods example using QWERTY +;; For a more complex but perhaps usable configuration, +;; see home-row-mod-advanced.kbd + +(defcfg + process-unmapped-keys yes +) +(defsrc + a s d f j k l ; +) +(defvar + ;; Note: consider using different time values for your different fingers. + ;; For example, your pinkies might be slower to release keys and index + ;; fingers faster. + tap-time 200 + hold-time 150 +) +(defalias + a (tap-hold $tap-time $hold-time a lmet) + s (tap-hold $tap-time $hold-time s lalt) + d (tap-hold $tap-time $hold-time d lctl) + f (tap-hold $tap-time $hold-time f lsft) + j (tap-hold $tap-time $hold-time j rsft) + k (tap-hold $tap-time $hold-time k rctl) + l (tap-hold $tap-time $hold-time l ralt) + ; (tap-hold $tap-time $hold-time ; rmet) +) +(deflayer base + @a @s @d @f @j @k @l @; +) \ No newline at end of file diff --git a/src/tests.rs b/src/tests.rs index 9c337ee54..76677b2e5 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -61,6 +61,22 @@ fn parse_all_keys() { .unwrap(); } +#[test] +fn parse_home_row_mods() { + let _lk = match CFG_PARSE_LOCK.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + }; + new_from_file(&std::path::PathBuf::from( + "./cfg_samples/home-row-mod-basic.kbd", + )) + .unwrap(); + new_from_file(&std::path::PathBuf::from( + "./cfg_samples/home-row-mod-advanced.kbd", + )) + .unwrap(); +} + #[test] fn sizeof_state() { assert_eq!( From 010338b14d0020098b9263a615ef2152c249d666 Mon Sep 17 00:00:00 2001 From: rszyma Date: Fri, 9 Feb 2024 07:46:53 +0100 Subject: [PATCH 134/819] fix(tcp): make kanata emit newlines (#730) Co-authored-by: jtroo --- src/tcp_server.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/tcp_server.rs b/src/tcp_server.rs index fe57aa4a9..b208d542f 100644 --- a/src/tcp_server.rs +++ b/src/tcp_server.rs @@ -32,10 +32,9 @@ pub enum ClientMessage { #[cfg(feature = "tcp_server")] impl ServerMessage { pub fn as_bytes(&self) -> Vec { - serde_json::to_string(self) - .expect("ServerMessage should serialize") - .as_bytes() - .to_vec() + let mut msg = serde_json::to_vec(self).expect("ServerMessage should serialize"); + msg.push(b'\n'); + msg } } From 0dd50db3cc357dd9b94a9312d470eee9d425f2a1 Mon Sep 17 00:00:00 2001 From: jtroo Date: Thu, 8 Feb 2024 22:51:55 -0800 Subject: [PATCH 135/819] doc: add logs+comments that describe TCP protocol (#728) --- Cargo.lock | 166 ++++++++++++++++++++++++++++++--- Cargo.toml | 1 + example_tcp_client/src/main.rs | 43 ++++++++- 3 files changed, 195 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 19cc41b51..7c6315d99 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,6 +44,17 @@ dependencies = [ "critical-section", ] +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -114,6 +125,23 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +dependencies = [ + "atty", + "bitflags", + "clap_derive 3.2.25", + "clap_lex 0.2.4", + "indexmap 1.9.3", + "once_cell", + "strsim", + "termcolor", + "textwrap 0.16.0", +] + [[package]] name = "clap" version = "4.4.7" @@ -121,7 +149,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac495e00dcec98c83465d5ad66c5c4fabd652fd6686e7c6269b117e729a6f17b" dependencies = [ "clap_builder", - "clap_derive", + "clap_derive 4.4.7", ] [[package]] @@ -131,10 +159,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c77ed9a32a62e6ca27175d00d29d05ca32e396ea1eb5fb01d8256b669cec7663" dependencies = [ "anstyle", - "clap_lex", + "clap_lex 0.6.0", "strsim", ] +[[package]] +name = "clap_derive" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "clap_derive" version = "4.4.7" @@ -144,7 +185,16 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.38", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", ] [[package]] @@ -262,6 +312,12 @@ dependencies = [ "byteorder", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.2" @@ -287,12 +343,31 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "hermit-abi" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + [[package]] name = "indexmap" version = "2.0.2" @@ -300,7 +375,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.14.2", ] [[package]] @@ -335,7 +410,7 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.3", "libc", "windows-sys", ] @@ -346,7 +421,7 @@ version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.3", "io-lifetimes", "rustix", "windows-sys", @@ -378,7 +453,7 @@ name = "kanata" version = "1.5.0" dependencies = [ "anyhow", - "clap", + "clap 4.4.7", "dirs", "encode_unicode", "evdev", @@ -452,6 +527,18 @@ dependencies = [ "thiserror", ] +[[package]] +name = "kanata_example_tcp_client" +version = "1.0.0" +dependencies = [ + "anyhow", + "clap 3.2.25", + "log", + "serde", + "serde_json", + "simplelog", +] + [[package]] name = "karabiner-driverkit" version = "0.1.3" @@ -536,7 +623,7 @@ dependencies = [ "supports-hyperlinks", "supports-unicode", "terminal_size", - "textwrap", + "textwrap 0.15.2", "thiserror", "unicode-width", ] @@ -549,7 +636,7 @@ checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.38", ] [[package]] @@ -638,7 +725,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 2.0.38", ] [[package]] @@ -682,6 +769,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "os_str_bytes" +version = "6.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" + [[package]] name = "owo-colors" version = "3.5.0" @@ -733,6 +826,30 @@ dependencies = [ "toml_edit", ] +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.69" @@ -872,7 +989,7 @@ checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.38", ] [[package]] @@ -977,6 +1094,17 @@ dependencies = [ "is-terminal", ] +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.38" @@ -1024,6 +1152,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "textwrap" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" + [[package]] name = "thiserror" version = "1.0.50" @@ -1041,7 +1175,7 @@ checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.38", ] [[package]] @@ -1087,7 +1221,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap", + "indexmap 2.0.2", "toml_datetime", "winnow", ] @@ -1110,6 +1244,12 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index 54638bc42..761bef320 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ "./", "parser", "keyberon", + "example_tcp_client" ] [package] diff --git a/example_tcp_client/src/main.rs b/example_tcp_client/src/main.rs index 6b98438ec..500cdf428 100644 --- a/example_tcp_client/src/main.rs +++ b/example_tcp_client/src/main.rs @@ -4,6 +4,7 @@ use simplelog::*; use std::io::{stdin, Read, Write}; use std::net::{SocketAddr, TcpStream}; +use std::process::exit; use std::str::FromStr; use std::time::Duration; @@ -12,7 +13,7 @@ use std::time::Duration; struct Args { /// Port that kanata's TCP server is listening on #[clap(short, long)] - port: u16, + port: Option, /// Enable debug logging #[clap(short, long)] @@ -26,9 +27,18 @@ struct Args { fn main() { let args = Args::parse(); init_logger(&args); + print_usage(); + + let port = match args.port { + Some(p) => p, + None => { + log::error!("no port provided via the -p|--port flag; exiting"); + exit(1); + } + }; log::info!("attempting to connect to kanata"); let kanata_conn = TcpStream::connect_timeout( - &SocketAddr::from(([127, 0, 0, 1], args.port)), + &SocketAddr::from(([127, 0, 0, 1], port)), Duration::from_secs(5), ) .expect("connect to kanata"); @@ -39,6 +49,29 @@ fn main() { read_from_kanata(reader_stream); } +fn print_usage() { + log::info!( + "\n\ + You can also use any other software to connect to kanata over TCP.\n\ + The protocol is plaintext JSON with newline terminated messages. +\n\ + Layer change notifications from kanata look like:\n\ + {} +\n\ + Requests to change kanata's layer look like:\n\ + {} + ", + serde_json::to_string(&ServerMessage::LayerChange { + new: "newly-changed-to-layer".into() + }) + .expect("deserializable"), + serde_json::to_string(&ClientMessage::ChangeLayer { + new: "requested-layer".into() + }) + .expect("deserializable"), + ) +} + fn init_logger(args: &Args) { let log_lvl = match (args.debug, args.trace) { (_, true) => LevelFilter::Trace, @@ -62,11 +95,17 @@ fn init_logger(args: &Args) { ); } +/// Example when serialized: +/// +/// {"LayerChange":{"new":"newly-changed-to-layer"}} #[derive(Debug, Serialize, Deserialize)] pub enum ServerMessage { LayerChange { new: String }, } +/// Example when serialized: +/// +/// {"ChangeLayer":{"new":"requested-layer"}} #[derive(Debug, Serialize, Deserialize)] pub enum ClientMessage { ChangeLayer { new: String }, From cdb782ab73f5e9e4c3754634cda08991a14a712c Mon Sep 17 00:00:00 2001 From: jtroo Date: Thu, 8 Feb 2024 22:54:19 -0800 Subject: [PATCH 136/819] feat: windows-interception-keyboard-hwids (#723) Add capability in Windows+Interception to specify keyboards to intercept and passthrough others. --- cfg_samples/kanata.kbd | 9 ++++ docs/config.adoc | 36 +++++++++++++--- parser/src/cfg/defcfg.rs | 63 ++++++++++++++++++++++++++-- src/kanata/mod.rs | 6 +++ src/kanata/windows/interception.rs | 66 ++++++++++++++++++++++++++---- 5 files changed, 163 insertions(+), 17 deletions(-) diff --git a/cfg_samples/kanata.kbd b/cfg_samples/kanata.kbd index 6b8fafb5b..3d6b488ba 100644 --- a/cfg_samples/kanata.kbd +++ b/cfg_samples/kanata.kbd @@ -167,6 +167,15 @@ If you need help, please feel welcome to ask in the GitHub discussions. ;; ;; windows-interception-mouse-hwid "70, 0, 90, 0, 20" + ;; List configuration for kanata-wintercept variants + ;; that allows intercepting only some connected keyboards. + ;; Use similarly to mouse-hwid above. + ;; + ;; windows-interception-keyboard-hwids ( + ;; "90, 80, 11, 34" + ;; "99, 88, 77, 66" + ;; ) + ;; Transparent keys on layers will delegate to the corresponding defsrc key ;; when found on a layer activated by `layer-switch`. This config entry ;; changes the behaviour to delegate to the action of the first layer, diff --git a/docs/config.adoc b/docs/config.adoc index 0692a3606..cdd0966d4 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -1998,12 +1998,12 @@ Known application with issues: GWSL/VcXsrv === Windows only: windows-interception-mouse-hwid[[windows-only-windows-interception-mouse-hwid]] <> -This defcfg item allows you to intercept mouse buttons for a specific mouse -device. This only works with the Interception driver (the -wintercept variants -of the binary). +This defcfg item allows you to intercept mouse buttons for a specific mouse device. +This only works with the Interception driver +(the -wintercept variants of the release binaries). -The intended use case for this is for laptops such as a Thinkpad, which have -mouse buttons that may be desirable to activate kanata actions with. +The original use case for this is for laptops such as a Thinkpad, +which have mouse buttons that may be desirable to activate kanata actions with. To know what numbers to put into the string, you can run the variant with this defcfg item defined with any numbers. Then when a button is first pressed on @@ -2021,6 +2021,32 @@ https://github.com/jtroo/kanata/issues/108[Relevant issue]. ) ---- + +=== Windows only: windows-interception-keyboard-hwids[[windows-only-windows-interception-keyboard-hwids]] +<> + +This defcfg item allows you to intercept only specific keyboards. +Its value must be a list of strings +with each string representing one hardware ID. + +To know what numbers to put into the string, +you can run the variant with this defcfg item empty. +Then when a button is first pressed on the keyboard, +kanata will print its hwid in the log. +You can then copy-paste that into this configuration entry. +If this defcfg item is not defined, the log will not print. + +.Example: +[source] +---- +(defcfg + windows-interception-keyboard-hwids ( + "70, 0, 60, 0" + "71, 72, 73, 74" + ) +) +---- + [[using-multiple-defcfg-options]] === Using multiple defcfg options <> diff --git a/parser/src/cfg/defcfg.rs b/parser/src/cfg/defcfg.rs index 1ca854875..43bad4090 100644 --- a/parser/src/cfg/defcfg.rs +++ b/parser/src/cfg/defcfg.rs @@ -42,6 +42,11 @@ pub struct CfgOptions { target_os = "unknown" ))] pub windows_interception_mouse_hwid: Option<[u8; HWID_ARR_SZ]>, + #[cfg(any( + all(feature = "interception_driver", target_os = "windows"), + target_os = "unknown" + ))] + pub windows_interception_keyboard_hwids: Option>, #[cfg(any(target_os = "macos", target_os = "unknown"))] pub macos_dev_names_include: Option>, } @@ -85,6 +90,11 @@ impl Default for CfgOptions { target_os = "unknown" ))] windows_interception_mouse_hwid: None, + #[cfg(any( + all(feature = "interception_driver", target_os = "windows"), + target_os = "unknown" + ))] + windows_interception_keyboard_hwids: None, #[cfg(any(target_os = "macos", target_os = "unknown"))] macos_dev_names_include: None, } @@ -250,12 +260,12 @@ pub fn parse_defcfg(expr: &[SExpr]) -> Result { hwid_bytes.push(b); hwid_bytes }) - }).map_err(|_| anyhow_expr!(val, "{label} format is invalid. It should consist of integers separated by commas"))?; + }).map_err(|_| anyhow_expr!(val, "{label} format is invalid. It should consist of numbers [0,255] separated by commas"))?; let hwid_slice = hwid_vec.iter().copied().enumerate() .try_fold([0u8; HWID_ARR_SZ], |mut hwid, idx_byte| { let (i, b) = idx_byte; if i > HWID_ARR_SZ { - bail_expr!(val, "{label} is too long; it should be up to {HWID_ARR_SZ} 8-bit unsigned integers") + bail_expr!(val, "{label} is too long; it should be up to {HWID_ARR_SZ} numbers [0,255]") } hwid[i] = b; Ok(hwid) @@ -263,6 +273,42 @@ pub fn parse_defcfg(expr: &[SExpr]) -> Result { cfg.windows_interception_mouse_hwid = Some(hwid_slice?); } } + "windows-interception-keyboard-hwids" => { + #[cfg(any( + all(feature = "interception_driver", target_os = "windows"), + target_os = "unknown" + ))] + { + let hwids = sexpr_to_list_or_err(val, label)?; + let mut parsed_hwids = vec![]; + for hwid_expr in hwids.iter() { + let hwid = sexpr_to_str_or_err( + hwid_expr, + "entry in windows-interception-keyboard-hwids", + )?; + log::trace!("win hwid: {hwid}"); + let hwid_vec = hwid + .split(',') + .try_fold(vec![], |mut hwid_bytes, hwid_byte| { + hwid_byte.trim_matches(' ').parse::().map(|b| { + hwid_bytes.push(b); + hwid_bytes + }) + }).map_err(|_| anyhow_expr!(hwid_expr, "Entry in {label} is invalid. Entries should be numbers [0,255] separated by commas"))?; + let hwid_slice = hwid_vec.iter().copied().enumerate() + .try_fold([0u8; HWID_ARR_SZ], |mut hwid, idx_byte| { + let (i, b) = idx_byte; + if i > HWID_ARR_SZ { + bail_expr!(hwid_expr, "entry in {label} is too long; it should be up to {HWID_ARR_SZ} 8-bit unsigned integers") + } + hwid[i] = b; + Ok(hwid) + }); + parsed_hwids.push(hwid_slice?); + } + cfg.windows_interception_keyboard_hwids = Some(parsed_hwids); + } + } "macos-dev-names-include" => { #[cfg(any(target_os = "macos", target_os = "unknown"))] { @@ -431,6 +477,17 @@ fn sexpr_to_str_or_err<'a>(expr: &'a SExpr, label: &str) -> Result<&'a str> { } } +#[cfg(any( + all(feature = "interception_driver", target_os = "windows"), + target_os = "unknown" +))] +fn sexpr_to_list_or_err<'a>(expr: &'a SExpr, label: &str) -> Result<&'a [SExpr]> { + match expr { + SExpr::Atom(_) => bail_expr!(expr, "The value for {label} must be a list"), + SExpr::List(l) => Ok(&l.t), + } +} + #[cfg(any(target_os = "linux", target_os = "unknown"))] #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct KeyRepeatSettings { @@ -466,7 +523,7 @@ impl Default for AltGrBehaviour { all(feature = "interception_driver", target_os = "windows"), target_os = "unknown" ))] -pub const HWID_ARR_SZ: usize = 128; +pub const HWID_ARR_SZ: usize = 1024; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ReplayDelayBehaviour { diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index e36e41f9f..c96c09b7a 100644 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -127,6 +127,10 @@ pub struct Kanata { /// Used to know which input device to treat as a mouse for intercepting and processing inputs /// by kanata. intercept_mouse_hwid: Option<[u8; HWID_ARR_SZ]>, + #[cfg(all(feature = "interception_driver", target_os = "windows"))] + /// Used to know which input device to treat as a mouse for intercepting and processing inputs + /// by kanata. + intercept_kb_hwids: Option>, /// User configuration to do logging of layer changes or not. log_layer_changes: bool, /// Tracks the caps-word state. Is Some(...) if caps-word is active and None otherwise. @@ -299,6 +303,8 @@ impl Kanata { exclude_names: cfg.items.linux_dev_names_exclude, #[cfg(all(feature = "interception_driver", target_os = "windows"))] intercept_mouse_hwid: cfg.items.windows_interception_mouse_hwid, + #[cfg(all(feature = "interception_driver", target_os = "windows"))] + intercept_kb_hwids: cfg.items.windows_interception_keyboard_hwids, dynamic_macro_replay_state: None, dynamic_macro_record_state: None, dynamic_macros: Default::default(), diff --git a/src/kanata/windows/interception.rs b/src/kanata/windows/interception.rs index 56a3487ea..b9ad0c13a 100644 --- a/src/kanata/windows/interception.rs +++ b/src/kanata/windows/interception.rs @@ -19,6 +19,7 @@ impl Kanata { information: 0, }; 32]; + let keyboards_to_intercept_hwids = kanata.lock().intercept_kb_hwids.clone(); let mouse_to_intercept_hwid: Option<[u8; HWID_ARR_SZ]> = kanata.lock().intercept_mouse_hwid; if mouse_to_intercept_hwid.is_some() { intrcptn.set_filter( @@ -35,6 +36,16 @@ impl Kanata { for i in 0..num_strokes { let mut key_event = match strokes[i] { ic::Stroke::Keyboard { state, .. } => { + if !is_keyboard_interceptable( + dev, + &intrcptn, + &keyboards_to_intercept_hwids, + &mut is_dev_interceptable, + ) { + log::debug!("stroke {:?} is from undesired device", strokes[i]); + intrcptn.send(dev, &strokes[i..i + 1]); + continue; + } log::debug!("got stroke {:?}", strokes[i]); let code = match OsCodeWrapper::try_from(strokes[i]) { Ok(c) => c.0, @@ -100,15 +111,36 @@ impl Kanata { } } -fn mouse_state_to_event( +fn is_keyboard_interceptable( input_dev: ic::Device, - allowed_hwid: &[u8; HWID_ARR_SZ], - state: ic::MouseState, - rolling: i16, intrcptn: &ic::Interception, - is_dev_interceptable: &mut HashMap, -) -> Option { - if !match is_dev_interceptable.get(&input_dev) { + allowed_hwids: &Option>, + cache: &mut HashMap, +) -> bool { + match allowed_hwids { + None => true, + Some(allowed) => match cache.get(&input_dev) { + Some(v) => *v, + None => { + let mut hwid = [0u8; HWID_ARR_SZ]; + log::trace!("getting hardware id for input dev: {input_dev}"); + let res = intrcptn.get_hardware_id(input_dev, &mut hwid); + let dev_is_interceptable = allowed.contains(&hwid); + log::info!("res {res}; device #{input_dev} hwid {hwid:?} matches allowed keyboard input: {dev_is_interceptable}"); + cache.insert(input_dev, dev_is_interceptable); + dev_is_interceptable + } + }, + } +} + +fn is_mouse_dev_interceptable( + input_dev: ic::Device, + intrcptn: &ic::Interception, + allowed_hwid: &[u8; HWID_ARR_SZ], + cache: &mut HashMap, +) -> bool { + match cache.get(&input_dev) { Some(v) => *v, None => { let mut hwid = [0u8; HWID_ARR_SZ]; @@ -116,10 +148,26 @@ fn mouse_state_to_event( let res = intrcptn.get_hardware_id(input_dev, &mut hwid); let dev_is_interceptable = hwid == *allowed_hwid; log::info!("res {res}; device #{input_dev} hwid {hwid:?} matches allowed mouse input: {dev_is_interceptable}"); - is_dev_interceptable.insert(input_dev, dev_is_interceptable); + cache.insert(input_dev, dev_is_interceptable); dev_is_interceptable } - } { + } +} + +fn mouse_state_to_event( + input_dev: ic::Device, + allowed_hwid: &[u8; HWID_ARR_SZ], + state: ic::MouseState, + rolling: i16, + intrcptn: &ic::Interception, + device_interceptability_cache: &mut HashMap, +) -> Option { + if !is_mouse_dev_interceptable( + input_dev, + intrcptn, + allowed_hwid, + device_interceptability_cache, + ) { return None; } From 6e65da8135a311fe2b13fa3231c44c0b85de4dd4 Mon Sep 17 00:00:00 2001 From: jtroo Date: Sat, 10 Feb 2024 15:45:21 -0800 Subject: [PATCH 137/819] github: update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 09d44efe7..d53dbaa04 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -9,7 +9,7 @@ body: label: Requirements description: Before you create a bug report, please check the following options: - - label: I've searched [issues](https://github.com/jtroo/kanata/issues) to see if this has not been reported before. + - label: I've searched [issues](https://github.com/jtroo/kanata/issues) and [discussions](https://github.com/jtroo/kanata/discussions) to see if this has been reported before. required: true - type: textarea id: summary @@ -22,7 +22,7 @@ body: id: config attributes: label: Relevant kanata config - description: E.g. defcfg, defsrc, deflayer, defalias items. If in doubt, feel free to include your entire config. + description: E.g. defcfg, defsrc, deflayer, defalias items. If in doubt, feel free to include your entire config. Please ensure to surround with triple backticks: \`\`\` to avoid `@` notifying people. validations: required: false - type: textarea From dcf1b9eee57f19ce2ce7002912032e3dcf82f17c Mon Sep 17 00:00:00 2001 From: jtroo Date: Sat, 10 Feb 2024 15:46:32 -0800 Subject: [PATCH 138/819] github: maybe fix invalid template? --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index d53dbaa04..63d71150f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -22,7 +22,7 @@ body: id: config attributes: label: Relevant kanata config - description: E.g. defcfg, defsrc, deflayer, defalias items. If in doubt, feel free to include your entire config. Please ensure to surround with triple backticks: \`\`\` to avoid `@` notifying people. + description: E.g. defcfg, defsrc, deflayer, defalias items. If in doubt, feel free to include your entire config. Please ensure to surround with triple backticks: ``` to avoid `@` notifying people. validations: required: false - type: textarea From 58886640770f89bb3b7ebe61f3c24ffc78f5cbe6 Mon Sep 17 00:00:00 2001 From: jtroo Date: Sat, 10 Feb 2024 15:47:14 -0800 Subject: [PATCH 139/819] github: try again to fix --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 63d71150f..bb18da83b 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -22,7 +22,7 @@ body: id: config attributes: label: Relevant kanata config - description: E.g. defcfg, defsrc, deflayer, defalias items. If in doubt, feel free to include your entire config. Please ensure to surround with triple backticks: ``` to avoid `@` notifying people. + description: E.g. defcfg, defsrc, deflayer, defalias items. If in doubt, feel free to include your entire config. Please ensure to use code formatting, e.g. surround with triple backticks to avoid pinging users with the @ character. validations: required: false - type: textarea From 96af3eeb63f0ec863206bedeac523a46db7879d6 Mon Sep 17 00:00:00 2001 From: jtroo Date: Sat, 10 Feb 2024 15:48:09 -0800 Subject: [PATCH 140/819] chore: fix description in tcp client toml --- example_tcp_client/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example_tcp_client/Cargo.toml b/example_tcp_client/Cargo.toml index 47b75186d..cb0a471fe 100644 --- a/example_tcp_client/Cargo.toml +++ b/example_tcp_client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kanata_example_tcp_client" -Description = "Example kanata TCP client" +description = "Example kanata TCP client" version = "1.0.0" edition = "2021" license = "LGPL-3.0" From f12a188422834ed6f2a4302a20f023414a503582 Mon Sep 17 00:00:00 2001 From: jtroo Date: Sat, 10 Feb 2024 15:49:48 -0800 Subject: [PATCH 141/819] chore(justfile): set windows shell to conveniently run on Windows --- justfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/justfile b/justfile index 749a9671e..d1ca27d32 100644 --- a/justfile +++ b/justfile @@ -1,3 +1,5 @@ +set windows-shell := ["powershell.exe", "-NoLogo", "-Command"] + # Build the release binaries for Linux and put the binaries+cfg in the output directory build_release_linux output_dir: cargo build --release @@ -8,7 +10,7 @@ build_release_linux output_dir: strip "{{output_dir}}/kanata_cmd_allowed" cp cfg_samples/kanata.kbd "{{output_dir}}" -# Build the release binaries for Windows and put the binaries+cfg in the output directory. Run as follows: `just --shell powershell.exe --shell-arg -c build_release_windows `. +# Build the release binaries for Windows and put the binaries+cfg in the output directory. build_release_windows output_dir: cargo build --release; cp target/release/kanata.exe "{{output_dir}}\kanata.exe" cargo build --release --features interception_driver; cp target/release/kanata.exe "{{output_dir}}\kanata_wintercept.exe" From 2c73b207d64f38a83e7c15effe683f7e2724d33b Mon Sep 17 00:00:00 2001 From: jtroo Date: Sat, 10 Feb 2024 15:50:30 -0800 Subject: [PATCH 142/819] feat(windows-llhook): add compile flag to send scancodes instead of vkeycodes --- Cargo.toml | 1 + src/oskbd/windows/mod.rs | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 761bef320..ea4987684 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,6 +71,7 @@ kanata-interception = { version = "0.2.0", optional = true } default = ["tcp_server"] perf_logging = [] tcp_server = [] +win_sendinput_send_scancodes = [] cmd = ["kanata-parser/cmd"] interception_driver = ["kanata-interception", "kanata-parser/interception_driver"] diff --git a/src/oskbd/windows/mod.rs b/src/oskbd/windows/mod.rs index fae97fbf1..bc09fccb1 100644 --- a/src/oskbd/windows/mod.rs +++ b/src/oskbd/windows/mod.rs @@ -68,7 +68,22 @@ fn send_key_sendinput(code: u16, is_key_up: bool) { if is_key_up { kb_input.dwFlags |= KEYEVENTF_KEYUP; } - kb_input.wVk = code; + + #[cfg(feature = "win_sendinput_send_scancodes")] + { + // GUI/Windows keys don't seem to like SCANCODE events so don't transform those. + if i32::from(code) == VK_RWIN || i32::from(code) == VK_LWIN { + kb_input.wVk = code; + } else { + let code_u32 = code as u32; + kb_input.dwFlags |= KEYEVENTF_SCANCODE; + kb_input.wScan = MapVirtualKeyA(code_u32, 0) as u16; + } + } + #[cfg(not(feature = "win_sendinput_send_scancodes"))] + { + kb_input.wVk = code; + } let mut inputs: [INPUT; 1] = mem::zeroed(); inputs[0].type_ = INPUT_KEYBOARD; From 0d56dd6e9c99e55f4c0b86086f8cc578e3c8e8bc Mon Sep 17 00:00:00 2001 From: jtroo Date: Sat, 10 Feb 2024 20:50:28 -0800 Subject: [PATCH 143/819] doc: update README with kanata-tray and minor edits --- README.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 3405af904..c3045436f 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ Some tips for running kanata in the background: - Windows: https://github.com/jtroo/kanata/discussions/193 - Linux: https://github.com/jtroo/kanata/discussions/130 +- Run from tray icon: https://github.com/rszyma/kanata-tray ### Pre-built executables @@ -218,6 +219,13 @@ to understand what changes to make. - If you know anything about writing a keyboard driver for Windows, starting an open-source alternative to the Interception driver would be lovely. + +## Community projects related to kanata + +- [vscode-kanata](https://github.com/rszyma/vscode-kanata): Language support for kanata configuration files in VS Code +- [komokana](https://github.com/LGUG2Z/komokana): Automatic application-aware layer switching for [`komorebi`](https://github.com/LGUG2Z/komorebi) +- [kanata-tray](https://github.com/rszyma/kanata-tray): Control kanata from a tray icon + ## What does the name mean? I wanted a "k" word since this relates to keyboards. According to Wikipedia, @@ -282,11 +290,6 @@ language and the prior work of the awesome [keyberon crate](https://github.com/T exists.
-## Community projects related to kanata - -- [vscode-kanata](https://github.com/rszyma/vscode-kanata): Language support for kanata configuration files in VS Code -- [komokana](https://github.com/LGUG2Z/komokana): Automatic application-aware layer switching for [`komorebi`](https://github.com/LGUG2Z/komorebi) - ## Similar Projects - [kmonad](https://github.com/kmonad/kmonad): The inspiration for kanata (Linux, Windows, Mac) @@ -302,7 +305,7 @@ exists. ### Why the list? -While kanata is the best tool for me (jtroo), it may not be the best tool for +While kanata is the best tool for some, it may not be the best tool for you. I'm happy to introduce you to tools that may better suit your needs. This list is also useful as reference/inspiration for functionality that could be added to kanata. From 7c7b28e55950c2bafbdfb9a0fa223779cac3d12b Mon Sep 17 00:00:00 2001 From: jtroo Date: Sat, 10 Feb 2024 20:52:16 -0800 Subject: [PATCH 144/819] doc: clean up first kanata-tray hyperlink --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c3045436f..be8bcf1d9 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Some tips for running kanata in the background: - Windows: https://github.com/jtroo/kanata/discussions/193 - Linux: https://github.com/jtroo/kanata/discussions/130 -- Run from tray icon: https://github.com/rszyma/kanata-tray +- Run from tray icon: [kanata-tray](https://github.com/rszyma/kanata-tray) ### Pre-built executables From 9e980d26f8111f0c481cf0a7c2ee9a39cdd8ccf2 Mon Sep 17 00:00:00 2001 From: jtroo Date: Sat, 10 Feb 2024 21:03:30 -0800 Subject: [PATCH 145/819] doc: add win-llhook arrow keys known issue --- docs/platform-known-issues.adoc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/platform-known-issues.adoc b/docs/platform-known-issues.adoc index bad337477..75cf86b0f 100644 --- a/docs/platform-known-issues.adoc +++ b/docs/platform-known-issues.adoc @@ -35,6 +35,10 @@ which map to the binaries ** https://github.com/espanso/espanso/issues/1488 * AltGr can misbehave ** https://github.com/jtroo/kanata/blob/main/docs/config.adoc#windows-only-windows-altgr +* NumLock state can mess with arrow keys in unexpected ways +** https://github.com/jtroo/kanata/issues/78 +** https://github.com/jtroo/kanata/issues/667 +** Workaround: use the correct [numlock state](https://github.com/jtroo/kanata/discussions/354) == Windows 10+11 Interception From 1b0dd6707c1f058a3c03b4071a0c314f3a3d46a9 Mon Sep 17 00:00:00 2001 From: jtroo Date: Sat, 10 Feb 2024 21:04:50 -0800 Subject: [PATCH 146/819] doc: fix hyperlink for adoc --- docs/platform-known-issues.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/platform-known-issues.adoc b/docs/platform-known-issues.adoc index 75cf86b0f..9b79889b5 100644 --- a/docs/platform-known-issues.adoc +++ b/docs/platform-known-issues.adoc @@ -38,7 +38,7 @@ which map to the binaries * NumLock state can mess with arrow keys in unexpected ways ** https://github.com/jtroo/kanata/issues/78 ** https://github.com/jtroo/kanata/issues/667 -** Workaround: use the correct [numlock state](https://github.com/jtroo/kanata/discussions/354) +** Workaround: use the correct https://github.com/jtroo/kanata/discussions/354[numlock state] == Windows 10+11 Interception From 35bd7fa50a3e833a27c624afedf66c4065868439 Mon Sep 17 00:00:00 2001 From: jtroo Date: Sat, 10 Feb 2024 21:10:25 -0800 Subject: [PATCH 147/819] doc: add linux unicode limitations to known issues --- docs/platform-known-issues.adoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/platform-known-issues.adoc b/docs/platform-known-issues.adoc index 9b79889b5..9bc728740 100644 --- a/docs/platform-known-issues.adoc +++ b/docs/platform-known-issues.adoc @@ -56,6 +56,8 @@ which map to the binaries * Key repeats can occur when they normally wouldn't in some cases ** https://github.com/jtroo/kanata/discussions/422 ** https://github.com/jtroo/kanata/issues/450 +* Unicode support has limitations, using xkb is a more consistent solution +** https://github.com/jtroo/kanata/discussions/703 == MacOS From 7b0cd426b19a8986fa0d9c1ae8b2434843b42aef Mon Sep 17 00:00:00 2001 From: jtroo Date: Sat, 10 Feb 2024 21:14:19 -0800 Subject: [PATCH 148/819] doc: add linux rapid press-release known issue --- docs/platform-known-issues.adoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/platform-known-issues.adoc b/docs/platform-known-issues.adoc index 9bc728740..b58c47acd 100644 --- a/docs/platform-known-issues.adoc +++ b/docs/platform-known-issues.adoc @@ -58,6 +58,8 @@ which map to the binaries ** https://github.com/jtroo/kanata/issues/450 * Unicode support has limitations, using xkb is a more consistent solution ** https://github.com/jtroo/kanata/discussions/703 +* Some special desktop-layer keys (e.g. `ISO_Level3_Latch`) behave incorrectly with rapid press-release +** https://github.com/jtroo/kanata/discussions/733 == MacOS From 34c86a352e6040fab387d97328b4129eb220b0e6 Mon Sep 17 00:00:00 2001 From: jtroo Date: Sat, 10 Feb 2024 22:37:48 -0800 Subject: [PATCH 149/819] github: link to platform-specific issues doc in bug report template --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index bb18da83b..ad8e44aea 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -9,7 +9,7 @@ body: label: Requirements description: Before you create a bug report, please check the following options: - - label: I've searched [issues](https://github.com/jtroo/kanata/issues) and [discussions](https://github.com/jtroo/kanata/discussions) to see if this has been reported before. + - label: I've searched [platform-specific issues](https://github.com/jtroo/kanata/blob/main/docs/platform-known-issues.adoc), [issues](https://github.com/jtroo/kanata/issues) and [discussions](https://github.com/jtroo/kanata/discussions) to see if this has been reported before. required: true - type: textarea id: summary From 76cda52737f07cd6d7b9a85b6c0647b18dd54998 Mon Sep 17 00:00:00 2001 From: jtroo Date: Sat, 10 Feb 2024 23:59:13 -0800 Subject: [PATCH 150/819] chore: minor Cargo.toml changes and run cargo update (#734) --- Cargo.lock | 464 +++++++++++++++++----------------- Cargo.toml | 25 +- example_tcp_client/Cargo.lock | 415 ------------------------------ example_tcp_client/Cargo.toml | 2 +- 4 files changed, 244 insertions(+), 662 deletions(-) delete mode 100644 example_tcp_client/Cargo.lock diff --git a/Cargo.lock b/Cargo.lock index 7c6315d99..81c05be5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,17 +17,59 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "anstream" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + [[package]] name = "anstyle" -version = "1.0.4" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" + +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] [[package]] name = "anyhow" -version = "1.0.75" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" [[package]] name = "arraydeque" @@ -37,24 +79,13 @@ checksum = "f0ffd3d69bd89910509a5d31d1f1353f38ccffdd116dd0099bbd6627f7bd8ad8" [[package]] name = "atomic-polyfill" -version = "0.1.11" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ff7eb3f316534d83a8a2c3d1674ace8a5a71198eba31e2e2b597833f699b28" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" dependencies = [ "critical-section", ] -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi 0.1.19", - "libc", - "winapi", -] - [[package]] name = "autocfg" version = "1.1.0" @@ -127,81 +158,49 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "3.2.25" +version = "4.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" -dependencies = [ - "atty", - "bitflags", - "clap_derive 3.2.25", - "clap_lex 0.2.4", - "indexmap 1.9.3", - "once_cell", - "strsim", - "termcolor", - "textwrap 0.16.0", -] - -[[package]] -name = "clap" -version = "4.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac495e00dcec98c83465d5ad66c5c4fabd652fd6686e7c6269b117e729a6f17b" +checksum = "80c21025abd42669a92efc996ef13cfb2c5c627858421ea58d5c3b331a6c134f" dependencies = [ "clap_builder", - "clap_derive 4.4.7", + "clap_derive", ] [[package]] name = "clap_builder" -version = "4.4.7" +version = "4.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c77ed9a32a62e6ca27175d00d29d05ca32e396ea1eb5fb01d8256b669cec7663" +checksum = "458bf1f341769dfcf849846f65dffdf9146daa56bcd2a47cb4e1de9915567c99" dependencies = [ + "anstream", "anstyle", - "clap_lex 0.6.0", + "clap_lex", "strsim", ] [[package]] name = "clap_derive" -version = "3.2.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008" -dependencies = [ - "heck", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "clap_derive" -version = "4.4.7" +version = "4.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.38", + "syn", ] [[package]] name = "clap_lex" -version = "0.2.4" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" -dependencies = [ - "os_str_bytes", -] +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" [[package]] -name = "clap_lex" -version = "0.6.0" +name = "colorchoice" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "critical-section" @@ -211,9 +210,9 @@ checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216" [[package]] name = "deranged" -version = "0.3.9" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", ] @@ -236,7 +235,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -259,12 +258,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.5" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -288,9 +287,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "getrandom" -version = "0.2.10" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", "libc", @@ -299,9 +298,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "hash32" @@ -314,21 +313,15 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.12.3" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "hashbrown" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" [[package]] name = "heapless" -version = "0.7.16" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db04bc24a18b9ea980628ecf00e6c0264f3c1426dac36c00cb49b6fbad8b0743" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" dependencies = [ "atomic-polyfill", "hash32", @@ -345,37 +338,18 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - -[[package]] -name = "hermit-abi" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" - -[[package]] -name = "indexmap" -version = "1.9.3" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", -] +checksum = "d0c62115964e08cb8039170eb33c1d0e2388a256930279edca206fff675f82c3" [[package]] name = "indexmap" -version = "2.0.2" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" +checksum = "824b2ae422412366ba479e8111fd301f7b5faece8149317bb81925979a53f520" dependencies = [ "equivalent", - "hashbrown 0.14.2", + "hashbrown", ] [[package]] @@ -410,9 +384,9 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ - "hermit-abi 0.3.3", + "hermit-abi", "libc", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -421,29 +395,29 @@ version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" dependencies = [ - "hermit-abi 0.3.3", + "hermit-abi", "io-lifetimes", "rustix", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] name = "is_ci" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616cde7c720bb2bb5824a224687d8f77bfd38922027f01d825cd7453be5099fb" +checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" [[package]] name = "itoa" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "jobserver" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" +checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" dependencies = [ "libc", ] @@ -453,12 +427,11 @@ name = "kanata" version = "1.5.0" dependencies = [ "anyhow", - "clap 4.4.7", + "clap", "dirs", "encode_unicode", "evdev", "inotify", - "is-terminal", "kanata-interception", "kanata-keyberon", "kanata-parser", @@ -532,7 +505,7 @@ name = "kanata_example_tcp_client" version = "1.0.0" dependencies = [ "anyhow", - "clap 3.2.25", + "clap", "log", "serde", "serde_json", @@ -557,9 +530,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.149" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "linux-raw-sys" @@ -585,9 +558,9 @@ checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "memchr" -version = "2.6.4" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "memoffset" @@ -623,7 +596,7 @@ dependencies = [ "supports-hyperlinks", "supports-unicode", "terminal_size", - "textwrap 0.15.2", + "textwrap", "thiserror", "unicode-width", ] @@ -636,28 +609,28 @@ checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn", ] [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" dependencies = [ "adler", ] [[package]] name = "mio" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", "log", "wasi", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -707,6 +680,12 @@ dependencies = [ "pin-utils", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num_enum" version = "0.6.1" @@ -725,7 +704,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.38", + "syn", ] [[package]] @@ -739,18 +718,18 @@ dependencies = [ [[package]] name = "object" -version = "0.32.1" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "option-ext" @@ -769,12 +748,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "os_str_bytes" -version = "6.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" - [[package]] name = "owo-colors" version = "3.5.0" @@ -801,7 +774,7 @@ dependencies = [ "libc", "redox_syscall 0.4.1", "smallvec", - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -826,44 +799,20 @@ dependencies = [ "toml_edit", ] -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -945,14 +894,14 @@ dependencies = [ "io-lifetimes", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] name = "ryu" -version = "1.0.15" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" [[package]] name = "scopeguard" @@ -968,35 +917,35 @@ checksum = "621e3680f3e07db4c9c2c3fb07c6223ab2fab2e54bd3c04c3ae037990f428c32" [[package]] name = "semver" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" +checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" [[package]] name = "serde" -version = "1.0.190" +version = "1.0.196" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7" +checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.190" +version = "1.0.196" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3" +checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn", ] [[package]] name = "serde_json" -version = "1.0.108" +version = "1.0.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" dependencies = [ "itoa", "ryu", @@ -1035,9 +984,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.1" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" +checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" [[package]] name = "smawk" @@ -1062,9 +1011,9 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "strsim" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" [[package]] name = "supports-color" @@ -1087,29 +1036,18 @@ dependencies = [ [[package]] name = "supports-unicode" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b6c2cb240ab5dd21ed4906895ee23fe5a48acdbd15a3ce388e7b62a9b66baf7" +checksum = "f850c19edd184a205e883199a261ed44471c81e39bd95b1357f5febbef00e77a" dependencies = [ "is-terminal", ] [[package]] name = "syn" -version = "1.0.109" +version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", @@ -1152,41 +1090,36 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "textwrap" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" - [[package]] name = "thiserror" -version = "1.0.50" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.50" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn", ] [[package]] name = "time" -version = "0.3.30" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" +checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" dependencies = [ "deranged", "itoa", "libc", + "num-conv", "num_threads", "powerfmt", "serde", @@ -1202,10 +1135,11 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.15" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" +checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" dependencies = [ + "num-conv", "time-core", ] @@ -1221,7 +1155,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.0.2", + "indexmap", "toml_datetime", "winnow", ] @@ -1245,10 +1179,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] -name = "version_check" -version = "0.9.4" +name = "utf8parse" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "wasi" @@ -1299,7 +1233,16 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", ] [[package]] @@ -1308,13 +1251,28 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", ] [[package]] @@ -1323,47 +1281,89 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + [[package]] name = "winnow" -version = "0.5.18" +version = "0.5.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "176b6138793677221d420fd2f0aeeced263f197688b36484660da767bca2fa32" +checksum = "5389a154b01683d28c77f8f68f49dea75f0a4da32557a58f68ee51ebba472d29" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index ea4987684..af9effe36 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,23 +20,19 @@ license = "LGPL-3.0" edition = "2021" [dependencies] -clap = { version = "4.1.6", features = [ "std", "derive", "help", "suggestions" ], default_features = false } -log = { version = "0.4.8", default_features = false } -simplelog = "0.12.0" anyhow = "1" -parking_lot = "0.12" +clap = { version = "4", features = [ "std", "derive", "help", "suggestions" ], default_features = false } +dirs = "5.0.1" +log = { version = "0.4.8", default_features = false } +miette = { version = "5.7.0", features = ["fancy"] } once_cell = "1" -serde = { version = "1", features = ["alloc", "derive"], default_features = false } -serde_json = { version = "1", features = ["alloc"], default_features = false } -serde_derive = "1.0" - +parking_lot = "0.12" radix_trie = "0.2" rustc-hash = "1.1.0" -miette = { version = "5.7.0", features = ["fancy"] } -dirs = "5.0.1" - -# Pinned to avoid including multiple versions of a dependency -is-terminal = "=0.4.7" +serde = { version = "1", features = ["alloc", "derive"], default_features = false } +serde_derive = "1.0" +serde_json = { version = "1", features = ["alloc"], default_features = false } +simplelog = "0.12.0" # kanata-keyberon = "0.150.4" # kanata-parser = "0.150.4" @@ -50,13 +46,14 @@ kanata-parser = { path = "parser" } karabiner-driverkit = "0.1.3" [target.'cfg(target_os = "linux")'.dependencies] -evdev = "=0.12.0" signal-hook = "0.3.14" inotify = { version = "0.10.0", default_features = false } mio = { version = "0.8.4", features = ["os-poll", "os-ext"] } nix = { version = "0.26.1", features = ["ioctl"] } sd-notify = "0.4.1" +evdev = "=0.12.0" # Pinned to avoid a bug in 0.12.1 + [target.'cfg(target_os = "windows")'.dependencies] encode_unicode = "0.3.6" winapi = { version = "0.3.9", features = [ diff --git a/example_tcp_client/Cargo.lock b/example_tcp_client/Cargo.lock deleted file mode 100644 index e327c9cbf..000000000 --- a/example_tcp_client/Cargo.lock +++ /dev/null @@ -1,415 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "anyhow" -version = "1.0.59" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91f1f46651137be86f3a2b9a8359f9ab421d04d941c62b5982e1ca21113adf9" - -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi", - "libc", - "winapi", -] - -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bumpalo" -version = "3.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "clap" -version = "3.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3dbbb6653e7c55cc8595ad3e1f7be8f32aba4eb7ff7f0fd1163d4f3d137c0a9" -dependencies = [ - "atty", - "bitflags", - "clap_derive", - "clap_lex", - "indexmap", - "once_cell", - "strsim", - "termcolor", - "textwrap", -] - -[[package]] -name = "clap_derive" -version = "3.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ba52acd3b0a5c33aeada5cdaa3267cdc7c594a98731d4268cdc1532f4264cb4" -dependencies = [ - "heck", - "proc-macro-error", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" -dependencies = [ - "os_str_bytes", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "heck" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" - -[[package]] -name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - -[[package]] -name = "indexmap" -version = "1.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" -dependencies = [ - "autocfg", - "hashbrown", -] - -[[package]] -name = "itoa" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" - -[[package]] -name = "js-sys" -version = "0.3.59" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "kanata_example_tcp_client" -version = "1.0.0" -dependencies = [ - "anyhow", - "clap", - "log", - "serde", - "serde_json", - "simplelog", -] - -[[package]] -name = "libc" -version = "0.2.127" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "505e71a4706fa491e9b1b55f51b95d4037d0821ee40131190475f692b35b009b" - -[[package]] -name = "log" -version = "0.4.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "num_threads" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" -dependencies = [ - "libc", -] - -[[package]] -name = "once_cell" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" - -[[package]] -name = "os_str_bytes" -version = "6.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "648001efe5d5c0102d8cea768e348da85d90af8ba91f0bea908f157951493cd4" - -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - -[[package]] -name = "proc-macro2" -version = "1.0.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "ryu" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" - -[[package]] -name = "serde" -version = "1.0.142" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e590c437916fb6b221e1d00df6e3294f3fccd70ca7e92541c475d6ed6ef5fee2" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.142" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34b5b8d809babe02f538c2cfec6f2c1ed10804c0e5a6a041a049a4f5588ccc2e" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38dd04e3c8279e75b31ef29dbdceebfe5ad89f4d0937213c53f7d49d01b3d5a7" -dependencies = [ - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "simplelog" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48dfff04aade74dd495b007c831cd6f4e0cee19c344dd9dc0884c0289b70a786" -dependencies = [ - "log", - "termcolor", - "time", -] - -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - -[[package]] -name = "syn" -version = "1.0.99" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "termcolor" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "textwrap" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" - -[[package]] -name = "time" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74b7cc93fc23ba97fde84f7eea56c55d1ba183f495c6715defdfc7b9cb8c870f" -dependencies = [ - "itoa", - "js-sys", - "libc", - "num_threads", - "time-macros", -] - -[[package]] -name = "time-macros" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" - -[[package]] -name = "unicode-ident" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" - -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "wasm-bindgen" -version = "0.2.82" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.82" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.82" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.82" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.82" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/example_tcp_client/Cargo.toml b/example_tcp_client/Cargo.toml index cb0a471fe..0f9dd2c75 100644 --- a/example_tcp_client/Cargo.toml +++ b/example_tcp_client/Cargo.toml @@ -8,7 +8,7 @@ authors = ["jtroo "] [dependencies] anyhow = "1" -clap = { version = "3", features = [ "derive" ] } +clap = { version = "4", features = [ "derive" ] } log = "0.4.8" simplelog = "0.12" serde = { version = "1", features = ["derive"] } From e355961993b29ce1aded1aa0431c51622bdb0e00 Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 11 Feb 2024 00:09:41 -0800 Subject: [PATCH 151/819] ver: 1.6.0-prerelease-1 --- Cargo.lock | 40 ++++++++++++++++++++++++++++++++++------ Cargo.toml | 10 +++++----- keyberon/Cargo.toml | 2 +- parser/Cargo.toml | 6 +++--- 4 files changed, 43 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 81c05be5a..e077d9f43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -424,7 +424,7 @@ dependencies = [ [[package]] name = "kanata" -version = "1.5.0" +version = "1.6.0-prerelease-1" dependencies = [ "anyhow", "clap", @@ -433,8 +433,8 @@ dependencies = [ "evdev", "inotify", "kanata-interception", - "kanata-keyberon", - "kanata-parser", + "kanata-keyberon 0.160.1 (registry+https://github.com/rust-lang/crates.io-index)", + "kanata-parser 0.160.1 (registry+https://github.com/rust-lang/crates.io-index)", "karabiner-driverkit", "log", "miette", @@ -468,7 +468,18 @@ dependencies = [ [[package]] name = "kanata-keyberon" -version = "0.150.4" +version = "0.160.1" +dependencies = [ + "arraydeque", + "heapless", + "kanata-keyberon-macros", +] + +[[package]] +name = "kanata-keyberon" +version = "0.160.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31ce7a4d56e0acf0194ad82ff6098584d8b4371fa8a4f41ea945d77b669ea165" dependencies = [ "arraydeque", "heapless", @@ -487,10 +498,27 @@ dependencies = [ [[package]] name = "kanata-parser" -version = "0.150.4" +version = "0.160.1" +dependencies = [ + "anyhow", + "kanata-keyberon 0.160.1 (registry+https://github.com/rust-lang/crates.io-index)", + "log", + "miette", + "once_cell", + "parking_lot", + "radix_trie", + "rustc-hash", + "thiserror", +] + +[[package]] +name = "kanata-parser" +version = "0.160.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "961ab2d3dcf6d9510bf56aaeb6d99fddcd9807d4e96e51344010a10cf07184fb" dependencies = [ "anyhow", - "kanata-keyberon", + "kanata-keyberon 0.160.1 (registry+https://github.com/rust-lang/crates.io-index)", "log", "miette", "once_cell", diff --git a/Cargo.toml b/Cargo.toml index af9effe36..d37636d58 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = [ [package] name = "kanata" -version = "1.5.0" +version = "1.6.0-prerelease-1" authors = ["jtroo "] description = "Multi-layer keyboard customization" keywords = ["cli", "linux", "windows", "keyboard", "layout"] @@ -34,13 +34,13 @@ serde_derive = "1.0" serde_json = { version = "1", features = ["alloc"], default_features = false } simplelog = "0.12.0" -# kanata-keyberon = "0.150.4" -# kanata-parser = "0.150.4" +kanata-keyberon = "0.160.1" +kanata-parser = "0.160.1" # Uncomment below and comment out above for testing local changes. # Otherwise any changes to the local files will not reflect in the compiled # binary. -kanata-keyberon = { path = "keyberon" } -kanata-parser = { path = "parser" } +# kanata-keyberon = { path = "keyberon" } +# kanata-parser = { path = "parser" } [target.'cfg(target_os = "macos")'.dependencies] karabiner-driverkit = "0.1.3" diff --git a/keyberon/Cargo.toml b/keyberon/Cargo.toml index 743178a9e..099f0dc70 100644 --- a/keyberon/Cargo.toml +++ b/keyberon/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kanata-keyberon" -version = "0.150.4" +version = "0.160.1" authors = ["Guillaume Pinot ", "Robin Krahl ", "jtroo "] edition = "2018" description = "Pure Rust keyboard firmware. Fork intended for use with kanata." diff --git a/parser/Cargo.toml b/parser/Cargo.toml index 0972bede9..880b5c7d0 100644 --- a/parser/Cargo.toml +++ b/parser/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kanata-parser" -version = "0.150.4" +version = "0.160.1" authors = ["jtroo "] description = "A parser for configuration language of kanata, a keyboard remapper." keywords = ["kanata", "parser"] @@ -20,11 +20,11 @@ rustc-hash = "1.1.0" miette = { version = "5.7.0", features = ["fancy"] } thiserror = "1.0.38" -# kanata-keyberon = "0.150.4" +kanata-keyberon = "0.160.1" # Uncomment below and comment out above for testing local changes. # Otherwise any changes to the local files will not reflect in the compiled # binary. -kanata-keyberon = { path = "../keyberon" } +# kanata-keyberon = { path = "../keyberon" } [features] cmd = [] From d6bf7bdcab062d3fd2caac5a34962e0e276dbebe Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 11 Feb 2024 00:50:46 -0800 Subject: [PATCH 152/819] chore: add windows scancode to justfile --- justfile | 1 + 1 file changed, 1 insertion(+) diff --git a/justfile b/justfile index d1ca27d32..ec89106bc 100644 --- a/justfile +++ b/justfile @@ -14,6 +14,7 @@ build_release_linux output_dir: build_release_windows output_dir: cargo build --release; cp target/release/kanata.exe "{{output_dir}}\kanata.exe" cargo build --release --features interception_driver; cp target/release/kanata.exe "{{output_dir}}\kanata_wintercept.exe" + cargo build --release --features win_sendinput_send_scancodes; cp target/release/kanata.exe "{{output_dir}}\kanata_scancode_experimental.exe" cargo build --release --features cmd; cp target/release/kanata.exe "{{output_dir}}\kanata_cmd_allowed.exe" cargo build --release --features cmd,interception_driver; cp target/release/kanata.exe "{{output_dir}}\kanata_wintercept_cmd_allowed.exe" cp cfg_samples/kanata.kbd "{{output_dir}}" From a37047b9ee57a2b9a5b3388f7c1465059e4cd81a Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 11 Feb 2024 00:51:46 -0800 Subject: [PATCH 153/819] chore: revert to local dependencies post-release --- Cargo.lock | 34 +++------------------------------- Cargo.toml | 8 ++++---- parser/Cargo.toml | 4 ++-- 3 files changed, 9 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e077d9f43..6798ae889 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -433,8 +433,8 @@ dependencies = [ "evdev", "inotify", "kanata-interception", - "kanata-keyberon 0.160.1 (registry+https://github.com/rust-lang/crates.io-index)", - "kanata-parser 0.160.1 (registry+https://github.com/rust-lang/crates.io-index)", + "kanata-keyberon", + "kanata-parser", "karabiner-driverkit", "log", "miette", @@ -475,17 +475,6 @@ dependencies = [ "kanata-keyberon-macros", ] -[[package]] -name = "kanata-keyberon" -version = "0.160.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31ce7a4d56e0acf0194ad82ff6098584d8b4371fa8a4f41ea945d77b669ea165" -dependencies = [ - "arraydeque", - "heapless", - "kanata-keyberon-macros", -] - [[package]] name = "kanata-keyberon-macros" version = "0.2.0" @@ -501,24 +490,7 @@ name = "kanata-parser" version = "0.160.1" dependencies = [ "anyhow", - "kanata-keyberon 0.160.1 (registry+https://github.com/rust-lang/crates.io-index)", - "log", - "miette", - "once_cell", - "parking_lot", - "radix_trie", - "rustc-hash", - "thiserror", -] - -[[package]] -name = "kanata-parser" -version = "0.160.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "961ab2d3dcf6d9510bf56aaeb6d99fddcd9807d4e96e51344010a10cf07184fb" -dependencies = [ - "anyhow", - "kanata-keyberon 0.160.1 (registry+https://github.com/rust-lang/crates.io-index)", + "kanata-keyberon", "log", "miette", "once_cell", diff --git a/Cargo.toml b/Cargo.toml index d37636d58..871654288 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,13 +34,13 @@ serde_derive = "1.0" serde_json = { version = "1", features = ["alloc"], default_features = false } simplelog = "0.12.0" -kanata-keyberon = "0.160.1" -kanata-parser = "0.160.1" +# kanata-keyberon = "0.160.1" +# kanata-parser = "0.160.1" # Uncomment below and comment out above for testing local changes. # Otherwise any changes to the local files will not reflect in the compiled # binary. -# kanata-keyberon = { path = "keyberon" } -# kanata-parser = { path = "parser" } +kanata-keyberon = { path = "keyberon" } +kanata-parser = { path = "parser" } [target.'cfg(target_os = "macos")'.dependencies] karabiner-driverkit = "0.1.3" diff --git a/parser/Cargo.toml b/parser/Cargo.toml index 880b5c7d0..b403be0eb 100644 --- a/parser/Cargo.toml +++ b/parser/Cargo.toml @@ -20,11 +20,11 @@ rustc-hash = "1.1.0" miette = { version = "5.7.0", features = ["fancy"] } thiserror = "1.0.38" -kanata-keyberon = "0.160.1" +# kanata-keyberon = "0.160.1" # Uncomment below and comment out above for testing local changes. # Otherwise any changes to the local files will not reflect in the compiled # binary. -# kanata-keyberon = { path = "../keyberon" } +kanata-keyberon = { path = "../keyberon" } [features] cmd = [] From 6f2ee9358cbcd8fb9b510c3bf2abd27585e8df9e Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 11 Feb 2024 14:19:00 -0800 Subject: [PATCH 154/819] doc: change ordering of output chord doc Output chord docs are reordered and special behaviour is emphasized to improve discoverability of the behaviour and the multi workaround. --- docs/config.adoc | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/docs/config.adoc b/docs/config.adoc index cdd0966d4..91583ea9b 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -578,6 +578,20 @@ normal key name with one or more of: These modifiers may be combined together if desired. +NOTE: A special behaviour of output chords is that if another key is pressed, +all of the chord keys will be released +before the newly pressed key action activates. +The modifier keys are often not desired for subsequent actions +and without this behaviour, +rapid typing can result in undesired modified key presses. +If you want keys to remain pressed, use <> instead. + +Output chords are typically used do one-off actions such as: + +- type a symbol, e.g. `S-1` +- type a special/accented character, e.g. `RA-a` +- do a special action like `C-c` to send `SIGTERM` in the terminal + .Example: [source] ---- @@ -594,18 +608,6 @@ These modifiers may be combined together if desired. ) ---- -A special behaviour of output chords is that if another key is pressed, -all of the chord keys will be released -before the newly pressed key action activates. -Output chords are typically used do one-off actions such as: - -- type a symbol, e.g. `S-1` -- type a special/accented character, e.g. `RA-a` -- do a special action like `C-c` to send `SIGTERM` in the terminal - -The modifier pressed with these one-off action -is usually not desired for subsequent actions. - [[repeat-key]] === Repeat key <> @@ -1172,7 +1174,7 @@ This means that with `+macro+` you can have some letters capitalized and others not. This is not possible with `+multi+`. The `+macro+` action accepts one or more keys, some actions, chords, and delays -(unit: ms). It also accepts a list prefixed with <> +(unit: ms). It also accepts a list prefixed with <> modifiers where the list is subject to the aforementioned restrictions. The number keys will be parsed as delays, so they must be aliased to be used in a macro. From e18f4a518f418ec6daa840f172fa296eb10b71fa Mon Sep 17 00:00:00 2001 From: jtroo Date: Tue, 13 Feb 2024 00:12:23 -0800 Subject: [PATCH 155/819] doc: update live reload --- cfg_samples/kanata.kbd | 14 +++++++++----- docs/config.adoc | 12 +++++++++--- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/cfg_samples/kanata.kbd b/cfg_samples/kanata.kbd index 3d6b488ba..9e494784e 100644 --- a/cfg_samples/kanata.kbd +++ b/cfg_samples/kanata.kbd @@ -586,11 +586,15 @@ If you need help, please feel welcome to ask in the GitHub discussions. _ _ _ _ _ _ _ ) -;; The `lrld` action stands for "live reload". This will re-parse everything -;; except for linux-dev. So in Linux, you cannot live reload and switch keyboard -;; devices at the time of writing. The variants `lrpv` and `lrnx` will cycle -;; between multiple configuration files, if they are specified in the startup. -;; arguments. The list action variant `lrld-num` takes a number parameter and +;; The `lrld` action stands for "live reload". +;; +;; NOTE: live reload does not read changes to device-related configurations, +;; such as `linux-dev`, `macos-dev-names-include`, +;; or `windows-only-windows-interception-keyboard-hwids`. +;; +;; The variants `lrpv` and `lrnx` will cycle between multiple configuration files +;; if they are specified in the startup arguments. +;; The list action variant `lrld-num` takes a number parameter and ;; reloads the configuration file specified by the number, according to the ;; order passed into the arguments on kanata startup. ;; diff --git a/docs/config.adoc b/docs/config.adoc index 91583ea9b..0bbee5ce3 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -418,9 +418,15 @@ This section explains the available actions. === Live reload <> -You can put the `+lrld+` action onto a key to live-reload your configuration -file. If kanata can't parse the file, it will continue using the previous -configuration. +You can put the `+lrld+` action onto a key to live reload your configuration file. +If kanata can't parse the file, +the previous configuration will continue to be used. +When live reload is activated, +the active kanata layer will be the first `deflayer` defined in the configuration. + +NOTE: live reload does not read or apply changes to device-related configurations, +such as `linux-dev`, `macos-dev-names-include`, +or `windows-only-windows-interception-keyboard-hwids`. .Example: [source] From 809670a2a0140698082f3aa6647cc28ead411786 Mon Sep 17 00:00:00 2001 From: jtroo Date: Tue, 13 Feb 2024 22:21:44 -0800 Subject: [PATCH 156/819] fix(Windows-LLHOOK): add idle check to non-blocking branch This commit fixes a bug where Win+L while an on-idle-fakekey action is active will not be cleared by the blocking branch of the processing loop, because that branch does not execute while on-idle-fakekey is pending. --- src/kanata/mod.rs | 60 ++++++++++++++++++++++++++++++++++--- src/kanata/windows/mod.rs | 3 +- src/oskbd/windows/llhook.rs | 10 +++++-- 3 files changed, 66 insertions(+), 7 deletions(-) diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index c96c09b7a..78b30fbff 100644 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -1540,12 +1540,13 @@ impl Kanata { let now = time::Instant::now() .checked_sub(time::Duration::from_millis(1)) .expect("subtract 1ms from current time"); + #[cfg(all( not(feature = "interception_driver"), target_os = "windows" ))] { - // If kanata has been blocking for long enough, clear all states. + // If kanata has been inactive for long enough, clear all states. // This won't trigger if there are macros running, or if a key is // held down for a long time and is sending OS repeats. The reason // for this code is in case like Win+L which locks the Windows @@ -1559,10 +1560,12 @@ impl Kanata { // the states that might be stuck. A real use case might be to have // a fake key pressed for a long period of time, so make sure those // are not cleared. - if (now - k.last_tick) > time::Duration::from_secs(60) { + if (now - k.kbd_out.last_action_time) + > time::Duration::from_secs(60) + { log::debug!( - "clearing keyberon normal key states due to blocking for a while" - ); + "clearing keyberon normal key states due to inactivity" + ); k.layout.bm().states.retain(|s| { !matches!( s, @@ -1581,6 +1584,7 @@ impl Kanata { } ) }); + PRESSED_KEYS.lock().clear(); } } k.last_tick = now; @@ -1661,6 +1665,54 @@ impl Kanata { (start.elapsed()).as_nanos() ); + #[cfg(all( + not(feature = "interception_driver"), + target_os = "windows" + ))] + { + // If kanata has been inactive for long enough, clear all states. + // This won't trigger if there are macros running, or if a key is + // held down for a long time and is sending OS repeats. The reason + // for this code is in case like Win+L which locks the Windows + // desktop. When this happens, the Win key and L key will be stuck + // as pressed in the kanata state because LLHOOK kanata cannot read + // keys in the lock screen or administrator applications. So this + // is heuristic to detect such an issue and clear states assuming + // that's what happened. + // + // Only states in the normal key row are cleared, since those are + // the states that might be stuck. A real use case might be to have + // a fake key pressed for a long period of time, so make sure those + // are not cleared. + if (std::time::Instant::now() - (k.kbd_out.last_action_time)) + > time::Duration::from_secs(60) + { + log::debug!( + "clearing keyberon normal key states due to inactivity" + ); + k.layout.bm().states.retain(|s| { + !matches!( + s, + State::NormalKey { + coord: (NORMAL_KEY_ROW, _), + .. + } | State::LayerModifier { + coord: (NORMAL_KEY_ROW, _), + .. + } | State::Custom { + coord: (NORMAL_KEY_ROW, _), + .. + } | State::RepeatingSequence { + coord: (NORMAL_KEY_ROW, _), + .. + } + ) + }); + PRESSED_KEYS.lock().clear(); + dbg!(&k.layout.bm().states); + } + } + drop(k); std::thread::sleep(time::Duration::from_millis(1)); } diff --git a/src/kanata/windows/mod.rs b/src/kanata/windows/mod.rs index 9deb70bb4..def9c2094 100644 --- a/src/kanata/windows/mod.rs +++ b/src/kanata/windows/mod.rs @@ -10,7 +10,8 @@ mod llhook; #[cfg(feature = "interception_driver")] mod interception; -static PRESSED_KEYS: Lazy>> = Lazy::new(|| Mutex::new(HashSet::default())); +pub static PRESSED_KEYS: Lazy>> = + Lazy::new(|| Mutex::new(HashSet::default())); pub static ALTGR_BEHAVIOUR: Lazy> = Lazy::new(|| Mutex::new(AltGrBehaviour::default())); diff --git a/src/oskbd/windows/llhook.rs b/src/oskbd/windows/llhook.rs index 5b2c98961..f55389b13 100644 --- a/src/oskbd/windows/llhook.rs +++ b/src/oskbd/windows/llhook.rs @@ -5,6 +5,7 @@ use std::cell::Cell; use std::io; +use std::time::Instant; use std::{mem, ptr}; use winapi::ctypes::*; @@ -148,15 +149,20 @@ unsafe extern "system" fn hook_proc(code: c_int, wparam: WPARAM, lparam: LPARAM) } /// Handle for writing keys to the OS. -pub struct KbdOut {} +pub struct KbdOut { + pub last_action_time: Instant, +} impl KbdOut { pub fn new() -> Result { - Ok(Self {}) + Ok(Self { + last_action_time: Instant::now(), + }) } pub fn write(&mut self, event: InputEvent) -> Result<(), io::Error> { super::send_key_sendinput(event.code as u16, event.up); + self.last_action_time = Instant::now(); Ok(()) } From 6769433fb5c8e98b7cf71615df680503cad83b0a Mon Sep 17 00:00:00 2001 From: jtroo Date: Fri, 16 Feb 2024 01:35:58 -0800 Subject: [PATCH 157/819] doc: add one-shot key rapidity link to known issues --- docs/platform-known-issues.adoc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/platform-known-issues.adoc b/docs/platform-known-issues.adoc index b58c47acd..b45eea237 100644 --- a/docs/platform-known-issues.adoc +++ b/docs/platform-known-issues.adoc @@ -58,8 +58,9 @@ which map to the binaries ** https://github.com/jtroo/kanata/issues/450 * Unicode support has limitations, using xkb is a more consistent solution ** https://github.com/jtroo/kanata/discussions/703 -* Some special desktop-layer keys (e.g. `ISO_Level3_Latch`) behave incorrectly with rapid press-release +* Key actions can behave incorrectly due to the rapidity of key events ** https://github.com/jtroo/kanata/discussions/733 +** https://github.com/jtroo/kanata/issues/740 == MacOS From 48e2d2f5208c700a12420a54eb84e4da13ef2bd4 Mon Sep 17 00:00:00 2001 From: reidprichard <66481248+reidprichard@users.noreply.github.com> Date: Fri, 16 Feb 2024 14:55:25 -0500 Subject: [PATCH 158/819] doc: add more detail about wintercept hwids (#747) --- docs/config.adoc | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/config.adoc b/docs/config.adoc index 0bbee5ce3..b83f24bf6 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -2019,6 +2019,10 @@ the mouse device, kanata will print its hwid in the log; you can then copy-paste that into this configuration entry. If this defcfg item is not defined, the log will not print. +Hwids in Kanata are byte array representations of a concatenation of the +ASCII hardware ids, which can be seen in Device Manager on Windows. As such, +they are an arbitrary length and can be very long. + https://github.com/jtroo/kanata/issues/108[Relevant issue]. .Example: @@ -2044,6 +2048,10 @@ kanata will print its hwid in the log. You can then copy-paste that into this configuration entry. If this defcfg item is not defined, the log will not print. +Hwids in Kanata are byte array representations of a concatenation of the +ASCII hardware ids, which can be seen in Device Manager on Windows. As such, +they are an arbitrary length and can be very long. + .Example: [source] ---- From 7baa1f4cd507180d112865729689e317439c5584 Mon Sep 17 00:00:00 2001 From: jtroo Date: Fri, 16 Feb 2024 18:47:13 -0800 Subject: [PATCH 159/819] feat: add `deftemplate` and `template-expand` (#745) Add functionality to aid with repetitive configurations. --- Cargo.lock | 16 ++ cfg_samples/kanata.kbd | 39 +++++ docs/config.adoc | 86 +++++++++++ parser/Cargo.toml | 1 + parser/src/cfg/deftemplate.rs | 282 ++++++++++++++++++++++++++++++++++ parser/src/cfg/mod.rs | 17 +- 6 files changed, 440 insertions(+), 1 deletion(-) create mode 100644 parser/src/cfg/deftemplate.rs diff --git a/Cargo.lock b/Cargo.lock index 6798ae889..3f2c0839e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -238,6 +238,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "either" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" + [[package]] name = "encode_unicode" version = "0.3.6" @@ -407,6 +413,15 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.10" @@ -490,6 +505,7 @@ name = "kanata-parser" version = "0.160.1" dependencies = [ "anyhow", + "itertools", "kanata-keyberon", "log", "miette", diff --git a/cfg_samples/kanata.kbd b/cfg_samples/kanata.kbd index 9e494784e..ced160022 100644 --- a/cfg_samples/kanata.kbd +++ b/cfg_samples/kanata.kbd @@ -1045,3 +1045,42 @@ If you need help, please feel welcome to ask in the GitHub discussions. ;; included files also cannot contain includes themselves. ;; ;; (include included-file.kbd) + + +;; The top-level item `deftemplate` declares a template +;; which can be expanded multiple times to reduce repetition. +;; +;; Expansion of a template is done via `expand-template`. + +;; This template defines a chord group and aliases that use the chord group. +;; The purpose is to easily define the same chord position behaviour +;; for multiple layers that have different underlying keys. +(deftemplate left-hand-chords (chordgroupname k1 k2 k3 k4 alias1 alias2 alias3 alias4) + (defalias + $alias1 (chord $chordgroupname $k1) + $alias2 (chord $chordgroupname $k2) + $alias3 (chord $chordgroupname $k3) + $alias4 (chord $chordgroupname $k4) + ) + (defchords $chordgroupname $chord-timeout + ($k1) $k1 + ($k2) $k2 + ($k3) $k3 + ($k4) $k4 + ($k1 $k2) lctl + ($k3 $k4) lsft + ) +) + +(defvar chord-timeout 200) + +(template-expand left-hand-chords qwerty a s d f qwa qws qwd qwf) +(template-expand left-hand-chords dvorak a o e u dva dvo dve dvu) + +(deflayer template-example + _ _ _ _ _ _ _ _ _ _ _ _ _ _ + _ @qwa @qws @qwd @qwf _ _ _ _ _ _ _ _ _ + _ @dva @dvo @dve @dvu _ _ _ _ _ _ _ _ + _ _ _ _ _ _ _ _ _ _ _ _ + _ _ _ _ _ _ _ +) \ No newline at end of file diff --git a/docs/config.adoc b/docs/config.adoc index b83f24bf6..115cc8cab 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -2524,6 +2524,92 @@ and 8 is 8th most recent key pressed. ) ---- +[[templates]] +=== Templates +<> + +The top-level configuration item `deftemplate` +declares a template that can be expanded multiple times +via the list item `template-expand`. + +The parameters to `deftemplate` in order are: + +* Template name +* List of template variables +* Template content (any combination of lists / strings) + +Template expansion happens after file includes and before any other parsing. +Within the template content, variable names prefixed with `$` +will be substituted with the expression passed into `template-expand`. + +The list item `template-expand` can be placed as a top-level list +or within another list. +Its parameters in order are: + +* template name +* parameters to substitute into the template + +.Example 1: +[source] +---- +(defvar chord-timeout 200) +(defcfg process-unmapped-keys yes) + +;; This template defines a chord group and aliases that use the chord group. +;; The purpose is to easily define the same chord position behaviour +;; for multiple layers that have different underlying keys. +(deftemplate left-hand-chords (chordgroupname k1 k2 k3 k4 alias1 alias2 alias3 alias4) + (defalias + $alias1 (chord $chordgroupname $k1) + $alias2 (chord $chordgroupname $k2) + $alias3 (chord $chordgroupname $k3) + $alias4 (chord $chordgroupname $k4) + ) + (defchords $chordgroupname $chord-timeout + ($k1) $k1 + ($k2) $k2 + ($k3) $k3 + ($k4) $k4 + ($k1 $k2) lctl + ($k3 $k4) lsft + ) +) + +(template-expand left-hand-chords qwerty a s d f qwa qws qwd qwf) +(template-expand left-hand-chords dvorak a o e u dva dvo dve dvu) + +(defsrc a s d f) +(deflayer dvorak @dva @dvo @dve @dvu) +(deflayer qwerty @qwa @qws @qwd @qwf) +---- + +.Example 2: +[source] +---- +;; This template defines a home row that customizes a single key's behaviour +(deftemplate home-row (j-behaviour) + a s d f g h $j-behaviour k l ; ' +) + +(defsrc + grv 1 2 3 4 5 6 7 8 9 0 - = bspc + tab q w e r t y u i o p [ ] \ + ;; usable even inside defsrc + caps (template-expand home-row j) ret + lsft z x c v b n m , . / rsft + lctl lmet lalt spc ralt rmet rctl +) + +(deflayer base + grv 1 2 3 4 5 6 7 8 9 0 - = bspc + tab q w e r t y u i o p [ ] \ + ;; lists can be passed in too! + caps (template-expand home-row (tap-hold 200 200 j lctl)) ret + lsft z x c v b n m , . / rsft + lctl lmet lalt spc ralt rmet rctl +) +---- + [[custom-tap-hold-behaviour]] === Custom tap-hold behaviour <> diff --git a/parser/Cargo.toml b/parser/Cargo.toml index b403be0eb..b7a897c72 100644 --- a/parser/Cargo.toml +++ b/parser/Cargo.toml @@ -11,6 +11,7 @@ license = "LGPL-3.0" edition = "2021" [dependencies] +itertools = "0.12" log = { version = "0.4.8", default_features = false } anyhow = "1" parking_lot = "0.12" diff --git a/parser/src/cfg/deftemplate.rs b/parser/src/cfg/deftemplate.rs new file mode 100644 index 000000000..93e890d67 --- /dev/null +++ b/parser/src/cfg/deftemplate.rs @@ -0,0 +1,282 @@ +//! This file is responsible for template expansion. +//! For simplicity of implementation, there is performance left off the table. +//! This code runs at parse time and not in runtime +//! so it is not performance critical. +//! +//! The known performance left off the table is: +//! +//! - Creating the expanded template recurses through all SExprs every time. +//! Instead the code could pre-compute the paths to access every variable +//! that needs substition. (perf_1) +//! +//! - Replacing the `template-expand` items with the expanded template +//! recreates the Vec for every replacement that happens at that layer. +//! Instead the code could do a single pass +//! and intelligently insert SExprs at the proper places. (perf_2) + +use crate::anyhow_expr; +use crate::anyhow_span; +use crate::bail_expr; +use crate::bail_span; +use crate::err_span; + +use super::error::*; +use super::sexpr::*; +use super::*; + +#[derive(Debug)] +struct Template { + name: String, + vars: Vec, + // Same as vars above but all names are prefixed with '$'. + vars_substitute_names: Vec, + content: Vec, +} + +/// Parse `deftemplate`s and expand `template-expand`s. +/// +/// Syntax of `deftemplate` is: +/// +/// `(deftemplate () )` +/// +/// Syntax of `template-expand` is: +/// +/// `(template-expand