Skip to content

Commit

Permalink
feat(sequences): add O-(...) for any-order overlapping keys (jtroo#989
Browse files Browse the repository at this point in the history
)

Gets a lot of the way to jtroo#979, with the caveat that it is really quite
inconvenient to configure. Perhaps an external parser can help.

For example:
- `defchords` can be used for basic single-chords
- for composite/contextual chords, a `defchords` output action can
trigger sequences, which themselves can use `O-(...)` for subsequent
chords

Example:
```
(defsrc f1)
(deflayer base lrld)
(defcfg process-unmapped-keys yes
	sequence-input-mode visible-backspaced
	concurrent-tap-hold true)
(deftemplate seq (vk-name seq-keys action)
	(defseq $vk-name $seq-keys)
	(defvirtualkeys $vk-name $action))

(defvirtualkeys rls-sft (multi (release-key lsft)(release-key rsft)))
(deftemplate rls-sft () (on-press tap-vkey rls-sft) 5)

(defchordsv2-experimental
	(d a y) (macro sldr d (t! rls-sft) a y spc nop0) 200 first-release ()
	(h l o) (macro h (t! rls-sft) e l l o sldr spc nop0) 200 first-release ()
)
(t! seq Monday (d a y spc nop0 O-(m o n)) (macro S-m (t! rls-sft) o n d a y nop9 sldr spc nop0))
(t! seq Tuesday (d a y spc nop0 O-(t u e)) (macro S-t (t! rls-sft) u e s d a y nop9 sldr spc nop0))
(t! seq DelSpace_. (spc nop0 .) (macro .))
(t! seq DelSpace_; (spc nop0 ;) (macro ;))
```

The configuration can write all of the below without having to manually
add or backspace the spaces, and only using shift+chords+punctuation.

```
day;
Day;
day hello
hello day
Hello day
hello Tuesday
hello Monday
Tuesday.
Monday.
```
  • Loading branch information
eugenesvk committed May 6, 2024
1 parent dd60107 commit 3081da9
Show file tree
Hide file tree
Showing 6 changed files with 553 additions and 95 deletions.
6 changes: 6 additions & 0 deletions cfg_samples/kanata.kbd
Original file line number Diff line number Diff line change
Expand Up @@ -1095,6 +1095,12 @@ If you need help, please feel welcome to ask in the GitHub discussions.
dotorg (nop8 nop9)
)

;; A key list within O-(...) signifies simultaneous presses.
(defseq
dotcom (O-(. c m))
dotorg (O-(. r g))
)

;; Input chording.
;;
;; Not to be confused with output chords (like C-S-a or the chords layer
Expand Down
50 changes: 50 additions & 0 deletions parser/src/cfg/permutations.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//! Implements Heap's algorithm.
/*
From Wikipedia:
procedure generate(k: integer, A : array of any):
if k = 1 then
output(A)
else
// Generate permutations with k-th unaltered
// Initially k = length(A)
generate(k - 1, A)
// Generate permutations for k-th swapped with each k-1 initial
for i := 0; i < k-1; i += 1 do
// Swap choice dependent on parity of k (even or odd)
if k is even then
swap(A[i], A[k-1]) // zero-indexed, the k-th is at k-1
else
swap(A[0], A[k-1])
end if
generate(k - 1, A)
end for
end if
*/

/// Heap's algorithm
pub fn gen_permutations<T: Clone + Default>(a: &[T]) -> Vec<Vec<T>> {
let mut a2 = vec![Default::default(); a.len()];
a2.clone_from_slice(a);
let mut outs = vec![];
heaps_alg(a.len(), &mut a2, &mut outs);
outs
}

fn heaps_alg<T: Clone>(k: usize, a: &mut [T], outs: &mut Vec<Vec<T>>) {
if k == 1 {
outs.push(a.to_vec());
} else {
heaps_alg(k - 1, a, outs);
for i in 0..k - 1 {
if (k % 2) == 0 {
a.swap(i, k - 1);
} else {
a.swap(0, k - 1);
}
heaps_alg(k - 1, a, outs);
}
}
}
7 changes: 7 additions & 0 deletions parser/src/sequences.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ use kanata_keyberon::key_code::KeyCode;

pub const MASK_KEYCODES: u16 = 0x03FF;
pub const MASK_MODDED: u16 = 0xFC00;
pub const KEY_OVERLAP: KeyCode = KeyCode::ErrorRollOver;
pub const KEY_OVERLAP_MARKER: u16 = 0x0400;

pub fn mod_mask_for_keycode(kc: KeyCode) -> u16 {
use KeyCode::*;
Expand All @@ -11,6 +13,11 @@ pub fn mod_mask_for_keycode(kc: KeyCode) -> u16 {
LAlt => 0x2000,
RAlt => 0x1000,
LGui | RGui => 0x0800,
// This is not real... this is a marker to help signify that key presses should be
// overlapping. The way this will look in the chord sequence is as such:
//
// [ (0x0400 | X), (0x0400 | Y), (0x0400) ]
ErrorRollOver => KEY_OVERLAP_MARKER,
_ => 0,
}
}
Expand Down
142 changes: 48 additions & 94 deletions src/kanata/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@
#[cfg(all(target_os = "windows", feature = "gui"))]
use crate::gui::win::*;
use anyhow::{bail, Result};
use kanata_parser::sequences::*;
use log::*;
use parking_lot::Mutex;
use std::sync::mpsc::{Receiver, Sender as ASender, SyncSender as Sender, TryRecvError};

use kanata_keyberon::key_code::*;
use kanata_keyberon::layout::*;
use kanata_keyberon::layout::{CustomEvent, Event, Layout, State};

use std::path::PathBuf;
#[cfg(not(feature = "passthru_ahk"))]
Expand All @@ -42,6 +43,9 @@ use dynamic_macro::*;

mod key_repeat;

mod sequences;
use sequences::*;

#[cfg(feature = "cmd")]
mod cmd;
#[cfg(feature = "cmd")]
Expand Down Expand Up @@ -183,13 +187,6 @@ pub struct MoveMouseAccelState {
pub max_distance: u16,
}

pub struct SequenceState {
pub sequence: Vec<u16>,
pub sequence_input_mode: SequenceInputMode,
pub ticks_until_timeout: u16,
pub sequence_timeout: u16,
}

use once_cell::sync::Lazy;

static MAPPED_KEYS: Lazy<Mutex<cfg::MappedKeys>> =
Expand Down Expand Up @@ -904,6 +901,37 @@ impl Kanata {
}
// #[cfg(feature="perf_logging")] log::debug!("🕐{}μs handle_keystate_changes prev_keys",(start.elapsed()).as_micros());

if cur_keys.is_empty() && !self.prev_keys.is_empty() {
if let Some(state) = &mut self.sequence_state {
use kanata_parser::trie::GetOrDescendentExistsResult::*;
state.overlapped_sequence.push(KEY_OVERLAP_MARKER);
match self
.sequences
.get_or_descendant_exists(&state.overlapped_sequence)
{
HasValue((i, j)) => {
do_successful_sequence_termination(
&mut self.kbd_out,
state,
layout,
i,
j,
EndSequenceType::Overlap,
)?;
self.sequence_state = None;
}
NotInTrie => {
// Overwrite overlapped with non-overlapped tracking
state.overlapped_sequence.clear();
state
.overlapped_sequence
.extend(state.sequence.iter().copied());
}
InTrie => {}
}
}
}

// Press keys that exist in the current state but are missing from the previous state. Comment above regarding Vec/HashSet also applies here.
log::trace!("{cur_keys:?}");
for k in cur_keys.iter() {
Expand All @@ -922,76 +950,16 @@ impl Kanata {
}
}
Some(state) => {
state.ticks_until_timeout = state.sequence_timeout;
let osc = OsCode::from(*k);
let pushed_into_seq = {
// Transform to OsCode and convert modifiers other than altgr/ralt
// (same key different names) to the left version, since that's
// how chords get transformed when building up sequences.
let mut base = u16::from(match osc {
OsCode::KEY_RIGHTSHIFT => OsCode::KEY_LEFTSHIFT,
OsCode::KEY_RIGHTMETA => OsCode::KEY_LEFTMETA,
OsCode::KEY_RIGHTCTRL => OsCode::KEY_LEFTCTRL,
osc => osc,
});
// Modify the upper unused bits of the u16 to signify that the key
// is activated alongside a modifier.
for k in cur_keys.iter().copied() {
base |= mod_mask_for_keycode(k);
}
base
};

state.sequence.push(pushed_into_seq);
match state.sequence_input_mode {
SequenceInputMode::VisibleBackspaced => {
press_key(&mut self.kbd_out, osc)?;
}
SequenceInputMode::HiddenSuppressed
| SequenceInputMode::HiddenDelayType => {}
}
log::debug!("sequence got {k:?}");

use kanata_parser::sequences::*;
use kanata_parser::trie::GetOrDescendentExistsResult::*;

// Check for invalid sequence termination.
let mut res = self.sequences.get_or_descendant_exists(&state.sequence);
if res == NotInTrie {
let is_invalid_termination = if self.sequence_backtrack_modcancel
&& (pushed_into_seq & MASK_MODDED > 0)
{
let mut no_valid_seqs = true;
for i in (0..state.sequence.len()).rev() {
// Note: proper bounds are immediately above.
// Can't use iter_mut due to borrowing issues.
state.sequence[i] &= MASK_KEYCODES;
res = self.sequences.get_or_descendant_exists(&state.sequence);
if res != NotInTrie {
no_valid_seqs = false;
break;
}
}
no_valid_seqs
} else {
true
};
if is_invalid_termination {
log::debug!("got invalid sequence; exiting sequence mode");
match state.sequence_input_mode {
SequenceInputMode::HiddenDelayType => {
for code in state.sequence.iter().copied() {
let code = code & MASK_KEYCODES;
if let Some(osc) = OsCode::from_u16(code) {
// BUG: chorded_hidden_delay_type
press_key(&mut self.kbd_out, osc)?;
release_key(&mut self.kbd_out, osc)?;
}
}
}
SequenceInputMode::HiddenSuppressed
| SequenceInputMode::VisibleBackspaced => {}
}
let clear_sequence_state = do_sequence_press_logic(
state,
k,
get_mod_mask_for_cur_keys(cur_keys),
&mut self.kbd_out,
&self.sequences,
self.sequence_backtrack_modcancel,
layout,
)?;
if clear_sequence_state {
self.sequence_state = None;
continue;
}
Expand Down Expand Up @@ -1327,6 +1295,7 @@ impl Kanata {
log::debug!("entering sequence mode");
self.sequence_state = Some(SequenceState {
sequence: vec![],
overlapped_sequence: vec![],
sequence_input_mode: *input_mode,
ticks_until_timeout: *timeout,
sequence_timeout: *timeout,
Expand Down Expand Up @@ -1941,21 +1910,6 @@ fn update_kbd_out(_cfg: &CfgOptions, _kbd_out: &KbdOut) -> Result<()> {
Ok(())
}

fn cancel_sequence(state: &SequenceState, kbd_out: &mut KbdOut) -> Result<()> {
match state.sequence_input_mode {
SequenceInputMode::HiddenDelayType => {
for code in state.sequence.iter().copied() {
if let Some(osc) = OsCode::from_u16(code) {
press_key(kbd_out, osc)?;
release_key(kbd_out, osc)?;
}
}
}
SequenceInputMode::HiddenSuppressed | SequenceInputMode::VisibleBackspaced => {}
}
Ok(())
}

pub fn handle_fakekey_action<'a, const C: usize, const R: usize, T>(
action: FakeKeyAction,
layout: &mut Layout<'a, C, R, T>,
Expand Down Expand Up @@ -2012,7 +1966,7 @@ where
} => {
debug!("fake: {} ", OsCode::from(fake_key));
}
// State::NormalKey {coord: (NORMAL_KEY_ROW, y),..}
// State::NormalKey {coord: (NORMAL_KEY_ROW, y),..}
State::LayerModifier {
coord: (NORMAL_KEY_ROW, y),
..
Expand Down
Loading

0 comments on commit 3081da9

Please sign in to comment.