From 800ea2d27b9ef7773be171aaa10b8fe6afce5f38 Mon Sep 17 00:00:00 2001 From: jtroo Date: Fri, 7 Jun 2024 01:10:33 -0700 Subject: [PATCH 01/54] initial commit --- Cargo.toml | 3 +- parser/Cargo.toml | 1 + parser/src/cfg/mod.rs | 42 ++- parser/src/cfg/tests.rs | 7 + parser/src/cfg/zippychord.rs | 318 ++++++++++++++++++ parser/src/lib.rs | 1 + parser/src/subset.rs | 145 ++++++++ parser/src/trie.rs | 47 +-- parser/test_cfgs/test.zch | 5 + parser/test_cfgs/testzch.kbd | 3 + src/kanata/mod.rs | 18 +- src/kanata/output_logic.rs | 49 ++- src/kanata/output_logic/zippychord.rs | 292 ++++++++++++++++ src/kanata/sequences.rs | 2 +- src/tests/sim_tests/chord_sim_tests.rs | 5 +- src/tests/sim_tests/mod.rs | 12 +- src/tests/sim_tests/zippychord_sim_tests.rs | 61 ++++ wasm/src/lib.rs | 4 +- .../src/windows/interception.rs | 2 +- 19 files changed, 979 insertions(+), 38 deletions(-) create mode 100644 parser/src/cfg/zippychord.rs create mode 100644 parser/src/subset.rs create mode 100644 parser/test_cfgs/test.zch create mode 100644 parser/test_cfgs/testzch.kbd create mode 100644 src/kanata/output_logic/zippychord.rs create mode 100644 src/tests/sim_tests/zippychord_sim_tests.rs diff --git a/Cargo.toml b/Cargo.toml index 6f4152776..cba178c20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -117,7 +117,7 @@ indoc = { version = "2.0.4", optional = true } regex = { version = "1.10.4", optional = true } [features] -default = ["tcp_server","win_sendinput_send_scancodes"] +default = ["tcp_server","win_sendinput_send_scancodes", "zippychord"] perf_logging = [] tcp_server = ["serde_json"] win_sendinput_send_scancodes = ["kanata-parser/win_sendinput_send_scancodes"] @@ -136,6 +136,7 @@ gui = ["win_manifest","kanata-parser/gui", "winapi/processthreadsapi", "native-windows-gui/tray-notification","native-windows-gui/message-window","native-windows-gui/menu","native-windows-gui/cursor","native-windows-gui/high-dpi","native-windows-gui/embed-resource","native-windows-gui/image-decoder","native-windows-gui/notice","native-windows-gui/animation-timer", ] +zippychord = ["kanata-parser/zippychord"] [profile.release] opt-level = "z" diff --git a/parser/Cargo.toml b/parser/Cargo.toml index 40380034b..e5519d31d 100644 --- a/parser/Cargo.toml +++ b/parser/Cargo.toml @@ -39,3 +39,4 @@ gui = [] lsp = [] win_llhook_read_scancodes = [] win_sendinput_send_scancodes = [] +zippychord = [] diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index ac1b1dd24..9c844e79e 100755 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -93,6 +93,9 @@ pub use key_outputs::*; mod permutations; use permutations::*; +mod zippychord; +pub use zippychord::*; + use crate::lsp_hints::{self, LspHints}; mod str_ext; @@ -221,7 +224,7 @@ type TapHoldCustomFunc = ) -> &'static (dyn Fn(QueuedIter) -> (Option, bool) + Send + Sync); pub type BorrowedKLayout<'a> = Layout<'a, KEYS_IN_ROW, 2, &'a &'a [&'a CustomAction]>; -pub type KeySeqsToFKeys = Trie; +pub type KeySeqsToFKeys = Trie<(u8, u16)>; pub struct KanataLayout { layout: KLayout, @@ -271,6 +274,8 @@ pub struct Cfg { pub fake_keys: HashMap, /// The maximum value of switch's key-timing item in the configuration. pub switch_max_key_timing: u16, + /// Zipchord-like configuration. + pub zippy: Option, } /// Parse a new configuration from a file. @@ -278,14 +283,17 @@ pub fn new_from_file(p: &Path) -> MResult { parse_cfg(p) } -pub fn new_from_str(cfg_text: &str) -> MResult { +pub fn new_from_str(cfg_text: &str, file_content: Option) -> MResult { let mut s = ParserState::default(); let icfg = parse_cfg_raw_string( cfg_text, &mut s, &PathBuf::from("configuration"), &mut FileContentProvider { - get_file_content_fn: &mut |_| Err("include is not supported".into()), + get_file_content_fn: &mut move |_| match &file_content { + Some(s) => Ok(s.clone()), + None => Err("include is not supported".into()), + }, }, DEF_LOCAL_KEYS, Err("environment variables are not supported".into()), @@ -322,6 +330,7 @@ pub fn new_from_str(cfg_text: &str) -> MResult { overrides: icfg.overrides, fake_keys, switch_max_key_timing, + zippy: icfg.zippy, }) } @@ -373,6 +382,7 @@ fn parse_cfg(p: &Path) -> MResult { overrides: icfg.overrides, fake_keys, switch_max_key_timing, + zippy: icfg.zippy, }) } @@ -409,6 +419,7 @@ pub struct IntermediateCfg { pub overrides: Overrides, pub chords_v2: Option>, pub start_action: Option<&'static KanataAction>, + pub zippy: Option, } // A snapshot of enviroment variables, or an error message with an explanation @@ -873,6 +884,29 @@ pub fn parse_cfg_raw_string( .into()); } + let zippy_exprs = root_exprs + .iter() + .filter(gen_first_atom_filter("defzippy-experimental")) + .collect::>(); + let zippy = match zippy_exprs.len() { + 0 => None, + 1 => { + let zippy = parse_zippy(zippy_exprs[0], s, file_content_provider)?; + Some(zippy) + } + _ => { + let spanned = spanned_root_exprs + .iter() + .filter(gen_first_atom_filter_spanned("defzippy-experimental")) + .nth(1) + .expect("> 2 overrides"); + bail_span!( + spanned, + "Only one defzippy allowed, found more.\nDelete the extras." + ) + } + }; + #[cfg(feature = "lsp")] LSP_VARIABLE_REFERENCES.with_borrow_mut(|refs| { s.lsp_hints @@ -893,6 +927,7 @@ pub fn parse_cfg_raw_string( overrides, chords_v2, start_action, + zippy, }) } @@ -926,6 +961,7 @@ fn error_on_unknown_top_level_atoms(exprs: &[Spanned>]) -> Result<()> | "defvar" | "deftemplate" | "defchordsv2-experimental" + | "defzippy-experimental" | "defseq" => Ok(()), _ => err_span!(expr, "Found unknown configuration item"), }) diff --git a/parser/src/cfg/tests.rs b/parser/src/cfg/tests.rs index c05a175c5..a2beeb5bb 100644 --- a/parser/src/cfg/tests.rs +++ b/parser/src/cfg/tests.rs @@ -313,6 +313,13 @@ fn parse_file_with_utf8_bom() { new_from_file(&std::path::PathBuf::from("./test_cfgs/utf8bom.kbd")).unwrap(); } +#[test] +#[cfg(feature = "zippychord")] +fn parse_zippychord_file() { + let _lk = lock(&CFG_PARSE_LOCK); + new_from_file(&std::path::PathBuf::from("./test_cfgs/testzch.kbd")).unwrap(); +} + #[test] fn disallow_nested_tap_hold() { let _lk = lock(&CFG_PARSE_LOCK); diff --git a/parser/src/cfg/zippychord.rs b/parser/src/cfg/zippychord.rs new file mode 100644 index 000000000..58784c649 --- /dev/null +++ b/parser/src/cfg/zippychord.rs @@ -0,0 +1,318 @@ +//! Zipchord-like parsing. Probably not 100% compatible. +//! +//! Example lines in input file. +//! The " => " string represents a tab character. +//! +//! "dy => day" +//! -> chord: (d y) +//! -> output: "day" +//! +//! "dy => day" +//! "dy 1 => Monday" +//! -> chord: (d y) +//! -> output: "day" +//! -> chord: (d y) +//! -> output: "Monday"; "day" gets erased +//! +//! " abc => Alphabet" +//! -> chord: (space a b c) +//! -> output: "Alphabet" +//! +//! "r df => recipient" +//! -> chord: (r) +//! -> output: nothing yet, just type r +//! -> chord: (d f) +//! -> output: "recipient" +//! +//! " w a => Washington" +//! -> chord: (space w) +//! -> output: nothing yet, type spacebar+w in whatever true order they were pressed +//! -> chord: (space a) +//! -> output: "Washington" +//! -> note: do observe the two spaces between 'w' and 'a' +use super::*; + +use crate::bail_expr; +use crate::subset::*; + +use parking_lot::Mutex; + +/// All possible chords. +#[derive(Debug, Clone, Default)] +pub struct ZchPossibleChords(pub SubsetMap>); +impl ZchPossibleChords { + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +/// Tracks current input to check against possible chords. +/// This does not store by the input order; +/// instead it is by some consistent ordering for +/// hashing into the possible chord map. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct ZchInputKeys { + zch_inputs: ZchSortedChord, +} +impl ZchInputKeys { + pub fn zchik_new() -> Self { + Self { + zch_inputs: ZchSortedChord { + zch_keys: Vec::new(), + }, + } + } + pub fn zchik_contains(&mut self, osc: OsCode) -> bool { + self.zch_inputs.zch_keys.contains(&osc.into()) + } + pub fn zchik_insert(&mut self, osc: OsCode) { + self.zch_inputs.zch_insert(osc.into()); + } + pub fn zchik_remove(&mut self, osc: OsCode) { + self.zch_inputs.zch_keys.retain(|k| *k != osc.into()); + } + pub fn zchik_len(&self) -> usize { + self.zch_inputs.zch_keys.len() + } + pub fn zchik_clear(&mut self) { + self.zch_inputs.zch_keys.clear() + } + pub fn zchik_keys(&self) -> &[u16] { + &self.zch_inputs.zch_keys + } +} + +#[derive(Debug, Default, Clone, Hash, PartialEq, Eq)] +/// Sorted consistently by some arbitrary key order; +/// as opposed to, for example, simple the user press order. +pub struct ZchSortedChord { + zch_keys: Vec, +} +impl ZchSortedChord { + pub fn zch_insert(&mut self, key: u16) { + match self.zch_keys.binary_search(&key) { + // Q: what is the meaning of Ok vs. Err? + // A: Ok means the element already in vector @ `pos`. Normally this wouldn't be + // expected to happen but it turns out that key repeat might get in the way of this + // assumption. Err means element does not exist and returns the correct insert position. + Ok(_pos) => {} + Err(pos) => self.zch_keys.insert(pos, key), + } + } +} + +/// A chord. +/// +/// If any followups exist it will be Some. +/// E.g. with: +/// - dy -> day +/// - dy 1 -> Monday +/// - dy 2 -> Tuesday +/// +/// the output will be "day" and the Monday+Tuesday chords will be in `followups`. +#[derive(Debug, Clone)] +pub struct ZchChordOutput { + pub zch_output: Box<[ZchOutput]>, + pub zch_followups: Option>>, +} + +/// Zch output can be uppercase or lowercase characters. +/// The parser should ensure all `OsCode`s within `Lowercase` and `Uppercase` +/// are visible characters that can be backspaced. +#[derive(Debug, Clone, Copy)] +pub enum ZchOutput { + Lowercase(OsCode), + Uppercase(OsCode), +} + +pub(crate) fn parse_zippy( + exprs: &[SExpr], + s: &ParserState, + f: &mut FileContentProvider, +) -> Result { + parse_zippy_inner(exprs, s, f) +} + +#[cfg(not(feature = "zippychord"))] +fn parse_zippy_inner( + exprs: &[SExpr], + _s: &ParserState, + _f: &mut FileContentProvider, +) -> Result { + bail_expr!(&exprs[0], "Kanata was not compiled with the \"zippychord\" feature. This configuration is unsupported") +} + +#[cfg(feature = "zippychord")] +fn parse_zippy_inner( + exprs: &[SExpr], + s: &ParserState, + f: &mut FileContentProvider, +) -> Result { + use crate::anyhow_expr; + use crate::subset::GetOrIsSubsetOfKnownKey::*; + + if exprs.len() != 2 { + bail_expr!( + &exprs[0], + "There must be exactly one filename following this definition.\nFound {}", + exprs.len() - 1 + ); + } + let Some(file_name) = exprs[1].atom(s.vars()) else { + bail_expr!(&exprs[1], "Filename must be a string, not a list."); + }; + let input_data = f + .get_file_content(file_name.as_ref()) + .map_err(|e| anyhow_expr!(&exprs[1], "Failed to read file:\n{e}"))?; + let res = input_data + .lines() + .enumerate() + .filter(|(_, line)| !line.trim().is_empty() && !line.trim().starts_with("//")) + .try_fold( + Arc::new(Mutex::new(ZchPossibleChords(SubsetMap::ssm_new()))), + |zch, (line_number, line)| { + let Some((input, output)) = line.split_once('\t') else { + bail_expr!( + &exprs[1], + "Input and output are separated by a tab, but found no tab:\n{}: {line}", + line_number + 1 + ); + }; + if input.is_empty() { + bail_expr!( + &exprs[1], + "No input defined; line must not begin with a tab:\n{}: {line}", + line_number + 1 + ); + } + + let mut char_buf: [u8; 4] = [0; 4]; + + let output = { + output + .chars() + .try_fold(vec![], |mut zch_output, out_char| -> Result<_> { + let out_key = out_char.to_lowercase().next().unwrap(); + let key_name = out_key.encode_utf8(&mut char_buf); + let osc = str_to_oscode(key_name).ok_or_else(|| { + anyhow_expr!( + &exprs[1], + "Unknown output key name '{}':\n{}: {line}", + out_char, + line_number + 1, + ) + })?; + let out = match out_char.is_uppercase() { + true => ZchOutput::Uppercase(osc), + false => ZchOutput::Lowercase(osc), + }; + zch_output.push(out); + Ok(zch_output) + })? + .into_boxed_slice() + }; + let mut input_left_to_parse = input; + let mut chord_chars; + let mut input_chord = ZchInputKeys::zchik_new(); + let mut is_space_included; + let mut possible_chords_map = zch.clone(); + let mut next_map: Option>>; + + while !input_left_to_parse.is_empty() { + input_chord.zchik_clear(); + + // Check for a starting space. + (is_space_included, input_left_to_parse) = + match input_left_to_parse.strip_prefix(' ') { + None => (false, input_left_to_parse), + Some(i) => (true, i), + }; + if is_space_included { + input_chord.zchik_insert(OsCode::KEY_SPACE); + } + + // Parse chord until next space. + (chord_chars, input_left_to_parse) = match input_left_to_parse.split_once(' ') { + Some(split) => split, + None => (input_left_to_parse, ""), + }; + + chord_chars + .chars() + .try_fold((), |_, chord_char| -> Result<()> { + let key_name = chord_char.encode_utf8(&mut char_buf); + let osc = str_to_oscode(key_name).ok_or_else(|| { + anyhow_expr!( + &exprs[1], + "Unknown input key name: '{key_name}':\n{}: {line}", + line_number + 1 + ) + })?; + input_chord.zchik_insert(osc); + Ok(()) + })?; + + let output_for_input_chord = possible_chords_map + .lock() + .0 + .ssm_get_or_is_subset_ksorted(input_chord.zchik_keys()); + match (input_left_to_parse.is_empty(), output_for_input_chord) { + (true, HasValue(_)) => { + bail_expr!( + &exprs[1], + "Found duplicate input chord, which is disallowed {input}:\n{}: {line}", + line_number + 1 + ); + } + (true, _) => { + possible_chords_map.lock().0.ssm_insert_ksorted( + input_chord.zchik_keys(), + Arc::new(ZchChordOutput { + zch_output: output, + zch_followups: None, + }), + ); + break; + } + (false, HasValue(next_nested_map)) => { + match &next_nested_map.zch_followups { + None => { + let map = Arc::new(Mutex::new(ZchPossibleChords( + SubsetMap::ssm_new(), + ))); + next_map = Some(map.clone()); + possible_chords_map.lock().0.ssm_insert_ksorted( + input_chord.zchik_keys(), + ZchChordOutput { + zch_output: next_nested_map.zch_output.clone(), + zch_followups: Some(map), + } + .into(), + ); + } + Some(followup) => { + next_map = Some(followup.clone()); + } + } + } + (false, _) => { + let map = Arc::new(Mutex::new(ZchPossibleChords(SubsetMap::ssm_new()))); + next_map = Some(map.clone()); + possible_chords_map.lock().0.ssm_insert_ksorted( + input_chord.zchik_keys(), + Arc::new(ZchChordOutput { + zch_output: Box::new([]), + zch_followups: Some(map), + }), + ); + } + }; + if let Some(map) = next_map.take() { + possible_chords_map = map; + } + } + Ok(zch) + }, + )?; + Ok(Arc::into_inner(res).expect("no other refs").into_inner()) +} diff --git a/parser/src/lib.rs b/parser/src/lib.rs index 1d8f7ba2d..1d359da13 100644 --- a/parser/src/lib.rs +++ b/parser/src/lib.rs @@ -6,4 +6,5 @@ pub mod keys; pub mod layers; pub mod lsp_hints; pub mod sequences; +pub mod subset; pub mod trie; diff --git a/parser/src/subset.rs b/parser/src/subset.rs new file mode 100644 index 000000000..d370e5e41 --- /dev/null +++ b/parser/src/subset.rs @@ -0,0 +1,145 @@ +//! Collection where keys are slices of a type, and supports a get operation to check if the key +//! exists, is a subset of any existing key, or is neither of the aforementioned cases. +//! +//! In the underlying structure the value is cloned for each participating member of slice key, so +//! you should ensure values are cheaply clonable. If the value is not, consider putting it inside +//! `Rc` or `Arc`. + +use rustc_hash::FxHashMap; +use std::hash::Hash; + +#[derive(Debug, Clone)] +pub struct SubsetMap { + map: FxHashMap>>, +} + +#[derive(Debug, Clone)] +struct SsmKeyValue { + key: Box<[K]>, + value: V, +} + +impl SsmKeyValue +where + K: Clone, +{ + fn ssmkv_new(key: impl AsRef<[K]>, value: V) -> Self { + Self { + key: key.as_ref().to_vec().into_boxed_slice(), + value, + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum GetOrIsSubsetOfKnownKey { + HasValue(T), + IsSubset, + Neither, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum SsmKeyExistedBeforeInsert { + Existed, + NotThere, +} + +use GetOrIsSubsetOfKnownKey::*; + +impl Default for SubsetMap +where + K: Clone + PartialEq + Ord + Hash, + V: Clone, +{ + fn default() -> Self { + Self::ssm_new() + } +} + +impl SubsetMap +where + K: Clone + PartialEq + Ord + Hash, + V: Clone, +{ + pub fn ssm_new() -> Self { + Self { + map: FxHashMap::default(), + } + } + + /// Inserts a potentially unsorted key. Sorts the key and then calls ssm_insert_ksorted. + pub fn ssm_insert(&mut self, mut key: impl AsMut<[K]>, val: V) -> SsmKeyExistedBeforeInsert { + key.as_mut().sort(); + self.ssm_insert_ksorted(key.as_mut(), val) + } + + /// Inserts a sorted key. Failure to enforce that the key is sorted results in defined but + /// unspecified behaviour. + pub fn ssm_insert_ksorted( + &mut self, + key: impl AsRef<[K]>, + val: V, + ) -> SsmKeyExistedBeforeInsert { + let mut key_existed = SsmKeyExistedBeforeInsert::NotThere; + for k in key.as_ref().iter().cloned() { + let keyvals_for_key_item = self.map.entry(k).or_default(); + match keyvals_for_key_item + .binary_search_by(|probe| probe.key.as_ref().cmp(key.as_ref())) + { + Ok(pos) => { + key_existed = SsmKeyExistedBeforeInsert::Existed; + keyvals_for_key_item[pos] = SsmKeyValue::ssmkv_new(key.as_ref(), val.clone()); + } + Err(pos) => { + keyvals_for_key_item + .insert(pos, SsmKeyValue::ssmkv_new(key.as_ref(), val.clone())); + } + } + } + key_existed + } + + /// Gets using a potentially unsorted key. Sorts the key then calls + /// ssm_get_or_is_subset_ksorted. + pub fn ssm_get_or_is_subset(&self, mut key: impl AsMut<[K]>) -> GetOrIsSubsetOfKnownKey { + key.as_mut().sort(); + self.ssm_get_or_is_subset_ksorted(key.as_mut()) + } + + /// Gets using a sorted key. Failure to enforce a sorted key results in defined but unspecified + /// behaviour. + pub fn ssm_get_or_is_subset_ksorted( + &self, + get_key: impl AsRef<[K]>, + ) -> GetOrIsSubsetOfKnownKey { + let get_key = get_key.as_ref(); + if get_key.is_empty() { + return match self.is_empty() { + true => Neither, + false => IsSubset, + }; + } + match self.map.get(&get_key[0]) { + None => Neither, + Some(keyvals_for_key_item) => { + match keyvals_for_key_item + .binary_search_by(|probe| probe.key.as_ref().cmp(get_key.as_ref())) + { + Ok(pos) => HasValue(keyvals_for_key_item[pos].value.clone()), + Err(_) => { + for kv in keyvals_for_key_item.iter() { + if get_key.iter().all(|kitem| kv.key.contains(kitem)) { + return IsSubset; + } + } + Neither + } + } + } + } + } + + pub fn is_empty(&self) -> bool { + self.map.is_empty() + } +} diff --git a/parser/src/trie.rs b/parser/src/trie.rs index 68f476f52..d74aedb6b 100644 --- a/parser/src/trie.rs +++ b/parser/src/trie.rs @@ -4,67 +4,74 @@ use bytemuck::cast_slice; use patricia_tree::map::PatriciaMap; pub type TrieKeyElement = u16; -pub type TrieKey = Vec; -pub type TrieVal = (u8, u16); #[derive(Debug, Clone)] -pub struct Trie { - inner: patricia_tree::map::PatriciaMap, +pub struct Trie { + inner: patricia_tree::map::PatriciaMap, } #[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum GetOrDescendentExistsResult { +pub enum GetOrDescendentExistsResult { NotInTrie, InTrie, - HasValue(TrieVal), + HasValue(T), } use GetOrDescendentExistsResult::*; -impl Default for Trie { +impl Default for Trie { fn default() -> Self { Self::new() } } -fn key_len(k: &TrieKey) -> usize { +fn key_len(k: impl AsRef<[u16]>) -> usize { debug_assert!(std::mem::size_of::() == 2 * std::mem::size_of::()); - k.len() * 2 + k.as_ref().len() * 2 } -impl Trie { +impl Trie { pub fn new() -> Self { Self { inner: PatriciaMap::new(), } } - pub fn ancestor_exists(&self, key: &TrieKey) -> bool { + pub fn ancestor_exists(&self, key: impl AsRef<[u16]>) -> bool { self.inner - .get_longest_common_prefix(cast_slice(key)) + .get_longest_common_prefix(cast_slice(key.as_ref())) .is_some() } - pub fn descendant_exists(&self, key: &TrieKey) -> bool { + pub fn descendant_exists(&self, key: impl AsRef<[u16]>) -> bool { // Length of the [u8] interpretation of the [u16] key is doubled. - self.inner.longest_common_prefix_len(cast_slice(key)) == key_len(key) + self.inner + .longest_common_prefix_len(cast_slice(key.as_ref())) + == key_len(key) } - pub fn insert(&mut self, key: TrieKey, val: TrieVal) { - self.inner.insert(cast_slice(&key), val); + pub fn insert(&mut self, key: impl AsRef<[u16]>, val: T) { + self.inner.insert(cast_slice(key.as_ref()), val); } - pub fn get_or_descendant_exists(&self, key: &TrieKey) -> GetOrDescendentExistsResult { - let mut descendants = self.inner.iter_prefix(cast_slice(key)); + pub fn get_or_descendant_exists(&self, key: impl AsRef<[u16]>) -> GetOrDescendentExistsResult + where + T: Clone, + { + let mut descendants = self.inner.iter_prefix(cast_slice(key.as_ref())); match descendants.next() { None => NotInTrie, Some(descendant) => { - if descendant.0.len() == key_len(key) { - HasValue(*descendant.1) + if descendant.0.len() == key_len(key.as_ref()) { + HasValue(descendant.1.clone()) } else { InTrie } } } } + + pub fn is_empty(&self) -> bool { + self.inner.is_empty() + } } diff --git a/parser/test_cfgs/test.zch b/parser/test_cfgs/test.zch new file mode 100644 index 000000000..b0c348fb0 --- /dev/null +++ b/parser/test_cfgs/test.zch @@ -0,0 +1,5 @@ +dy day +dy 1 Monday + abc Alphabet +r df recipient + w a Washington diff --git a/parser/test_cfgs/testzch.kbd b/parser/test_cfgs/testzch.kbd new file mode 100644 index 000000000..32cc1cbf9 --- /dev/null +++ b/parser/test_cfgs/testzch.kbd @@ -0,0 +1,3 @@ +(defsrc) +(deflayer base) +(defzippy-experimental test.zch) diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index 5624fbbf2..55b9d57fd 100755 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -326,6 +326,10 @@ impl Kanata { set_win_altgr_behaviour(cfg.options.windows_altgr); *MAPPED_KEYS.lock() = cfg.mapped_keys; + #[cfg(feature = "zippychord")] + { + zch().zch_configure(cfg.zippy.unwrap_or_default()); + } Ok(Self { kbd_out, @@ -424,8 +428,8 @@ impl Kanata { Ok(Arc::new(Mutex::new(Self::new(args)?))) } - pub fn new_from_str(cfg: &str) -> Result { - let cfg = match cfg::new_from_str(cfg) { + pub fn new_from_str(cfg: &str, file_content: Option) -> Result { + let cfg = match cfg::new_from_str(cfg, file_content) { Ok(c) => c, Err(e) => { bail!("{e:?}"); @@ -451,6 +455,10 @@ impl Kanata { }; *MAPPED_KEYS.lock() = cfg.mapped_keys; + #[cfg(feature = "zippychord")] + { + zch().zch_configure(cfg.zippy.unwrap_or_default()); + } Ok(Self { kbd_out, @@ -601,6 +609,10 @@ impl Kanata { self.gui_opts.notify_error = cfg.options.gui_opts.notify_error; self.gui_opts.tooltip_size = cfg.options.gui_opts.tooltip_size; } + #[cfg(feature = "zippychord")] + { + zch().zch_configure(cfg.zippy.unwrap_or_default()); + } *MAPPED_KEYS.lock() = cfg.mapped_keys; #[cfg(target_os = "linux")] @@ -774,6 +786,7 @@ impl Kanata { self.tick_sequence_state()?; self.tick_idle_timeout(); tick_record_state(&mut self.dynamic_macro_record_state); + zippy_tick(); self.prev_keys.clear(); self.prev_keys.append(&mut self.cur_keys); #[cfg(feature = "simulated_output")] @@ -2008,6 +2021,7 @@ impl Kanata { let pressed_keys_means_not_idle = !self.waiting_for_idle.is_empty() || self.live_reload_requested; self.layout.b().queue.is_empty() + && zippy_is_idle() && self.layout.b().waiting.is_none() && self.layout.b().last_press_tracker.tap_hold_timeout == 0 && (self.layout.b().oneshot.timeout == 0 || self.layout.b().oneshot.keys.is_empty()) diff --git a/src/kanata/output_logic.rs b/src/kanata/output_logic.rs index c22b62f24..adc1b44ce 100644 --- a/src/kanata/output_logic.rs +++ b/src/kanata/output_logic.rs @@ -1,5 +1,10 @@ use super::*; +#[cfg(feature = "zippychord")] +mod zippychord; +#[cfg(feature = "zippychord")] +pub(crate) use zippychord::*; + // Functions to send keys except those that fall in the ignorable range. // And also have been repurposed to have additional logic to send mouse events, out of convenience. // @@ -46,7 +51,7 @@ pub(super) fn press_key(kb: &mut KbdOut, osc: OsCode) -> Result<(), std::io::Err let direction = osc_to_wheel_direction(osc); kb.scroll(direction, HI_RES_SCROLL_UNITS_IN_LO_RES) } - _ => kb.press_key(osc), + _ => post_filter_press(kb, osc), }, } } @@ -64,7 +69,7 @@ pub(super) fn release_key(kb: &mut KbdOut, osc: OsCode) -> Result<(), std::io::E // of release. Ok(()) } - _ => kb.release_key(osc), + _ => post_filter_release(kb, osc), }, } } @@ -91,3 +96,43 @@ fn osc_to_wheel_direction(osc: OsCode) -> MWheelDirection { _ => unreachable!("called osc_to_wheel_direction with bad value {osc}"), } } + +fn post_filter_press(kb: &mut KbdOut, osc: OsCode) -> Result<(), std::io::Error> { + #[cfg(not(feature = "zippychord"))] + { + kb.press_key(osc) + } + #[cfg(feature = "zippychord")] + { + zch().zch_press_key(kb, osc) + } +} + +fn post_filter_release(kb: &mut KbdOut, osc: OsCode) -> Result<(), std::io::Error> { + #[cfg(not(feature = "zippychord"))] + { + kb.release_key(osc) + } + #[cfg(feature = "zippychord")] + { + zch().zch_release_key(kb, osc) + } +} + +pub(super) fn zippy_is_idle() -> bool { + #[cfg(not(feature = "zippychord"))] + { + true + } + #[cfg(feature = "zippychord")] + { + zch().zch_is_idle() + } +} + +pub(super) fn zippy_tick() { + #[cfg(feature = "zippychord")] + { + zch().zch_tick() + } +} diff --git a/src/kanata/output_logic/zippychord.rs b/src/kanata/output_logic/zippychord.rs new file mode 100644 index 000000000..3383bb663 --- /dev/null +++ b/src/kanata/output_logic/zippychord.rs @@ -0,0 +1,292 @@ +use super::*; + +use kanata_parser::subset::GetOrIsSubsetOfKnownKey::*; +use rustc_hash::FxHashSet; + +use std::sync::Arc; +use std::sync::Mutex; +use std::sync::MutexGuard; + +// TODO: suffixes - only active while disabled, to complete a word. +// TODO: prefix vs. non-prefix: one outputs space, the other not (I guess can be done in parser). + +static ZCH: Lazy> = Lazy::new(|| Mutex::new(Default::default())); + +pub(crate) fn zch() -> MutexGuard<'static, ZchState> { + match ZCH.lock() { + Ok(guard) => guard, + Err(poisoned) => { + let mut inner = poisoned.into_inner(); + inner.zchd.zchd_reset(); + inner + } + } +} + +#[derive(Debug)] +pub(crate) struct ZchConfig { + zch_cfg_ticks_wait_enable: u16, +} +impl Default for ZchConfig { + fn default() -> Self { + Self { + zch_cfg_ticks_wait_enable: 300, + } + } +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +enum ZchEnabledState { + #[default] + Enabled, + WaitEnable, + Disabled, +} + +#[derive(Debug, Default)] +struct ZchDynamicState { + /// Input to compare against configured available chords to output. + zchd_input_keys: ZchInputKeys, + /// Whether chording should be enabled or disabled. + /// Chording will be disabled if: + /// - further presses cannot possibly activate a chord + /// - a release happens with no chord having been activated + /// TODO: is the above true or even desirable? + /// + /// Once disabled, chording will be enabled when: + /// - all keys have been released + zchd_enabled_state: ZchEnabledState, + /// Is Some when a chord has been activated which has possible follow-up chords. + /// E.g. dy -> day + /// dy 1 -> Monday + /// dy 2 -> Tuesday + /// Using the example above, when dy has been activated, the `1` and `2` activations will be + /// contained within `zchd_prioritized_chords`. This is cleared if the input is such that an + /// activation is no longer possible. + zchd_prioritized_chords: Option>>, + /// Tracks the previous output because it may need to be erased (see `zchd_prioritized_chords). + zchd_previous_activation_output: Option>, + /// In case of output being empty for interim chord activations, this tracks the number of + /// characters that need to be erased. + zchd_characters_to_delete_on_next_activation: u16, + /// Tracker for time until previous state change to know if potential stale data should be + /// cleared. This is a contingency in case of bugs or weirdness with OS interactions, e.g. + /// Windows lock screen weirdness. + /// + /// This counts upwards to a "reset state" number. + zchd_ticks_since_state_change: u16, + /// Zch has a time delay between being disabled->pending-enabled->truly-enabled to mitigate + /// against unintended activations. This counts downwards from a configured number until 0, and + /// at 0 the state transitions from pending-enabled to truly-enabled if applicable. + zchd_ticks_until_enabled: u16, + /// Tracks the actually pressed keys to know when state can be reset. + zchd_pressed_keys: FxHashSet, +} + +impl ZchDynamicState { + fn zchd_is_disabled(&self) -> bool { + self.zchd_enabled_state == ZchEnabledState::Disabled + } + fn zchd_tick(&mut self) { + const TICKS_UNTIL_FORCE_STATE_RESET: u16 = 10000; + self.zchd_ticks_since_state_change += 1; + if self.zchd_enabled_state == ZchEnabledState::WaitEnable { + self.zchd_ticks_until_enabled = self.zchd_ticks_until_enabled.saturating_sub(1); + if self.zchd_ticks_until_enabled == 0 { + self.zchd_enabled_state = ZchEnabledState::Enabled; + } + } + if self.zchd_ticks_since_state_change > TICKS_UNTIL_FORCE_STATE_RESET { + self.zchd_reset(); + } + } + fn zchd_state_change(&mut self, cfg: &ZchConfig) { + self.zchd_ticks_since_state_change = 0; + self.zchd_ticks_until_enabled = cfg.zch_cfg_ticks_wait_enable; + } + /// Clean up the state. + fn zchd_reset(&mut self) { + log::debug!("zchd reset state"); + self.zchd_enabled_state = ZchEnabledState::Enabled; + self.zchd_pressed_keys.clear(); + self.zchd_input_keys.zchik_clear(); + self.zchd_prioritized_chords = None; + self.zchd_previous_activation_output = None; + self.zchd_characters_to_delete_on_next_activation = 0; + } + /// Returns true if dynamic zch state is such that idling optimization can activate. + fn zchd_is_idle(&self) -> bool { + let is_idle = self.zchd_enabled_state == ZchEnabledState::Enabled + && self.zchd_pressed_keys.is_empty(); + log::trace!("zch is idle: {is_idle}"); + is_idle + } + fn zchd_press_key(&mut self, osc: OsCode) { + self.zchd_pressed_keys.insert(osc); + self.zchd_input_keys.zchik_insert(osc); + } + fn zchd_release_key(&mut self, osc: OsCode) { + self.zchd_pressed_keys.remove(&osc); + self.zchd_input_keys.zchik_remove(osc); + self.zchd_enabled_state = match self.zchd_pressed_keys.is_empty() { + true => ZchEnabledState::WaitEnable, + false => ZchEnabledState::Disabled, + }; + } +} + +#[derive(Debug, Default)] +pub(crate) struct ZchState { + /// Dynamic state. Maybe doesn't make sense to separate this from zch_chords and to instead + /// just flatten the structures. + zchd: ZchDynamicState, + /// Chords configured by the user. This is fixed at runtime other than live-reloads replacing + /// the state. + zch_chords: ZchPossibleChords, + /// Options to configure behaviour. + /// TODO: needs parser configuration. + zch_cfg: ZchConfig, +} + +impl ZchState { + pub(crate) fn zch_configure(&mut self, chords: ZchPossibleChords) { + self.zch_chords = chords; + self.zchd.zchd_reset(); + } + /// Zch handling for key presses. + pub(crate) fn zch_press_key( + &mut self, + kb: &mut KbdOut, + osc: OsCode, + ) -> Result<(), std::io::Error> { + if self.zch_chords.is_empty() || self.zchd.zchd_is_disabled() || osc.is_modifier() { + return kb.press_key(osc); + } + self.zchd.zchd_state_change(&self.zch_cfg); + self.zchd.zchd_press_key(osc); + // There might be an activation. + // - delete typed keys + // - output activation + // + // Deletion of typed keys will be based on input keys if `zchd_previous_activation_output` is + // `None` or the previous output otherwise. + // + // Output activation will save into `zchd_previous_activation_output` if there is potential + // for subsequent activations, i.e. if zch_followups is `Some`. + let mut activation = Neither; + if let Some(pchords) = &self.zchd.zchd_prioritized_chords { + activation = pchords + .lock() + .0 + .ssm_get_or_is_subset_ksorted(self.zchd.zchd_input_keys.zchik_keys()); + } + if !matches!(activation, HasValue(..)) { + activation = self + .zch_chords + .0 + .ssm_get_or_is_subset_ksorted(self.zchd.zchd_input_keys.zchik_keys()); + } + match activation { + HasValue(a) => { + if a.zch_output.is_empty() { + self.zchd.zchd_characters_to_delete_on_next_activation += + self.zchd.zchd_input_keys.zchik_len() as u16; + } else { + let num_backspaces_to_send = match &self.zchd.zchd_previous_activation_output { + Some(prev_output) => { + usize::from(self.zchd.zchd_characters_to_delete_on_next_activation) + + self.zchd.zchd_input_keys.zchik_len() + + prev_output.len() + - 1 // subtract one because most recent press isn't sent + } + None => self.zchd.zchd_input_keys.zchik_len() - 1, + }; + self.zchd.zchd_characters_to_delete_on_next_activation = 0; + for _ in 0..num_backspaces_to_send { + kb.press_key(OsCode::KEY_BACKSPACE)?; + kb.release_key(OsCode::KEY_BACKSPACE)?; + } + } + self.zchd.zchd_prioritized_chords = a.zch_followups.clone(); + let mut released_lsft = false; + for key_to_send in &a.zch_output { + match key_to_send { + ZchOutput::Lowercase(osc) => { + if self.zchd.zchd_pressed_keys.contains(osc) { + kb.release_key(*osc)?; + self.zchd.zchd_pressed_keys.remove(osc); + } + kb.press_key(*osc)?; + kb.release_key(*osc)?; + } + ZchOutput::Uppercase(osc) => { + if self.zchd.zchd_pressed_keys.contains(osc) { + kb.release_key(*osc)?; + self.zchd.zchd_pressed_keys.remove(osc); + } + kb.press_key(OsCode::KEY_LEFTSHIFT)?; + kb.press_key(*osc)?; + kb.release_key(*osc)?; + kb.release_key(OsCode::KEY_LEFTSHIFT)?; + } + } + if !released_lsft { + // TODO: continue to not respect shift key, but do respect caps-word in + // kanata. Might want to re-press shift at the end though? + // Also maybe don't blindly release; do so only if actually pressed? + released_lsft = true; + kb.release_key(OsCode::KEY_LEFTSHIFT)?; + } + } + self.zchd.zchd_previous_activation_output = Some(a.zch_output.clone()); + + // Note: it is incorrect to clear input keys. + // Zippychord will eagerly output chords even if there is an overlapping chord that + // may be activated earlier. + // E.g. + // ab => Abba + // abc => Alphabet + // + // If (b a) are typed, "Abba" is outputted. + // If (b a) are continued to be held and (c) is subsequently pressed, + // "Abba" gets erased and "Alphabet" is outputted. + // + // WRONG: + // self.zchd.zchd_input_keys.zchik_clear() + + Ok(()) + } + IsSubset => { + self.zchd.zchd_input_keys.zchik_insert(osc); + kb.press_key(osc) + } + Neither => { + self.zchd.zchd_reset(); + self.zchd.zchd_enabled_state = ZchEnabledState::Disabled; + kb.press_key(osc) + } + } + } + // Zch handling for key releases. + pub(crate) fn zch_release_key( + &mut self, + kb: &mut KbdOut, + osc: OsCode, + ) -> Result<(), std::io::Error> { + if self.zch_chords.is_empty() || osc.is_modifier() { + return kb.release_key(osc); + } + self.zchd.zchd_state_change(&self.zch_cfg); + self.zchd.zchd_release_key(osc); + kb.release_key(osc) + } + /// Tick the zch output state. + pub(crate) fn zch_tick(&mut self) { + self.zchd.zchd_tick(); + } + /// Returns true if zch state has no further processing so the idling optimization can + /// activate. + pub(crate) fn zch_is_idle(&self) -> bool { + self.zchd.zchd_is_idle() + } +} diff --git a/src/kanata/sequences.rs b/src/kanata/sequences.rs index a8268daa9..d78e497ed 100644 --- a/src/kanata/sequences.rs +++ b/src/kanata/sequences.rs @@ -93,7 +93,7 @@ pub(super) fn do_sequence_press_logic( k: &KeyCode, mod_mask: u16, kbd_out: &mut KbdOut, - sequences: &kanata_parser::trie::Trie, + sequences: &kanata_parser::trie::Trie<(u8, u16)>, sequence_backtrack_modcancel: bool, layout: &mut BorrowedKLayout, ) -> Result<(), anyhow::Error> { diff --git a/src/tests/sim_tests/chord_sim_tests.rs b/src/tests/sim_tests/chord_sim_tests.rs index cc9986581..310b82013 100644 --- a/src/tests/sim_tests/chord_sim_tests.rs +++ b/src/tests/sim_tests/chord_sim_tests.rs @@ -268,10 +268,9 @@ static CHORD_WITH_TRANSPARENCY: &str = "\ )"; #[test] +#[should_panic] fn sim_denies_transparent() { - Kanata::new_from_str(CHORD_WITH_TRANSPARENCY) - .map(|_| ()) - .expect_err("trans in defchordsv2 should error"); + simulate(CHORD_WITH_TRANSPARENCY, ""); } #[test] diff --git a/src/tests/sim_tests/mod.rs b/src/tests/sim_tests/mod.rs index 28203c2ca..6c5522843 100644 --- a/src/tests/sim_tests/mod.rs +++ b/src/tests/sim_tests/mod.rs @@ -21,15 +21,21 @@ mod seq_sim_tests; mod switch_sim_tests; mod unicode_sim_tests; mod unmod_sim_tests; +mod zippychord_sim_tests; -fn simulate(cfg: &str, sim: &str) -> String { +fn simulate>(cfg: S, sim: S) -> String { + simulate_with_file_content(cfg, sim, None) +} + +fn simulate_with_file_content>(cfg: S, sim: S, file_content: Option) -> String { init_log(); let _lk = match CFG_PARSE_LOCK.lock() { Ok(guard) => guard, Err(poisoned) => poisoned.into_inner(), }; - let mut k = Kanata::new_from_str(cfg).expect("failed to parse cfg"); - for pair in sim.split_whitespace() { + let mut k = Kanata::new_from_str(cfg.as_ref(), file_content.map(|s| s.as_ref().to_owned())) + .expect("failed to parse cfg"); + for pair in sim.as_ref().split_whitespace() { match pair.split_once(':') { Some((kind, val)) => match kind { "t" => { diff --git a/src/tests/sim_tests/zippychord_sim_tests.rs b/src/tests/sim_tests/zippychord_sim_tests.rs new file mode 100644 index 000000000..472cd003c --- /dev/null +++ b/src/tests/sim_tests/zippychord_sim_tests.rs @@ -0,0 +1,61 @@ +use super::*; + +static ZIPPY_CFG: &str = "(defsrc)(deflayer base)(defzippy-experimental file)"; +static ZIPPY_FILE_CONTENT: &str = " +dy day +dy 1 Monday + abc Alphabet +r df recipient + w a Washington +"; + +#[test] +fn sim_zippychord_capitalize() { + let result = simulate_with_file_content( + ZIPPY_CFG, + "d:a t:10 d:b t:10 d:spc t:10 d:c t:300", + Some(ZIPPY_FILE_CONTENT), + ) + .to_ascii(); + assert_eq!( + "dn:A t:10ms dn:B t:10ms dn:Space t:10ms \ + dn:BSpace up:BSpace dn:BSpace up:BSpace dn:BSpace up:BSpace \ + up:A dn:LShift dn:A up:A up:LShift up:LShift \ + dn:L up:L dn:P up:P dn:H up:H dn:A up:A up:B dn:B up:B dn:E up:E dn:T up:T", + result + ); +} + +#[test] +fn sim_zippychord_followup_with_prev() { + let result = simulate_with_file_content( + ZIPPY_CFG, + "d:d t:10 d:y t:10 u:d u:y t:10 d:1 t:300", + Some(ZIPPY_FILE_CONTENT), + ) + .to_ascii(); + assert_eq!( + "dn:D t:10ms dn:BSpace up:BSpace \ + up:D dn:D up:D up:LShift dn:A up:A up:Y dn:Y up:Y \ + t:10ms up:D t:1ms up:Y t:9ms \ + dn:BSpace up:BSpace dn:BSpace up:BSpace dn:BSpace up:BSpace \ + dn:LShift dn:M up:M up:LShift up:LShift dn:O up:O dn:N up:N dn:D up:D dn:A up:A dn:Y up:Y", + result + ); +} + +#[test] +fn sim_zippychord_followup_no_prev() { + let result = simulate_with_file_content( + ZIPPY_CFG, + "d:r t:10 u:r t:10 d:d d:f t:10 t:300", + Some(ZIPPY_FILE_CONTENT), + ) + .to_ascii(); + assert_eq!( + "t:10ms up:R t:10ms dn:D t:1ms \ + dn:BSpace up:BSpace dn:BSpace up:BSpace \ + dn:R up:R up:LShift dn:E up:E dn:C up:C dn:I up:I dn:P up:P dn:I up:I dn:E up:E dn:N up:N dn:T up:T", + result + ); +} diff --git a/wasm/src/lib.rs b/wasm/src/lib.rs index d1455e255..630b70377 100644 --- a/wasm/src/lib.rs +++ b/wasm/src/lib.rs @@ -15,7 +15,7 @@ pub fn init() { #[wasm_bindgen] pub fn check_config(cfg: &str) -> JsValue { - let res = Kanata::new_from_str(cfg); + let res = Kanata::new_from_str(cfg, None); JsValue::from_str(&match res { Ok(_) => "Config is good!".to_owned(), Err(e) => format!("{e:?}"), @@ -31,7 +31,7 @@ pub fn simulate(cfg: &str, sim: &str) -> JsValue { } pub fn simulate_impl(cfg: &str, sim: &str) -> Result { - let mut k = Kanata::new_from_str(cfg)?; + let mut k = Kanata::new_from_str(cfg, None)?; let mut accumulated_ticks = 0; for l in sim.lines() { for pair in l.split_whitespace() { diff --git a/windows_key_tester/src/windows/interception.rs b/windows_key_tester/src/windows/interception.rs index ba9bef528..d5f6a052c 100644 --- a/windows_key_tester/src/windows/interception.rs +++ b/windows_key_tester/src/windows/interception.rs @@ -219,7 +219,7 @@ impl TryFrom for OsCode { #[allow(unused)] #[allow(non_camel_case_types)] -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum OsCode { KEY_RESERVED = 0, KEY_ESC = 1, From bc66d84622cbfa8c9da1f0848d8cf5cdc60d37e2 Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 20 Oct 2024 11:04:29 -0700 Subject: [PATCH 02/54] add todo around smart spacing --- src/kanata/output_logic/zippychord.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/kanata/output_logic/zippychord.rs b/src/kanata/output_logic/zippychord.rs index 3383bb663..01bcc0726 100644 --- a/src/kanata/output_logic/zippychord.rs +++ b/src/kanata/output_logic/zippychord.rs @@ -9,6 +9,7 @@ use std::sync::MutexGuard; // TODO: suffixes - only active while disabled, to complete a word. // TODO: prefix vs. non-prefix: one outputs space, the other not (I guess can be done in parser). +// TODO: smart spacing around words static ZCH: Lazy> = Lazy::new(|| Mutex::new(Default::default())); From a724438023bf2985142afe76a6dae71d66f391ab Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 20 Oct 2024 11:24:29 -0700 Subject: [PATCH 03/54] add test, fix bug, found new unfixed bug --- src/kanata/output_logic/zippychord.rs | 6 +++--- src/tests/sim_tests/zippychord_sim_tests.rs | 18 +++++++++++++++++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/kanata/output_logic/zippychord.rs b/src/kanata/output_logic/zippychord.rs index 01bcc0726..1146b1d66 100644 --- a/src/kanata/output_logic/zippychord.rs +++ b/src/kanata/output_logic/zippychord.rs @@ -190,9 +190,9 @@ impl ZchState { match activation { HasValue(a) => { if a.zch_output.is_empty() { - self.zchd.zchd_characters_to_delete_on_next_activation += - self.zchd.zchd_input_keys.zchik_len() as u16; + kb.press_key(osc)?; } else { + // TODO: something is broken about backspace count let num_backspaces_to_send = match &self.zchd.zchd_previous_activation_output { Some(prev_output) => { usize::from(self.zchd.zchd_characters_to_delete_on_next_activation) @@ -258,7 +258,7 @@ impl ZchState { Ok(()) } IsSubset => { - self.zchd.zchd_input_keys.zchik_insert(osc); + self.zchd.zchd_characters_to_delete_on_next_activation += 1; kb.press_key(osc) } Neither => { diff --git a/src/tests/sim_tests/zippychord_sim_tests.rs b/src/tests/sim_tests/zippychord_sim_tests.rs index 472cd003c..a0dd8458b 100644 --- a/src/tests/sim_tests/zippychord_sim_tests.rs +++ b/src/tests/sim_tests/zippychord_sim_tests.rs @@ -7,6 +7,8 @@ dy 1 Monday abc Alphabet r df recipient w a Washington +rq request +rqa request␣assistance "; #[test] @@ -53,9 +55,23 @@ fn sim_zippychord_followup_no_prev() { ) .to_ascii(); assert_eq!( - "t:10ms up:R t:10ms dn:D t:1ms \ + "dn:R t:10ms up:R t:10ms dn:D t:1ms \ dn:BSpace up:BSpace dn:BSpace up:BSpace \ dn:R up:R up:LShift dn:E up:E dn:C up:C dn:I up:I dn:P up:P dn:I up:I dn:E up:E dn:N up:N dn:T up:T", result ); } + +#[test] +fn sim_zippychord_overlap() { + let result = simulate_with_file_content( + ZIPPY_CFG, + "d:r t:10 d:q t:10 d:a t:10", + Some(ZIPPY_FILE_CONTENT), + ) + .to_ascii(); + assert_eq!( + "", + result + ); +} From d5078867f08671a95e3fbd1953dd5970c3910599 Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 20 Oct 2024 11:33:23 -0700 Subject: [PATCH 04/54] fix bspc count hopefully --- src/kanata/output_logic/zippychord.rs | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/kanata/output_logic/zippychord.rs b/src/kanata/output_logic/zippychord.rs index 1146b1d66..8edeb9b0e 100644 --- a/src/kanata/output_logic/zippychord.rs +++ b/src/kanata/output_logic/zippychord.rs @@ -190,23 +190,14 @@ impl ZchState { match activation { HasValue(a) => { if a.zch_output.is_empty() { + self.zchd.zchd_characters_to_delete_on_next_activation += 1; kb.press_key(osc)?; } else { - // TODO: something is broken about backspace count - let num_backspaces_to_send = match &self.zchd.zchd_previous_activation_output { - Some(prev_output) => { - usize::from(self.zchd.zchd_characters_to_delete_on_next_activation) - + self.zchd.zchd_input_keys.zchik_len() - + prev_output.len() - - 1 // subtract one because most recent press isn't sent - } - None => self.zchd.zchd_input_keys.zchik_len() - 1, - }; - self.zchd.zchd_characters_to_delete_on_next_activation = 0; - for _ in 0..num_backspaces_to_send { + for _ in 0..self.zchd.zchd_characters_to_delete_on_next_activation { kb.press_key(OsCode::KEY_BACKSPACE)?; kb.release_key(OsCode::KEY_BACKSPACE)?; } + self.zchd.zchd_characters_to_delete_on_next_activation = 0; } self.zchd.zchd_prioritized_chords = a.zch_followups.clone(); let mut released_lsft = false; @@ -219,6 +210,7 @@ impl ZchState { } kb.press_key(*osc)?; kb.release_key(*osc)?; + self.zchd.zchd_characters_to_delete_on_next_activation += 1; } ZchOutput::Uppercase(osc) => { if self.zchd.zchd_pressed_keys.contains(osc) { @@ -229,6 +221,7 @@ impl ZchState { kb.press_key(*osc)?; kb.release_key(*osc)?; kb.release_key(OsCode::KEY_LEFTSHIFT)?; + self.zchd.zchd_characters_to_delete_on_next_activation += 1; } } if !released_lsft { From af0e60a2d34fc9809292c1462a6cfdd56506baf9 Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 20 Oct 2024 12:06:01 -0700 Subject: [PATCH 05/54] use expected result for overlap test --- src/tests/sim_tests/zippychord_sim_tests.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/tests/sim_tests/zippychord_sim_tests.rs b/src/tests/sim_tests/zippychord_sim_tests.rs index a0dd8458b..2a9c9ac88 100644 --- a/src/tests/sim_tests/zippychord_sim_tests.rs +++ b/src/tests/sim_tests/zippychord_sim_tests.rs @@ -71,7 +71,13 @@ fn sim_zippychord_overlap() { ) .to_ascii(); assert_eq!( - "", + "dn:R t:10ms dn:BSpace up:BSpace \ + up:R dn:R up:R up:LShift dn:E up:E up:Q dn:Q up:Q dn:U up:U dn:E up:E dn:S up:S dn:T up:T t:10ms \ + dn:BSpace up:BSpace dn:BSpace up:BSpace dn:BSpace up:BSpace dn:BSpace up:BSpace \ + dn:BSpace up:BSpace dn:BSpace up:BSpace dn:BSpace up:BSpace \ + dn:R up:R up:LShift dn:E up:E dn:Q up:Q dn:U up:U dn:E up:E dn:S up:S dn:T up:T \ + dn:Space up:Space \ + up:A dn:A up:A dn:S up:S dn:S up:S dn:I up:I dn:S up:S dn:T up:T dn:A up:A dn:N up:N dn:C up:C dn:E up:E", result ); } From 9fdf66859c5e8f4fc8be4f9a8bb8467672b0a9bf Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 20 Oct 2024 12:39:29 -0700 Subject: [PATCH 06/54] hopefully actually fix backspace now --- src/kanata/output_logic/zippychord.rs | 32 ++++++++++++++++----- src/tests/sim_tests/zippychord_sim_tests.rs | 8 +++++- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/kanata/output_logic/zippychord.rs b/src/kanata/output_logic/zippychord.rs index 8edeb9b0e..8884cc41b 100644 --- a/src/kanata/output_logic/zippychord.rs +++ b/src/kanata/output_logic/zippychord.rs @@ -65,8 +65,9 @@ struct ZchDynamicState { /// contained within `zchd_prioritized_chords`. This is cleared if the input is such that an /// activation is no longer possible. zchd_prioritized_chords: Option>>, - /// Tracks the previous output because it may need to be erased (see `zchd_prioritized_chords). - zchd_previous_activation_output: Option>, + /// Tracks the previous output character count + /// because it may need to be erased (see `zchd_prioritized_chords). + zchd_previous_activation_output_count: Option, /// In case of output being empty for interim chord activations, this tracks the number of /// characters that need to be erased. zchd_characters_to_delete_on_next_activation: u16, @@ -112,7 +113,7 @@ impl ZchDynamicState { self.zchd_pressed_keys.clear(); self.zchd_input_keys.zchik_clear(); self.zchd_prioritized_chords = None; - self.zchd_previous_activation_output = None; + self.zchd_previous_activation_output_count = None; self.zchd_characters_to_delete_on_next_activation = 0; } /// Returns true if dynamic zch state is such that idling optimization can activate. @@ -129,6 +130,9 @@ impl ZchDynamicState { fn zchd_release_key(&mut self, osc: OsCode) { self.zchd_pressed_keys.remove(&osc); self.zchd_input_keys.zchik_remove(osc); + if self.zchd_input_keys.zchik_len() == 0 { + self.zchd_characters_to_delete_on_next_activation = 0; + } self.zchd_enabled_state = match self.zchd_pressed_keys.is_empty() { true => ZchEnabledState::WaitEnable, false => ZchEnabledState::Disabled, @@ -169,10 +173,11 @@ impl ZchState { // - delete typed keys // - output activation // - // Deletion of typed keys will be based on input keys if `zchd_previous_activation_output` is + // Deletion of typed keys will be based on input keys if + // `zchd_previous_activation_output_count` is // `None` or the previous output otherwise. // - // Output activation will save into `zchd_previous_activation_output` if there is potential + // Output activation will save into `zchd_previous_activation_output_count` if there is potential // for subsequent activations, i.e. if zch_followups is `Some`. let mut activation = Neither; if let Some(pchords) = &self.zchd.zchd_prioritized_chords { @@ -181,23 +186,37 @@ impl ZchState { .0 .ssm_get_or_is_subset_ksorted(self.zchd.zchd_input_keys.zchik_keys()); } + let mut is_prioritized_activation = false; if !matches!(activation, HasValue(..)) { activation = self .zch_chords .0 .ssm_get_or_is_subset_ksorted(self.zchd.zchd_input_keys.zchik_keys()); + } else { + is_prioritized_activation = true; } match activation { HasValue(a) => { if a.zch_output.is_empty() { self.zchd.zchd_characters_to_delete_on_next_activation += 1; + self.zchd.zchd_previous_activation_output_count = Some(1); kb.press_key(osc)?; } else { - for _ in 0..self.zchd.zchd_characters_to_delete_on_next_activation { + for _ in 0..(self.zchd.zchd_characters_to_delete_on_next_activation + + if is_prioritized_activation { + self.zchd.zchd_previous_activation_output_count.expect( + "previous activation should exist for prioritized activation", + ) + } else { + 0 + }) + { kb.press_key(OsCode::KEY_BACKSPACE)?; kb.release_key(OsCode::KEY_BACKSPACE)?; } self.zchd.zchd_characters_to_delete_on_next_activation = 0; + self.zchd.zchd_previous_activation_output_count = + Some(a.zch_output.len() as u16); } self.zchd.zchd_prioritized_chords = a.zch_followups.clone(); let mut released_lsft = false; @@ -232,7 +251,6 @@ impl ZchState { kb.release_key(OsCode::KEY_LEFTSHIFT)?; } } - self.zchd.zchd_previous_activation_output = Some(a.zch_output.clone()); // Note: it is incorrect to clear input keys. // Zippychord will eagerly output chords even if there is an overlapping chord that diff --git a/src/tests/sim_tests/zippychord_sim_tests.rs b/src/tests/sim_tests/zippychord_sim_tests.rs index 2a9c9ac88..e36a132a1 100644 --- a/src/tests/sim_tests/zippychord_sim_tests.rs +++ b/src/tests/sim_tests/zippychord_sim_tests.rs @@ -15,12 +15,18 @@ rqa request␣assistance fn sim_zippychord_capitalize() { let result = simulate_with_file_content( ZIPPY_CFG, - "d:a t:10 d:b t:10 d:spc t:10 d:c t:300", + "d:a t:10 d:b t:10 d:spc t:10 d:c u:a u:b u:c u:spc t:300 \ + d:a t:10 d:b t:10 d:spc t:10 d:c t:300", Some(ZIPPY_FILE_CONTENT), ) .to_ascii(); assert_eq!( "dn:A t:10ms dn:B t:10ms dn:Space t:10ms \ + dn:BSpace up:BSpace dn:BSpace up:BSpace dn:BSpace up:BSpace \ + up:A dn:LShift dn:A up:A up:LShift up:LShift \ + dn:L up:L dn:P up:P dn:H up:H dn:A up:A up:B dn:B up:B dn:E up:E dn:T up:T \ + t:1ms up:A t:1ms up:B t:1ms up:C t:1ms up:Space t:296ms \ + dn:A t:10ms dn:B t:10ms dn:Space t:10ms \ dn:BSpace up:BSpace dn:BSpace up:BSpace dn:BSpace up:BSpace \ up:A dn:LShift dn:A up:A up:LShift up:LShift \ dn:L up:L dn:P up:P dn:H up:H dn:A up:A up:B dn:B up:B dn:E up:E dn:T up:T", From da9a461e4c05ad900e4e08b760740ef63f70fa36 Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 20 Oct 2024 12:53:41 -0700 Subject: [PATCH 07/54] one more backspacing fix... --- src/kanata/output_logic/zippychord.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kanata/output_logic/zippychord.rs b/src/kanata/output_logic/zippychord.rs index 8884cc41b..6867beb3c 100644 --- a/src/kanata/output_logic/zippychord.rs +++ b/src/kanata/output_logic/zippychord.rs @@ -199,7 +199,7 @@ impl ZchState { HasValue(a) => { if a.zch_output.is_empty() { self.zchd.zchd_characters_to_delete_on_next_activation += 1; - self.zchd.zchd_previous_activation_output_count = Some(1); + self.zchd.zchd_previous_activation_output_count = Some(self.zchd.zchd_input_keys.zchik_keys().len() as u16); kb.press_key(osc)?; } else { for _ in 0..(self.zchd.zchd_characters_to_delete_on_next_activation From 336be8f36ab2ccc783e6725cecfa656c29281e4c Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 20 Oct 2024 12:56:13 -0700 Subject: [PATCH 08/54] remove option for prev activation --- src/kanata/output_logic/zippychord.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/kanata/output_logic/zippychord.rs b/src/kanata/output_logic/zippychord.rs index 6867beb3c..6b37299f1 100644 --- a/src/kanata/output_logic/zippychord.rs +++ b/src/kanata/output_logic/zippychord.rs @@ -67,7 +67,7 @@ struct ZchDynamicState { zchd_prioritized_chords: Option>>, /// Tracks the previous output character count /// because it may need to be erased (see `zchd_prioritized_chords). - zchd_previous_activation_output_count: Option, + zchd_previous_activation_output_count: u16, /// In case of output being empty for interim chord activations, this tracks the number of /// characters that need to be erased. zchd_characters_to_delete_on_next_activation: u16, @@ -113,7 +113,7 @@ impl ZchDynamicState { self.zchd_pressed_keys.clear(); self.zchd_input_keys.zchik_clear(); self.zchd_prioritized_chords = None; - self.zchd_previous_activation_output_count = None; + self.zchd_previous_activation_output_count = 0; self.zchd_characters_to_delete_on_next_activation = 0; } /// Returns true if dynamic zch state is such that idling optimization can activate. @@ -199,14 +199,13 @@ impl ZchState { HasValue(a) => { if a.zch_output.is_empty() { self.zchd.zchd_characters_to_delete_on_next_activation += 1; - self.zchd.zchd_previous_activation_output_count = Some(self.zchd.zchd_input_keys.zchik_keys().len() as u16); + self.zchd.zchd_previous_activation_output_count = + self.zchd.zchd_input_keys.zchik_keys().len() as u16; kb.press_key(osc)?; } else { for _ in 0..(self.zchd.zchd_characters_to_delete_on_next_activation + if is_prioritized_activation { - self.zchd.zchd_previous_activation_output_count.expect( - "previous activation should exist for prioritized activation", - ) + self.zchd.zchd_previous_activation_output_count } else { 0 }) @@ -215,8 +214,7 @@ impl ZchState { kb.release_key(OsCode::KEY_BACKSPACE)?; } self.zchd.zchd_characters_to_delete_on_next_activation = 0; - self.zchd.zchd_previous_activation_output_count = - Some(a.zch_output.len() as u16); + self.zchd.zchd_previous_activation_output_count = a.zch_output.len() as u16; } self.zchd.zchd_prioritized_chords = a.zch_followups.clone(); let mut released_lsft = false; From 9980ab3ed32dd8c5e70e59b7ef810f967a915d8b Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 20 Oct 2024 14:22:22 -0700 Subject: [PATCH 09/54] adjust extra press/release and maintain shift state properly --- parser/src/cfg/zippychord.rs | 5 +- src/kanata/mod.rs | 2 +- src/kanata/output_logic.rs | 4 +- src/kanata/output_logic/zippychord.rs | 110 +++++++++++++++----- src/tests/sim_tests/zippychord_sim_tests.rs | 29 +++--- 5 files changed, 106 insertions(+), 44 deletions(-) diff --git a/parser/src/cfg/zippychord.rs b/parser/src/cfg/zippychord.rs index 58784c649..ac4de3d12 100644 --- a/parser/src/cfg/zippychord.rs +++ b/parser/src/cfg/zippychord.rs @@ -80,11 +80,14 @@ impl ZchInputKeys { pub fn zchik_keys(&self) -> &[u16] { &self.zch_inputs.zch_keys } + pub fn zchik_is_empty(&self) -> bool { + self.zch_inputs.zch_keys.len() == 0 + } } #[derive(Debug, Default, Clone, Hash, PartialEq, Eq)] /// Sorted consistently by some arbitrary key order; -/// as opposed to, for example, simple the user press order. +/// as opposed to, for example, simply the user press order. pub struct ZchSortedChord { zch_keys: Vec, } diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index 55b9d57fd..d451c2ea8 100755 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -786,7 +786,7 @@ impl Kanata { self.tick_sequence_state()?; self.tick_idle_timeout(); tick_record_state(&mut self.dynamic_macro_record_state); - zippy_tick(); + zippy_tick(self.caps_word.is_some()); self.prev_keys.clear(); self.prev_keys.append(&mut self.cur_keys); #[cfg(feature = "simulated_output")] diff --git a/src/kanata/output_logic.rs b/src/kanata/output_logic.rs index adc1b44ce..0ea2d094b 100644 --- a/src/kanata/output_logic.rs +++ b/src/kanata/output_logic.rs @@ -130,9 +130,9 @@ pub(super) fn zippy_is_idle() -> bool { } } -pub(super) fn zippy_tick() { +pub(super) fn zippy_tick(_caps_word_is_active: bool) { #[cfg(feature = "zippychord")] { - zch().zch_tick() + zch().zch_tick(_caps_word_is_active) } } diff --git a/src/kanata/output_logic/zippychord.rs b/src/kanata/output_logic/zippychord.rs index 6b37299f1..95ed57dbc 100644 --- a/src/kanata/output_logic/zippychord.rs +++ b/src/kanata/output_logic/zippychord.rs @@ -1,7 +1,6 @@ use super::*; use kanata_parser::subset::GetOrIsSubsetOfKnownKey::*; -use rustc_hash::FxHashSet; use std::sync::Arc; use std::sync::Mutex; @@ -81,17 +80,22 @@ struct ZchDynamicState { /// against unintended activations. This counts downwards from a configured number until 0, and /// at 0 the state transitions from pending-enabled to truly-enabled if applicable. zchd_ticks_until_enabled: u16, - /// Tracks the actually pressed keys to know when state can be reset. - zchd_pressed_keys: FxHashSet, + /// Current state of caps-word, which is a factor in handling capitalization. + zchd_is_caps_word_active: bool, + /// Current state of lsft which is a factor in handling capitalization. + zchd_is_lsft_active: bool, + /// Current state of rsft which is a factor in handling capitalization. + zchd_is_rsft_active: bool, } impl ZchDynamicState { fn zchd_is_disabled(&self) -> bool { self.zchd_enabled_state == ZchEnabledState::Disabled } - fn zchd_tick(&mut self) { + fn zchd_tick(&mut self, is_caps_word_active: bool) { const TICKS_UNTIL_FORCE_STATE_RESET: u16 = 10000; self.zchd_ticks_since_state_change += 1; + self.zchd_is_caps_word_active = is_caps_word_active; if self.zchd_enabled_state == ZchEnabledState::WaitEnable { self.zchd_ticks_until_enabled = self.zchd_ticks_until_enabled.saturating_sub(1); if self.zchd_ticks_until_enabled == 0 { @@ -106,34 +110,38 @@ impl ZchDynamicState { self.zchd_ticks_since_state_change = 0; self.zchd_ticks_until_enabled = cfg.zch_cfg_ticks_wait_enable; } + /// Clean up the state. fn zchd_reset(&mut self) { log::debug!("zchd reset state"); self.zchd_enabled_state = ZchEnabledState::Enabled; - self.zchd_pressed_keys.clear(); + self.zchd_is_caps_word_active = false; + self.zchd_is_lsft_active = false; + self.zchd_is_rsft_active = false; self.zchd_input_keys.zchik_clear(); self.zchd_prioritized_chords = None; self.zchd_previous_activation_output_count = 0; self.zchd_characters_to_delete_on_next_activation = 0; } + /// Returns true if dynamic zch state is such that idling optimization can activate. fn zchd_is_idle(&self) -> bool { let is_idle = self.zchd_enabled_state == ZchEnabledState::Enabled - && self.zchd_pressed_keys.is_empty(); + && self.zchd_input_keys.zchik_is_empty(); log::trace!("zch is idle: {is_idle}"); is_idle } + fn zchd_press_key(&mut self, osc: OsCode) { - self.zchd_pressed_keys.insert(osc); self.zchd_input_keys.zchik_insert(osc); } + fn zchd_release_key(&mut self, osc: OsCode) { - self.zchd_pressed_keys.remove(&osc); self.zchd_input_keys.zchik_remove(osc); - if self.zchd_input_keys.zchik_len() == 0 { + if self.zchd_input_keys.zchik_is_empty() { self.zchd_characters_to_delete_on_next_activation = 0; } - self.zchd_enabled_state = match self.zchd_pressed_keys.is_empty() { + self.zchd_enabled_state = match self.zchd_input_keys.zchik_is_empty() { true => ZchEnabledState::WaitEnable, false => ZchEnabledState::Disabled, }; @@ -154,16 +162,27 @@ pub(crate) struct ZchState { } impl ZchState { + /// Configure zippychord behaviour. pub(crate) fn zch_configure(&mut self, chords: ZchPossibleChords) { self.zch_chords = chords; self.zchd.zchd_reset(); } + /// Zch handling for key presses. pub(crate) fn zch_press_key( &mut self, kb: &mut KbdOut, osc: OsCode, ) -> Result<(), std::io::Error> { + match osc { + OsCode::KEY_LEFTSHIFT => { + self.zchd.zchd_is_lsft_active = true; + } + OsCode::KEY_RIGHTSHIFT => { + self.zchd.zchd_is_rsft_active = true; + } + _ => {} + } if self.zch_chords.is_empty() || self.zchd.zchd_is_disabled() || osc.is_modifier() { return kb.press_key(osc); } @@ -221,32 +240,55 @@ impl ZchState { for key_to_send in &a.zch_output { match key_to_send { ZchOutput::Lowercase(osc) => { - if self.zchd.zchd_pressed_keys.contains(osc) { + if self.zchd.zchd_input_keys.zchik_contains(*osc) { + kb.release_key(*osc)?; + kb.press_key(*osc)?; + } else { + kb.press_key(*osc)?; kb.release_key(*osc)?; - self.zchd.zchd_pressed_keys.remove(osc); } - kb.press_key(*osc)?; - kb.release_key(*osc)?; - self.zchd.zchd_characters_to_delete_on_next_activation += 1; } ZchOutput::Uppercase(osc) => { - if self.zchd.zchd_pressed_keys.contains(osc) { + if !self.zchd.zchd_is_caps_word_active + && !self.zchd.zchd_is_lsft_active + && !self.zchd.zchd_is_rsft_active + { + kb.press_key(OsCode::KEY_LEFTSHIFT)?; + } + if self.zchd.zchd_input_keys.zchik_contains(*osc) { kb.release_key(*osc)?; - self.zchd.zchd_pressed_keys.remove(osc); + kb.press_key(*osc)?; + } else { + kb.press_key(*osc)?; + kb.release_key(*osc)?; + } + if !self.zchd.zchd_is_caps_word_active + && !self.zchd.zchd_is_lsft_active + && !self.zchd.zchd_is_rsft_active + { + kb.release_key(OsCode::KEY_LEFTSHIFT)?; } - kb.press_key(OsCode::KEY_LEFTSHIFT)?; - kb.press_key(*osc)?; - kb.release_key(*osc)?; - kb.release_key(OsCode::KEY_LEFTSHIFT)?; - self.zchd.zchd_characters_to_delete_on_next_activation += 1; } } + self.zchd.zchd_characters_to_delete_on_next_activation += 1; if !released_lsft { - // TODO: continue to not respect shift key, but do respect caps-word in - // kanata. Might want to re-press shift at the end though? - // Also maybe don't blindly release; do so only if actually pressed? released_lsft = true; - kb.release_key(OsCode::KEY_LEFTSHIFT)?; + if !self.zchd.zchd_is_caps_word_active { + if self.zchd.zchd_is_lsft_active { + kb.release_key(OsCode::KEY_LEFTSHIFT)?; + } + if self.zchd.zchd_is_rsft_active { + kb.release_key(OsCode::KEY_RIGHTSHIFT)?; + } + } + } + } + if !self.zchd.zchd_is_caps_word_active { + if self.zchd.zchd_is_lsft_active { + kb.press_key(OsCode::KEY_LEFTSHIFT)?; + } + if self.zchd.zchd_is_rsft_active { + kb.press_key(OsCode::KEY_RIGHTSHIFT)?; } } @@ -277,12 +319,22 @@ impl ZchState { } } } + // Zch handling for key releases. pub(crate) fn zch_release_key( &mut self, kb: &mut KbdOut, osc: OsCode, ) -> Result<(), std::io::Error> { + match osc { + OsCode::KEY_LEFTSHIFT => { + self.zchd.zchd_is_lsft_active = false; + } + OsCode::KEY_RIGHTSHIFT => { + self.zchd.zchd_is_rsft_active = false; + } + _ => {} + } if self.zch_chords.is_empty() || osc.is_modifier() { return kb.release_key(osc); } @@ -290,10 +342,12 @@ impl ZchState { self.zchd.zchd_release_key(osc); kb.release_key(osc) } + /// Tick the zch output state. - pub(crate) fn zch_tick(&mut self) { - self.zchd.zchd_tick(); + pub(crate) fn zch_tick(&mut self, is_caps_word_active: bool) { + self.zchd.zchd_tick(is_caps_word_active); } + /// Returns true if zch state has no further processing so the idling optimization can /// activate. pub(crate) fn zch_is_idle(&self) -> bool { diff --git a/src/tests/sim_tests/zippychord_sim_tests.rs b/src/tests/sim_tests/zippychord_sim_tests.rs index e36a132a1..af2bcd7c9 100644 --- a/src/tests/sim_tests/zippychord_sim_tests.rs +++ b/src/tests/sim_tests/zippychord_sim_tests.rs @@ -23,13 +23,13 @@ fn sim_zippychord_capitalize() { assert_eq!( "dn:A t:10ms dn:B t:10ms dn:Space t:10ms \ dn:BSpace up:BSpace dn:BSpace up:BSpace dn:BSpace up:BSpace \ - up:A dn:LShift dn:A up:A up:LShift up:LShift \ - dn:L up:L dn:P up:P dn:H up:H dn:A up:A up:B dn:B up:B dn:E up:E dn:T up:T \ + dn:LShift up:A dn:A up:LShift \ + dn:L up:L dn:P up:P dn:H up:H up:A dn:A up:B dn:B dn:E up:E dn:T up:T \ t:1ms up:A t:1ms up:B t:1ms up:C t:1ms up:Space t:296ms \ dn:A t:10ms dn:B t:10ms dn:Space t:10ms \ dn:BSpace up:BSpace dn:BSpace up:BSpace dn:BSpace up:BSpace \ - up:A dn:LShift dn:A up:A up:LShift up:LShift \ - dn:L up:L dn:P up:P dn:H up:H dn:A up:A up:B dn:B up:B dn:E up:E dn:T up:T", + dn:LShift up:A dn:A up:LShift \ + dn:L up:L dn:P up:P dn:H up:H up:A dn:A up:B dn:B dn:E up:E dn:T up:T", result ); } @@ -44,10 +44,10 @@ fn sim_zippychord_followup_with_prev() { .to_ascii(); assert_eq!( "dn:D t:10ms dn:BSpace up:BSpace \ - up:D dn:D up:D up:LShift dn:A up:A up:Y dn:Y up:Y \ - t:10ms up:D t:1ms up:Y t:9ms \ - dn:BSpace up:BSpace dn:BSpace up:BSpace dn:BSpace up:BSpace \ - dn:LShift dn:M up:M up:LShift up:LShift dn:O up:O dn:N up:N dn:D up:D dn:A up:A dn:Y up:Y", + up:D dn:D dn:A up:A up:Y dn:Y \ + t:10ms up:D t:1ms up:Y t:9ms \ + dn:BSpace up:BSpace dn:BSpace up:BSpace dn:BSpace up:BSpace \ + dn:LShift dn:M up:M up:LShift dn:O up:O dn:N up:N dn:D up:D dn:A up:A dn:Y up:Y", result ); } @@ -63,7 +63,7 @@ fn sim_zippychord_followup_no_prev() { assert_eq!( "dn:R t:10ms up:R t:10ms dn:D t:1ms \ dn:BSpace up:BSpace dn:BSpace up:BSpace \ - dn:R up:R up:LShift dn:E up:E dn:C up:C dn:I up:I dn:P up:P dn:I up:I dn:E up:E dn:N up:N dn:T up:T", + dn:R up:R dn:E up:E dn:C up:C dn:I up:I dn:P up:P dn:I up:I dn:E up:E dn:N up:N dn:T up:T", result ); } @@ -78,12 +78,17 @@ fn sim_zippychord_overlap() { .to_ascii(); assert_eq!( "dn:R t:10ms dn:BSpace up:BSpace \ - up:R dn:R up:R up:LShift dn:E up:E up:Q dn:Q up:Q dn:U up:U dn:E up:E dn:S up:S dn:T up:T t:10ms \ + up:R dn:R dn:E up:E up:Q dn:Q dn:U up:U dn:E up:E dn:S up:S dn:T up:T t:10ms \ dn:BSpace up:BSpace dn:BSpace up:BSpace dn:BSpace up:BSpace dn:BSpace up:BSpace \ dn:BSpace up:BSpace dn:BSpace up:BSpace dn:BSpace up:BSpace \ - dn:R up:R up:LShift dn:E up:E dn:Q up:Q dn:U up:U dn:E up:E dn:S up:S dn:T up:T \ + up:R dn:R dn:E up:E up:Q dn:Q dn:U up:U dn:E up:E dn:S up:S dn:T up:T \ dn:Space up:Space \ - up:A dn:A up:A dn:S up:S dn:S up:S dn:I up:I dn:S up:S dn:T up:T dn:A up:A dn:N up:N dn:C up:C dn:E up:E", + up:A dn:A dn:S up:S dn:S up:S dn:I up:I dn:S up:S dn:T up:T up:A dn:A dn:N up:N dn:C up:C dn:E up:E", result ); } + +// TODO: +// - test for lsft-already-pressed capitalization state +// - test for rsft-already-pressed capitalization state +// - test for caps-word state From a0c1280c9439211bf6aedfe9fccd2af141630717 Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 20 Oct 2024 14:30:34 -0700 Subject: [PATCH 10/54] fix clippy --- keyberon/src/layout.rs | 9 +++------ parser/src/cfg/zippychord.rs | 2 +- src/kanata/mod.rs | 7 ++----- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/keyberon/src/layout.rs b/keyberon/src/layout.rs index 30209e3e5..68e3c097c 100644 --- a/keyberon/src/layout.rs +++ b/keyberon/src/layout.rs @@ -1344,12 +1344,9 @@ impl<'a, const C: usize, const R: usize, T: 'a + Copy + std::fmt::Debug> Layout< seq.tapped = None; } else { // Pull the next SequenceEvent - match seq.remaining_events { - [e, tail @ ..] => { - seq.cur_event = Some(*e); - seq.remaining_events = tail; - } - [] => (), + if let [e, tail @ ..] = seq.remaining_events { + seq.cur_event = Some(*e); + seq.remaining_events = tail; } // Process it (SequenceEvent) match seq.cur_event { diff --git a/parser/src/cfg/zippychord.rs b/parser/src/cfg/zippychord.rs index ac4de3d12..a19f35df0 100644 --- a/parser/src/cfg/zippychord.rs +++ b/parser/src/cfg/zippychord.rs @@ -81,7 +81,7 @@ impl ZchInputKeys { &self.zch_inputs.zch_keys } pub fn zchik_is_empty(&self) -> bool { - self.zch_inputs.zch_keys.len() == 0 + self.zch_inputs.zch_keys.is_empty() } } diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index d451c2ea8..b98f4529d 100755 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -1428,11 +1428,8 @@ impl Kanata { } } #[cfg(feature = "tcp_server")] - match self.tcp_server_address { - None => { - log::warn!("{} was used, but TCP server is not running. did you specify a port?", PUSH_MESSAGE); - } - Some(_) => {} + if self.tcp_server_address.is_none() { + log::warn!("{} was used, but TCP server is not running. did you specify a port?", PUSH_MESSAGE); } #[cfg(not(feature = "tcp_server"))] log::warn!( From 587445403a86919ed4c23eb0e18068e768c72629 Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 20 Oct 2024 14:53:15 -0700 Subject: [PATCH 11/54] update tests --- src/kanata/output_logic/zippychord.rs | 10 +- src/tests/sim_tests/zippychord_sim_tests.rs | 103 +++++++++++++++++++- 2 files changed, 103 insertions(+), 10 deletions(-) diff --git a/src/kanata/output_logic/zippychord.rs b/src/kanata/output_logic/zippychord.rs index 95ed57dbc..99b7ea5b8 100644 --- a/src/kanata/output_logic/zippychord.rs +++ b/src/kanata/output_logic/zippychord.rs @@ -250,8 +250,8 @@ impl ZchState { } ZchOutput::Uppercase(osc) => { if !self.zchd.zchd_is_caps_word_active - && !self.zchd.zchd_is_lsft_active - && !self.zchd.zchd_is_rsft_active + && (released_lsft + || !self.zchd.zchd_is_lsft_active && !self.zchd.zchd_is_rsft_active) { kb.press_key(OsCode::KEY_LEFTSHIFT)?; } @@ -263,8 +263,8 @@ impl ZchState { kb.release_key(*osc)?; } if !self.zchd.zchd_is_caps_word_active - && !self.zchd.zchd_is_lsft_active - && !self.zchd.zchd_is_rsft_active + && (released_lsft + || !self.zchd.zchd_is_lsft_active && !self.zchd.zchd_is_rsft_active) { kb.release_key(OsCode::KEY_LEFTSHIFT)?; } @@ -272,8 +272,8 @@ impl ZchState { } self.zchd.zchd_characters_to_delete_on_next_activation += 1; if !released_lsft { - released_lsft = true; if !self.zchd.zchd_is_caps_word_active { + released_lsft = true; if self.zchd.zchd_is_lsft_active { kb.release_key(OsCode::KEY_LEFTSHIFT)?; } diff --git a/src/tests/sim_tests/zippychord_sim_tests.rs b/src/tests/sim_tests/zippychord_sim_tests.rs index af2bcd7c9..c0778074f 100644 --- a/src/tests/sim_tests/zippychord_sim_tests.rs +++ b/src/tests/sim_tests/zippychord_sim_tests.rs @@ -1,12 +1,13 @@ use super::*; -static ZIPPY_CFG: &str = "(defsrc)(deflayer base)(defzippy-experimental file)"; +static ZIPPY_CFG: &str = "(defsrc lalt)(deflayer base (caps-word 2000))(defzippy-experimental file)"; static ZIPPY_FILE_CONTENT: &str = " dy day dy 1 Monday abc Alphabet r df recipient w a Washington +xy WxYz rq request rqa request␣assistance "; @@ -88,7 +89,99 @@ fn sim_zippychord_overlap() { ); } -// TODO: -// - test for lsft-already-pressed capitalization state -// - test for rsft-already-pressed capitalization state -// - test for caps-word state +#[test] +fn sim_zippychord_lsft() { + let result = simulate_with_file_content( + ZIPPY_CFG, + "d:lsft t:10 d:d t:10 d:y t:10", + Some(ZIPPY_FILE_CONTENT), + ) + .to_ascii(); + assert_eq!( + "dn:LShift t:10ms dn:D t:10ms dn:BSpace up:BSpace up:D dn:D up:LShift dn:A up:A up:Y dn:Y dn:LShift", + result + ); + let result = simulate_with_file_content( + ZIPPY_CFG, + "d:lsft t:10 d:x t:10 d:y t:10", + Some(ZIPPY_FILE_CONTENT), + ) + .to_ascii(); + assert_eq!( + "dn:LShift t:10ms dn:X t:10ms dn:BSpace up:BSpace \ + dn:W up:W up:LShift up:X dn:X dn:LShift up:Y dn:Y up:LShift dn:Z up:Z dn:LShift", + result + ); + let result = simulate_with_file_content( + ZIPPY_CFG, + "d:lsft t:10 d:d u:lsft t:10 d:y t:10", + Some(ZIPPY_FILE_CONTENT), + ) + .to_ascii(); + assert_eq!( + "dn:LShift t:10ms dn:D t:1ms up:LShift t:9ms dn:BSpace up:BSpace up:D dn:D dn:A up:A up:Y dn:Y", + result + ); + let result = simulate_with_file_content( + ZIPPY_CFG, + "d:lsft t:10 d:x u:lsft t:10 d:y t:10", + Some(ZIPPY_FILE_CONTENT), + ) + .to_ascii(); + assert_eq!( + "dn:LShift t:10ms dn:X t:1ms up:LShift t:9ms dn:BSpace up:BSpace \ + dn:LShift dn:W up:W up:LShift up:X dn:X dn:LShift up:Y dn:Y up:LShift dn:Z up:Z", + result + ); +} + +#[test] +fn sim_zippychord_rsft() { + let result = simulate_with_file_content( + ZIPPY_CFG, + "d:rsft t:10 d:d t:10 d:y t:10", + Some(ZIPPY_FILE_CONTENT), + ) + .to_ascii(); + assert_eq!( + "dn:RShift t:10ms dn:D t:10ms dn:BSpace up:BSpace up:D dn:D up:RShift dn:A up:A up:Y dn:Y dn:RShift", + result + ); + let result = simulate_with_file_content( + ZIPPY_CFG, + "d:rsft t:10 d:x t:10 d:y t:10", + Some(ZIPPY_FILE_CONTENT), + ) + .to_ascii(); + assert_eq!( + "dn:RShift t:10ms dn:X t:10ms dn:BSpace up:BSpace \ + dn:W up:W up:RShift up:X dn:X dn:LShift up:Y dn:Y up:LShift dn:Z up:Z dn:RShift", + result + ); +} + +#[test] +fn sim_zippychord_caps_word() { + let result = simulate_with_file_content( + ZIPPY_CFG, + "d:lalt t:10 d:d t:10 d:y t:10", + Some(ZIPPY_FILE_CONTENT), + ) + .to_ascii(); + assert_eq!( + "t:10ms dn:LShift dn:D t:10ms dn:BSpace up:BSpace up:D dn:D dn:A up:A up:Y dn:Y", + result + ); + let result = simulate_with_file_content( + ZIPPY_CFG, + "d:lalt t:10 d:x t:10 d:y t:10", + Some(ZIPPY_FILE_CONTENT), + ) + .to_ascii(); + assert_eq!( + "t:10ms dn:LShift dn:X t:10ms dn:BSpace up:BSpace \ + dn:W up:W up:X dn:X up:Y dn:Y dn:Z up:Z", + result + ); +} + From 241dc17445786779640ed88ed7c3d46b5b062716 Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 20 Oct 2024 15:12:26 -0700 Subject: [PATCH 12/54] add more tests --- src/tests/sim_tests/zippychord_sim_tests.rs | 43 ++++++++++++++++++--- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/src/tests/sim_tests/zippychord_sim_tests.rs b/src/tests/sim_tests/zippychord_sim_tests.rs index c0778074f..7f9ba51d9 100644 --- a/src/tests/sim_tests/zippychord_sim_tests.rs +++ b/src/tests/sim_tests/zippychord_sim_tests.rs @@ -91,6 +91,7 @@ fn sim_zippychord_overlap() { #[test] fn sim_zippychord_lsft() { + // test lsft behaviour while pressed let result = simulate_with_file_content( ZIPPY_CFG, "d:lsft t:10 d:d t:10 d:y t:10", @@ -112,6 +113,8 @@ fn sim_zippychord_lsft() { dn:W up:W up:LShift up:X dn:X dn:LShift up:Y dn:Y up:LShift dn:Z up:Z dn:LShift", result ); + + // ensure lsft-held behaviour goes away when released let result = simulate_with_file_content( ZIPPY_CFG, "d:lsft t:10 d:d u:lsft t:10 d:y t:10", @@ -137,6 +140,7 @@ fn sim_zippychord_lsft() { #[test] fn sim_zippychord_rsft() { + // test rsft behaviour while pressed let result = simulate_with_file_content( ZIPPY_CFG, "d:rsft t:10 d:d t:10 d:y t:10", @@ -158,30 +162,57 @@ fn sim_zippychord_rsft() { dn:W up:W up:RShift up:X dn:X dn:LShift up:Y dn:Y up:LShift dn:Z up:Z dn:RShift", result ); + + // ensure rsft-held behaviour goes away when released + let result = simulate_with_file_content( + ZIPPY_CFG, + "d:rsft t:10 d:d u:rsft t:10 d:y t:10", + Some(ZIPPY_FILE_CONTENT), + ) + .to_ascii(); + assert_eq!( + "dn:RShift t:10ms dn:D t:1ms up:RShift t:9ms dn:BSpace up:BSpace up:D dn:D dn:A up:A up:Y dn:Y", + result + ); + let result = simulate_with_file_content( + ZIPPY_CFG, + "d:rsft t:10 d:x u:rsft t:10 d:y t:10", + Some(ZIPPY_FILE_CONTENT), + ) + .to_ascii(); + assert_eq!( + "dn:RShift t:10ms dn:X t:1ms up:RShift t:9ms dn:BSpace up:BSpace \ + dn:LShift dn:W up:W up:LShift up:X dn:X dn:LShift up:Y dn:Y up:LShift dn:Z up:Z", + result + ); } #[test] fn sim_zippychord_caps_word() { let result = simulate_with_file_content( ZIPPY_CFG, - "d:lalt t:10 d:d t:10 d:y t:10", + "d:lalt u:lalt t:10 d:d t:10 d:y t:10 u:d u:y t:10 d:spc u:spc t:10 d:d d:y t:10", Some(ZIPPY_FILE_CONTENT), ) .to_ascii(); assert_eq!( - "t:10ms dn:LShift dn:D t:10ms dn:BSpace up:BSpace up:D dn:D dn:A up:A up:Y dn:Y", + "t:10ms dn:LShift dn:D t:10ms dn:BSpace up:BSpace up:D dn:D dn:A up:A up:Y dn:Y \ + t:10ms up:D t:1ms up:LShift up:Y t:9ms dn:Space t:1ms up:Space \ + t:9ms dn:D t:1ms dn:BSpace up:BSpace up:D dn:D dn:A up:A up:Y dn:Y", result ); let result = simulate_with_file_content( ZIPPY_CFG, - "d:lalt t:10 d:x t:10 d:y t:10", + "d:lalt t:10 d:y t:10 d:x t:10 u:x u:y t:10 d:spc u:spc t:10 d:y d:x t:10", Some(ZIPPY_FILE_CONTENT), ) .to_ascii(); assert_eq!( - "t:10ms dn:LShift dn:X t:10ms dn:BSpace up:BSpace \ - dn:W up:W up:X dn:X up:Y dn:Y dn:Z up:Z", + "t:10ms dn:LShift dn:Y t:10ms dn:BSpace up:BSpace \ + dn:W up:W up:X dn:X up:Y dn:Y dn:Z up:Z \ + t:10ms up:X t:1ms up:LShift up:Y t:9ms dn:Space t:1ms up:Space \ + t:9ms dn:Y t:1ms dn:BSpace up:BSpace dn:LShift dn:W up:W up:LShift \ + up:X dn:X dn:LShift up:Y dn:Y up:LShift dn:Z up:Z", result ); } - From 20a32b6dbec91e3262705e127399ff33266faace Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 20 Oct 2024 15:17:07 -0700 Subject: [PATCH 13/54] fmt --- src/kanata/output_logic/zippychord.rs | 6 ++++-- src/tests/sim_tests/zippychord_sim_tests.rs | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/kanata/output_logic/zippychord.rs b/src/kanata/output_logic/zippychord.rs index 99b7ea5b8..c5cde5a32 100644 --- a/src/kanata/output_logic/zippychord.rs +++ b/src/kanata/output_logic/zippychord.rs @@ -251,7 +251,8 @@ impl ZchState { ZchOutput::Uppercase(osc) => { if !self.zchd.zchd_is_caps_word_active && (released_lsft - || !self.zchd.zchd_is_lsft_active && !self.zchd.zchd_is_rsft_active) + || !self.zchd.zchd_is_lsft_active + && !self.zchd.zchd_is_rsft_active) { kb.press_key(OsCode::KEY_LEFTSHIFT)?; } @@ -264,7 +265,8 @@ impl ZchState { } if !self.zchd.zchd_is_caps_word_active && (released_lsft - || !self.zchd.zchd_is_lsft_active && !self.zchd.zchd_is_rsft_active) + || !self.zchd.zchd_is_lsft_active + && !self.zchd.zchd_is_rsft_active) { kb.release_key(OsCode::KEY_LEFTSHIFT)?; } diff --git a/src/tests/sim_tests/zippychord_sim_tests.rs b/src/tests/sim_tests/zippychord_sim_tests.rs index 7f9ba51d9..d030592fe 100644 --- a/src/tests/sim_tests/zippychord_sim_tests.rs +++ b/src/tests/sim_tests/zippychord_sim_tests.rs @@ -1,6 +1,7 @@ use super::*; -static ZIPPY_CFG: &str = "(defsrc lalt)(deflayer base (caps-word 2000))(defzippy-experimental file)"; +static ZIPPY_CFG: &str = + "(defsrc lalt)(deflayer base (caps-word 2000))(defzippy-experimental file)"; static ZIPPY_FILE_CONTENT: &str = " dy day dy 1 Monday From c30ded359b61e24088f1b4b13af4ab0170a23da4 Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 20 Oct 2024 19:29:50 -0700 Subject: [PATCH 14/54] documentation --- parser/src/keys/mod.rs | 30 ++++++++++++++++++++++++ src/kanata/output_logic/zippychord.rs | 33 +++++++++++++++++++-------- 2 files changed, 54 insertions(+), 9 deletions(-) diff --git a/parser/src/keys/mod.rs b/parser/src/keys/mod.rs index 2d2053a8a..e2e2b870f 100644 --- a/parser/src/keys/mod.rs +++ b/parser/src/keys/mod.rs @@ -80,6 +80,36 @@ impl OsCode { | OsCode::KEY_RIGHTALT ) } + + /// Returns true if punctuation or whitespace. Also backspace, delete, arrow keys. + pub fn is_grammatical_or_structural(self) -> bool { + matches!( + self, + OsCode::KEY_BACKSPACE + | OsCode::KEY_DELETE + | OsCode::KEY_ENTER + | OsCode::KEY_SPACE + | OsCode::KEY_TAB + | OsCode::KEY_COMMA + | OsCode::KEY_DOT + | OsCode::KEY_SEMICOLON + | OsCode::KEY_APOSTROPHE + | OsCode::KEY_SLASH + | OsCode::KEY_BACKSLASH + | OsCode::KEY_GRAVE + | OsCode::KEY_MINUS + | OsCode::KEY_LEFTBRACE + | OsCode::KEY_RIGHTBRACE + | OsCode::KEY_UP + | OsCode::KEY_DOWN + | OsCode::KEY_LEFT + | OsCode::KEY_RIGHT + | OsCode::KEY_HOME + | OsCode::KEY_END + | OsCode::KEY_PAGEUP + | OsCode::KEY_PAGEDOWN + ) + } } static CUSTOM_STRS_TO_OSCODES: Lazy>> = Lazy::new(|| { diff --git a/src/kanata/output_logic/zippychord.rs b/src/kanata/output_logic/zippychord.rs index c5cde5a32..5df9d6ed9 100644 --- a/src/kanata/output_logic/zippychord.rs +++ b/src/kanata/output_logic/zippychord.rs @@ -6,9 +6,14 @@ use std::sync::Arc; use std::sync::Mutex; use std::sync::MutexGuard; -// TODO: suffixes - only active while disabled, to complete a word. -// TODO: prefix vs. non-prefix: one outputs space, the other not (I guess can be done in parser). -// TODO: smart spacing around words +// Maybe-todos: +// --- +// Feature-parity: smart spacing around words +// - fixup whitespace around punctuation? +// Feature-parity: suffixes - only active while disabled, to complete a word. +// Feature-parity: prefix vs. non-prefix. Assuming smart spacing is implemented and enabled, +// standard activations would output space one outputs space, but not prefixes. +// I guess can be done in parser. static ZCH: Lazy> = Lazy::new(|| Mutex::new(Default::default())); @@ -183,21 +188,28 @@ impl ZchState { } _ => {} } + if self.zch_chords.is_empty() || self.zchd.zchd_is_disabled() || osc.is_modifier() { + if osc.is_grammatical_or_structural() { + // Motivation: if a key is pressed that can potentially be followed by a brand new + // word, quickly re-enable zippychording so user doesn't have to wait for the + // "not-regular-typing-anymore" timeout. + self.zchd.zchd_enabled_state = ZchEnabledState::Enabled; + } return kb.press_key(osc); } + self.zchd.zchd_state_change(&self.zch_cfg); self.zchd.zchd_press_key(osc); + // There might be an activation. // - delete typed keys // - output activation // - // Deletion of typed keys will be based on input keys if - // `zchd_previous_activation_output_count` is - // `None` or the previous output otherwise. - // - // Output activation will save into `zchd_previous_activation_output_count` if there is potential - // for subsequent activations, i.e. if zch_followups is `Some`. + // Key deletion needs to remove typed keys as well as past activations that need to be + // cleaned up, e.g. either the previous chord in a "combo chord" or an eagerly-activated + // chord using fewer keys, but user has still held that chord and pressed further keys, + // activating a chord with the same+extra keys. let mut activation = Neither; if let Some(pchords) = &self.zchd.zchd_prioritized_chords { activation = pchords @@ -214,6 +226,7 @@ impl ZchState { } else { is_prioritized_activation = true; } + match activation { HasValue(a) => { if a.zch_output.is_empty() { @@ -310,10 +323,12 @@ impl ZchState { Ok(()) } + IsSubset => { self.zchd.zchd_characters_to_delete_on_next_activation += 1; kb.press_key(osc) } + Neither => { self.zchd.zchd_reset(); self.zchd.zchd_enabled_state = ZchEnabledState::Disabled; From 074b5e11fef5da7e9baaa24ab0f3d2c190ef92c5 Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 20 Oct 2024 19:52:14 -0700 Subject: [PATCH 15/54] clippy fixes --- src/kanata/output_logic/zippychord.rs | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/kanata/output_logic/zippychord.rs b/src/kanata/output_logic/zippychord.rs index 5df9d6ed9..adb32ea7c 100644 --- a/src/kanata/output_logic/zippychord.rs +++ b/src/kanata/output_logic/zippychord.rs @@ -143,11 +143,11 @@ impl ZchDynamicState { fn zchd_release_key(&mut self, osc: OsCode) { self.zchd_input_keys.zchik_remove(osc); - if self.zchd_input_keys.zchik_is_empty() { - self.zchd_characters_to_delete_on_next_activation = 0; - } self.zchd_enabled_state = match self.zchd_input_keys.zchik_is_empty() { - true => ZchEnabledState::WaitEnable, + true => { + self.zchd_characters_to_delete_on_next_activation = 0; + ZchEnabledState::WaitEnable + } false => ZchEnabledState::Disabled, }; } @@ -286,18 +286,17 @@ impl ZchState { } } self.zchd.zchd_characters_to_delete_on_next_activation += 1; - if !released_lsft { - if !self.zchd.zchd_is_caps_word_active { - released_lsft = true; - if self.zchd.zchd_is_lsft_active { - kb.release_key(OsCode::KEY_LEFTSHIFT)?; - } - if self.zchd.zchd_is_rsft_active { - kb.release_key(OsCode::KEY_RIGHTSHIFT)?; - } + if !released_lsft && !self.zchd.zchd_is_caps_word_active { + released_lsft = true; + if self.zchd.zchd_is_lsft_active { + kb.release_key(OsCode::KEY_LEFTSHIFT)?; + } + if self.zchd.zchd_is_rsft_active { + kb.release_key(OsCode::KEY_RIGHTSHIFT)?; } } } + if !self.zchd.zchd_is_caps_word_active { if self.zchd.zchd_is_lsft_active { kb.press_key(OsCode::KEY_LEFTSHIFT)?; From 717fd81c6fe759d222626fa060d7c9fe7a9ab5cb Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 20 Oct 2024 20:40:51 -0700 Subject: [PATCH 16/54] parse space in output, more backspace fixes --- parser/src/cfg/zippychord.rs | 19 +++++++++++-------- src/kanata/output_logic/zippychord.rs | 3 ++- src/tests/sim_tests/zippychord_sim_tests.rs | 21 +++++++++++++++++++++ 3 files changed, 34 insertions(+), 9 deletions(-) diff --git a/parser/src/cfg/zippychord.rs b/parser/src/cfg/zippychord.rs index a19f35df0..4caaf7be1 100644 --- a/parser/src/cfg/zippychord.rs +++ b/parser/src/cfg/zippychord.rs @@ -197,14 +197,17 @@ fn parse_zippy_inner( .try_fold(vec![], |mut zch_output, out_char| -> Result<_> { let out_key = out_char.to_lowercase().next().unwrap(); let key_name = out_key.encode_utf8(&mut char_buf); - let osc = str_to_oscode(key_name).ok_or_else(|| { - anyhow_expr!( - &exprs[1], - "Unknown output key name '{}':\n{}: {line}", - out_char, - line_number + 1, - ) - })?; + let osc = match key_name as &str { + " " => OsCode::KEY_SPACE, + _ => str_to_oscode(key_name).ok_or_else(|| { + anyhow_expr!( + &exprs[1], + "Unknown output key name '{}':\n{}: {line}", + out_char, + line_number + 1, + ) + })?, + }; let out = match out_char.is_uppercase() { true => ZchOutput::Uppercase(osc), false => ZchOutput::Lowercase(osc), diff --git a/src/kanata/output_logic/zippychord.rs b/src/kanata/output_logic/zippychord.rs index adb32ea7c..f8a2fc024 100644 --- a/src/kanata/output_logic/zippychord.rs +++ b/src/kanata/output_logic/zippychord.rs @@ -232,7 +232,8 @@ impl ZchState { if a.zch_output.is_empty() { self.zchd.zchd_characters_to_delete_on_next_activation += 1; self.zchd.zchd_previous_activation_output_count = - self.zchd.zchd_input_keys.zchik_keys().len() as u16; + self.zchd.zchd_input_keys.zchik_keys().len() as u16 + + self.zchd.zchd_previous_activation_output_count; kb.press_key(osc)?; } else { for _ in 0..(self.zchd.zchd_characters_to_delete_on_next_activation diff --git a/src/tests/sim_tests/zippychord_sim_tests.rs b/src/tests/sim_tests/zippychord_sim_tests.rs index d030592fe..f9e32f8e3 100644 --- a/src/tests/sim_tests/zippychord_sim_tests.rs +++ b/src/tests/sim_tests/zippychord_sim_tests.rs @@ -11,6 +11,8 @@ r df recipient xy WxYz rq request rqa request␣assistance +.g git +.g f p git fetch -p "; #[test] @@ -217,3 +219,22 @@ fn sim_zippychord_caps_word() { result ); } + +#[test] +fn sim_zippychord_triple_combo() { + let result = simulate_with_file_content( + ZIPPY_CFG, + "d:. d:g t:10 u:. u:g d:f t:10 u:f d:p t:10", + Some(ZIPPY_FILE_CONTENT), + ) + .to_ascii(); + assert_eq!( + "dn:Dot t:1ms dn:BSpace up:BSpace up:G dn:G dn:I up:I dn:T up:T t:9ms up:Dot t:1ms up:G \ + t:1ms dn:F t:8ms up:F t:1ms \ + dn:BSpace up:BSpace dn:BSpace up:BSpace dn:BSpace up:BSpace dn:BSpace up:BSpace \ + dn:G up:G dn:I up:I dn:T up:T dn:Space up:Space \ + dn:F up:F dn:E up:E dn:T up:T dn:C up:C dn:H up:H dn:Space up:Space \ + dn:Minus up:Minus up:P dn:P", + result + ); +} From 1d078383c69a948ee3ff644aee247f085db97f64 Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 20 Oct 2024 20:47:21 -0700 Subject: [PATCH 17/54] clippy --- src/kanata/output_logic/zippychord.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/kanata/output_logic/zippychord.rs b/src/kanata/output_logic/zippychord.rs index f8a2fc024..c59ed58d2 100644 --- a/src/kanata/output_logic/zippychord.rs +++ b/src/kanata/output_logic/zippychord.rs @@ -231,9 +231,8 @@ impl ZchState { HasValue(a) => { if a.zch_output.is_empty() { self.zchd.zchd_characters_to_delete_on_next_activation += 1; - self.zchd.zchd_previous_activation_output_count = - self.zchd.zchd_input_keys.zchik_keys().len() as u16 - + self.zchd.zchd_previous_activation_output_count; + self.zchd.zchd_previous_activation_output_count += + self.zchd.zchd_input_keys.zchik_keys().len() as u16; kb.press_key(osc)?; } else { for _ in 0..(self.zchd.zchd_characters_to_delete_on_next_activation From 178e4b2979b2184d93d33aaadd3f427f4e5b0465 Mon Sep 17 00:00:00 2001 From: jtroo Date: Tue, 22 Oct 2024 22:23:34 -0700 Subject: [PATCH 18/54] Reset state correctly Co-authored-by: Martin Mauch --- src/kanata/output_logic/zippychord.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/kanata/output_logic/zippychord.rs b/src/kanata/output_logic/zippychord.rs index c59ed58d2..a3c463639 100644 --- a/src/kanata/output_logic/zippychord.rs +++ b/src/kanata/output_logic/zippychord.rs @@ -126,6 +126,7 @@ impl ZchDynamicState { self.zchd_input_keys.zchik_clear(); self.zchd_prioritized_chords = None; self.zchd_previous_activation_output_count = 0; + self.zchd_ticks_since_state_change = 0; self.zchd_characters_to_delete_on_next_activation = 0; } From 1cb396219bada76f9e6807ac127ef87fabc371c6 Mon Sep 17 00:00:00 2001 From: jtroo Date: Mon, 21 Oct 2024 00:29:52 -0700 Subject: [PATCH 19/54] parse configurations --- parser/src/cfg/mod.rs | 4 +-- parser/src/cfg/zippychord.rs | 41 +++++++++++++++++++++++---- src/kanata/output_logic/zippychord.rs | 17 ++--------- 3 files changed, 40 insertions(+), 22 deletions(-) diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index 9c844e79e..eabfe1b77 100755 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -275,7 +275,7 @@ pub struct Cfg { /// The maximum value of switch's key-timing item in the configuration. pub switch_max_key_timing: u16, /// Zipchord-like configuration. - pub zippy: Option, + pub zippy: Option<(ZchPossibleChords, ZchConfig)>, } /// Parse a new configuration from a file. @@ -419,7 +419,7 @@ pub struct IntermediateCfg { pub overrides: Overrides, pub chords_v2: Option>, pub start_action: Option<&'static KanataAction>, - pub zippy: Option, + pub zippy: Option<(ZchPossibleChords, ZchConfig)>, } // A snapshot of enviroment variables, or an error message with an explanation diff --git a/parser/src/cfg/zippychord.rs b/parser/src/cfg/zippychord.rs index 4caaf7be1..027b97ae2 100644 --- a/parser/src/cfg/zippychord.rs +++ b/parser/src/cfg/zippychord.rs @@ -128,11 +128,23 @@ pub enum ZchOutput { Uppercase(OsCode), } +#[derive(Debug)] +pub struct ZchConfig { + pub zch_cfg_ticks_wait_enable: u16, +} +impl Default for ZchConfig { + fn default() -> Self { + Self { + zch_cfg_ticks_wait_enable: 300, + } + } +} + pub(crate) fn parse_zippy( exprs: &[SExpr], s: &ParserState, f: &mut FileContentProvider, -) -> Result { +) -> Result<(ZchPossibleChords, ZchConfig)> { parse_zippy_inner(exprs, s, f) } @@ -141,7 +153,7 @@ fn parse_zippy_inner( exprs: &[SExpr], _s: &ParserState, _f: &mut FileContentProvider, -) -> Result { +) -> Result<(ZchPossibleChords, ZchConfig)> { bail_expr!(&exprs[0], "Kanata was not compiled with the \"zippychord\" feature. This configuration is unsupported") } @@ -150,23 +162,40 @@ fn parse_zippy_inner( exprs: &[SExpr], s: &ParserState, f: &mut FileContentProvider, -) -> Result { +) -> Result<(ZchPossibleChords, ZchConfig)> { use crate::anyhow_expr; use crate::subset::GetOrIsSubsetOfKnownKey::*; - if exprs.len() != 2 { + if exprs.len() < 2 { bail_expr!( &exprs[0], - "There must be exactly one filename following this definition.\nFound {}", + "There must be a filename following the zippy definition.\nFound {}", exprs.len() - 1 ); } + let Some(file_name) = exprs[1].atom(s.vars()) else { bail_expr!(&exprs[1], "Filename must be a string, not a list."); }; let input_data = f .get_file_content(file_name.as_ref()) .map_err(|e| anyhow_expr!(&exprs[1], "Failed to read file:\n{e}"))?; + + let mut config = ZchConfig::default(); + + // Parse other zippy configurations + // Parse cfgs as name-value pairs + let mut pairs = exprs[2..].chunks_exact(2); + for pair in pairs.by_ref() { + let _config_name = &pair[0]; + let _config_value = &pair[0]; + // todo: add charaters with mappings + } + let rem = pairs.remainder(); + if !rem.is_empty() { + bail_expr!(&rem[0], "zippy config name must be followed by the config value"); + } + let res = input_data .lines() .enumerate() @@ -320,5 +349,5 @@ fn parse_zippy_inner( Ok(zch) }, )?; - Ok(Arc::into_inner(res).expect("no other refs").into_inner()) + Ok((Arc::into_inner(res).expect("no other refs").into_inner(), config)) } diff --git a/src/kanata/output_logic/zippychord.rs b/src/kanata/output_logic/zippychord.rs index a3c463639..a94602353 100644 --- a/src/kanata/output_logic/zippychord.rs +++ b/src/kanata/output_logic/zippychord.rs @@ -28,18 +28,6 @@ pub(crate) fn zch() -> MutexGuard<'static, ZchState> { } } -#[derive(Debug)] -pub(crate) struct ZchConfig { - zch_cfg_ticks_wait_enable: u16, -} -impl Default for ZchConfig { - fn default() -> Self { - Self { - zch_cfg_ticks_wait_enable: 300, - } - } -} - #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] enum ZchEnabledState { #[default] @@ -169,8 +157,9 @@ pub(crate) struct ZchState { impl ZchState { /// Configure zippychord behaviour. - pub(crate) fn zch_configure(&mut self, chords: ZchPossibleChords) { - self.zch_chords = chords; + pub(crate) fn zch_configure(&mut self, cfg: (ZchPossibleChords, ZchConfig)) { + self.zch_chords = cfg.0; + self.zch_cfg = cfg.1; self.zchd.zchd_reset(); } From 43fec15f6eb2d6ee23cad6482668f947a65d8a44 Mon Sep 17 00:00:00 2001 From: jtroo Date: Mon, 21 Oct 2024 00:44:59 -0700 Subject: [PATCH 20/54] wip zippy configuration --- parser/src/cfg/zippychord.rs | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/parser/src/cfg/zippychord.rs b/parser/src/cfg/zippychord.rs index 027b97ae2..6acbd94bb 100644 --- a/parser/src/cfg/zippychord.rs +++ b/parser/src/cfg/zippychord.rs @@ -130,12 +130,19 @@ pub enum ZchOutput { #[derive(Debug)] pub struct ZchConfig { + /// When, during typing, chord fails to activate, zippychord functionality becomes temporarily + /// disabled. This is to avoid accidental chord activations when typing normally, as opposed to + /// intentionally trying to activate a chord. The duration of temporary disabling is determined + /// by this configuration item. Re-enabling also happens when word-splitting characters are + /// typed, for example typing a space or a comma, but a pause of all typing activity lasting a + /// number of milliseconds equal to this configuration will also re-enable chording even if + /// typing within a single word. pub zch_cfg_ticks_wait_enable: u16, } impl Default for ZchConfig { fn default() -> Self { Self { - zch_cfg_ticks_wait_enable: 300, + zch_cfg_ticks_wait_enable: 500, } } } @@ -187,13 +194,25 @@ fn parse_zippy_inner( // Parse cfgs as name-value pairs let mut pairs = exprs[2..].chunks_exact(2); for pair in pairs.by_ref() { - let _config_name = &pair[0]; - let _config_value = &pair[0]; - // todo: add charaters with mappings + let config_name = &pair[0]; + let config_value = &pair[1]; + match config_name.atom(s.vars()).ok_or_else(|| { + anyhow_expr!( + config_name, + "A configuration name must be a string, not a list" + ) + })? { + "idle-reactivate-time" => { + config.zch_cfg_ticks_wait_enable = + parse_u16(config_value, s, "idle-reactivate-time")?; + } + "key-name-mappings" => {todo!()} + _ => bail_expr!(config_name, "Unknown zippy configuration name"), + } } let rem = pairs.remainder(); if !rem.is_empty() { - bail_expr!(&rem[0], "zippy config name must be followed by the config value"); + bail_expr!(&rem[0], "zippy config name is missing its value"); } let res = input_data @@ -349,5 +368,8 @@ fn parse_zippy_inner( Ok(zch) }, )?; - Ok((Arc::into_inner(res).expect("no other refs").into_inner(), config)) + Ok(( + Arc::into_inner(res).expect("no other refs").into_inner(), + config, + )) } From 0d4796041e012a20f00dec4c447ce9bb6295dd33 Mon Sep 17 00:00:00 2001 From: jtroo Date: Thu, 24 Oct 2024 01:57:16 -0700 Subject: [PATCH 21/54] add deadline configuration --- parser/src/cfg/zippychord.rs | 22 ++++++++++++- src/kanata/output_logic/zippychord.rs | 46 ++++++++++++++++++++++++--- 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/parser/src/cfg/zippychord.rs b/parser/src/cfg/zippychord.rs index 6acbd94bb..ac94c576d 100644 --- a/parser/src/cfg/zippychord.rs +++ b/parser/src/cfg/zippychord.rs @@ -138,11 +138,25 @@ pub struct ZchConfig { /// number of milliseconds equal to this configuration will also re-enable chording even if /// typing within a single word. pub zch_cfg_ticks_wait_enable: u16, + + /// Assuming zippychording is enabled, when the first press happens this deadline will begin + /// and if no chords are completed within the deadline, zippychording will be disabled + /// temporarily (see `zch_cfg_ticks_wait_enable`). You may want a long or short deadline + /// depending on your use case. If you are primarily typing normally, with chords being used + /// occasionally being used, you may want a short deadline so that regular typing will be + /// unlikely to activate any chord. However, if you primarily type with chords, you may want a + /// longer deadline to give you more time to complete the intended chord (e.g. in case of + /// overlaps). With a long deadline you should be very intentional about pressing and releasing + /// an individual key to begin a sequence of regular typing to trigger the disabling of + /// zippychord. If, after the first press, a chord activates, this deadline will reset to + /// enable further chord activations. + pub zch_cfg_ticks_chord_deadline: u16, } impl Default for ZchConfig { fn default() -> Self { Self { zch_cfg_ticks_wait_enable: 500, + zch_cfg_ticks_chord_deadline: 100, } } } @@ -206,7 +220,13 @@ fn parse_zippy_inner( config.zch_cfg_ticks_wait_enable = parse_u16(config_value, s, "idle-reactivate-time")?; } - "key-name-mappings" => {todo!()} + "on-first-press-chord-deadline" => { + config.zch_cfg_ticks_chord_deadline = + parse_u16(config_value, s, "on-first-press-chord-deadline")?; + } + "key-name-mappings" => { + todo!() + } _ => bail_expr!(config_name, "Unknown zippy configuration name"), } } diff --git a/src/kanata/output_logic/zippychord.rs b/src/kanata/output_logic/zippychord.rs index a94602353..522c01d20 100644 --- a/src/kanata/output_logic/zippychord.rs +++ b/src/kanata/output_logic/zippychord.rs @@ -73,6 +73,10 @@ struct ZchDynamicState { /// against unintended activations. This counts downwards from a configured number until 0, and /// at 0 the state transitions from pending-enabled to truly-enabled if applicable. zchd_ticks_until_enabled: u16, + /// Zch has a time delay between being disabled->pending-enabled->truly-enabled to mitigate + /// against unintended activations. This counts downwards from a configured number until 0, and + /// at 0 the state transitions from pending-enabled to truly-enabled if applicable. + zchd_ticks_until_disable: u16, /// Current state of caps-word, which is a factor in handling capitalization. zchd_is_caps_word_active: bool, /// Current state of lsft which is a factor in handling capitalization. @@ -85,25 +89,50 @@ impl ZchDynamicState { fn zchd_is_disabled(&self) -> bool { self.zchd_enabled_state == ZchEnabledState::Disabled } + fn zchd_tick(&mut self, is_caps_word_active: bool) { const TICKS_UNTIL_FORCE_STATE_RESET: u16 = 10000; self.zchd_ticks_since_state_change += 1; self.zchd_is_caps_word_active = is_caps_word_active; - if self.zchd_enabled_state == ZchEnabledState::WaitEnable { - self.zchd_ticks_until_enabled = self.zchd_ticks_until_enabled.saturating_sub(1); - if self.zchd_ticks_until_enabled == 0 { - self.zchd_enabled_state = ZchEnabledState::Enabled; + match self.zchd_enabled_state { + ZchEnabledState::WaitEnable => { + self.zchd_ticks_until_enabled = self.zchd_ticks_until_enabled.saturating_sub(1); + if self.zchd_ticks_until_enabled == 0 { + self.zchd_enabled_state = ZchEnabledState::Enabled; + } + } + ZchEnabledState::Enabled => { + // Only run disable-check logic if ticks is already greater than zero, because zero + // means deadline has never been triggered by an press yet. + if self.zchd_ticks_until_disable > 0 { + self.zchd_ticks_until_disable = self.zchd_ticks_until_disable.saturating_sub(1); + if self.zchd_ticks_until_disable == 0 { + self.zchd_enabled_state = ZchEnabledState::Disabled; + } + } } + ZchEnabledState::Disabled => {} } if self.zchd_ticks_since_state_change > TICKS_UNTIL_FORCE_STATE_RESET { self.zchd_reset(); } } + fn zchd_state_change(&mut self, cfg: &ZchConfig) { self.zchd_ticks_since_state_change = 0; self.zchd_ticks_until_enabled = cfg.zch_cfg_ticks_wait_enable; } + fn zchd_activate_chord_deadline(&mut self, deadline_ticks: u16) { + if self.zchd_ticks_until_disable == 0 { + self.zchd_ticks_until_disable = deadline_ticks; + } + } + + fn zchd_restart_deadline(&mut self, deadline_ticks: u16) { + self.zchd_ticks_until_disable = deadline_ticks; + } + /// Clean up the state. fn zchd_reset(&mut self) { log::debug!("zchd reset state"); @@ -115,6 +144,8 @@ impl ZchDynamicState { self.zchd_prioritized_chords = None; self.zchd_previous_activation_output_count = 0; self.zchd_ticks_since_state_change = 0; + self.zchd_ticks_until_disable = 0; + self.zchd_ticks_until_enabled = 0; self.zchd_characters_to_delete_on_next_activation = 0; } @@ -151,7 +182,6 @@ pub(crate) struct ZchState { /// the state. zch_chords: ZchPossibleChords, /// Options to configure behaviour. - /// TODO: needs parser configuration. zch_cfg: ZchConfig, } @@ -189,6 +219,10 @@ impl ZchState { return kb.press_key(osc); } + // Zippychording is enabled. Ensure the deadline to disable it if no chord activates is + // active. + self.zchd + .zchd_activate_chord_deadline(self.zch_cfg.zch_cfg_ticks_chord_deadline); self.zchd.zchd_state_change(&self.zch_cfg); self.zchd.zchd_press_key(osc); @@ -219,6 +253,8 @@ impl ZchState { match activation { HasValue(a) => { + self.zchd + .zchd_restart_deadline(self.zch_cfg.zch_cfg_ticks_chord_deadline); if a.zch_output.is_empty() { self.zchd.zchd_characters_to_delete_on_next_activation += 1; self.zchd.zchd_previous_activation_output_count += From d40399fe441fa04d4b4e31541eb49c858951b9f9 Mon Sep 17 00:00:00 2001 From: jtroo Date: Fri, 25 Oct 2024 01:34:51 -0700 Subject: [PATCH 22/54] rename/move/adjust quick enable function, add wip doc --- docs/config.adoc | 37 +++++++++++++++++++++++++++ parser/src/keys/mod.rs | 30 ---------------------- src/kanata/output_logic/zippychord.rs | 32 ++++++++++++++++++++++- 3 files changed, 68 insertions(+), 31 deletions(-) diff --git a/docs/config.adoc b/docs/config.adoc index 7e8757f51..706997d7f 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -3975,3 +3975,40 @@ And key names are defined in the https://github.com/jtroo/kanata/blob/main/parse for example, `1` for the numeric key 1 or `kp1`/`🔢₁` for the keypad numeric key 1 Using unicode symbols `🕐`,`↓`,`↑`,`⟳` allows skipping the `:` separator, e.g., `↓k` ≝ `↓:k` ≝ `d:k` + +[[zippychord]] +=== Zippychord +<> + +Zippychord is yet another chording mechanism in Kanata. +The inspiration behind it is primarily the +https://github.com/psoukie/zipchord[zipchord project]. +The name is similar; it is named "zippy" instead of "zip" because +Kanata's implementation is not a port and does not aim for 100% +behavioural compatibility. + +The intended use case is accelerating and reducing effort of typing. +The inputs are keycodes and the outputs are also purely keycodes. +In other words, all other actions are unsupported; +e.g. layers, switch, one-shot. + +Unlike chords(v1) and chordsv2, zippychord behaves on output keycodes. +This is similar to how sequences operate. + +To give an example, if one configures zippychord with a line like: + +[source] +---- +gi git +---- + +then either of the following typing event sequences +will erase the input characters +and then proceed to type the output "git" +like if it was `(macro bspc bspc g i t)`. + +[source] +---- +(press g) (press i) +(press i) (press g) +---- diff --git a/parser/src/keys/mod.rs b/parser/src/keys/mod.rs index e2e2b870f..2d2053a8a 100644 --- a/parser/src/keys/mod.rs +++ b/parser/src/keys/mod.rs @@ -80,36 +80,6 @@ impl OsCode { | OsCode::KEY_RIGHTALT ) } - - /// Returns true if punctuation or whitespace. Also backspace, delete, arrow keys. - pub fn is_grammatical_or_structural(self) -> bool { - matches!( - self, - OsCode::KEY_BACKSPACE - | OsCode::KEY_DELETE - | OsCode::KEY_ENTER - | OsCode::KEY_SPACE - | OsCode::KEY_TAB - | OsCode::KEY_COMMA - | OsCode::KEY_DOT - | OsCode::KEY_SEMICOLON - | OsCode::KEY_APOSTROPHE - | OsCode::KEY_SLASH - | OsCode::KEY_BACKSLASH - | OsCode::KEY_GRAVE - | OsCode::KEY_MINUS - | OsCode::KEY_LEFTBRACE - | OsCode::KEY_RIGHTBRACE - | OsCode::KEY_UP - | OsCode::KEY_DOWN - | OsCode::KEY_LEFT - | OsCode::KEY_RIGHT - | OsCode::KEY_HOME - | OsCode::KEY_END - | OsCode::KEY_PAGEUP - | OsCode::KEY_PAGEDOWN - ) - } } static CUSTOM_STRS_TO_OSCODES: Lazy>> = Lazy::new(|| { diff --git a/src/kanata/output_logic/zippychord.rs b/src/kanata/output_logic/zippychord.rs index 522c01d20..09416b6ce 100644 --- a/src/kanata/output_logic/zippychord.rs +++ b/src/kanata/output_logic/zippychord.rs @@ -210,7 +210,7 @@ impl ZchState { } if self.zch_chords.is_empty() || self.zchd.zchd_is_disabled() || osc.is_modifier() { - if osc.is_grammatical_or_structural() { + if osc_triggers_quick_enable(osc) { // Motivation: if a key is pressed that can potentially be followed by a brand new // word, quickly re-enable zippychording so user doesn't have to wait for the // "not-regular-typing-anymore" timeout. @@ -396,3 +396,33 @@ impl ZchState { self.zchd.zchd_is_idle() } } + +/// Currently only returns true if the key is space. +fn osc_triggers_quick_enable(osc: OsCode) -> bool { + matches!(osc, OsCode::KEY_SPACE) + // Old implementation. + // ~~Returns true if punctuation or whitespace. Also backspace, delete, arrow keys.~~ + // OsCode::KEY_BACKSPACE + // | OsCode::KEY_DELETE + // | OsCode::KEY_ENTER + // | OsCode::KEY_SPACE + // | OsCode::KEY_TAB + // | OsCode::KEY_COMMA + // | OsCode::KEY_DOT + // | OsCode::KEY_SEMICOLON + // | OsCode::KEY_APOSTROPHE + // | OsCode::KEY_SLASH + // | OsCode::KEY_BACKSLASH + // | OsCode::KEY_GRAVE + // | OsCode::KEY_MINUS + // | OsCode::KEY_LEFTBRACE + // | OsCode::KEY_RIGHTBRACE + // | OsCode::KEY_UP + // | OsCode::KEY_DOWN + // | OsCode::KEY_LEFT + // | OsCode::KEY_RIGHT + // | OsCode::KEY_HOME + // | OsCode::KEY_END + // | OsCode::KEY_PAGEUP + // | OsCode::KEY_PAGEDOWN +} From 6805250cbf369c279276152e341cae1f160bad4f Mon Sep 17 00:00:00 2001 From: jtroo Date: Fri, 25 Oct 2024 01:36:36 -0700 Subject: [PATCH 23/54] fix a comment --- src/kanata/output_logic/zippychord.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kanata/output_logic/zippychord.rs b/src/kanata/output_logic/zippychord.rs index 09416b6ce..8ff566f4b 100644 --- a/src/kanata/output_logic/zippychord.rs +++ b/src/kanata/output_logic/zippychord.rs @@ -334,7 +334,7 @@ impl ZchState { // Note: it is incorrect to clear input keys. // Zippychord will eagerly output chords even if there is an overlapping chord that - // may be activated earlier. + // may be activated later by an additional keypress before any releases happen. // E.g. // ab => Abba // abc => Alphabet From 4399230b775565aabcff0a5a7426c598dc5cf791 Mon Sep 17 00:00:00 2001 From: jtroo Date: Fri, 25 Oct 2024 01:40:09 -0700 Subject: [PATCH 24/54] disable properly in waitenable --- src/kanata/output_logic/zippychord.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/kanata/output_logic/zippychord.rs b/src/kanata/output_logic/zippychord.rs index 8ff566f4b..c688fa5e6 100644 --- a/src/kanata/output_logic/zippychord.rs +++ b/src/kanata/output_logic/zippychord.rs @@ -87,7 +87,10 @@ struct ZchDynamicState { impl ZchDynamicState { fn zchd_is_disabled(&self) -> bool { - self.zchd_enabled_state == ZchEnabledState::Disabled + matches!( + self.zchd_enabled_state, + ZchEnabledState::Disabled | ZchEnabledState::WaitEnable + ) } fn zchd_tick(&mut self, is_caps_word_active: bool) { From c98da8bddecd31831d1d794ce32df1d5b8216dcf Mon Sep 17 00:00:00 2001 From: jtroo Date: Fri, 25 Oct 2024 01:48:26 -0700 Subject: [PATCH 25/54] change reset behaviours --- parser/src/cfg/zippychord.rs | 2 +- src/kanata/output_logic/zippychord.rs | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/parser/src/cfg/zippychord.rs b/parser/src/cfg/zippychord.rs index ac94c576d..f508dc3d1 100644 --- a/parser/src/cfg/zippychord.rs +++ b/parser/src/cfg/zippychord.rs @@ -156,7 +156,7 @@ impl Default for ZchConfig { fn default() -> Self { Self { zch_cfg_ticks_wait_enable: 500, - zch_cfg_ticks_chord_deadline: 100, + zch_cfg_ticks_chord_deadline: 500, } } } diff --git a/src/kanata/output_logic/zippychord.rs b/src/kanata/output_logic/zippychord.rs index c688fa5e6..a71f5d501 100644 --- a/src/kanata/output_logic/zippychord.rs +++ b/src/kanata/output_logic/zippychord.rs @@ -136,13 +136,19 @@ impl ZchDynamicState { self.zchd_ticks_until_disable = deadline_ticks; } - /// Clean up the state. + /// Clean up the state, potentially causing inaccuracies with regards to what the user is + /// currently still pressing. fn zchd_reset(&mut self) { log::debug!("zchd reset state"); self.zchd_enabled_state = ZchEnabledState::Enabled; self.zchd_is_caps_word_active = false; self.zchd_is_lsft_active = false; self.zchd_is_rsft_active = false; + self.zchd_soft_reset(); + } + + fn zchd_soft_reset(&mut self) { + log::debug!("zchd soft reset state"); self.zchd_input_keys.zchik_clear(); self.zchd_prioritized_chords = None; self.zchd_previous_activation_output_count = 0; @@ -217,7 +223,7 @@ impl ZchState { // Motivation: if a key is pressed that can potentially be followed by a brand new // word, quickly re-enable zippychording so user doesn't have to wait for the // "not-regular-typing-anymore" timeout. - self.zchd.zchd_enabled_state = ZchEnabledState::Enabled; + self.zchd.zchd_soft_reset() } return kb.press_key(osc); } @@ -358,7 +364,7 @@ impl ZchState { } Neither => { - self.zchd.zchd_reset(); + self.zchd.zchd_soft_reset(); self.zchd.zchd_enabled_state = ZchEnabledState::Disabled; kb.press_key(osc) } From a321bd0539e9b3ed51117b097cb6556508d5e522 Mon Sep 17 00:00:00 2001 From: jtroo Date: Fri, 25 Oct 2024 01:50:12 -0700 Subject: [PATCH 26/54] move enable to soft reset --- src/kanata/output_logic/zippychord.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kanata/output_logic/zippychord.rs b/src/kanata/output_logic/zippychord.rs index a71f5d501..0080d3a7f 100644 --- a/src/kanata/output_logic/zippychord.rs +++ b/src/kanata/output_logic/zippychord.rs @@ -140,7 +140,6 @@ impl ZchDynamicState { /// currently still pressing. fn zchd_reset(&mut self) { log::debug!("zchd reset state"); - self.zchd_enabled_state = ZchEnabledState::Enabled; self.zchd_is_caps_word_active = false; self.zchd_is_lsft_active = false; self.zchd_is_rsft_active = false; @@ -149,6 +148,7 @@ impl ZchDynamicState { fn zchd_soft_reset(&mut self) { log::debug!("zchd soft reset state"); + self.zchd_enabled_state = ZchEnabledState::Enabled; self.zchd_input_keys.zchik_clear(); self.zchd_prioritized_chords = None; self.zchd_previous_activation_output_count = 0; From 8fad871c0fbbe981393800edd598a8e5ccccc5e3 Mon Sep 17 00:00:00 2001 From: jtroo Date: Fri, 25 Oct 2024 02:02:17 -0700 Subject: [PATCH 27/54] maybe fix sequential activation --- src/kanata/output_logic/zippychord.rs | 33 ++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/src/kanata/output_logic/zippychord.rs b/src/kanata/output_logic/zippychord.rs index 0080d3a7f..45bc1ef36 100644 --- a/src/kanata/output_logic/zippychord.rs +++ b/src/kanata/output_logic/zippychord.rs @@ -36,6 +36,13 @@ enum ZchEnabledState { Disabled, } +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +enum ZchLastPressClassification { + IsChord, + #[default] + NotChord, +} + #[derive(Debug, Default)] struct ZchDynamicState { /// Input to compare against configured available chords to output. @@ -83,6 +90,8 @@ struct ZchDynamicState { zchd_is_lsft_active: bool, /// Current state of rsft which is a factor in handling capitalization. zchd_is_rsft_active: bool, + /// Tracks whether last press was part of a chord or not. + zchd_last_press: ZchLastPressClassification, } impl ZchDynamicState { @@ -172,13 +181,22 @@ impl ZchDynamicState { fn zchd_release_key(&mut self, osc: OsCode) { self.zchd_input_keys.zchik_remove(osc); - self.zchd_enabled_state = match self.zchd_input_keys.zchik_is_empty() { - true => { + match (self.zchd_last_press, self.zchd_input_keys.zchik_is_empty()) { + (ZchLastPressClassification::NotChord, true) => { + self.zchd_enabled_state = ZchEnabledState::WaitEnable; self.zchd_characters_to_delete_on_next_activation = 0; - ZchEnabledState::WaitEnable } - false => ZchEnabledState::Disabled, - }; + (ZchLastPressClassification::NotChord, false) => { + self.zchd_enabled_state = ZchEnabledState::Disabled; + } + (ZchLastPressClassification::IsChord, true) => { + if self.zchd_prioritized_chords.is_none() { + self.zchd_previous_activation_output_count = 0; + self.zchd_characters_to_delete_on_next_activation = 0; + } + } + (ZchLastPressClassification::IsChord, false) => {} + } } } @@ -219,7 +237,7 @@ impl ZchState { } if self.zch_chords.is_empty() || self.zchd.zchd_is_disabled() || osc.is_modifier() { - if osc_triggers_quick_enable(osc) { + if !self.zch_chords.is_empty() && osc_triggers_quick_enable(osc) { // Motivation: if a key is pressed that can potentially be followed by a brand new // word, quickly re-enable zippychording so user doesn't have to wait for the // "not-regular-typing-anymore" timeout. @@ -355,15 +373,18 @@ impl ZchState { // WRONG: // self.zchd.zchd_input_keys.zchik_clear() + self.zchd.zchd_last_press = ZchLastPressClassification::IsChord; Ok(()) } IsSubset => { + self.zchd.zchd_last_press = ZchLastPressClassification::NotChord; self.zchd.zchd_characters_to_delete_on_next_activation += 1; kb.press_key(osc) } Neither => { + self.zchd.zchd_last_press = ZchLastPressClassification::NotChord; self.zchd.zchd_soft_reset(); self.zchd.zchd_enabled_state = ZchEnabledState::Disabled; kb.press_key(osc) From 2122c25ae12d6ed6ab1a7072874a400b6e2d2bdd Mon Sep 17 00:00:00 2001 From: jtroo Date: Fri, 25 Oct 2024 21:01:51 -0700 Subject: [PATCH 28/54] fix state --- src/kanata/output_logic/zippychord.rs | 37 ++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/src/kanata/output_logic/zippychord.rs b/src/kanata/output_logic/zippychord.rs index 45bc1ef36..6cc37535e 100644 --- a/src/kanata/output_logic/zippychord.rs +++ b/src/kanata/output_logic/zippychord.rs @@ -38,8 +38,9 @@ enum ZchEnabledState { #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] enum ZchLastPressClassification { - IsChord, #[default] + IsChord, + IsQuickEnable, NotChord, } @@ -110,6 +111,7 @@ impl ZchDynamicState { ZchEnabledState::WaitEnable => { self.zchd_ticks_until_enabled = self.zchd_ticks_until_enabled.saturating_sub(1); if self.zchd_ticks_until_enabled == 0 { + log::debug!("zippy wait enable->enable"); self.zchd_enabled_state = ZchEnabledState::Enabled; } } @@ -119,6 +121,7 @@ impl ZchDynamicState { if self.zchd_ticks_until_disable > 0 { self.zchd_ticks_until_disable = self.zchd_ticks_until_disable.saturating_sub(1); if self.zchd_ticks_until_disable == 0 { + log::debug!("zippy enable->disable"); self.zchd_enabled_state = ZchEnabledState::Disabled; } } @@ -158,6 +161,7 @@ impl ZchDynamicState { fn zchd_soft_reset(&mut self) { log::debug!("zchd soft reset state"); self.zchd_enabled_state = ZchEnabledState::Enabled; + self.zchd_last_press = ZchLastPressClassification::IsChord; self.zchd_input_keys.zchik_clear(); self.zchd_prioritized_chords = None; self.zchd_previous_activation_output_count = 0; @@ -183,19 +187,32 @@ impl ZchDynamicState { self.zchd_input_keys.zchik_remove(osc); match (self.zchd_last_press, self.zchd_input_keys.zchik_is_empty()) { (ZchLastPressClassification::NotChord, true) => { + log::debug!("all released->zippy wait enable"); self.zchd_enabled_state = ZchEnabledState::WaitEnable; self.zchd_characters_to_delete_on_next_activation = 0; } (ZchLastPressClassification::NotChord, false) => { + log::debug!("release but not all->zippy disable"); self.zchd_enabled_state = ZchEnabledState::Disabled; } (ZchLastPressClassification::IsChord, true) => { + log::debug!("all released->zippy enabled"); if self.zchd_prioritized_chords.is_none() { + log::debug!("no continuation->zippy clear key erase state"); self.zchd_previous_activation_output_count = 0; - self.zchd_characters_to_delete_on_next_activation = 0; } + self.zchd_characters_to_delete_on_next_activation = 0; + self.zchd_ticks_until_disable = 0; + } + (ZchLastPressClassification::IsChord, false) => { + log::debug!("some released->zippy enabled"); + self.zchd_ticks_until_disable = 0; + } + (ZchLastPressClassification::IsQuickEnable, _) => { + log::debug!("quick enable release->clear characters"); + self.zchd_previous_activation_output_count = 0; + self.zchd_characters_to_delete_on_next_activation = 0; } - (ZchLastPressClassification::IsChord, false) => {} } } } @@ -236,14 +253,20 @@ impl ZchState { _ => {} } - if self.zch_chords.is_empty() || self.zchd.zchd_is_disabled() || osc.is_modifier() { - if !self.zch_chords.is_empty() && osc_triggers_quick_enable(osc) { + if self.zch_chords.is_empty() || osc.is_modifier() { + return kb.press_key(osc); + } + if osc_triggers_quick_enable(osc) { + if self.zchd.zchd_is_disabled() { + log::debug!("zippy quick enable"); // Motivation: if a key is pressed that can potentially be followed by a brand new // word, quickly re-enable zippychording so user doesn't have to wait for the // "not-regular-typing-anymore" timeout. - self.zchd.zchd_soft_reset() + self.zchd.zchd_soft_reset(); + return kb.press_key(osc); + } else { + self.zchd.zchd_last_press = ZchLastPressClassification::IsQuickEnable; } - return kb.press_key(osc); } // Zippychording is enabled. Ensure the deadline to disable it if no chord activates is From 575dd1b850d686c7d54822a207151a3f6103730a Mon Sep 17 00:00:00 2001 From: jtroo Date: Sat, 26 Oct 2024 01:57:44 -0700 Subject: [PATCH 29/54] progress in map parsing --- parser/src/cfg/zippychord.rs | 116 +++++++++++++++++++++++++++++++---- 1 file changed, 104 insertions(+), 12 deletions(-) diff --git a/parser/src/cfg/zippychord.rs b/parser/src/cfg/zippychord.rs index f508dc3d1..766d0caa3 100644 --- a/parser/src/cfg/zippychord.rs +++ b/parser/src/cfg/zippychord.rs @@ -198,43 +198,136 @@ fn parse_zippy_inner( let Some(file_name) = exprs[1].atom(s.vars()) else { bail_expr!(&exprs[1], "Filename must be a string, not a list."); }; - let input_data = f - .get_file_content(file_name.as_ref()) - .map_err(|e| anyhow_expr!(&exprs[1], "Failed to read file:\n{e}"))?; let mut config = ZchConfig::default(); + const KEY_NAME_MAPPINGS: &'static str = "key-name-mappings"; + const IDLE_REACTIVATE_TIME: &'static str = "idle-reactivate-time"; + const CHORD_DEADLINE: &'static str = "on-first-press-chord-deadline"; + + let mut idle_reactivate_time_seen = false; + let mut key_name_mappings_seen = false; + let mut chord_deadline_seen = false; + // Parse other zippy configurations - // Parse cfgs as name-value pairs let mut pairs = exprs[2..].chunks_exact(2); for pair in pairs.by_ref() { let config_name = &pair[0]; let config_value = &pair[1]; + match config_name.atom(s.vars()).ok_or_else(|| { anyhow_expr!( config_name, "A configuration name must be a string, not a list" ) })? { - "idle-reactivate-time" => { + IDLE_REACTIVATE_TIME => { + if idle_reactivate_time_seen { + bail_expr!( + config_name, + "This is the 2nd instance; it can only be defined once" + ); + } + idle_reactivate_time_seen = true; config.zch_cfg_ticks_wait_enable = - parse_u16(config_value, s, "idle-reactivate-time")?; + parse_u16(config_value, s, IDLE_REACTIVATE_TIME)?; } - "on-first-press-chord-deadline" => { - config.zch_cfg_ticks_chord_deadline = - parse_u16(config_value, s, "on-first-press-chord-deadline")?; + + CHORD_DEADLINE => { + if chord_deadline_seen { + bail_expr!( + config_name, + "This is the 2nd instance; it can only be defined once" + ); + } + chord_deadline_seen = true; + config.zch_cfg_ticks_chord_deadline = parse_u16(config_value, s, CHORD_DEADLINE)?; } - "key-name-mappings" => { - todo!() + + KEY_NAME_MAPPINGS => { + if key_name_mappings_seen { + bail_expr!( + config_name, + "This is the 2nd instance; it can only be defined once" + ); + } + key_name_mappings_seen = true; + let mut mappings = config_value + .list(s.vars()) + .ok_or_else(|| { + anyhow_expr!( + config_value, + "{KEY_NAME_MAPPINGS} must be followed by a list" + ) + })? + .chunks_exact(2); + + for mapping_pair in mappings.by_ref() { + let input = mapping_pair[0] + .atom(None) + .ok_or_else(|| { + anyhow_expr!(&mapping_pair[0], "key mapping does not use lists") + })? + .trim_atom_quotes(); + if input.chars().count() != 1 { + bail_expr!(&mapping_pair[0], "Inputs should be exactly one character"); + } + let input_char = input.chars().next(); + + let output = mapping_pair[1].atom(s.vars()).ok_or_else(|| { + anyhow_expr!(&mapping_pair[1], "key mapping does not use lists") + })?; + let (output_mods, output_key) = parse_mod_prefix(output)?; + if output_mods.contains(&KeyCode::LShift) + && output_mods.contains(&KeyCode::RShift) + { + bail_expr!( + &mapping_pair[1], + "Both shifts are used which is redundant, use only one." + ); + } + if output_mods + .iter() + .any(|m| !matches!(m, KeyCode::LShift | KeyCode::RShift | KeyCode::RAlt)) + { + bail_expr!(&mapping_pair[1], "Only S- and AG-/RA- are supported."); + } + let output_osc = str_to_oscode(output_key) + .ok_or_else(|| anyhow_expr!(&mapping_pair[1], "unknown key name"))?; + match output_mods.len() { + 0 => { + todo!("raw osc") + } + 1 => { + todo!("check whether shift or altgr") + } + 2 => { + todo!("both shift and altgr") + } + _ => { + unreachable!("contains max of: altgr and one of the shifts") + } + } + } + + let rem = mappings.remainder(); + if !rem.is_empty() { + bail_expr!(&rem[0], "zippy input is missing its output mapping"); + } } _ => bail_expr!(config_name, "Unknown zippy configuration name"), } } + let rem = pairs.remainder(); if !rem.is_empty() { bail_expr!(&rem[0], "zippy config name is missing its value"); } + // process zippy file + let input_data = f + .get_file_content(file_name.as_ref()) + .map_err(|e| anyhow_expr!(&exprs[1], "Failed to read file:\n{e}"))?; let res = input_data .lines() .enumerate() @@ -258,7 +351,6 @@ fn parse_zippy_inner( } let mut char_buf: [u8; 4] = [0; 4]; - let output = { output .chars() From 8640408bb1d389d029e61a5dac77860ec33dc176 Mon Sep 17 00:00:00 2001 From: jtroo Date: Sat, 26 Oct 2024 17:19:44 -0700 Subject: [PATCH 30/54] completed output mapping --- parser/src/cfg/zippychord.rs | 49 ++++++++------- src/kanata/output_logic/zippychord.rs | 85 ++++++++++++++++++--------- 2 files changed, 86 insertions(+), 48 deletions(-) diff --git a/parser/src/cfg/zippychord.rs b/parser/src/cfg/zippychord.rs index 766d0caa3..4eb443049 100644 --- a/parser/src/cfg/zippychord.rs +++ b/parser/src/cfg/zippychord.rs @@ -62,7 +62,7 @@ impl ZchInputKeys { }, } } - pub fn zchik_contains(&mut self, osc: OsCode) -> bool { + pub fn zchik_contains(&self, osc: OsCode) -> bool { self.zch_inputs.zch_keys.contains(&osc.into()) } pub fn zchik_insert(&mut self, osc: OsCode) { @@ -119,13 +119,15 @@ pub struct ZchChordOutput { pub zch_followups: Option>>, } -/// Zch output can be uppercase or lowercase characters. -/// The parser should ensure all `OsCode`s within `Lowercase` and `Uppercase` -/// are visible characters that can be backspaced. +/// Zch output can be uppercase, lowercase, altgr, and shift-altgr characters. +/// The parser should ensure all `OsCode`s in variants containing them +/// are visible characters that are backspacable. #[derive(Debug, Clone, Copy)] pub enum ZchOutput { Lowercase(OsCode), Uppercase(OsCode), + AltGr(OsCode), + ShiftAltGr(OsCode), } #[derive(Debug)] @@ -201,13 +203,14 @@ fn parse_zippy_inner( let mut config = ZchConfig::default(); - const KEY_NAME_MAPPINGS: &'static str = "key-name-mappings"; - const IDLE_REACTIVATE_TIME: &'static str = "idle-reactivate-time"; - const CHORD_DEADLINE: &'static str = "on-first-press-chord-deadline"; + const KEY_NAME_MAPPINGS: &str = "key-name-mappings"; + const IDLE_REACTIVATE_TIME: &str = "idle-reactivate-time"; + const CHORD_DEADLINE: &str = "on-first-press-chord-deadline"; let mut idle_reactivate_time_seen = false; let mut key_name_mappings_seen = false; let mut chord_deadline_seen = false; + let mut user_cfg_char_to_output: HashMap = HashMap::default(); // Parse other zippy configurations let mut pairs = exprs[2..].chunks_exact(2); @@ -272,7 +275,7 @@ fn parse_zippy_inner( if input.chars().count() != 1 { bail_expr!(&mapping_pair[0], "Inputs should be exactly one character"); } - let input_char = input.chars().next(); + let input_char = input.chars().next().expect("count is 1"); let output = mapping_pair[1].atom(s.vars()).ok_or_else(|| { anyhow_expr!(&mapping_pair[1], "key mapping does not use lists") @@ -290,23 +293,24 @@ fn parse_zippy_inner( .iter() .any(|m| !matches!(m, KeyCode::LShift | KeyCode::RShift | KeyCode::RAlt)) { - bail_expr!(&mapping_pair[1], "Only S- and AG-/RA- are supported."); + bail_expr!(&mapping_pair[1], "Only S- and AG- are supported."); } let output_osc = str_to_oscode(output_key) .ok_or_else(|| anyhow_expr!(&mapping_pair[1], "unknown key name"))?; - match output_mods.len() { - 0 => { - todo!("raw osc") - } - 1 => { - todo!("check whether shift or altgr") - } - 2 => { - todo!("both shift and altgr") - } + let output = match output_mods.len() { + 0 => ZchOutput::Lowercase(output_osc), + 1 => match output_mods[0] { + KeyCode::LShift | KeyCode::RShift => ZchOutput::Uppercase(output_osc), + KeyCode::RAlt => ZchOutput::AltGr(output_osc), + _ => unreachable!("forbidden by earlier parsing"), + }, + 2 => ZchOutput::ShiftAltGr(output_osc), _ => { - unreachable!("contains max of: altgr and one of the shifts") + unreachable!("contains at most: altgr and one of the shifts") } + }; + if user_cfg_char_to_output.insert(input_char, output).is_some() { + bail_expr!(&mapping_pair[0], "Duplicate character, not allowed"); } } @@ -355,6 +359,11 @@ fn parse_zippy_inner( output .chars() .try_fold(vec![], |mut zch_output, out_char| -> Result<_> { + if let Some(out) = user_cfg_char_to_output.get(&out_char) { + zch_output.push(*out); + return Ok(zch_output); + } + let out_key = out_char.to_lowercase().next().unwrap(); let key_name = out_key.encode_utf8(&mut char_buf); let osc = match key_name as &str { diff --git a/src/kanata/output_logic/zippychord.rs b/src/kanata/output_logic/zippychord.rs index 6cc37535e..43bef8f7e 100644 --- a/src/kanata/output_logic/zippychord.rs +++ b/src/kanata/output_logic/zippychord.rs @@ -329,36 +329,28 @@ impl ZchState { for key_to_send in &a.zch_output { match key_to_send { ZchOutput::Lowercase(osc) => { - if self.zchd.zchd_input_keys.zchik_contains(*osc) { - kb.release_key(*osc)?; - kb.press_key(*osc)?; - } else { - kb.press_key(*osc)?; - kb.release_key(*osc)?; - } + type_osc(*osc, kb, &self.zchd)?; } ZchOutput::Uppercase(osc) => { - if !self.zchd.zchd_is_caps_word_active - && (released_lsft - || !self.zchd.zchd_is_lsft_active - && !self.zchd.zchd_is_rsft_active) - { - kb.press_key(OsCode::KEY_LEFTSHIFT)?; - } - if self.zchd.zchd_input_keys.zchik_contains(*osc) { - kb.release_key(*osc)?; - kb.press_key(*osc)?; - } else { - kb.press_key(*osc)?; - kb.release_key(*osc)?; - } - if !self.zchd.zchd_is_caps_word_active - && (released_lsft - || !self.zchd.zchd_is_lsft_active - && !self.zchd.zchd_is_rsft_active) - { - kb.release_key(OsCode::KEY_LEFTSHIFT)?; - } + maybe_press_sft_during_activation(released_lsft, kb, &self.zchd)?; + type_osc(*osc, kb, &self.zchd)?; + maybe_release_sft_during_activation(released_lsft, kb, &self.zchd)?; + } + ZchOutput::AltGr(osc) => { + // Note, unlike shift which probably has a good reason to be maybe + // already held during chording, I don't currently see ralt as having + // any reason to already be held during chording; just use normal + // characters. + kb.press_key(OsCode::KEY_RIGHTALT)?; + type_osc(*osc, kb, &self.zchd)?; + kb.release_key(OsCode::KEY_RIGHTALT)?; + } + ZchOutput::ShiftAltGr(osc) => { + kb.press_key(OsCode::KEY_RIGHTALT)?; + maybe_press_sft_during_activation(released_lsft, kb, &self.zchd)?; + type_osc(*osc, kb, &self.zchd)?; + maybe_release_sft_during_activation(released_lsft, kb, &self.zchd)?; + kb.release_key(OsCode::KEY_RIGHTALT)?; } } self.zchd.zchd_characters_to_delete_on_next_activation += 1; @@ -479,3 +471,40 @@ fn osc_triggers_quick_enable(osc: OsCode) -> bool { // | OsCode::KEY_PAGEUP // | OsCode::KEY_PAGEDOWN } + +fn type_osc(osc: OsCode, kb: &mut KbdOut, zchd: &ZchDynamicState) -> Result<(), std::io::Error> { + if zchd.zchd_input_keys.zchik_contains(osc) { + kb.release_key(osc)?; + kb.press_key(osc)?; + } else { + kb.press_key(osc)?; + kb.release_key(osc)?; + } + Ok(()) +} + +fn maybe_press_sft_during_activation( + sft_already_released: bool, + kb: &mut KbdOut, + zchd: &ZchDynamicState, +) -> Result<(), std::io::Error> { + if !zchd.zchd_is_caps_word_active + && (sft_already_released || !zchd.zchd_is_lsft_active && !zchd.zchd_is_rsft_active) + { + kb.press_key(OsCode::KEY_LEFTSHIFT)?; + } + Ok(()) +} + +fn maybe_release_sft_during_activation( + sft_already_released: bool, + kb: &mut KbdOut, + zchd: &ZchDynamicState, +) -> Result<(), std::io::Error> { + if !zchd.zchd_is_caps_word_active + && (sft_already_released || !zchd.zchd_is_lsft_active && !zchd.zchd_is_rsft_active) + { + kb.release_key(OsCode::KEY_LEFTSHIFT)?; + } + Ok(()) +} From 3cea3c270a8984f9d3bfb50765af88934c9aa9c7 Mon Sep 17 00:00:00 2001 From: jtroo Date: Sat, 26 Oct 2024 17:20:13 -0700 Subject: [PATCH 31/54] output mapping --- parser/src/cfg/zippychord.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parser/src/cfg/zippychord.rs b/parser/src/cfg/zippychord.rs index 4eb443049..b7014a9a0 100644 --- a/parser/src/cfg/zippychord.rs +++ b/parser/src/cfg/zippychord.rs @@ -203,7 +203,7 @@ fn parse_zippy_inner( let mut config = ZchConfig::default(); - const KEY_NAME_MAPPINGS: &str = "key-name-mappings"; + const KEY_NAME_MAPPINGS: &str = "output-character-mappings"; const IDLE_REACTIVATE_TIME: &str = "idle-reactivate-time"; const CHORD_DEADLINE: &str = "on-first-press-chord-deadline"; From 8e35e31e004c36ee19a461e5d2a14487267b92f2 Mon Sep 17 00:00:00 2001 From: jtroo Date: Sat, 26 Oct 2024 20:56:18 -0700 Subject: [PATCH 32/54] disable quick enable, add washington test --- src/kanata/output_logic/zippychord.rs | 7 +++--- src/tests/sim_tests/zippychord_sim_tests.rs | 25 +++++++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/kanata/output_logic/zippychord.rs b/src/kanata/output_logic/zippychord.rs index 43bef8f7e..13a0fef23 100644 --- a/src/kanata/output_logic/zippychord.rs +++ b/src/kanata/output_logic/zippychord.rs @@ -442,9 +442,10 @@ impl ZchState { } } -/// Currently only returns true if the key is space. -fn osc_triggers_quick_enable(osc: OsCode) -> bool { - matches!(osc, OsCode::KEY_SPACE) +/// Maybe not a good idea, TODO: delete? +fn osc_triggers_quick_enable(_osc: OsCode) -> bool { + false + // matches!(osc, OsCode::KEY_SPACE) // Old implementation. // ~~Returns true if punctuation or whitespace. Also backspace, delete, arrow keys.~~ // OsCode::KEY_BACKSPACE diff --git a/src/tests/sim_tests/zippychord_sim_tests.rs b/src/tests/sim_tests/zippychord_sim_tests.rs index f9e32f8e3..fbcc9c896 100644 --- a/src/tests/sim_tests/zippychord_sim_tests.rs +++ b/src/tests/sim_tests/zippychord_sim_tests.rs @@ -72,6 +72,31 @@ fn sim_zippychord_followup_no_prev() { ); } +#[test] +fn sim_zippychord_washington() { + let result = simulate_with_file_content( + ZIPPY_CFG, + "d:spc u:spc t:10 + d:spc u:spc t:10 + d:spc u:spc t:10 + d:w d:spc t:10 + u:w u:spc t:10 + d:a d:spc t:10 + u:a u:spc t:300", + Some(ZIPPY_FILE_CONTENT), + ) + .to_ascii(); + assert_eq!( + "dn:Space t:1ms up:Space t:9ms dn:Space t:1ms up:Space t:9ms dn:Space t:1ms up:Space \ + t:9ms dn:W t:1ms dn:Space t:9ms up:W t:1ms up:Space t:9ms \ + dn:A t:1ms dn:BSpace up:BSpace dn:BSpace up:BSpace dn:BSpace up:BSpace \ + dn:LShift dn:W up:W up:LShift \ + up:A dn:A dn:S up:S dn:H up:H dn:I up:I dn:N up:N dn:G up:G dn:T up:T dn:O up:O dn:N up:N \ + t:9ms up:A t:1ms up:Space", + result + ); +} + #[test] fn sim_zippychord_overlap() { let result = simulate_with_file_content( From 860a4ba29e452c1afa19248341d46f219880878e Mon Sep 17 00:00:00 2001 From: jtroo Date: Sat, 26 Oct 2024 20:59:05 -0700 Subject: [PATCH 33/54] remove dead code --- src/kanata/output_logic/zippychord.rs | 56 --------------------------- 1 file changed, 56 deletions(-) diff --git a/src/kanata/output_logic/zippychord.rs b/src/kanata/output_logic/zippychord.rs index 13a0fef23..d826f8f70 100644 --- a/src/kanata/output_logic/zippychord.rs +++ b/src/kanata/output_logic/zippychord.rs @@ -40,7 +40,6 @@ enum ZchEnabledState { enum ZchLastPressClassification { #[default] IsChord, - IsQuickEnable, NotChord, } @@ -96,13 +95,6 @@ struct ZchDynamicState { } impl ZchDynamicState { - fn zchd_is_disabled(&self) -> bool { - matches!( - self.zchd_enabled_state, - ZchEnabledState::Disabled | ZchEnabledState::WaitEnable - ) - } - fn zchd_tick(&mut self, is_caps_word_active: bool) { const TICKS_UNTIL_FORCE_STATE_RESET: u16 = 10000; self.zchd_ticks_since_state_change += 1; @@ -208,11 +200,6 @@ impl ZchDynamicState { log::debug!("some released->zippy enabled"); self.zchd_ticks_until_disable = 0; } - (ZchLastPressClassification::IsQuickEnable, _) => { - log::debug!("quick enable release->clear characters"); - self.zchd_previous_activation_output_count = 0; - self.zchd_characters_to_delete_on_next_activation = 0; - } } } } @@ -256,18 +243,6 @@ impl ZchState { if self.zch_chords.is_empty() || osc.is_modifier() { return kb.press_key(osc); } - if osc_triggers_quick_enable(osc) { - if self.zchd.zchd_is_disabled() { - log::debug!("zippy quick enable"); - // Motivation: if a key is pressed that can potentially be followed by a brand new - // word, quickly re-enable zippychording so user doesn't have to wait for the - // "not-regular-typing-anymore" timeout. - self.zchd.zchd_soft_reset(); - return kb.press_key(osc); - } else { - self.zchd.zchd_last_press = ZchLastPressClassification::IsQuickEnable; - } - } // Zippychording is enabled. Ensure the deadline to disable it if no chord activates is // active. @@ -442,37 +417,6 @@ impl ZchState { } } -/// Maybe not a good idea, TODO: delete? -fn osc_triggers_quick_enable(_osc: OsCode) -> bool { - false - // matches!(osc, OsCode::KEY_SPACE) - // Old implementation. - // ~~Returns true if punctuation or whitespace. Also backspace, delete, arrow keys.~~ - // OsCode::KEY_BACKSPACE - // | OsCode::KEY_DELETE - // | OsCode::KEY_ENTER - // | OsCode::KEY_SPACE - // | OsCode::KEY_TAB - // | OsCode::KEY_COMMA - // | OsCode::KEY_DOT - // | OsCode::KEY_SEMICOLON - // | OsCode::KEY_APOSTROPHE - // | OsCode::KEY_SLASH - // | OsCode::KEY_BACKSLASH - // | OsCode::KEY_GRAVE - // | OsCode::KEY_MINUS - // | OsCode::KEY_LEFTBRACE - // | OsCode::KEY_RIGHTBRACE - // | OsCode::KEY_UP - // | OsCode::KEY_DOWN - // | OsCode::KEY_LEFT - // | OsCode::KEY_RIGHT - // | OsCode::KEY_HOME - // | OsCode::KEY_END - // | OsCode::KEY_PAGEUP - // | OsCode::KEY_PAGEDOWN -} - fn type_osc(osc: OsCode, kb: &mut KbdOut, zchd: &ZchDynamicState) -> Result<(), std::io::Error> { if zchd.zchd_input_keys.zchik_contains(osc) { kb.release_key(osc)?; From fe398698db97413c16107ac48b9edc85ae63926a Mon Sep 17 00:00:00 2001 From: jtroo Date: Sat, 26 Oct 2024 22:33:54 -0700 Subject: [PATCH 34/54] add workaround for interception driver weirdness --- src/kanata/output_logic/zippychord.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/kanata/output_logic/zippychord.rs b/src/kanata/output_logic/zippychord.rs index d826f8f70..2db45f5e8 100644 --- a/src/kanata/output_logic/zippychord.rs +++ b/src/kanata/output_logic/zippychord.rs @@ -301,7 +301,21 @@ impl ZchState { } self.zchd.zchd_prioritized_chords = a.zch_followups.clone(); let mut released_lsft = false; + + #[cfg(feature = "interception_driver")] + let mut send_count = 0; + for key_to_send in &a.zch_output { + #[cfg(feature = "interception_driver")] + { + send_count += 1; + if send_count % 5 == 0 { + std::thread::sleep(std::time::Duration::from_millis(1)); + } + } + // Note: every 5 keys on Windows Interception, do a sleep because + // sending too quickly apparently causes weird behaviour... + // I guess there's some buffer in the Interception code that is filling up. match key_to_send { ZchOutput::Lowercase(osc) => { type_osc(*osc, kb, &self.zchd)?; From 5209ef10ebf38bde73bd8c80d53091d1b7e80c6d Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 27 Oct 2024 01:48:19 -0700 Subject: [PATCH 35/54] add basic kanata.kbd example --- cfg_samples/kanata.kbd | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/cfg_samples/kanata.kbd b/cfg_samples/kanata.kbd index 55a93c013..22de78047 100644 --- a/cfg_samples/kanata.kbd +++ b/cfg_samples/kanata.kbd @@ -1391,3 +1391,39 @@ Syntax (5-tuples): (a b z) (macro h e l l o) 250 first-release (arrows) (a b z y) (macro b y e) 400 first-release (arrows) ) + +#| + +Yet another chording implementation - zippychord: + +(defzippy-experimental + zippy.txt + on-first-press-chord-deadline 500 + idle-reactivate-time 500 + output-character-mappings ( + % S-5 + "(" S-9 + ")" S-0 + : S-; + < S-, + > S-. + r#"""# S-' + | S-\ + _ S-- + ) +) + +Example file content of zippy.txt: +--- +dy day +dy 1 Monday + abc Alphabet +r df recipient + w a Washington +rq request +rqa request␣assistance +--- + +You can read in more detail in the configuration guide. + +|# From 42b9c2764cf06c1b5734fddf56d6d728909a038e Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 27 Oct 2024 02:09:40 -0700 Subject: [PATCH 36/54] fix disable bug --- src/kanata/output_logic/zippychord.rs | 7 ++++- src/tests.rs | 2 +- src/tests/sim_tests/zippychord_sim_tests.rs | 30 ++++++++++++++------- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/kanata/output_logic/zippychord.rs b/src/kanata/output_logic/zippychord.rs index 2db45f5e8..d7ba3a0fa 100644 --- a/src/kanata/output_logic/zippychord.rs +++ b/src/kanata/output_logic/zippychord.rs @@ -105,6 +105,7 @@ impl ZchDynamicState { if self.zchd_ticks_until_enabled == 0 { log::debug!("zippy wait enable->enable"); self.zchd_enabled_state = ZchEnabledState::Enabled; + self.zchd_ticks_until_disable = 0; } } ZchEnabledState::Enabled => { @@ -195,6 +196,7 @@ impl ZchDynamicState { } self.zchd_characters_to_delete_on_next_activation = 0; self.zchd_ticks_until_disable = 0; + self.zchd_enabled_state = ZchEnabledState::Enabled; } (ZchLastPressClassification::IsChord, false) => { log::debug!("some released->zippy enabled"); @@ -240,7 +242,10 @@ impl ZchState { _ => {} } - if self.zch_chords.is_empty() || osc.is_modifier() { + if self.zch_chords.is_empty() + || osc.is_modifier() + || self.zchd.zchd_enabled_state != ZchEnabledState::Enabled + { return kb.press_key(osc); } diff --git a/src/tests.rs b/src/tests.rs index 08433a9c1..8b8f3c052 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -24,7 +24,7 @@ fn init_log() { CombinedLogger::init(vec![TermLogger::new( // Note: set to a different level to see logs in tests. // Also, not all tests call init_log so you might have to add the call there too. - LevelFilter::Off, + LevelFilter::Debug, log_cfg.build(), TerminalMode::Stderr, ColorChoice::AlwaysAnsi, diff --git a/src/tests/sim_tests/zippychord_sim_tests.rs b/src/tests/sim_tests/zippychord_sim_tests.rs index fbcc9c896..9d771b8f2 100644 --- a/src/tests/sim_tests/zippychord_sim_tests.rs +++ b/src/tests/sim_tests/zippychord_sim_tests.rs @@ -76,10 +76,7 @@ fn sim_zippychord_followup_no_prev() { fn sim_zippychord_washington() { let result = simulate_with_file_content( ZIPPY_CFG, - "d:spc u:spc t:10 - d:spc u:spc t:10 - d:spc u:spc t:10 - d:w d:spc t:10 + "d:w d:spc t:10 u:w u:spc t:10 d:a d:spc t:10 u:a u:spc t:300", @@ -87,8 +84,7 @@ fn sim_zippychord_washington() { ) .to_ascii(); assert_eq!( - "dn:Space t:1ms up:Space t:9ms dn:Space t:1ms up:Space t:9ms dn:Space t:1ms up:Space \ - t:9ms dn:W t:1ms dn:Space t:9ms up:W t:1ms up:Space t:9ms \ + "dn:W t:1ms dn:Space t:9ms up:W t:1ms up:Space t:9ms \ dn:A t:1ms dn:BSpace up:BSpace dn:BSpace up:BSpace dn:BSpace up:BSpace \ dn:LShift dn:W up:W up:LShift \ up:A dn:A dn:S up:S dn:H up:H dn:I up:I dn:N up:N dn:G up:G dn:T up:T dn:O up:O dn:N up:N \ @@ -219,19 +215,19 @@ fn sim_zippychord_rsft() { fn sim_zippychord_caps_word() { let result = simulate_with_file_content( ZIPPY_CFG, - "d:lalt u:lalt t:10 d:d t:10 d:y t:10 u:d u:y t:10 d:spc u:spc t:10 d:d d:y t:10", + "d:lalt u:lalt t:10 d:d t:10 d:y t:10 u:d u:y t:10 d:spc u:spc t:2000 d:d d:y t:10", Some(ZIPPY_FILE_CONTENT), ) .to_ascii(); assert_eq!( "t:10ms dn:LShift dn:D t:10ms dn:BSpace up:BSpace up:D dn:D dn:A up:A up:Y dn:Y \ t:10ms up:D t:1ms up:LShift up:Y t:9ms dn:Space t:1ms up:Space \ - t:9ms dn:D t:1ms dn:BSpace up:BSpace up:D dn:D dn:A up:A up:Y dn:Y", + t:1999ms dn:D t:1ms dn:BSpace up:BSpace up:D dn:D dn:A up:A up:Y dn:Y", result ); let result = simulate_with_file_content( ZIPPY_CFG, - "d:lalt t:10 d:y t:10 d:x t:10 u:x u:y t:10 d:spc u:spc t:10 d:y d:x t:10", + "d:lalt t:10 d:y t:10 d:x t:10 u:x u:y t:10 d:spc u:spc t:1000 d:y d:x t:10", Some(ZIPPY_FILE_CONTENT), ) .to_ascii(); @@ -239,7 +235,7 @@ fn sim_zippychord_caps_word() { "t:10ms dn:LShift dn:Y t:10ms dn:BSpace up:BSpace \ dn:W up:W up:X dn:X up:Y dn:Y dn:Z up:Z \ t:10ms up:X t:1ms up:LShift up:Y t:9ms dn:Space t:1ms up:Space \ - t:9ms dn:Y t:1ms dn:BSpace up:BSpace dn:LShift dn:W up:W up:LShift \ + t:999ms dn:Y t:1ms dn:BSpace up:BSpace dn:LShift dn:W up:W up:LShift \ up:X dn:X dn:LShift up:Y dn:Y up:LShift dn:Z up:Z", result ); @@ -263,3 +259,17 @@ fn sim_zippychord_triple_combo() { result ); } + +#[test] +fn sim_zippychord_disabled_by_typing() { + let result = simulate_with_file_content( + ZIPPY_CFG, + "d:v u:v t:10 d:d d:y t:100", + Some(ZIPPY_FILE_CONTENT), + ) + .to_ascii(); + assert_eq!( + "dn:V t:1ms up:V t:9ms dn:D t:1ms dn:Y", + result + ); +} From c790b81fc5f8ac089b2dfab92f50f8ec3d8da8eb Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 27 Oct 2024 17:32:00 -0700 Subject: [PATCH 37/54] config doc --- docs/config.adoc | 98 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 94 insertions(+), 4 deletions(-) diff --git a/docs/config.adoc b/docs/config.adoc index 73596725c..eb38786c7 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -3980,6 +3980,62 @@ Using unicode symbols `🕐`,`↓`,`↑`,`⟳` allows skipping the `:` separator === Zippychord <> +==== Reference + +You may define a single `+defzippy-experimental+` configuration item. +As suggested by the -experimental label, this feature is relatively recent. +When using this you are at higher likelihood of bugs and breaking changes. + +===== Configuration syntax within the kanata configuration + +[source] +---- +(defzippy-experimental + $filename ;; required + on-first-press-chord-deadline $deadline-millis ;; optional + idle-reactivate-time $idle-time-millis ;; optional + output-character-mappings ( ;; optional + $character1 $output-mapping1 + $character2 $output-mapping2 + ... + $characterN $output-mappingN + ) +) +---- + +[cols="1,2"] +|=== +| `$filename` +| Relative or absolute file path. +If relative, its path is relative to the kanata configuration file's location. + +| `$deadline-millis` for `on-first-press-chord-deadline` +| Number of milliseconds after a press, while zippy is enabled, +to complete a chord; after which if no chord activates, zippy +is temporarily disabled. + +| `$idle-time-millis` for `idle-reactivate-time` +| Number of milliseconds after typing ends that zippy will be re-enabled +from being temporarily disabled. + +| `$characterN` for `output-character-mappings` +| A single unicode character that can be used in the output column +of the zippy configuration file. + +| `$output-mappingN` for `output-character-mappings` +| Key or output chord to tell kanata how to type `$characterN` +when seen in the zippy file output column. +Must be a single key or output chord. +The output chord may contain `AG-` to tell kanata to press with AltGr +and may contain `S-` to tell kanata to press with Shift. +|=== + +===== Configuration syntax within the zippy configuration file + + + +==== Guide + Zippychord is yet another chording mechanism in Kanata. The inspiration behind it is primarily the https://github.com/psoukie/zipchord[zipchord project]. @@ -3987,13 +4043,19 @@ The name is similar; it is named "zippy" instead of "zip" because Kanata's implementation is not a port and does not aim for 100% behavioural compatibility. -The intended use case is accelerating and reducing effort of typing. -The inputs are keycodes and the outputs are also purely keycodes. +The intended use case is shorthands, or accelerating character output. +Within zippychord, inputs are keycode chords or sequences, +and the outputs are also purely keycodes. In other words, all other actions are unsupported; e.g. layers, switch, one-shot. -Unlike chords(v1) and chordsv2, zippychord behaves on output keycodes. -This is similar to how sequences operate. +Zippychord behaves on outputted keycodes, i.e. the key outputs +after kanata has finished processing your +inputs, layers, switch logic and other configurations. +This is similar to how sequences operate +and is unlike chords(v1) and chordsv2. +Furthermore, outputs are all eager like `visible-backspaced` on sequences. +If a zippchord activation occurs, typed keys are backspaced. To give an example, if one configures zippychord with a line like: @@ -4012,3 +4074,31 @@ like if it was `(macro bspc bspc g i t)`. (press g) (press i) (press i) (press g) ---- + +Note that there aren't any release events listed. +To contrast, the following event sequence would not result in an activation: + +[source] +---- +(press g) (release g) (press i) +---- + +Zippychord supports fully overlapping chords and sequences. +For example, this configuration is allowed: + +[source] +---- +gi git␣ +gi s git␣status +gi c git checkout␣ +gi c b git checkout -b␣ +gi c a git commit --amend␣ +gi c n git commit --amend --no-edit +gi c a m git commit --amend -m 'FIX_THIS_COMMIT_MESSAGE' +---- + +When you begin with the `(g i)` chord, you can follow up +with various character sequences to output different git commands. +This use case is quite similar to git aliases. +One advantage of zippychord is eagerly showing you +the true command you are running as you type. From ca37d9552302de014ab9630c0a70e33e387c0bba Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 27 Oct 2024 19:01:27 -0700 Subject: [PATCH 38/54] fix and docs --- cfg_samples/kanata.kbd | 1 + docs/config.adoc | 96 ++++++++++++++++++--- src/kanata/output_logic/zippychord.rs | 2 +- src/tests.rs | 2 +- src/tests/sim_tests/zippychord_sim_tests.rs | 5 +- 5 files changed, 86 insertions(+), 20 deletions(-) diff --git a/cfg_samples/kanata.kbd b/cfg_samples/kanata.kbd index 22de78047..d74674132 100644 --- a/cfg_samples/kanata.kbd +++ b/cfg_samples/kanata.kbd @@ -1410,6 +1410,7 @@ Yet another chording implementation - zippychord: r#"""# S-' | S-\ _ S-- + ® AG-r ) ) diff --git a/docs/config.adoc b/docs/config.adoc index eb38786c7..a9099f11e 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -3991,13 +3991,13 @@ When using this you are at higher likelihood of bugs and breaking changes. [source] ---- (defzippy-experimental - $filename ;; required + $zippy-filename ;; required on-first-press-chord-deadline $deadline-millis ;; optional idle-reactivate-time $idle-time-millis ;; optional output-character-mappings ( ;; optional $character1 $output-mapping1 $character2 $output-mapping2 - ... + ;; ... $characterN $output-mappingN ) ) @@ -4005,22 +4005,25 @@ When using this you are at higher likelihood of bugs and breaking changes. [cols="1,2"] |=== -| `$filename` +| `$zippy-filename` | Relative or absolute file path. -If relative, its path is relative to the kanata configuration file's location. +If relative, its path is relative to +the directory containing the kanata configuration file. | `$deadline-millis` for `on-first-press-chord-deadline` -| Number of milliseconds after a press, while zippy is enabled, -to complete a chord; after which if no chord activates, zippy -is temporarily disabled. +| Number of milliseconds. +After the first press while zippy is enabled, +if no chord activates within this configured time, +zippy is temporarily disabled. | `$idle-time-millis` for `idle-reactivate-time` -| Number of milliseconds after typing ends that zippy will be re-enabled -from being temporarily disabled. +| Number of milliseconds. +After typing ends and this configured number of milliseconds elapses, +zippy will be re-enabled from being temporarily disabled. | `$characterN` for `output-character-mappings` -| A single unicode character that can be used in the output column -of the zippy configuration file. +| A single unicode character for use +in the output column of the zippy configuration file. | `$output-mappingN` for `output-character-mappings` | Key or output chord to tell kanata how to type `$characterN` @@ -4030,11 +4033,76 @@ The output chord may contain `AG-` to tell kanata to press with AltGr and may contain `S-` to tell kanata to press with Shift. |=== +Regarding output mappings, +you can configure the output of the special-lisp-syntax characters +`+) ( "+` via these lines: + +[source] +---- + ")" $right-paren-output + "(" $left-paren-output + r#"""# $double-quote-output +---- + +As an example, for the US layout these should be the correct lines: + +[source] +---- + ")" S-0 + "(" S-9 + r#"""# S-' +---- + ===== Configuration syntax within the zippy configuration file +[source] +---- +// This is a comment. +// inputs ↹ outputs +$chord1 $follow-chord1.1...1.M $output1 +$chord2 $follow-chord2.1...2.M $output2 +;; ... +$chordN $follow-chordN.1...N.M $outputN +---- +The format is two columns separated by a single Tab character. +The first column is input and the second is output. + +[cols="1,2"] +|=== +|`$chordN` +| A set of characters. +The order is not important; this represents all input keys being +pressed simultaneously according to kanata. +You can use space by including it as the first character in the chord. +For example, ` ap`. +When there are 0 optional follow chords, +the corresponding output on the same line `$outputN` will activate. + +| `$chord-followN.M` +| 0 or more chords, used the same way as `$chordN`. +Having follow chords means the `$outputN` on the same line +will activate upon first activating the earlier chord(s) in the same line, +releasing all keys, and pressing the keys in `$chord-followN.M`. +Follow chords are separated from the previous chord by a space. +If using a space in the follow chord, use two spaces; e.g. ` ap bc`. + +| `$outputN` +| The characters to type when the chord and optional follow chord(s) +are all pressed by the user. +This is separated from the input `$chordN $follow-chordN.1...N.M` column +by a single Tab character. +The characters are typed in sequence +and must all be singular-name key names +as one would configure in `defsrc`. +As an exception, capitalized single-character key names +will be parsed successfully +and these will be outputted alongside "shift" to output the capital key. +Additionally, `output-character-mappings` configuration can be used +to inform kanata of additional mappings that may use Shift or AltGr. +|=== -==== Guide +==== Description Zippychord is yet another chording mechanism in Kanata. The inspiration behind it is primarily the @@ -4100,5 +4168,5 @@ gi c a m git commit --amend -m 'FIX_THIS_COMMIT_MESSAGE' When you begin with the `(g i)` chord, you can follow up with various character sequences to output different git commands. This use case is quite similar to git aliases. -One advantage of zippychord is eagerly showing you -the true command you are running as you type. +One advantage of zippychord is that it eagerly shows you +the true underlying command as you type. diff --git a/src/kanata/output_logic/zippychord.rs b/src/kanata/output_logic/zippychord.rs index d7ba3a0fa..5641005dc 100644 --- a/src/kanata/output_logic/zippychord.rs +++ b/src/kanata/output_logic/zippychord.rs @@ -393,8 +393,8 @@ impl ZchState { } Neither => { - self.zchd.zchd_last_press = ZchLastPressClassification::NotChord; self.zchd.zchd_soft_reset(); + self.zchd.zchd_last_press = ZchLastPressClassification::NotChord; self.zchd.zchd_enabled_state = ZchEnabledState::Disabled; kb.press_key(osc) } diff --git a/src/tests.rs b/src/tests.rs index 8b8f3c052..08433a9c1 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -24,7 +24,7 @@ fn init_log() { CombinedLogger::init(vec![TermLogger::new( // Note: set to a different level to see logs in tests. // Also, not all tests call init_log so you might have to add the call there too. - LevelFilter::Debug, + LevelFilter::Off, log_cfg.build(), TerminalMode::Stderr, ColorChoice::AlwaysAnsi, diff --git a/src/tests/sim_tests/zippychord_sim_tests.rs b/src/tests/sim_tests/zippychord_sim_tests.rs index 9d771b8f2..281521a45 100644 --- a/src/tests/sim_tests/zippychord_sim_tests.rs +++ b/src/tests/sim_tests/zippychord_sim_tests.rs @@ -268,8 +268,5 @@ fn sim_zippychord_disabled_by_typing() { Some(ZIPPY_FILE_CONTENT), ) .to_ascii(); - assert_eq!( - "dn:V t:1ms up:V t:9ms dn:D t:1ms dn:Y", - result - ); + assert_eq!("dn:V t:1ms up:V t:9ms dn:D t:1ms dn:Y", result); } From b6ad6794614211e5f4dd465a7ce7c24065213e50 Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 27 Oct 2024 23:00:59 -0700 Subject: [PATCH 39/54] update config.adoc --- docs/config.adoc | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/config.adoc b/docs/config.adoc index a9099f11e..c03c9fa71 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -4009,6 +4009,7 @@ When using this you are at higher likelihood of bugs and breaking changes. | Relative or absolute file path. If relative, its path is relative to the directory containing the kanata configuration file. +This must be the first item following `defzippy-experimental`. | `$deadline-millis` for `on-first-press-chord-deadline` | Number of milliseconds. @@ -4061,7 +4062,7 @@ As an example, for the US layout these should be the correct lines: // inputs ↹ outputs $chord1 $follow-chord1.1...1.M $output1 $chord2 $follow-chord2.1...2.M $output2 -;; ... +// ... $chordN $follow-chordN.1...N.M $outputN ---- @@ -4072,12 +4073,13 @@ The first column is input and the second is output. |=== |`$chordN` | A set of characters. -The order is not important; this represents all input keys being -pressed simultaneously according to kanata. You can use space by including it as the first character in the chord. -For example, ` ap`. -When there are 0 optional follow chords, -the corresponding output on the same line `$outputN` will activate. +For example, `+ ap+`. +With 0 optional follow chords, +the corresponding output on the same line (`$outputN`) +will activate when zippy is enabled +and all the defined chord keys are pressed simultaneously. +The order of presses is not important. | `$chord-followN.M` | 0 or more chords, used the same way as `$chordN`. @@ -4085,7 +4087,7 @@ Having follow chords means the `$outputN` on the same line will activate upon first activating the earlier chord(s) in the same line, releasing all keys, and pressing the keys in `$chord-followN.M`. Follow chords are separated from the previous chord by a space. -If using a space in the follow chord, use two spaces; e.g. ` ap bc`. +If using a space in the follow chord, use two spaces; e.g. `+ ab cd+`. | `$outputN` | The characters to type when the chord and optional follow chord(s) @@ -4097,7 +4099,7 @@ and must all be singular-name key names as one would configure in `defsrc`. As an exception, capitalized single-character key names will be parsed successfully -and these will be outputted alongside "shift" to output the capital key. +and these will be outputted alongside Shift to output the capital key. Additionally, `output-character-mappings` configuration can be used to inform kanata of additional mappings that may use Shift or AltGr. |=== From f24556b0df890b7c16a2205b3310cb8de7b8422b Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 27 Oct 2024 23:07:23 -0700 Subject: [PATCH 40/54] fix formatting --- docs/config.adoc | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/docs/config.adoc b/docs/config.adoc index c03c9fa71..88db7c687 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -4073,8 +4073,8 @@ The first column is input and the second is output. |=== |`$chordN` | A set of characters. -You can use space by including it as the first character in the chord. -For example, `+ ap+`. +You can use space by including it as the first character in the chord; +see `Alphabet` in the sample zippy file below. With 0 optional follow chords, the corresponding output on the same line (`$outputN`) will activate when zippy is enabled @@ -4087,7 +4087,8 @@ Having follow chords means the `$outputN` on the same line will activate upon first activating the earlier chord(s) in the same line, releasing all keys, and pressing the keys in `$chord-followN.M`. Follow chords are separated from the previous chord by a space. -If using a space in the follow chord, use two spaces; e.g. `+ ab cd+`. +If using a space in the follow chord, use two spaces; +see `Washington` as an example in the sample configuration. | `$outputN` | The characters to type when the chord and optional follow chord(s) @@ -4104,6 +4105,19 @@ Additionally, `output-character-mappings` configuration can be used to inform kanata of additional mappings that may use Shift or AltGr. |=== +==== Sample zippy file content + +[source] +---- +dy day +dy 1 Monday +dy 2 Tuesday + abc alphabet + w a Washington +gi git +gi f p git fetch -p +---- + ==== Description Zippychord is yet another chording mechanism in Kanata. From 310666b4373ce281b3fbcee3ad2b992435a475fd Mon Sep 17 00:00:00 2001 From: jtroo Date: Sun, 27 Oct 2024 23:08:31 -0700 Subject: [PATCH 41/54] adjust wording --- docs/config.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/config.adoc b/docs/config.adoc index 88db7c687..a9da4f3de 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -4074,7 +4074,7 @@ The first column is input and the second is output. |`$chordN` | A set of characters. You can use space by including it as the first character in the chord; -see `Alphabet` in the sample zippy file below. +for an example see `Alphabet` in the sample below. With 0 optional follow chords, the corresponding output on the same line (`$outputN`) will activate when zippy is enabled @@ -4088,7 +4088,7 @@ will activate upon first activating the earlier chord(s) in the same line, releasing all keys, and pressing the keys in `$chord-followN.M`. Follow chords are separated from the previous chord by a space. If using a space in the follow chord, use two spaces; -see `Washington` as an example in the sample configuration. +for an example see `Washington` in the sample below. | `$outputN` | The characters to type when the chord and optional follow chord(s) From 49503f51c625941a6da7e247e00bcc03cc597ce9 Mon Sep 17 00:00:00 2001 From: jtroo Date: Mon, 28 Oct 2024 00:20:44 -0700 Subject: [PATCH 42/54] smartspace wip --- parser/src/keys/mod.rs | 9 ++++++ src/kanata/output_logic/zippychord.rs | 43 +++++++++++++++++++++++---- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/parser/src/keys/mod.rs b/parser/src/keys/mod.rs index 2d2053a8a..59698c577 100644 --- a/parser/src/keys/mod.rs +++ b/parser/src/keys/mod.rs @@ -80,6 +80,15 @@ impl OsCode { | OsCode::KEY_RIGHTALT ) } + + pub fn is_punctuation(self) -> bool { + matches!( + self, + OsCode::KEY_DOT + | OsCode::KEY_SEMICOLON + | OsCode::KEY_COMMA + ) + } } static CUSTOM_STRS_TO_OSCODES: Lazy>> = Lazy::new(|| { diff --git a/src/kanata/output_logic/zippychord.rs b/src/kanata/output_logic/zippychord.rs index 5641005dc..cab745390 100644 --- a/src/kanata/output_logic/zippychord.rs +++ b/src/kanata/output_logic/zippychord.rs @@ -43,6 +43,13 @@ enum ZchLastPressClassification { NotChord, } +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +enum ZchSmartSpaceState { + #[default] + NoActivation, + Sent, +} + #[derive(Debug, Default)] struct ZchDynamicState { /// Input to compare against configured available chords to output. @@ -66,10 +73,10 @@ struct ZchDynamicState { zchd_prioritized_chords: Option>>, /// Tracks the previous output character count /// because it may need to be erased (see `zchd_prioritized_chords). - zchd_previous_activation_output_count: u16, + zchd_previous_activation_output_count: i16, /// In case of output being empty for interim chord activations, this tracks the number of /// characters that need to be erased. - zchd_characters_to_delete_on_next_activation: u16, + zchd_characters_to_delete_on_next_activation: i16, /// Tracker for time until previous state change to know if potential stale data should be /// cleared. This is a contingency in case of bugs or weirdness with OS interactions, e.g. /// Windows lock screen weirdness. @@ -92,6 +99,9 @@ struct ZchDynamicState { zchd_is_rsft_active: bool, /// Tracks whether last press was part of a chord or not. zchd_last_press: ZchLastPressClassification, + /// Tracks smart spacing state so punctuation can know whether a space needs to be erased or + /// not. + zchd_smart_space_state: ZchSmartSpaceState, } impl ZchDynamicState { @@ -162,6 +172,7 @@ impl ZchDynamicState { self.zchd_ticks_until_disable = 0; self.zchd_ticks_until_enabled = 0; self.zchd_characters_to_delete_on_next_activation = 0; + self.zchd_smart_space_state = ZchSmartSpaceState::NoActivation; } /// Returns true if dynamic zch state is such that idling optimization can activate. @@ -244,10 +255,16 @@ impl ZchState { if self.zch_chords.is_empty() || osc.is_modifier() - || self.zchd.zchd_enabled_state != ZchEnabledState::Enabled { return kb.press_key(osc); } + if osc.is_punctuation() && self.zchd.zchd_smart_space_state == ZchSmartSpaceState::Sent { + kb.press_key(OsCode::KEY_BACKSPACE)?; + kb.release_key(OsCode::KEY_BACKSPACE)?; + } + if self.zchd.zchd_enabled_state != ZchEnabledState::Enabled { + return kb.press_key(osc); + } // Zippychording is enabled. Ensure the deadline to disable it if no chord activates is // active. @@ -288,7 +305,7 @@ impl ZchState { if a.zch_output.is_empty() { self.zchd.zchd_characters_to_delete_on_next_activation += 1; self.zchd.zchd_previous_activation_output_count += - self.zchd.zchd_input_keys.zchik_keys().len() as u16; + self.zchd.zchd_input_keys.zchik_keys().len() as i16; kb.press_key(osc)?; } else { for _ in 0..(self.zchd.zchd_characters_to_delete_on_next_activation @@ -302,7 +319,7 @@ impl ZchState { kb.release_key(OsCode::KEY_BACKSPACE)?; } self.zchd.zchd_characters_to_delete_on_next_activation = 0; - self.zchd.zchd_previous_activation_output_count = a.zch_output.len() as u16; + self.zchd.zchd_previous_activation_output_count = a.zch_output.len() as i16; } self.zchd.zchd_prioritized_chords = a.zch_followups.clone(); let mut released_lsft = false; @@ -347,7 +364,19 @@ impl ZchState { kb.release_key(OsCode::KEY_RIGHTALT)?; } } - self.zchd.zchd_characters_to_delete_on_next_activation += 1; + + if osc == OsCode::KEY_BACKSPACE { + self.zchd.zchd_characters_to_delete_on_next_activation -= 1; + // Improvement: there are many other keycodes that might be sent that + // aren't printable. But for now, just include backspace. + // backspace might be fairly common to do something like: + // 1. stp -> staple + // 2. ing -> stapleing + // 3. ing ei -> stapling + } else { + self.zchd.zchd_characters_to_delete_on_next_activation += 1; + } + if !released_lsft && !self.zchd.zchd_is_caps_word_active { released_lsft = true; if self.zchd.zchd_is_lsft_active { @@ -359,6 +388,8 @@ impl ZchState { } } + // TODO: smart spacing + if !self.zchd.zchd_is_caps_word_active { if self.zchd.zchd_is_lsft_active { kb.press_key(OsCode::KEY_LEFTSHIFT)?; From 1443f6baef1e4c3ddf4a5f2f5aee54a94202cb52 Mon Sep 17 00:00:00 2001 From: jtroo Date: Mon, 28 Oct 2024 00:50:23 -0700 Subject: [PATCH 43/54] wip smart space --- parser/src/cfg/zippychord.rs | 49 +++++++++++++++++++++++++++ parser/src/keys/mod.rs | 4 +-- src/kanata/output_logic/zippychord.rs | 38 +++++++++++++++------ 3 files changed, 77 insertions(+), 14 deletions(-) diff --git a/parser/src/cfg/zippychord.rs b/parser/src/cfg/zippychord.rs index b7014a9a0..46a6d1b17 100644 --- a/parser/src/cfg/zippychord.rs +++ b/parser/src/cfg/zippychord.rs @@ -130,6 +130,27 @@ pub enum ZchOutput { ShiftAltGr(OsCode), } +impl ZchOutput { + pub fn osc(self) -> OsCode { + use ZchOutput::*; + match self { + Lowercase(osc) | Uppercase(osc) | AltGr(osc) | ShiftAltGr(osc) => osc, + } + } +} + +/// User configuration for smart space. +/// +/// - `Full` = add spaces after words, remove these spaces after typing punctuation. +/// - `AddSpaceOnly` = add spaces after words +/// - `Disabled` = do nothing +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ZchSmartSpaceCfg { + Full, + AddSpaceOnly, + Disabled, +} + #[derive(Debug)] pub struct ZchConfig { /// When, during typing, chord fails to activate, zippychord functionality becomes temporarily @@ -153,12 +174,17 @@ pub struct ZchConfig { /// zippychord. If, after the first press, a chord activates, this deadline will reset to /// enable further chord activations. pub zch_cfg_ticks_chord_deadline: u16, + + /// User configuration for smart space. See `pub enum ZchSmartSpaceCfg`. + pub zch_cfg_smart_space: ZchSmartSpaceCfg, } + impl Default for ZchConfig { fn default() -> Self { Self { zch_cfg_ticks_wait_enable: 500, zch_cfg_ticks_chord_deadline: 500, + zch_cfg_smart_space: ZchSmartSpaceCfg::Disabled, } } } @@ -206,10 +232,12 @@ fn parse_zippy_inner( const KEY_NAME_MAPPINGS: &str = "output-character-mappings"; const IDLE_REACTIVATE_TIME: &str = "idle-reactivate-time"; const CHORD_DEADLINE: &str = "on-first-press-chord-deadline"; + const SMART_SPACE: &str = "smart-space"; let mut idle_reactivate_time_seen = false; let mut key_name_mappings_seen = false; let mut chord_deadline_seen = false; + let mut smart_space_seen = false; let mut user_cfg_char_to_output: HashMap = HashMap::default(); // Parse other zippy configurations @@ -247,6 +275,27 @@ fn parse_zippy_inner( config.zch_cfg_ticks_chord_deadline = parse_u16(config_value, s, CHORD_DEADLINE)?; } + SMART_SPACE => { + if smart_space_seen { + bail_expr!( + config_name, + "This is the 2nd instance; it can only be defined once" + ); + } + smart_space_seen = true; + config.zch_cfg_smart_space = config_value + .atom(s.vars()) + .and_then(|val| match val { + "none" => Some(ZchSmartSpaceCfg::Disabled), + "full" => Some(ZchSmartSpaceCfg::Full), + "add-space-only" => Some(ZchSmartSpaceCfg::AddSpaceOnly), + _ => None, + }) + .ok_or_else(|| { + anyhow_expr!(&config_value, "Must be: none | full | add-space-only") + })?; + } + KEY_NAME_MAPPINGS => { if key_name_mappings_seen { bail_expr!( diff --git a/parser/src/keys/mod.rs b/parser/src/keys/mod.rs index 59698c577..9c8f11cf7 100644 --- a/parser/src/keys/mod.rs +++ b/parser/src/keys/mod.rs @@ -84,9 +84,7 @@ impl OsCode { pub fn is_punctuation(self) -> bool { matches!( self, - OsCode::KEY_DOT - | OsCode::KEY_SEMICOLON - | OsCode::KEY_COMMA + OsCode::KEY_DOT | OsCode::KEY_SEMICOLON | OsCode::KEY_COMMA ) } } diff --git a/src/kanata/output_logic/zippychord.rs b/src/kanata/output_logic/zippychord.rs index cab745390..922c60f2b 100644 --- a/src/kanata/output_logic/zippychord.rs +++ b/src/kanata/output_logic/zippychord.rs @@ -46,7 +46,7 @@ enum ZchLastPressClassification { #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] enum ZchSmartSpaceState { #[default] - NoActivation, + Inactive, Sent, } @@ -58,7 +58,6 @@ struct ZchDynamicState { /// Chording will be disabled if: /// - further presses cannot possibly activate a chord /// - a release happens with no chord having been activated - /// TODO: is the above true or even desirable? /// /// Once disabled, chording will be enabled when: /// - all keys have been released @@ -172,7 +171,7 @@ impl ZchDynamicState { self.zchd_ticks_until_disable = 0; self.zchd_ticks_until_enabled = 0; self.zchd_characters_to_delete_on_next_activation = 0; - self.zchd_smart_space_state = ZchSmartSpaceState::NoActivation; + self.zchd_smart_space_state = ZchSmartSpaceState::Inactive; } /// Returns true if dynamic zch state is such that idling optimization can activate. @@ -243,25 +242,28 @@ impl ZchState { kb: &mut KbdOut, osc: OsCode, ) -> Result<(), std::io::Error> { + if self.zch_chords.is_empty() { + return kb.press_key(osc); + } match osc { OsCode::KEY_LEFTSHIFT => { self.zchd.zchd_is_lsft_active = true; + return kb.press_key(osc); } OsCode::KEY_RIGHTSHIFT => { self.zchd.zchd_is_rsft_active = true; + return kb.press_key(osc); + } + osc if osc.is_modifier() => { + return kb.press_key(osc); } _ => {} } - - if self.zch_chords.is_empty() - || osc.is_modifier() - { - return kb.press_key(osc); - } - if osc.is_punctuation() && self.zchd.zchd_smart_space_state == ZchSmartSpaceState::Sent { + if self.zchd.zchd_smart_space_state == ZchSmartSpaceState::Sent && osc.is_punctuation() { kb.press_key(OsCode::KEY_BACKSPACE)?; kb.release_key(OsCode::KEY_BACKSPACE)?; } + self.zchd.zchd_smart_space_state = ZchSmartSpaceState::Inactive; if self.zchd.zchd_enabled_state != ZchEnabledState::Enabled { return kb.press_key(osc); } @@ -388,7 +390,21 @@ impl ZchState { } } - // TODO: smart spacing + if self.zch_cfg.zch_cfg_smart_space != ZchSmartSpaceCfg::Disabled + && !matches!( + a.zch_output + .last() + .expect("empty outputs are not expected") + .osc(), + OsCode::KEY_SPACE | OsCode::KEY_BACKSPACE + ) + { + if self.zch_cfg.zch_cfg_smart_space == ZchSmartSpaceCfg::Full { + self.zchd.zchd_smart_space_state = ZchSmartSpaceState::Sent; + } + kb.press_key(OsCode::KEY_SPACE)?; + kb.release_key(OsCode::KEY_SPACE)?; + } if !self.zchd.zchd_is_caps_word_active { if self.zchd.zchd_is_lsft_active { From f662db0f108d4a6b1b56b057376e0063f096cf0e Mon Sep 17 00:00:00 2001 From: jtroo Date: Mon, 28 Oct 2024 01:03:11 -0700 Subject: [PATCH 44/54] add tests --- src/tests/sim_tests/zippychord_sim_tests.rs | 94 +++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/src/tests/sim_tests/zippychord_sim_tests.rs b/src/tests/sim_tests/zippychord_sim_tests.rs index 281521a45..95cda12af 100644 --- a/src/tests/sim_tests/zippychord_sim_tests.rs +++ b/src/tests/sim_tests/zippychord_sim_tests.rs @@ -6,6 +6,7 @@ static ZIPPY_FILE_CONTENT: &str = " dy day dy 1 Monday abc Alphabet +pr pre ⌫ r df recipient w a Washington xy WxYz @@ -270,3 +271,96 @@ fn sim_zippychord_disabled_by_typing() { .to_ascii(); assert_eq!("dn:V t:1ms up:V t:9ms dn:D t:1ms dn:Y", result); } + +#[test] +fn sim_zippychord_smartspace_full() { + let result = simulate_with_file_content( + "(defsrc)(deflayer base)(defzippy-experimental file + smart-space full)", + "d:d d:y t:10 u:d u:y t:100 d:. t:10 u:. t:10", + Some(ZIPPY_FILE_CONTENT), + ) + .to_ascii(); + assert_eq!( + "dn:D t:1ms dn:BSpace up:BSpace up:D dn:D dn:A up:A up:Y dn:Y dn:Space up:Space \ + t:9ms up:D t:1ms up:Y t:99ms dn:BSpace up:BSpace dn:Dot t:10ms up:Dot", + result + ); + + // Test that prefix works as intended. + let result = simulate_with_file_content( + "(defsrc)(deflayer base)(defzippy-experimental file + smart-space add-space-only)", + "d:p d:r t:10 u:p u:r t:100 d:. t:10 u:. t:10", + Some(ZIPPY_FILE_CONTENT), + ) + .to_ascii(); + assert_eq!( + "dn:P t:1ms dn:BSpace up:BSpace up:P dn:P up:R dn:R dn:E up:E \ + dn:Space up:Space dn:BSpace up:BSpace \ + t:9ms up:P t:1ms up:R t:99ms dn:Dot t:10ms up:Dot", + result + ); +} + +#[test] +fn sim_zippychord_smartspace_spaceonly() { + let result = simulate_with_file_content( + "(defsrc)(deflayer base)(defzippy-experimental file + smart-space add-space-only)", + "d:d d:y t:10 u:d u:y t:100 d:. t:10 u:. t:10", + Some(ZIPPY_FILE_CONTENT), + ) + .to_ascii(); + assert_eq!( + "dn:D t:1ms dn:BSpace up:BSpace up:D dn:D dn:A up:A up:Y dn:Y dn:Space up:Space \ + t:9ms up:D t:1ms up:Y t:99ms dn:Dot t:10ms up:Dot", + result + ); + + // Test that prefix works as intended. + let result = simulate_with_file_content( + "(defsrc)(deflayer base)(defzippy-experimental file + smart-space add-space-only)", + "d:p d:r t:10 u:p u:r t:100 d:. t:10 u:. t:10", + Some(ZIPPY_FILE_CONTENT), + ) + .to_ascii(); + assert_eq!( + "dn:P t:1ms dn:BSpace up:BSpace up:P dn:P up:R dn:R dn:E up:E \ + dn:Space up:Space dn:BSpace up:BSpace \ + t:9ms up:P t:1ms up:R t:99ms dn:Dot t:10ms up:Dot", + result + ); +} + +#[test] +fn sim_zippychord_smartspace_none() { + let result = simulate_with_file_content( + "(defsrc)(deflayer base)(defzippy-experimental file + smart-space none)", + "d:d d:y t:10 u:d u:y t:100 d:. t:10 u:. t:10", + Some(ZIPPY_FILE_CONTENT), + ) + .to_ascii(); + assert_eq!( + "dn:D t:1ms dn:BSpace up:BSpace up:D dn:D dn:A up:A up:Y dn:Y \ + t:9ms up:D t:1ms up:Y t:99ms dn:Dot t:10ms up:Dot", + result + ); + + // Test that prefix works as intended. + let result = simulate_with_file_content( + "(defsrc)(deflayer base)(defzippy-experimental file + smart-space add-space-only)", + "d:p d:r t:10 u:p u:r t:100 d:. t:10 u:. t:10", + Some(ZIPPY_FILE_CONTENT), + ) + .to_ascii(); + assert_eq!( + "dn:P t:1ms dn:BSpace up:BSpace up:P dn:P up:R dn:R dn:E up:E \ + dn:Space up:Space dn:BSpace up:BSpace \ + t:9ms up:P t:1ms up:R t:99ms dn:Dot t:10ms up:Dot", + result + ); +} From 8593abfc844f23049d487041ee4bf0ad3d08b46c Mon Sep 17 00:00:00 2001 From: jtroo Date: Mon, 28 Oct 2024 01:22:27 -0700 Subject: [PATCH 45/54] add more prefix tests and fix --- parser/src/cfg/zippychord.rs | 9 ++++ src/kanata/output_logic/zippychord.rs | 10 ++++- src/tests/sim_tests/zippychord_sim_tests.rs | 46 ++++++++++++++++++--- 3 files changed, 57 insertions(+), 8 deletions(-) diff --git a/parser/src/cfg/zippychord.rs b/parser/src/cfg/zippychord.rs index 46a6d1b17..cee45f133 100644 --- a/parser/src/cfg/zippychord.rs +++ b/parser/src/cfg/zippychord.rs @@ -137,6 +137,15 @@ impl ZchOutput { Lowercase(osc) | Uppercase(osc) | AltGr(osc) | ShiftAltGr(osc) => osc, } } + pub fn display_len(outs: impl AsRef<[Self]>) -> i16 { + outs.as_ref().iter().copied().fold(0i16, |mut len, out| { + len += match out.osc() { + OsCode::KEY_BACKSPACE => -1, + _ => 1, + }; + len + }) + } } /// User configuration for smart space. diff --git a/src/kanata/output_logic/zippychord.rs b/src/kanata/output_logic/zippychord.rs index 922c60f2b..87fb387d1 100644 --- a/src/kanata/output_logic/zippychord.rs +++ b/src/kanata/output_logic/zippychord.rs @@ -321,7 +321,7 @@ impl ZchState { kb.release_key(OsCode::KEY_BACKSPACE)?; } self.zchd.zchd_characters_to_delete_on_next_activation = 0; - self.zchd.zchd_previous_activation_output_count = a.zch_output.len() as i16; + self.zchd.zchd_previous_activation_output_count = ZchOutput::display_len(&a.zch_output); } self.zchd.zchd_prioritized_chords = a.zch_followups.clone(); let mut released_lsft = false; @@ -337,17 +337,21 @@ impl ZchState { std::thread::sleep(std::time::Duration::from_millis(1)); } } + + let typed_osc; // Note: every 5 keys on Windows Interception, do a sleep because // sending too quickly apparently causes weird behaviour... // I guess there's some buffer in the Interception code that is filling up. match key_to_send { ZchOutput::Lowercase(osc) => { type_osc(*osc, kb, &self.zchd)?; + typed_osc = osc; } ZchOutput::Uppercase(osc) => { maybe_press_sft_during_activation(released_lsft, kb, &self.zchd)?; type_osc(*osc, kb, &self.zchd)?; maybe_release_sft_during_activation(released_lsft, kb, &self.zchd)?; + typed_osc = osc; } ZchOutput::AltGr(osc) => { // Note, unlike shift which probably has a good reason to be maybe @@ -357,6 +361,7 @@ impl ZchState { kb.press_key(OsCode::KEY_RIGHTALT)?; type_osc(*osc, kb, &self.zchd)?; kb.release_key(OsCode::KEY_RIGHTALT)?; + typed_osc = osc; } ZchOutput::ShiftAltGr(osc) => { kb.press_key(OsCode::KEY_RIGHTALT)?; @@ -364,10 +369,11 @@ impl ZchState { type_osc(*osc, kb, &self.zchd)?; maybe_release_sft_during_activation(released_lsft, kb, &self.zchd)?; kb.release_key(OsCode::KEY_RIGHTALT)?; + typed_osc = osc; } } - if osc == OsCode::KEY_BACKSPACE { + if *typed_osc == OsCode::KEY_BACKSPACE { self.zchd.zchd_characters_to_delete_on_next_activation -= 1; // Improvement: there are many other keycodes that might be sent that // aren't printable. But for now, just include backspace. diff --git a/src/tests/sim_tests/zippychord_sim_tests.rs b/src/tests/sim_tests/zippychord_sim_tests.rs index 95cda12af..7b881d999 100644 --- a/src/tests/sim_tests/zippychord_sim_tests.rs +++ b/src/tests/sim_tests/zippychord_sim_tests.rs @@ -7,6 +7,8 @@ dy day dy 1 Monday abc Alphabet pr pre ⌫ +pra partner +pr q pull request r df recipient w a Washington xy WxYz @@ -272,6 +274,38 @@ fn sim_zippychord_disabled_by_typing() { assert_eq!("dn:V t:1ms up:V t:9ms dn:D t:1ms dn:Y", result); } +#[test] +fn sim_zippychord_prefix() { + let result = simulate_with_file_content( + ZIPPY_CFG, + "d:p d:r u:p u:r t:10 d:q u:q t:10", + Some(ZIPPY_FILE_CONTENT), + ) + .to_ascii(); + assert_eq!( + "dn:P t:1ms dn:BSpace up:BSpace up:P dn:P up:R dn:R dn:E up:E dn:Space up:Space \ + dn:BSpace up:BSpace t:1ms up:P t:1ms up:R t:7ms \ + dn:BSpace up:BSpace dn:BSpace up:BSpace dn:BSpace up:BSpace \ + dn:P up:P dn:U up:U dn:L up:L dn:L up:L dn:Space up:Space \ + dn:R up:R dn:E up:E up:Q dn:Q dn:U up:U dn:E up:E dn:S up:S dn:T up:T t:1ms up:Q", + result + ); + let result = simulate_with_file_content( + ZIPPY_CFG, + "d:p d:r d:a t:10 u:d u:r u:a", + Some(ZIPPY_FILE_CONTENT), + ) + .to_ascii() + .no_time() + .no_releases(); + assert_eq!( + "dn:P dn:BSpace \ + dn:P dn:R dn:E dn:Space dn:BSpace \ + dn:BSpace dn:BSpace dn:BSpace dn:P dn:A dn:R dn:T dn:N dn:E dn:R", + result + ); +} + #[test] fn sim_zippychord_smartspace_full() { let result = simulate_with_file_content( @@ -284,7 +318,7 @@ fn sim_zippychord_smartspace_full() { assert_eq!( "dn:D t:1ms dn:BSpace up:BSpace up:D dn:D dn:A up:A up:Y dn:Y dn:Space up:Space \ t:9ms up:D t:1ms up:Y t:99ms dn:BSpace up:BSpace dn:Dot t:10ms up:Dot", - result + result ); // Test that prefix works as intended. @@ -299,7 +333,7 @@ fn sim_zippychord_smartspace_full() { "dn:P t:1ms dn:BSpace up:BSpace up:P dn:P up:R dn:R dn:E up:E \ dn:Space up:Space dn:BSpace up:BSpace \ t:9ms up:P t:1ms up:R t:99ms dn:Dot t:10ms up:Dot", - result + result ); } @@ -315,7 +349,7 @@ fn sim_zippychord_smartspace_spaceonly() { assert_eq!( "dn:D t:1ms dn:BSpace up:BSpace up:D dn:D dn:A up:A up:Y dn:Y dn:Space up:Space \ t:9ms up:D t:1ms up:Y t:99ms dn:Dot t:10ms up:Dot", - result + result ); // Test that prefix works as intended. @@ -330,7 +364,7 @@ fn sim_zippychord_smartspace_spaceonly() { "dn:P t:1ms dn:BSpace up:BSpace up:P dn:P up:R dn:R dn:E up:E \ dn:Space up:Space dn:BSpace up:BSpace \ t:9ms up:P t:1ms up:R t:99ms dn:Dot t:10ms up:Dot", - result + result ); } @@ -346,7 +380,7 @@ fn sim_zippychord_smartspace_none() { assert_eq!( "dn:D t:1ms dn:BSpace up:BSpace up:D dn:D dn:A up:A up:Y dn:Y \ t:9ms up:D t:1ms up:Y t:99ms dn:Dot t:10ms up:Dot", - result + result ); // Test that prefix works as intended. @@ -361,6 +395,6 @@ fn sim_zippychord_smartspace_none() { "dn:P t:1ms dn:BSpace up:BSpace up:P dn:P up:R dn:R dn:E up:E \ dn:Space up:Space dn:BSpace up:BSpace \ t:9ms up:P t:1ms up:R t:99ms dn:Dot t:10ms up:Dot", - result + result ); } From 5402f8e2d201242fa5c6bab94e32f5189f0e4491 Mon Sep 17 00:00:00 2001 From: jtroo Date: Mon, 28 Oct 2024 01:23:23 -0700 Subject: [PATCH 46/54] fmt --- src/kanata/output_logic/zippychord.rs | 3 ++- src/tests/sim_tests/zippychord_sim_tests.rs | 16 ++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/kanata/output_logic/zippychord.rs b/src/kanata/output_logic/zippychord.rs index 87fb387d1..3b533980d 100644 --- a/src/kanata/output_logic/zippychord.rs +++ b/src/kanata/output_logic/zippychord.rs @@ -321,7 +321,8 @@ impl ZchState { kb.release_key(OsCode::KEY_BACKSPACE)?; } self.zchd.zchd_characters_to_delete_on_next_activation = 0; - self.zchd.zchd_previous_activation_output_count = ZchOutput::display_len(&a.zch_output); + self.zchd.zchd_previous_activation_output_count = + ZchOutput::display_len(&a.zch_output); } self.zchd.zchd_prioritized_chords = a.zch_followups.clone(); let mut released_lsft = false; diff --git a/src/tests/sim_tests/zippychord_sim_tests.rs b/src/tests/sim_tests/zippychord_sim_tests.rs index 7b881d999..7bb3039b7 100644 --- a/src/tests/sim_tests/zippychord_sim_tests.rs +++ b/src/tests/sim_tests/zippychord_sim_tests.rs @@ -288,7 +288,7 @@ fn sim_zippychord_prefix() { dn:BSpace up:BSpace dn:BSpace up:BSpace dn:BSpace up:BSpace \ dn:P up:P dn:U up:U dn:L up:L dn:L up:L dn:Space up:Space \ dn:R up:R dn:E up:E up:Q dn:Q dn:U up:U dn:E up:E dn:S up:S dn:T up:T t:1ms up:Q", - result + result ); let result = simulate_with_file_content( ZIPPY_CFG, @@ -302,7 +302,7 @@ fn sim_zippychord_prefix() { "dn:P dn:BSpace \ dn:P dn:R dn:E dn:Space dn:BSpace \ dn:BSpace dn:BSpace dn:BSpace dn:P dn:A dn:R dn:T dn:N dn:E dn:R", - result + result ); } @@ -318,7 +318,7 @@ fn sim_zippychord_smartspace_full() { assert_eq!( "dn:D t:1ms dn:BSpace up:BSpace up:D dn:D dn:A up:A up:Y dn:Y dn:Space up:Space \ t:9ms up:D t:1ms up:Y t:99ms dn:BSpace up:BSpace dn:Dot t:10ms up:Dot", - result + result ); // Test that prefix works as intended. @@ -333,7 +333,7 @@ fn sim_zippychord_smartspace_full() { "dn:P t:1ms dn:BSpace up:BSpace up:P dn:P up:R dn:R dn:E up:E \ dn:Space up:Space dn:BSpace up:BSpace \ t:9ms up:P t:1ms up:R t:99ms dn:Dot t:10ms up:Dot", - result + result ); } @@ -349,7 +349,7 @@ fn sim_zippychord_smartspace_spaceonly() { assert_eq!( "dn:D t:1ms dn:BSpace up:BSpace up:D dn:D dn:A up:A up:Y dn:Y dn:Space up:Space \ t:9ms up:D t:1ms up:Y t:99ms dn:Dot t:10ms up:Dot", - result + result ); // Test that prefix works as intended. @@ -364,7 +364,7 @@ fn sim_zippychord_smartspace_spaceonly() { "dn:P t:1ms dn:BSpace up:BSpace up:P dn:P up:R dn:R dn:E up:E \ dn:Space up:Space dn:BSpace up:BSpace \ t:9ms up:P t:1ms up:R t:99ms dn:Dot t:10ms up:Dot", - result + result ); } @@ -380,7 +380,7 @@ fn sim_zippychord_smartspace_none() { assert_eq!( "dn:D t:1ms dn:BSpace up:BSpace up:D dn:D dn:A up:A up:Y dn:Y \ t:9ms up:D t:1ms up:Y t:99ms dn:Dot t:10ms up:Dot", - result + result ); // Test that prefix works as intended. @@ -395,6 +395,6 @@ fn sim_zippychord_smartspace_none() { "dn:P t:1ms dn:BSpace up:BSpace up:P dn:P up:R dn:R dn:E up:E \ dn:Space up:Space dn:BSpace up:BSpace \ t:9ms up:P t:1ms up:R t:99ms dn:Dot t:10ms up:Dot", - result + result ); } From cee6d2bf2f709b0cf05da5aadb5efe49707325b7 Mon Sep 17 00:00:00 2001 From: jtroo Date: Mon, 28 Oct 2024 01:26:54 -0700 Subject: [PATCH 47/54] clippy --- src/kanata/output_logic/zippychord.rs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/kanata/output_logic/zippychord.rs b/src/kanata/output_logic/zippychord.rs index 3b533980d..f9230aba9 100644 --- a/src/kanata/output_logic/zippychord.rs +++ b/src/kanata/output_logic/zippychord.rs @@ -333,26 +333,25 @@ impl ZchState { for key_to_send in &a.zch_output { #[cfg(feature = "interception_driver")] { + // Note: every 5 keys on Windows Interception, do a sleep because + // sending too quickly apparently causes weird behaviour... + // I guess there's some buffer in the Interception code that is filling up. send_count += 1; if send_count % 5 == 0 { std::thread::sleep(std::time::Duration::from_millis(1)); } } - let typed_osc; - // Note: every 5 keys on Windows Interception, do a sleep because - // sending too quickly apparently causes weird behaviour... - // I guess there's some buffer in the Interception code that is filling up. - match key_to_send { + let typed_osc = match key_to_send { ZchOutput::Lowercase(osc) => { type_osc(*osc, kb, &self.zchd)?; - typed_osc = osc; + osc } ZchOutput::Uppercase(osc) => { maybe_press_sft_during_activation(released_lsft, kb, &self.zchd)?; type_osc(*osc, kb, &self.zchd)?; maybe_release_sft_during_activation(released_lsft, kb, &self.zchd)?; - typed_osc = osc; + osc } ZchOutput::AltGr(osc) => { // Note, unlike shift which probably has a good reason to be maybe @@ -362,7 +361,7 @@ impl ZchState { kb.press_key(OsCode::KEY_RIGHTALT)?; type_osc(*osc, kb, &self.zchd)?; kb.release_key(OsCode::KEY_RIGHTALT)?; - typed_osc = osc; + osc } ZchOutput::ShiftAltGr(osc) => { kb.press_key(OsCode::KEY_RIGHTALT)?; @@ -370,9 +369,9 @@ impl ZchState { type_osc(*osc, kb, &self.zchd)?; maybe_release_sft_during_activation(released_lsft, kb, &self.zchd)?; kb.release_key(OsCode::KEY_RIGHTALT)?; - typed_osc = osc; + osc } - } + }; if *typed_osc == OsCode::KEY_BACKSPACE { self.zchd.zchd_characters_to_delete_on_next_activation -= 1; From 4956f1f52087dd913e9fc9592fbae2d066cf8efb Mon Sep 17 00:00:00 2001 From: jtroo Date: Mon, 28 Oct 2024 21:15:04 -0700 Subject: [PATCH 48/54] doc for smart space --- docs/config.adoc | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/config.adoc b/docs/config.adoc index a9da4f3de..129fa70f9 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -3994,6 +3994,7 @@ When using this you are at higher likelihood of bugs and breaking changes. $zippy-filename ;; required on-first-press-chord-deadline $deadline-millis ;; optional idle-reactivate-time $idle-time-millis ;; optional + smart-space $smart-space-cfg ;; optional output-character-mappings ( ;; optional $character1 $output-mapping1 $character2 $output-mapping2 @@ -4022,6 +4023,17 @@ zippy is temporarily disabled. After typing ends and this configured number of milliseconds elapses, zippy will be re-enabled from being temporarily disabled. +| `$smart-space-cfg` for `smart-space` +| Determines the smart space behaviour. +The options are `none | add-space-only | full`. +With `none`, outputs are typed as-is. +With `add-space-only`, spaces are automatically added after outputs +which end with neither a space or a backspace ⌫. +With `full`, the `add-space-only` behaviour applies +and additional behaviour is active: +typing punctuation — `, . ;` — after a zippy activation +will delete a prior automatically-added space. + | `$characterN` for `output-character-mappings` | A single unicode character for use in the output column of the zippy configuration file. From b46482201a5af92c9cec5b02cbad357a7a45dcf5 Mon Sep 17 00:00:00 2001 From: jtroo Date: Mon, 28 Oct 2024 23:38:21 -0700 Subject: [PATCH 49/54] doc, ignore more keys --- parser/src/keys/mod.rs | 18 ++++++++++++++++++ src/kanata/output_logic/zippychord.rs | 26 ++++++++++++++++---------- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/parser/src/keys/mod.rs b/parser/src/keys/mod.rs index 9c8f11cf7..2291d7dd8 100644 --- a/parser/src/keys/mod.rs +++ b/parser/src/keys/mod.rs @@ -81,6 +81,24 @@ impl OsCode { ) } + #[cfg(feature = "zippychord")] + pub fn is_zippy_ignored(self) -> bool { + matches!( + self, + OsCode::KEY_LEFTSHIFT + | OsCode::KEY_RIGHTSHIFT + | OsCode::KEY_LEFTMETA + | OsCode::KEY_RIGHTMETA + | OsCode::KEY_LEFTCTRL + | OsCode::KEY_RIGHTCTRL + | OsCode::KEY_LEFTALT + | OsCode::KEY_RIGHTALT + | OsCode::KEY_ESC + | OsCode::KEY_BACKSPACE + | OsCode::KEY_DELETE + ) + } + pub fn is_punctuation(self) -> bool { matches!( self, diff --git a/src/kanata/output_logic/zippychord.rs b/src/kanata/output_logic/zippychord.rs index f9230aba9..8f978b9cc 100644 --- a/src/kanata/output_logic/zippychord.rs +++ b/src/kanata/output_logic/zippychord.rs @@ -61,6 +61,7 @@ struct ZchDynamicState { /// /// Once disabled, chording will be enabled when: /// - all keys have been released + /// - zchd_ticks_until_enabled shrinks to 0 zchd_enabled_state: ZchEnabledState, /// Is Some when a chord has been activated which has possible follow-up chords. /// E.g. dy -> day @@ -73,8 +74,8 @@ struct ZchDynamicState { /// Tracks the previous output character count /// because it may need to be erased (see `zchd_prioritized_chords). zchd_previous_activation_output_count: i16, - /// In case of output being empty for interim chord activations, this tracks the number of - /// characters that need to be erased. + /// Tracks the number of characters typed to complete an activation, which will be erased if an + /// activation completes succesfully. zchd_characters_to_delete_on_next_activation: i16, /// Tracker for time until previous state change to know if potential stale data should be /// cleared. This is a contingency in case of bugs or weirdness with OS interactions, e.g. @@ -86,9 +87,9 @@ struct ZchDynamicState { /// against unintended activations. This counts downwards from a configured number until 0, and /// at 0 the state transitions from pending-enabled to truly-enabled if applicable. zchd_ticks_until_enabled: u16, - /// Zch has a time delay between being disabled->pending-enabled->truly-enabled to mitigate - /// against unintended activations. This counts downwards from a configured number until 0, and - /// at 0 the state transitions from pending-enabled to truly-enabled if applicable. + /// There is a deadline between the first press happening and a chord activation being + /// possible; after which if a chord has not been activated, zippychording is disabled. This + /// state is the counter for this deadline. zchd_ticks_until_disable: u16, /// Current state of caps-word, which is a factor in handling capitalization. zchd_is_caps_word_active: bool, @@ -97,9 +98,11 @@ struct ZchDynamicState { /// Current state of rsft which is a factor in handling capitalization. zchd_is_rsft_active: bool, /// Tracks whether last press was part of a chord or not. + /// Upon releasing keys, this state determines if zippychording should remain enabled or + /// disabled. zchd_last_press: ZchLastPressClassification, - /// Tracks smart spacing state so punctuation can know whether a space needs to be erased or - /// not. + /// Tracks smart spacing state so punctuation characters + /// can know whether a space needs to be erased or not. zchd_smart_space_state: ZchSmartSpaceState, } @@ -119,7 +122,7 @@ impl ZchDynamicState { } ZchEnabledState::Enabled => { // Only run disable-check logic if ticks is already greater than zero, because zero - // means deadline has never been triggered by an press yet. + // means deadline has never been triggered by any press. if self.zchd_ticks_until_disable > 0 { self.zchd_ticks_until_disable = self.zchd_ticks_until_disable.saturating_sub(1); if self.zchd_ticks_until_disable == 0 { @@ -254,7 +257,7 @@ impl ZchState { self.zchd.zchd_is_rsft_active = true; return kb.press_key(osc); } - osc if osc.is_modifier() => { + osc if osc.is_zippy_ignored() => { return kb.press_key(osc); } _ => {} @@ -460,6 +463,9 @@ impl ZchState { kb: &mut KbdOut, osc: OsCode, ) -> Result<(), std::io::Error> { + if self.zch_chords.is_empty() { + return kb.press_key(osc); + } match osc { OsCode::KEY_LEFTSHIFT => { self.zchd.zchd_is_lsft_active = false; @@ -469,7 +475,7 @@ impl ZchState { } _ => {} } - if self.zch_chords.is_empty() || osc.is_modifier() { + if osc.is_zippy_ignored() { return kb.release_key(osc); } self.zchd.zchd_state_change(&self.zch_cfg); From de8122fcfc0c1427ba191f1e0c53275f50bf06b8 Mon Sep 17 00:00:00 2001 From: jtroo Date: Tue, 29 Oct 2024 17:30:58 -0700 Subject: [PATCH 50/54] fix test --- src/kanata/output_logic/zippychord.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kanata/output_logic/zippychord.rs b/src/kanata/output_logic/zippychord.rs index 8f978b9cc..378d6742e 100644 --- a/src/kanata/output_logic/zippychord.rs +++ b/src/kanata/output_logic/zippychord.rs @@ -464,7 +464,7 @@ impl ZchState { osc: OsCode, ) -> Result<(), std::io::Error> { if self.zch_chords.is_empty() { - return kb.press_key(osc); + return kb.release_key(osc); } match osc { OsCode::KEY_LEFTSHIFT => { From 2eef329dda465a192075f8796ed6f29e722e0742 Mon Sep 17 00:00:00 2001 From: jtroo Date: Wed, 30 Oct 2024 22:57:28 -0700 Subject: [PATCH 51/54] fix smartspace followups and overlaps --- src/kanata/output_logic/zippychord.rs | 29 ++++++--- src/tests/sim_tests/zippychord_sim_tests.rs | 66 +++++++++++++++++++++ 2 files changed, 88 insertions(+), 7 deletions(-) diff --git a/src/kanata/output_logic/zippychord.rs b/src/kanata/output_logic/zippychord.rs index 378d6742e..8e7d71260 100644 --- a/src/kanata/output_logic/zippychord.rs +++ b/src/kanata/output_logic/zippychord.rs @@ -263,6 +263,7 @@ impl ZchState { _ => {} } if self.zchd.zchd_smart_space_state == ZchSmartSpaceState::Sent && osc.is_punctuation() { + self.zchd.zchd_characters_to_delete_on_next_activation -= 1; kb.press_key(OsCode::KEY_BACKSPACE)?; kb.release_key(OsCode::KEY_BACKSPACE)?; } @@ -400,17 +401,31 @@ impl ZchState { } if self.zch_cfg.zch_cfg_smart_space != ZchSmartSpaceCfg::Disabled - && !matches!( - a.zch_output - .last() - .expect("empty outputs are not expected") - .osc(), - OsCode::KEY_SPACE | OsCode::KEY_BACKSPACE - ) + && a.zch_output + .last() + .map(|out| !matches!(out.osc(), OsCode::KEY_SPACE | OsCode::KEY_BACKSPACE)) + .unwrap_or(false) + // if output empty, don't add space { if self.zch_cfg.zch_cfg_smart_space == ZchSmartSpaceCfg::Full { self.zchd.zchd_smart_space_state = ZchSmartSpaceState::Sent; } + + // It might look unusual to add to both. + // This is correct to do. + // zchd_previous_activation_output_count only applies to followup activations, + // which should only occur after a full release+repress of a new chord. + // The full release will set zchd_characters_to_delete_on_next_activation to 0. + // Overlapping chords do not use zchd_previous_activation_output_count but + // instead keep track of characters to delete via + // zchd_characters_to_delete_on_next_activation, + // which is incremented both by typing characters + // to achieve a chord in the first place, + // as well as by chord activations that are overlapped + // by the intended final chord. + self.zchd.zchd_previous_activation_output_count += 1; + self.zchd.zchd_characters_to_delete_on_next_activation += 1; + kb.press_key(OsCode::KEY_SPACE)?; kb.release_key(OsCode::KEY_SPACE)?; } diff --git a/src/tests/sim_tests/zippychord_sim_tests.rs b/src/tests/sim_tests/zippychord_sim_tests.rs index 7bb3039b7..9d16c0f13 100644 --- a/src/tests/sim_tests/zippychord_sim_tests.rs +++ b/src/tests/sim_tests/zippychord_sim_tests.rs @@ -16,6 +16,8 @@ rq request rqa request␣assistance .g git .g f p git fetch -p +12 hi +1234 bye "; #[test] @@ -114,6 +116,15 @@ fn sim_zippychord_overlap() { up:A dn:A dn:S up:S dn:S up:S dn:I up:I dn:S up:S dn:T up:T up:A dn:A dn:N up:N dn:C up:C dn:E up:E", result ); + let result = + simulate_with_file_content(ZIPPY_CFG, "d:1 d:2 d:3 d:4 t:20", Some(ZIPPY_FILE_CONTENT)) + .to_ascii(); + assert_eq!( + "dn:Kb1 t:1ms dn:BSpace up:BSpace dn:H up:H dn:I up:I t:1ms dn:Kb3 t:1ms \ + dn:BSpace up:BSpace dn:BSpace up:BSpace dn:BSpace up:BSpace \ + dn:B up:B dn:Y up:Y dn:E up:E", + result + ); } #[test] @@ -398,3 +409,58 @@ fn sim_zippychord_smartspace_none() { result ); } + +#[test] +fn sim_zippychord_smartspace_overlap() { + let result = simulate_with_file_content( + "(defsrc)(deflayer base)(defzippy-experimental file + smart-space full)", + "d:r t:10 d:q t:10 d:a t:10", + Some(ZIPPY_FILE_CONTENT), + ) + .to_ascii(); + assert_eq!( + "dn:R t:10ms dn:BSpace up:BSpace \ + up:R dn:R dn:E up:E up:Q dn:Q dn:U up:U dn:E up:E dn:S up:S dn:T up:T dn:Space up:Space t:10ms \ + dn:BSpace up:BSpace dn:BSpace up:BSpace dn:BSpace up:BSpace dn:BSpace up:BSpace \ + dn:BSpace up:BSpace dn:BSpace up:BSpace dn:BSpace up:BSpace dn:BSpace up:BSpace \ + up:R dn:R dn:E up:E up:Q dn:Q dn:U up:U dn:E up:E dn:S up:S dn:T up:T \ + dn:Space up:Space \ + up:A dn:A dn:S up:S dn:S up:S dn:I up:I dn:S up:S dn:T up:T up:A dn:A dn:N up:N dn:C up:C dn:E up:E \ + dn:Space up:Space", + result + ); + let result = simulate_with_file_content( + "(defsrc)(deflayer base)(defzippy-experimental file + smart-space full)", + "d:1 d:2 d:3 d:4 t:20", + Some(ZIPPY_FILE_CONTENT), + ) + .to_ascii(); + assert_eq!( + "dn:Kb1 t:1ms dn:BSpace up:BSpace dn:H up:H dn:I up:I dn:Space up:Space \ + t:1ms dn:Kb3 t:1ms \ + dn:BSpace up:BSpace dn:BSpace up:BSpace dn:BSpace up:BSpace dn:BSpace up:BSpace \ + dn:B up:B dn:Y up:Y dn:E up:E dn:Space up:Space", + result + ); +} + +#[test] +fn sim_zippychord_smartspace_followup() { + let result = simulate_with_file_content( + "(defsrc)(deflayer base)(defzippy-experimental file + smart-space full)", + "d:d t:10 d:y t:10 u:d u:y t:10 d:1 t:300", + Some(ZIPPY_FILE_CONTENT), + ) + .to_ascii(); + assert_eq!( + "dn:D t:10ms dn:BSpace up:BSpace \ + up:D dn:D dn:A up:A up:Y dn:Y dn:Space up:Space \ + t:10ms up:D t:1ms up:Y t:9ms \ + dn:BSpace up:BSpace dn:BSpace up:BSpace dn:BSpace up:BSpace dn:BSpace up:BSpace \ + dn:LShift dn:M up:M up:LShift dn:O up:O dn:N up:N dn:D up:D dn:A up:A dn:Y up:Y dn:Space up:Space", + result + ); +} From a68924731b02fe3862c7683f2f2d4313226f28b9 Mon Sep 17 00:00:00 2001 From: jtroo Date: Fri, 1 Nov 2024 00:35:34 -0700 Subject: [PATCH 52/54] Allow custom punctuation --- docs/config.adoc | 12 +++++- parser/src/cfg/zippychord.rs | 58 +++++++++++++++++++++++++-- parser/src/keys/mod.rs | 7 ---- src/kanata/output_logic/zippychord.rs | 57 ++++++++++++++++++++------ 4 files changed, 110 insertions(+), 24 deletions(-) diff --git a/docs/config.adoc b/docs/config.adoc index 129fa70f9..fb2a53bd8 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -4001,6 +4001,7 @@ When using this you are at higher likelihood of bugs and breaking changes. ;; ... $characterN $output-mappingN ) + smart-space-punctuation ($punc1 ... $puncN) ) ---- @@ -4031,8 +4032,8 @@ With `add-space-only`, spaces are automatically added after outputs which end with neither a space or a backspace ⌫. With `full`, the `add-space-only` behaviour applies and additional behaviour is active: -typing punctuation — `, . ;` — after a zippy activation -will delete a prior automatically-added space. +typing punctuation — US-layout `, . ;` by default — +after a zippy activation will delete a prior automatically-added space. | `$characterN` for `output-character-mappings` | A single unicode character for use @@ -4044,6 +4045,13 @@ when seen in the zippy file output column. Must be a single key or output chord. The output chord may contain `AG-` to tell kanata to press with AltGr and may contain `S-` to tell kanata to press with Shift. + +| `$puncN` for `smart-space-punctuation` +| A character defined in `output-character-mappings` +or a known key name, which shall be considered as punctuation. +The full list of within `smart-space-punctuation` +will overwrite the default punctuation list +considered by smart-space. |=== Regarding output mappings, diff --git a/parser/src/cfg/zippychord.rs b/parser/src/cfg/zippychord.rs index cee45f133..fd52d2e2c 100644 --- a/parser/src/cfg/zippychord.rs +++ b/parser/src/cfg/zippychord.rs @@ -122,7 +122,7 @@ pub struct ZchChordOutput { /// Zch output can be uppercase, lowercase, altgr, and shift-altgr characters. /// The parser should ensure all `OsCode`s in variants containing them /// are visible characters that are backspacable. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ZchOutput { Lowercase(OsCode), Uppercase(OsCode), @@ -186,6 +186,9 @@ pub struct ZchConfig { /// User configuration for smart space. See `pub enum ZchSmartSpaceCfg`. pub zch_cfg_smart_space: ZchSmartSpaceCfg, + + /// Define keys for punctuation, which is relevant to smart space auto-erasure of added spaces. + pub zch_cfg_smart_space_punctuation: Box<[ZchOutput]>, } impl Default for ZchConfig { @@ -194,6 +197,12 @@ impl Default for ZchConfig { zch_cfg_ticks_wait_enable: 500, zch_cfg_ticks_chord_deadline: 500, zch_cfg_smart_space: ZchSmartSpaceCfg::Disabled, + zch_cfg_smart_space_punctuation: vec![ + ZchOutput::Lowercase(OsCode::KEY_DOT), + ZchOutput::Lowercase(OsCode::KEY_COMMA), + ZchOutput::Lowercase(OsCode::KEY_SEMICOLON), + ] + .into_boxed_slice(), } } } @@ -242,14 +251,16 @@ fn parse_zippy_inner( const IDLE_REACTIVATE_TIME: &str = "idle-reactivate-time"; const CHORD_DEADLINE: &str = "on-first-press-chord-deadline"; const SMART_SPACE: &str = "smart-space"; + const SMART_SPACE_PUNCTUATION: &str = "smart-space-punctuation"; let mut idle_reactivate_time_seen = false; let mut key_name_mappings_seen = false; let mut chord_deadline_seen = false; let mut smart_space_seen = false; - let mut user_cfg_char_to_output: HashMap = HashMap::default(); + let mut smart_space_punctuation_seen = false; + let mut smart_space_punctuation_val_expr = None; - // Parse other zippy configurations + let mut user_cfg_char_to_output: HashMap = HashMap::default(); let mut pairs = exprs[2..].chunks_exact(2); for pair in pairs.by_ref() { let config_name = &pair[0]; @@ -305,6 +316,18 @@ fn parse_zippy_inner( })?; } + SMART_SPACE_PUNCTUATION => { + if smart_space_punctuation_seen { + bail_expr!( + config_name, + "This is the 2nd instance; it can only be defined once" + ); + } + smart_space_punctuation_seen = true; + // Need to save and parse this later since it makes use of KEY_NAME_MAPPINGS. + smart_space_punctuation_val_expr = Some(config_value); + } + KEY_NAME_MAPPINGS => { if key_name_mappings_seen { bail_expr!( @@ -386,6 +409,35 @@ fn parse_zippy_inner( bail_expr!(&rem[0], "zippy config name is missing its value"); } + if let Some(val) = smart_space_punctuation_val_expr { + config.zch_cfg_smart_space_punctuation = val + .list(s.vars()) + .ok_or_else(|| { + anyhow_expr!(val, "{SMART_SPACE_PUNCTUATION} must be followed by a list") + })? + .iter() + .try_fold(vec![], |mut puncs, punc_expr| -> Result> { + let punc = punc_expr + .atom(s.vars()) + .ok_or_else(|| anyhow_expr!(&punc_expr, "Lists are not allowed"))?; + + if punc.chars().count() == 1 { + let c = punc.chars().next().expect("checked count above"); + if let Some(out) = user_cfg_char_to_output.get(&c) { + puncs.push(*out); + return Ok(puncs); + } + } + + let osc = str_to_oscode(punc) + .ok_or_else(|| anyhow_expr!(&punc_expr, "Unknown key name"))?; + puncs.push(ZchOutput::Lowercase(osc)); + + Ok(puncs) + })? + .into_boxed_slice(); + } + // process zippy file let input_data = f .get_file_content(file_name.as_ref()) diff --git a/parser/src/keys/mod.rs b/parser/src/keys/mod.rs index 2291d7dd8..dbd31b342 100644 --- a/parser/src/keys/mod.rs +++ b/parser/src/keys/mod.rs @@ -98,13 +98,6 @@ impl OsCode { | OsCode::KEY_DELETE ) } - - pub fn is_punctuation(self) -> bool { - matches!( - self, - OsCode::KEY_DOT | OsCode::KEY_SEMICOLON | OsCode::KEY_COMMA - ) - } } static CUSTOM_STRS_TO_OSCODES: Lazy>> = Lazy::new(|| { diff --git a/src/kanata/output_logic/zippychord.rs b/src/kanata/output_logic/zippychord.rs index 8e7d71260..c9ecf96c6 100644 --- a/src/kanata/output_logic/zippychord.rs +++ b/src/kanata/output_logic/zippychord.rs @@ -97,6 +97,8 @@ struct ZchDynamicState { zchd_is_lsft_active: bool, /// Current state of rsft which is a factor in handling capitalization. zchd_is_rsft_active: bool, + /// Current state of altgr which is a factor in smart space erasure. + zchd_is_altgr_active: bool, /// Tracks whether last press was part of a chord or not. /// Upon releasing keys, this state determines if zippychording should remain enabled or /// disabled. @@ -160,6 +162,7 @@ impl ZchDynamicState { self.zchd_is_caps_word_active = false; self.zchd_is_lsft_active = false; self.zchd_is_rsft_active = false; + self.zchd_is_altgr_active = false; self.zchd_soft_reset(); } @@ -257,12 +260,29 @@ impl ZchState { self.zchd.zchd_is_rsft_active = true; return kb.press_key(osc); } + OsCode::KEY_RIGHTALT => { + self.zchd.zchd_is_altgr_active = true; + return kb.press_key(osc); + } osc if osc.is_zippy_ignored() => { return kb.press_key(osc); } _ => {} } - if self.zchd.zchd_smart_space_state == ZchSmartSpaceState::Sent && osc.is_punctuation() { + if self.zchd.zchd_smart_space_state == ZchSmartSpaceState::Sent + && self + .zch_cfg + .zch_cfg_smart_space_punctuation + .contains(&match ( + self.zchd.zchd_is_lsft_active | self.zchd.zchd_is_rsft_active, + self.zchd.zchd_is_altgr_active, + ) { + (false, false) => ZchOutput::Lowercase(osc), + (true, false) => ZchOutput::Uppercase(osc), + (false, true) => ZchOutput::AltGr(osc), + (true, true) => ZchOutput::ShiftAltGr(osc), + }) + { self.zchd.zchd_characters_to_delete_on_next_activation -= 1; kb.press_key(OsCode::KEY_BACKSPACE)?; kb.release_key(OsCode::KEY_BACKSPACE)?; @@ -329,11 +349,15 @@ impl ZchState { ZchOutput::display_len(&a.zch_output); } self.zchd.zchd_prioritized_chords = a.zch_followups.clone(); - let mut released_lsft = false; + let mut released_sft = false; #[cfg(feature = "interception_driver")] let mut send_count = 0; + if self.zchd.zchd_is_altgr_active { + kb.release_key(OsCode::KEY_RIGHTALT)?; + } + for key_to_send in &a.zch_output { #[cfg(feature = "interception_driver")] { @@ -352,16 +376,21 @@ impl ZchState { osc } ZchOutput::Uppercase(osc) => { - maybe_press_sft_during_activation(released_lsft, kb, &self.zchd)?; + maybe_press_sft_during_activation(released_sft, kb, &self.zchd)?; type_osc(*osc, kb, &self.zchd)?; - maybe_release_sft_during_activation(released_lsft, kb, &self.zchd)?; + maybe_release_sft_during_activation(released_sft, kb, &self.zchd)?; osc } ZchOutput::AltGr(osc) => { - // Note, unlike shift which probably has a good reason to be maybe - // already held during chording, I don't currently see ralt as having - // any reason to already be held during chording; just use normal - // characters. + // A note regarding maybe_press|release_sft + // in contrast to always pressing|releasing altgr: + // + // The maybe-logic is valuable with Shift to capitalize the first + // typed output during activation. + // However, altgr - if already held - + // does not seem useful to keep held on the first typed output so it is + // always released at the beginning and pressed at the end if it was + // previously being held. kb.press_key(OsCode::KEY_RIGHTALT)?; type_osc(*osc, kb, &self.zchd)?; kb.release_key(OsCode::KEY_RIGHTALT)?; @@ -369,9 +398,9 @@ impl ZchState { } ZchOutput::ShiftAltGr(osc) => { kb.press_key(OsCode::KEY_RIGHTALT)?; - maybe_press_sft_during_activation(released_lsft, kb, &self.zchd)?; + maybe_press_sft_during_activation(released_sft, kb, &self.zchd)?; type_osc(*osc, kb, &self.zchd)?; - maybe_release_sft_during_activation(released_lsft, kb, &self.zchd)?; + maybe_release_sft_during_activation(released_sft, kb, &self.zchd)?; kb.release_key(OsCode::KEY_RIGHTALT)?; osc } @@ -389,8 +418,8 @@ impl ZchState { self.zchd.zchd_characters_to_delete_on_next_activation += 1; } - if !released_lsft && !self.zchd.zchd_is_caps_word_active { - released_lsft = true; + if !released_sft && !self.zchd.zchd_is_caps_word_active { + released_sft = true; if self.zchd.zchd_is_lsft_active { kb.release_key(OsCode::KEY_LEFTSHIFT)?; } @@ -431,6 +460,7 @@ impl ZchState { } if !self.zchd.zchd_is_caps_word_active { + // When expanding, lsft/rsft will be released after the first press. if self.zchd.zchd_is_lsft_active { kb.press_key(OsCode::KEY_LEFTSHIFT)?; } @@ -438,6 +468,9 @@ impl ZchState { kb.press_key(OsCode::KEY_RIGHTSHIFT)?; } } + if self.zchd.zchd_is_altgr_active { + kb.press_key(OsCode::KEY_RIGHTALT)?; + } // Note: it is incorrect to clear input keys. // Zippychord will eagerly output chords even if there is an overlapping chord that From 5662b1a8e80c5a043c441787d50dae427c23f154 Mon Sep 17 00:00:00 2001 From: jtroo Date: Fri, 1 Nov 2024 00:52:22 -0700 Subject: [PATCH 53/54] use set for punc; update docs --- cfg_samples/kanata.kbd | 9 +++++-- docs/config.adoc | 46 +++++++++++++++++++++++++++++------- parser/src/cfg/zippychord.rs | 22 ++++++++++------- 3 files changed, 57 insertions(+), 20 deletions(-) diff --git a/cfg_samples/kanata.kbd b/cfg_samples/kanata.kbd index d74674132..1dae4287c 100644 --- a/cfg_samples/kanata.kbd +++ b/cfg_samples/kanata.kbd @@ -1396,11 +1396,16 @@ Syntax (5-tuples): Yet another chording implementation - zippychord: + +;; This is a sample for US international layout. (defzippy-experimental zippy.txt on-first-press-chord-deadline 500 idle-reactivate-time 500 + smart-space-punctuation (? ! . , ; :) output-character-mappings ( + ! S-1 + ? S-/ % S-5 "(" S-9 ")" S-0 @@ -1422,9 +1427,9 @@ dy 1 Monday r df recipient w a Washington rq request -rqa request␣assistance +rqa request assistance --- -You can read in more detail in the configuration guide. +You can read about zippychord in more detail in the configuration guide. |# diff --git a/docs/config.adoc b/docs/config.adoc index fb2a53bd8..2e9350960 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -3995,13 +3995,14 @@ When using this you are at higher likelihood of bugs and breaking changes. on-first-press-chord-deadline $deadline-millis ;; optional idle-reactivate-time $idle-time-millis ;; optional smart-space $smart-space-cfg ;; optional - output-character-mappings ( ;; optional + smart-space-punctuation ( ;; optional + $punc1 $punc2 ... $puncN) + output-character-mappings ( ;; optional $character1 $output-mapping1 $character2 $output-mapping2 ;; ... $characterN $output-mappingN ) - smart-space-punctuation ($punc1 ... $puncN) ) ---- @@ -4035,6 +4036,13 @@ and additional behaviour is active: typing punctuation — US-layout `, . ;` by default — after a zippy activation will delete a prior automatically-added space. +| `$puncN` for `smart-space-punctuation` +| A character defined in `output-character-mappings` +or a known key name, which shall be considered as punctuation. +The full list within `smart-space-punctuation` +will overwrite the default punctuation list +considered by smart-space. + | `$characterN` for `output-character-mappings` | A single unicode character for use in the output column of the zippy configuration file. @@ -4045,13 +4053,6 @@ when seen in the zippy file output column. Must be a single key or output chord. The output chord may contain `AG-` to tell kanata to press with AltGr and may contain `S-` to tell kanata to press with Shift. - -| `$puncN` for `smart-space-punctuation` -| A character defined in `output-character-mappings` -or a known key name, which shall be considered as punctuation. -The full list of within `smart-space-punctuation` -will overwrite the default punctuation list -considered by smart-space. |=== Regarding output mappings, @@ -4125,6 +4126,33 @@ Additionally, `output-character-mappings` configuration can be used to inform kanata of additional mappings that may use Shift or AltGr. |=== +==== Sample kanata configuration + +[source] +---- +(defzippy-experimental + zippy.txt + on-first-press-chord-deadline 500 + idle-reactivate-time 500 + smart-space-punctuation (? ! . , ; :) + output-character-mappings ( + ;; This should work for US international. + ! S-1 + ? S-/ + % S-5 + "(" S-9 + ")" S-0 + : S-; + < S-, + > S-. + r#"""# S-' + | S-\ + _ S-- + ® AG-r + ) +) +---- + ==== Sample zippy file content [source] diff --git a/parser/src/cfg/zippychord.rs b/parser/src/cfg/zippychord.rs index fd52d2e2c..21e3bf746 100644 --- a/parser/src/cfg/zippychord.rs +++ b/parser/src/cfg/zippychord.rs @@ -122,7 +122,7 @@ pub struct ZchChordOutput { /// Zch output can be uppercase, lowercase, altgr, and shift-altgr characters. /// The parser should ensure all `OsCode`s in variants containing them /// are visible characters that are backspacable. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ZchOutput { Lowercase(OsCode), Uppercase(OsCode), @@ -188,7 +188,7 @@ pub struct ZchConfig { pub zch_cfg_smart_space: ZchSmartSpaceCfg, /// Define keys for punctuation, which is relevant to smart space auto-erasure of added spaces. - pub zch_cfg_smart_space_punctuation: Box<[ZchOutput]>, + pub zch_cfg_smart_space_punctuation: HashSet, } impl Default for ZchConfig { @@ -197,12 +197,14 @@ impl Default for ZchConfig { zch_cfg_ticks_wait_enable: 500, zch_cfg_ticks_chord_deadline: 500, zch_cfg_smart_space: ZchSmartSpaceCfg::Disabled, - zch_cfg_smart_space_punctuation: vec![ - ZchOutput::Lowercase(OsCode::KEY_DOT), - ZchOutput::Lowercase(OsCode::KEY_COMMA), - ZchOutput::Lowercase(OsCode::KEY_SEMICOLON), - ] - .into_boxed_slice(), + zch_cfg_smart_space_punctuation: { + let mut puncs = HashSet::default(); + puncs.insert(ZchOutput::Lowercase(OsCode::KEY_DOT)); + puncs.insert(ZchOutput::Lowercase(OsCode::KEY_COMMA)); + puncs.insert(ZchOutput::Lowercase(OsCode::KEY_SEMICOLON)); + puncs.shrink_to_fit(); + puncs + }, } } } @@ -435,7 +437,9 @@ fn parse_zippy_inner( Ok(puncs) })? - .into_boxed_slice(); + .into_iter() + .collect(); + config.zch_cfg_smart_space_punctuation.shrink_to_fit(); } // process zippy file From 691eb40e9826b30c928f16e15654f110e9e3b0a1 Mon Sep 17 00:00:00 2001 From: jtroo Date: Fri, 1 Nov 2024 22:43:04 -0700 Subject: [PATCH 54/54] add tests and 1 minor fix for smart space --- src/kanata/output_logic/zippychord.rs | 4 +- src/tests/sim_tests/zippychord_sim_tests.rs | 105 ++++++++++++++++++++ 2 files changed, 107 insertions(+), 2 deletions(-) diff --git a/src/kanata/output_logic/zippychord.rs b/src/kanata/output_logic/zippychord.rs index c9ecf96c6..40cea1c0a 100644 --- a/src/kanata/output_logic/zippychord.rs +++ b/src/kanata/output_logic/zippychord.rs @@ -354,7 +354,7 @@ impl ZchState { #[cfg(feature = "interception_driver")] let mut send_count = 0; - if self.zchd.zchd_is_altgr_active { + if self.zchd.zchd_is_altgr_active && !a.zch_output.is_empty() { kb.release_key(OsCode::KEY_RIGHTALT)?; } @@ -468,7 +468,7 @@ impl ZchState { kb.press_key(OsCode::KEY_RIGHTSHIFT)?; } } - if self.zchd.zchd_is_altgr_active { + if self.zchd.zchd_is_altgr_active && !a.zch_output.is_empty() { kb.press_key(OsCode::KEY_RIGHTALT)?; } diff --git a/src/tests/sim_tests/zippychord_sim_tests.rs b/src/tests/sim_tests/zippychord_sim_tests.rs index 9d16c0f13..181e13989 100644 --- a/src/tests/sim_tests/zippychord_sim_tests.rs +++ b/src/tests/sim_tests/zippychord_sim_tests.rs @@ -464,3 +464,108 @@ fn sim_zippychord_smartspace_followup() { result ); } + +const CUSTOM_PUNC_CFG: &str = "\ +(defsrc) +(deflayer base) +(defzippy-experimental file + smart-space full + smart-space-punctuation (z ! ® *) + output-character-mappings ( + ® AG-r + * S-AG-v + ! S-1))"; + +#[test] +fn sim_zippychord_smartspace_custom_punc() { + // 1 without lsft: no smart-space-erase + let result = simulate_with_file_content( + CUSTOM_PUNC_CFG, + "d:d t:10 d:y t:10 u:d u:y t:10 d:1 t:300", + Some(ZIPPY_FILE_CONTENT), + ) + .to_ascii(); + assert_eq!( + "dn:D t:10ms dn:BSpace up:BSpace \ + up:D dn:D dn:A up:A up:Y dn:Y dn:Space up:Space \ + t:10ms up:D t:1ms up:Y t:9ms \ + dn:BSpace up:BSpace dn:BSpace up:BSpace dn:BSpace up:BSpace dn:BSpace up:BSpace \ + dn:LShift dn:M up:M up:LShift dn:O up:O dn:N up:N dn:D up:D dn:A up:A dn:Y up:Y dn:Space up:Space", + result + ); + + // S-1 = !: smart-space-erase + let result = simulate_with_file_content( + CUSTOM_PUNC_CFG, + "d:1 d:2 t:10 u:1 u:2 t:10 d:lsft d:1 u:1 u:lsft t:300", + Some(ZIPPY_FILE_CONTENT), + ) + .to_ascii(); + assert_eq!( + "dn:Kb1 t:1ms dn:BSpace up:BSpace \ + dn:H up:H dn:I up:I dn:Space up:Space t:9ms \ + up:Kb1 t:1ms up:Kb2 t:9ms \ + dn:LShift t:1ms dn:BSpace up:BSpace dn:Kb1 t:1ms up:Kb1 t:1ms up:LShift", + result + ); + + // z: smart-space-erase + let result = simulate_with_file_content( + CUSTOM_PUNC_CFG, + "d:1 d:2 t:10 u:1 u:2 t:10 d:z u:z t:300", + Some(ZIPPY_FILE_CONTENT), + ) + .to_ascii(); + assert_eq!( + "dn:Kb1 t:1ms dn:BSpace up:BSpace \ + dn:H up:H dn:I up:I dn:Space up:Space t:9ms \ + up:Kb1 t:1ms up:Kb2 t:9ms \ + dn:BSpace up:BSpace dn:Z t:1ms up:Z", + result + ); + + // r no altgr: no smart-space-erase + let result = simulate_with_file_content( + CUSTOM_PUNC_CFG, + "d:1 d:2 t:10 u:1 u:2 t:10 d:r u:r t:300", + Some(ZIPPY_FILE_CONTENT), + ) + .to_ascii(); + assert_eq!( + "dn:Kb1 t:1ms dn:BSpace up:BSpace \ + dn:H up:H dn:I up:I dn:Space up:Space t:9ms \ + up:Kb1 t:1ms up:Kb2 t:9ms \ + dn:R t:1ms up:R", + result + ); + + // r with altgr: smart-space-erase + let result = simulate_with_file_content( + CUSTOM_PUNC_CFG, + "d:1 d:2 t:10 u:1 u:2 t:10 d:ralt d:r u:r u:ralt t:300", + Some(ZIPPY_FILE_CONTENT), + ) + .to_ascii(); + assert_eq!( + "dn:Kb1 t:1ms dn:BSpace up:BSpace \ + dn:H up:H dn:I up:I dn:Space up:Space t:9ms \ + up:Kb1 t:1ms up:Kb2 t:9ms \ + dn:RAlt t:1ms dn:BSpace up:BSpace dn:R t:1ms up:R t:1ms up:RAlt", + result + ); + + // v with altgr+lsft: smart-space-erase + let result = simulate_with_file_content( + CUSTOM_PUNC_CFG, + "d:1 d:2 t:10 u:1 u:2 t:10 d:ralt d:lsft d:v u:v u:ralt u:lsft t:300", + Some(ZIPPY_FILE_CONTENT), + ) + .to_ascii(); + assert_eq!( + "dn:Kb1 t:1ms dn:BSpace up:BSpace \ + dn:H up:H dn:I up:I dn:Space up:Space t:9ms \ + up:Kb1 t:1ms up:Kb2 t:9ms \ + dn:RAlt t:1ms dn:LShift t:1ms dn:BSpace up:BSpace dn:V t:1ms up:V t:1ms up:RAlt t:1ms up:LShift", + result + ); +}