From 779661ec601678706fba853a382b1e712101107f Mon Sep 17 00:00:00 2001 From: Endolite <41879690+Endolite@users.noreply.github.com> Date: Mon, 1 Sep 2025 21:58:50 -0400 Subject: [PATCH 1/3] feat: added rpt-exclude-bspace --- Cargo.toml | 117 +++++++++++++++++++++--------------- docs/config.adoc | 17 +++++- parser/src/cfg/mod.rs | 1 + parser/src/custom_action.rs | 1 + src/kanata/mod.rs | 16 ++++- 5 files changed, 101 insertions(+), 51 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0a4c23150..551e4161e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,19 +1,15 @@ [workspace] members = [ - "./", - "parser", - "keyberon", - "example_tcp_client", - "tcp_protocol", - "windows_key_tester", - "simulated_input", - "simulated_passthru", -] -exclude = [ - "interception", - "key-sort-add", - "wasm", + "./", + "parser", + "keyberon", + "example_tcp_client", + "tcp_protocol", + "windows_key_tester", + "simulated_input", + "simulated_passthru", ] +exclude = ["interception", "key-sort-add", "wasm"] resolver = "2" [package] @@ -40,7 +36,12 @@ path = "src/main.rs" [dependencies] anyhow = "1" -clap = { version = "4", features = [ "std", "derive", "help", "suggestions" ], default-features = false } +clap = { version = "4", features = [ + "std", + "derive", + "help", + "suggestions", +], default-features = false } dirs = "5.0.1" indoc = { version = "2.0.4", optional = true } log = { version = "0.4.8", default-features = false } @@ -50,12 +51,14 @@ parking_lot = "0.12" radix_trie = "0.2" rustc-hash = "1.1.0" simplelog = "0.12.0" -serde_json = { version = "1", features = ["std"], default-features = false, optional = true } +serde_json = { version = "1", features = [ + "std", +], default-features = false, optional = true } time = "0.3.36" web-time = "1.1.0" kanata-keyberon = { path = "keyberon", version = "0.190.0" } -kanata-parser = { path = "parser", version = "0.190.0" } +kanata-parser = { path = "parser", version = "0.190.0" } kanata-tcp-protocol = { path = "tcp_protocol", version = "0.190.0" } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] @@ -81,37 +84,39 @@ sd-notify = "0.4.1" [target.'cfg(target_os = "windows")'.dependencies] encode_unicode = "0.3.6" winapi = { version = "0.3.9", features = [ - "wincon", - "timeapi", - "mmsystem", - "winuser", - "windef", - "minwindef", + "wincon", + "timeapi", + "mmsystem", + "winuser", + "windef", + "minwindef", ] } windows-sys = { version = "0.52.0", features = [ - "Win32_Devices_DeviceAndDriverInstallation", - "Win32_Devices_Usb", - "Win32_Foundation", - "Win32_Graphics_Gdi", - "Win32_Security", - "Win32_System_Diagnostics_Debug", - "Win32_System_Registry", - "Win32_System_Threading", - "Win32_UI_Controls", - "Win32_UI_Shell", - "Win32_UI_HiDpi", - "Win32_UI_WindowsAndMessaging", - "Win32_System_SystemInformation", - "Wdk", - "Wdk_System", - "Wdk_System_SystemServices", -], optional=true } -native-windows-gui = { version = "1.0.13", default-features = false} + "Win32_Devices_DeviceAndDriverInstallation", + "Win32_Devices_Usb", + "Win32_Foundation", + "Win32_Graphics_Gdi", + "Win32_Security", + "Win32_System_Diagnostics_Debug", + "Win32_System_Registry", + "Win32_System_Threading", + "Win32_UI_Controls", + "Win32_UI_Shell", + "Win32_UI_HiDpi", + "Win32_UI_WindowsAndMessaging", + "Win32_System_SystemInformation", + "Wdk", + "Wdk_System", + "Wdk_System_SystemServices", +], optional = true } +native-windows-gui = { version = "1.0.13", default-features = false } regex = { version = "1.10.4", optional = true } kanata-interception = { version = "0.3.0", optional = true } muldiv = { version = "1.0.1", optional = true } strip-ansi-escapes = { version = "0.2.0", optional = true } -open = { version = "5", features = ["shellexecute-on-windows"], optional = true} +open = { version = "5", features = [ + "shellexecute-on-windows", +], optional = true } # shellexecute fix allows opening files already opened for writing, needs _detached mode [target.'cfg(target_os = "windows")'.build-dependencies] @@ -120,23 +125,39 @@ indoc = { version = "2.0.4", optional = true } regex = { version = "1.10.4", optional = true } [features] -default = ["tcp_server","win_sendinput_send_scancodes", "zippychord"] +default = ["tcp_server", "win_sendinput_send_scancodes", "zippychord"] perf_logging = [] tcp_server = ["serde_json"] win_sendinput_send_scancodes = ["kanata-parser/win_sendinput_send_scancodes"] win_llhook_read_scancodes = ["kanata-parser/win_llhook_read_scancodes"] win_manifest = ["embed-resource", "indoc", "regex"] cmd = ["kanata-parser/cmd"] -interception_driver = ["kanata-interception", "kanata-parser/interception_driver"] +interception_driver = [ + "kanata-interception", + "kanata-parser/interception_driver", +] simulated_output = ["indoc"] simulated_input = ["indoc"] -passthru_ahk = ["simulated_input","simulated_output"] -gui = ["win_manifest","kanata-parser/gui", - "win_sendinput_send_scancodes","win_llhook_read_scancodes", - "muldiv","strip-ansi-escapes","open", +passthru_ahk = ["simulated_input", "simulated_output"] +gui = [ + "win_manifest", + "kanata-parser/gui", + "win_sendinput_send_scancodes", + "win_llhook_read_scancodes", + "muldiv", + "strip-ansi-escapes", + "open", "dep:windows-sys", "winapi/processthreadsapi", - "native-windows-gui/tray-notification","native-windows-gui/message-window","native-windows-gui/menu","native-windows-gui/cursor","native-windows-gui/high-dpi","native-windows-gui/embed-resource","native-windows-gui/image-decoder","native-windows-gui/notice","native-windows-gui/animation-timer", + "native-windows-gui/tray-notification", + "native-windows-gui/message-window", + "native-windows-gui/menu", + "native-windows-gui/cursor", + "native-windows-gui/high-dpi", + "native-windows-gui/embed-resource", + "native-windows-gui/image-decoder", + "native-windows-gui/notice", + "native-windows-gui/animation-timer", ] zippychord = ["kanata-parser/zippychord"] diff --git a/docs/config.adoc b/docs/config.adoc index d7c60cfa2..6fa47511b 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -1160,6 +1160,9 @@ The output chord prefix strings are: | `rpt-any` | String action that outputs the most-recently outputted action. + +| `rpt-exclude-bspace` +| String action that outputs the single most-recently typed key, ignoring backspace. |=== **Description** @@ -1186,12 +1189,24 @@ There is a variant `rpt-any` which will repeat any previous action and would output `ctrl+c` in the example case. +.Example: +[source] ---- (deflayer has-repeat-any rpt-any a s d f ) ---- +The `rpt-exclude-bspace` action repeats the last key output that isn't backspace. Note that this does not output chords, so `ctrl+c bspace` will output `c`. + +.Example: +[source] +--- +(deflayer has-repeat-skip-bspace + rpt-skip-bspace a s d f +) +--- + [[release-a-key-or-layer]] === Release a key or layer @@ -4519,7 +4534,7 @@ https://github.com/jtroo/kanata/blob/main/example_tcp_client/src/main.rs[example The TCP server supports live configuration reloading through the following commands: - `{"Reload":{}}` - Reload the current configuration file (equivalent to `lrld` keyboard action) -- `{"ReloadNext":{}}` - Reload the next configuration file (equivalent to `lrnx` keyboard action) +- `{"ReloadNext":{}}` - Reload the next configuration file (equivalent to `lrnx` keyboard action) - `{"ReloadPrev":{}}` - Reload the previous configuration file (equivalent to `lrpv` keyboard action) - `{"ReloadNum":{"index":N}}` - Reload configuration file at index N (equivalent to `lrld-num N` keyboard action) - `{"ReloadFile":{"path":"/path/to/config.kbd"}}` - Reload a specific configuration file (equivalent to `lrld-file` keyboard action) diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index 3f7002524..053eed253 100644 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -1702,6 +1702,7 @@ fn parse_action_atom(ac_span: &Spanned, s: &ParserState) -> Result<&'sta ); } "rpt" | "repeat" | "rpt-key" => return custom(CustomAction::Repeat, &s.a), + "rpt-exclude-bspace" => return custom(CustomAction::RepeatExcludeBSpace, &s.a), "rpt-any" => return Ok(s.a.sref(Action::Repeat)), "dynamic-macro-record-stop" => { return custom(CustomAction::DynamicMacroRecordStop(0), &s.a); diff --git a/parser/src/custom_action.rs b/parser/src/custom_action.rs index 7ed201067..f8c017669 100644 --- a/parser/src/custom_action.rs +++ b/parser/src/custom_action.rs @@ -71,6 +71,7 @@ pub enum CustomAction { LiveReloadNum(u16), LiveReloadFile(String), Repeat, + RepeatExcludeBSpace, CancelMacroOnRelease, CancelMacroOnNextPress(u32), DynamicMacroRecord(u16), diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index 31f08bdf8..0c4a28fd1 100644 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -238,6 +238,8 @@ pub struct Kanata { unshifted_keys: Vec, /// Keep track of last pressed key for [`CustomAction::Repeat`]. last_pressed_key: KeyCode, + /// Keep track of last pressed non-backspace key for [`CustomAction::RepeatSkipBspace`] + last_pressed_nonbspace_key: KeyCode, #[cfg(feature = "tcp_server")] /// Names of fake keys mapped to their index in the fake keys row pub virtual_keys: HashMap, @@ -470,6 +472,7 @@ impl Kanata { unmodded_mods: UnmodMods::empty(), unshifted_keys: vec![], last_pressed_key: KeyCode::No, + last_pressed_nonbspace_key: KeyCode::No, #[cfg(feature = "tcp_server")] virtual_keys: cfg.fake_keys, switch_max_key_timing: cfg.switch_max_key_timing, @@ -618,6 +621,7 @@ impl Kanata { unmodded_mods: UnmodMods::empty(), unshifted_keys: vec![], last_pressed_key: KeyCode::No, + last_pressed_nonbspace_key: KeyCode::No, #[cfg(feature = "tcp_server")] virtual_keys: cfg.fake_keys, switch_max_key_timing: cfg.switch_max_key_timing, @@ -1299,6 +1303,11 @@ impl Kanata { self.prev_keys.push(*k); self.last_pressed_key = *k; + self.last_pressed_nonbspace_key = match k { + &KeyCode::BSpace => self.last_pressed_nonbspace_key, + _ => *k, + }; + if self.sequence_always_on && self.sequence_state.is_inactive() { self.sequence_state .activate(self.sequence_input_mode, self.sequence_timeout); @@ -1586,8 +1595,11 @@ impl Kanata { add_noerase(state, *noerase_count); } } - CustomAction::Repeat => { - let keycode = self.last_pressed_key; + CustomAction::Repeat | CustomAction::RepeatExcludeBSpace => { + let keycode = match custact { + CustomAction::Repeat => self.last_pressed_key, + _ => self.last_pressed_nonbspace_key, + }; let osc: OsCode = keycode.into(); log::debug!("repeating a keypress {osc:?}"); let mut do_caps_word = false; From f402a78e213d81d3a3dc3f1ef65bebaa843526d3 Mon Sep 17 00:00:00 2001 From: Endolite <41879690+Endolite@users.noreply.github.com> Date: Tue, 2 Sep 2025 00:12:13 -0400 Subject: [PATCH 2/3] changed to smart-rpt --- docs/config.adoc | 8 +++--- parser/src/cfg/mod.rs | 2 +- parser/src/custom_action.rs | 2 +- src/kanata/mod.rs | 50 ++++++++++++++++++++++++++++--------- 4 files changed, 44 insertions(+), 18 deletions(-) diff --git a/docs/config.adoc b/docs/config.adoc index 6fa47511b..b20d539cd 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -1161,8 +1161,8 @@ The output chord prefix strings are: | `rpt-any` | String action that outputs the most-recently outputted action. -| `rpt-exclude-bspace` -| String action that outputs the single most-recently typed key, ignoring backspace. +| `smart-rpt` +| String action that outputs the most-recently typed key and goes one key back in history after backspace. |=== **Description** @@ -1197,13 +1197,13 @@ and would output `ctrl+c` in the example case. ) ---- -The `rpt-exclude-bspace` action repeats the last key output that isn't backspace. Note that this does not output chords, so `ctrl+c bspace` will output `c`. +The `smart-rpt` action repeats the most recently input key but goes back at most one additional key on backspace. For example, the sequence ` ` outputs `Hello`, while replacing `smart-rpt` with `rpt` outputs `Heo`. .Example: [source] --- (deflayer has-repeat-skip-bspace - rpt-skip-bspace a s d f + smart-rpt a s d f ) --- diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index 053eed253..8a0afeeac 100644 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -1702,7 +1702,7 @@ fn parse_action_atom(ac_span: &Spanned, s: &ParserState) -> Result<&'sta ); } "rpt" | "repeat" | "rpt-key" => return custom(CustomAction::Repeat, &s.a), - "rpt-exclude-bspace" => return custom(CustomAction::RepeatExcludeBSpace, &s.a), + "smart-rpt" => return custom(CustomAction::SmartRepeat, &s.a), "rpt-any" => return Ok(s.a.sref(Action::Repeat)), "dynamic-macro-record-stop" => { return custom(CustomAction::DynamicMacroRecordStop(0), &s.a); diff --git a/parser/src/custom_action.rs b/parser/src/custom_action.rs index f8c017669..62890f010 100644 --- a/parser/src/custom_action.rs +++ b/parser/src/custom_action.rs @@ -71,7 +71,7 @@ pub enum CustomAction { LiveReloadNum(u16), LiveReloadFile(String), Repeat, - RepeatExcludeBSpace, + SmartRepeat, CancelMacroOnRelease, CancelMacroOnNextPress(u32), DynamicMacroRecord(u16), diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index 0c4a28fd1..f40567223 100644 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -238,8 +238,9 @@ pub struct Kanata { unshifted_keys: Vec, /// Keep track of last pressed key for [`CustomAction::Repeat`]. last_pressed_key: KeyCode, - /// Keep track of last pressed non-backspace key for [`CustomAction::RepeatSkipBspace`] - last_pressed_nonbspace_key: KeyCode, + /// Keep track of last pressed non-backspace key for [`CustomAction::SmartRepeat`] + last_last_pressed_key: KeyCode, + repeat_count: i8, #[cfg(feature = "tcp_server")] /// Names of fake keys mapped to their index in the fake keys row pub virtual_keys: HashMap, @@ -472,7 +473,8 @@ impl Kanata { unmodded_mods: UnmodMods::empty(), unshifted_keys: vec![], last_pressed_key: KeyCode::No, - last_pressed_nonbspace_key: KeyCode::No, + last_last_pressed_key: KeyCode::No, + repeat_count: 0, #[cfg(feature = "tcp_server")] virtual_keys: cfg.fake_keys, switch_max_key_timing: cfg.switch_max_key_timing, @@ -621,7 +623,8 @@ impl Kanata { unmodded_mods: UnmodMods::empty(), unshifted_keys: vec![], last_pressed_key: KeyCode::No, - last_pressed_nonbspace_key: KeyCode::No, + last_last_pressed_key: KeyCode::No, + repeat_count: 0, #[cfg(feature = "tcp_server")] virtual_keys: cfg.fake_keys, switch_max_key_timing: cfg.switch_max_key_timing, @@ -1301,12 +1304,20 @@ impl Kanata { // logic there and is easier to add here since we already have // allocations and logic. self.prev_keys.push(*k); + + if ![self.last_pressed_key, *k].contains(&KeyCode::BSpace) { + // never save backspace and avoid accidentally overwriting when the next key is backspace + self.last_last_pressed_key = self.last_pressed_key; + } + self.last_pressed_key = *k; - self.last_pressed_nonbspace_key = match k { - &KeyCode::BSpace => self.last_pressed_nonbspace_key, - _ => *k, - }; + if *k == KeyCode::BSpace { + // cancels a repeat + self.repeat_count = std::cmp::max(self.repeat_count - 1, -1); + } else { + self.repeat_count = 0; + } if self.sequence_always_on && self.sequence_state.is_inactive() { self.sequence_state @@ -1595,10 +1606,25 @@ impl Kanata { add_noerase(state, *noerase_count); } } - CustomAction::Repeat | CustomAction::RepeatExcludeBSpace => { - let keycode = match custact { - CustomAction::Repeat => self.last_pressed_key, - _ => self.last_pressed_nonbspace_key, + CustomAction::Repeat | CustomAction::SmartRepeat => { + let n = self.repeat_count; + log::error!("{n}"); + let keycode = match (custact, self.repeat_count) { + (CustomAction::SmartRepeat, -1) => self.last_last_pressed_key, + (CustomAction::SmartRepeat, _) => { + self.repeat_count += 1; + if self.last_pressed_key != KeyCode::BSpace { + self.last_pressed_key + } else { + self.last_last_pressed_key + } + } + _ => { + if self.last_pressed_key == KeyCode::BSpace { + self.repeat_count = 0; + } + self.last_pressed_key + } }; let osc: OsCode = keycode.into(); log::debug!("repeating a keypress {osc:?}"); From a5ab81c2ded7327dae5780353df1c93007c8a2b3 Mon Sep 17 00:00:00 2001 From: Endolite <41879690+Endolite@users.noreply.github.com> Date: Tue, 2 Sep 2025 00:23:30 -0400 Subject: [PATCH 3/3] removed debug statements --- src/kanata/mod.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index f40567223..274dc3627 100644 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -240,6 +240,7 @@ pub struct Kanata { last_pressed_key: KeyCode, /// Keep track of last pressed non-backspace key for [`CustomAction::SmartRepeat`] last_last_pressed_key: KeyCode, + /// Keep track of how many non-backspace repeats have been used in a row for [`CustomAction::SmartRepeat`] repeat_count: i8, #[cfg(feature = "tcp_server")] /// Names of fake keys mapped to their index in the fake keys row @@ -1607,8 +1608,6 @@ impl Kanata { } } CustomAction::Repeat | CustomAction::SmartRepeat => { - let n = self.repeat_count; - log::error!("{n}"); let keycode = match (custact, self.repeat_count) { (CustomAction::SmartRepeat, -1) => self.last_last_pressed_key, (CustomAction::SmartRepeat, _) => {