Skip to content

Commit

Permalink
feat(sequences): add O-(...) for any-order overlapping keys (#989)
Browse files Browse the repository at this point in the history
Gets a lot of the way to #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
jtroo authored Apr 28, 2024
1 parent 097cfd4 commit cdd524d
Show file tree
Hide file tree
Showing 8 changed files with 738 additions and 208 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
105 changes: 88 additions & 17 deletions docs/config.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2625,11 +2625,11 @@ precisely, the action triggered is:
dotcom (. S-3)
dotorg (. S-4)
;; The shifted letters in parentheses means a single press of lsft
;; must remain held while both h and then s are pressed.
;; This is not the same as S-h S-s, which means that the lsft key
;; must be released and repressed between the h and s presses.
https (. S-(h s))
;; The shifted letters in parentheses means a single press of lsft
;; must remain held while both h and then s are pressed.
;; This is not the same as S-h S-s, which means that the lsft key
;; must be released and repressed between the h and s presses.
https (S-(h s))
)
(defvirtualkeys
dotcom (macro . c o m)
Expand All @@ -2652,11 +2652,12 @@ alongside templates to define sequences below.
(deflayer base
sldr nop0 nop1 nop2)
(deftemplate seq (vk-name input-keys output-action)
(defvirtualkeys $vk-name $output-action)
(defseq $vk-name $input-keys)
sldr(defvirtualkeys $vk-name $output-action)
sldr(defseq $vk-name $input-keys)
)
(template-expand seq dotcom (nop0 nop1) (macro . c o m))
(template-expand seq dotorg (nop0 nop2) (macro . o r g))
;; template-expand has a shortened form: t!
(t! seq dotcom (nop0 nop1) (macro . c o m))
(t! seq dotorg (nop0 nop2) (macro . o r g))
----

If 10 special nop keys do not seem sufficient,
Expand All @@ -2683,11 +2684,71 @@ while treating `nop7-nop9` as prefixes:
)
----

For more context about sequences, you can read the
https://github.com/jtroo/kanata/issues/97[design and motivation of sequences].
You may also be interested in
https://github.com/jtroo/kanata/blob/main/docs/sequence-adding-chords-ideas.md[the document describing chords in sequences]
to read about how chords in sequences behave.
==== Overlapping keys in any order

Within the key list of `defseq` configuration items,
the special `O-` list prefix can be used to denote a set of keys that must
all be pressed before any are released in order to match the sequence.

For an example, `O-(a b c)` is equivalent to `O-(c b a)`.

.Example:
[source]
----
(defvirtualkey hello (macro h (unshift e l) 5 (unshift l o)))
(defseq hello (O-(h l o)))
----

.Sample of more advanced usage
[%collapsible]
====
The configuration below showcases context-dependent chording
with auto-space and auto-deleted spaces from typing punctuation.
For example, chording `(d a y)` and then `(t u e)` will output
`Tuesday`, while chording `(t u e)` by itself does nothing.
.Example configuration:
[source]
----
(defsrc f1)
(deflayer base lrld)
(defcfg process-unmapped-keys yes
sequence-input-mode visible-backspaced
concurrent-tap-hold true)
(deftemplate seq (vk-name in out)
(defvirtualkeys $vk-name $out)
(defseq $vk-name $in))
(defvirtualkeys rls-sft (multi (release-key lsft)(release-key rsft)))
(defvar rls-sft (on-press tap-vkey rls-sft))
(deftemplate rls-sft () $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 $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 $rls-sft u e s d a y nop9 sldr spc nop0))
(t! seq DelSpace_. (spc nop0 .) (macro .))
(t! seq DelSpace_; (spc nop0 ;) (macro ;))
----
.Try using the above configuration to type the text:
[source]
----
day;
Day;
Tuesday.
day hello
hello day
Hello day.
hello Tuesday
Hello Monday;
----
====

==== Override the global timeout and input mode

Expand All @@ -2714,6 +2775,14 @@ The second parameter is an override for `sequence-input-mode`:
(defalias dot-sequence (macro (sequence 250 hidden-delay-type) 10 .))
----

==== More about sequences

For more context about sequences, you can read the
https://github.com/jtroo/kanata/issues/97[design and motivation of sequences].
You may also be interested in
https://github.com/jtroo/kanata/blob/main/docs/sequence-adding-chords-ideas.md[the document describing chords in sequences]
to read about how chords in sequences behave.

[[input-chords]]
=== Input chords
<<table-of-contents,Back to ToC>>
Expand Down Expand Up @@ -3089,6 +3158,7 @@ because the current input is in the recency `1` slot.
The top-level configuration item `deftemplate`
declares a template that can be expanded multiple times
via the list item `template-expand`.
The short form of `template-expand` is `t!`.

The parameters to `deftemplate` in order are:

Expand Down Expand Up @@ -3161,7 +3231,8 @@ you wouldn't want this. Rather than retyping the code with `fork` and
)
(template-expand left-hand-chords qwerty a s d f qwa qws qwd qwf)
(template-expand left-hand-chords dvorak a o e u dva dvo dve dvu)
;; t! is short for template-expand
(t! left-hand-chords dvorak a o e u dva dvo dve dvu)
(defsrc a s d f)
(deflayer dvorak @dva @dvo @dve @dvu)
Expand All @@ -3180,7 +3251,7 @@ you wouldn't want this. Rather than retyping the code with `fork` and
grv 1 2 3 4 5 6 7 8 9 0 - = bspc
tab q w e r t y u i o p [ ] \
;; usable even inside defsrc
caps (template-expand home-row j) ret
caps (t! home-row j) ret
lsft z x c v b n m , . / rsft
lctl lmet lalt spc ralt rmet rctl
)
Expand All @@ -3189,7 +3260,7 @@ you wouldn't want this. Rather than retyping the code with `fork` and
grv 1 2 3 4 5 6 7 8 9 0 - = bspc
tab q w e r t y u i o p [ ] \
;; lists can be passed in too!
caps (template-expand home-row (tap-hold 200 200 j lctl)) ret
caps (t! home-row (tap-hold 200 200 j lctl)) ret
lsft z x c v b n m , . / rsft
lctl lmet lalt spc ralt rmet rctl
)
Expand Down
116 changes: 98 additions & 18 deletions parser/src/cfg/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ pub(crate) mod alloc;
use alloc::*;

mod key_override;
use crate::sequences::*;
use kanata_keyberon::chord::ChordsV2;
pub use key_override::*;

Expand Down Expand Up @@ -84,6 +85,9 @@ use is_a_button::*;
mod key_outputs;
pub use key_outputs::*;

mod permutations;
use permutations::*;

use crate::trie::Trie;
use anyhow::anyhow;
use std::cell::Cell;
Expand Down Expand Up @@ -1503,6 +1507,9 @@ fn parse_action_atom(ac_span: &Spanned<String>, s: &ParserState) -> Result<&'sta
})?
.into(),
);
if keys.contains(&KEY_OVERLAP) {
bail!("O- is only valid in sequences for lists of keys");
}
Ok(s.a.sref(Action::MultipleKeyCodes(s.a.sref(s.a.sref_vec(keys)))))
}

Expand Down Expand Up @@ -1847,6 +1854,14 @@ fn parse_macro(
(events, params_remainder) = parse_macro_item(params_remainder, s)?;
all_events.append(&mut events);
}
if all_events.iter().any(|e| match e {
SequenceEvent::Tap(kc) | SequenceEvent::Press(kc) | SequenceEvent::Release(kc) => {
*kc == KEY_OVERLAP
}
_ => false,
}) {
bail!("macro contains O- which is only valid within defseq")
}
all_events.push(SequenceEvent::Complete);
all_events.shrink_to_fit();
match repeat {
Expand Down Expand Up @@ -2011,7 +2026,7 @@ fn parse_mods_held_for_submacro<'a>(
Ok((mod_keys, unparsed_str))
}

static KEYMODI: [(&str, KeyCode); 31] = [
static KEYMODI: &[(&str, KeyCode)] = &[
("S-", KeyCode::LShift),
("‹⇧", KeyCode::LShift),
("⇧›", KeyCode::RShift),
Expand Down Expand Up @@ -2043,6 +2058,7 @@ static KEYMODI: [(&str, KeyCode); 31] = [
("◆", KeyCode::LGui),
("⌘", KeyCode::LGui),
("❖", KeyCode::LGui),
("O-", KEY_OVERLAP),
];

/// Parses mod keys like `C-S-`. Returns the `KeyCode`s for the modifiers parsed and the unparsed
Expand All @@ -2052,7 +2068,7 @@ pub fn parse_mod_prefix(mods: &str) -> Result<(Vec<KeyCode>, &str)> {
let mut rem = mods;
loop {
let mut found_none = true;
for (key_s, key_code) in &KEYMODI {
for (key_s, key_code) in KEYMODI {
if let Some(rest) = rem.strip_prefix(key_s) {
if key_stack.contains(key_code) {
bail!("Redundant \"{key_code:?}\" in {mods:?}");
Expand Down Expand Up @@ -2990,30 +3006,80 @@ fn parse_sequences(exprs: &[&Vec<SExpr>], s: &ParserState) -> Result<KeySeqsToFK
if key_seq.is_empty() {
bail_expr!(key_seq_expr, "{SEQ_ERR}\nkey_list cannot be empty");
}

let keycode_seq = parse_sequence_keys(key_seq, s)?;
if sequences.ancestor_exists(&keycode_seq) {
bail_expr!(
key_seq_expr,
"Sequence has a conflict: its sequence contains an earlier defined sequence"
);

// Generate permutations of sequences for overlapping keys.
let mut permutations = vec![vec![]];
let mut vals = keycode_seq.iter().copied();
while let Some(val) = vals.next() {
if val & KEY_OVERLAP_MARKER == 0 {
for p in permutations.iter_mut() {
p.push(val);
}
continue;
}

if val == 0x0400 {
bail_expr!(
key_seq_expr,
"O-(...) lists must have a minimum of 2 elements"
);
}
let mut values_to_permute = vec![val];
for val in vals.by_ref() {
if val == 0x0400 {
break;
}
values_to_permute.push(val);
}

if values_to_permute.len() < 2 {
bail_expr!(
key_seq_expr,
"O-(...) lists must have a minimum of 2 elements"
);
}
let ps = gen_permutations(&values_to_permute[..]);

let mut new_permutations: Vec<Vec<u16>> = vec![];
for p in permutations.iter() {
for p2 in ps.iter() {
new_permutations.push(
p.iter()
.copied()
.chain(p2.iter().copied().chain([KEY_OVERLAP_MARKER]))
.collect(),
);
}
}
permutations = new_permutations;
}
if sequences.descendant_exists(&keycode_seq) {
bail_expr!(key_seq_expr, "Sequence has a conflict: its sequence is contained within an earlier defined seqence");

for p in permutations.into_iter() {
if sequences.ancestor_exists(&p) {
bail_expr!(
key_seq_expr,
"Sequence has a conflict: its sequence contains an earlier defined sequence"
);
}
if sequences.descendant_exists(&p) {
bail_expr!(key_seq_expr, "Sequence has a conflict: its sequence is contained within an earlier defined seqence");
}
sequences.insert(
p,
s.virtual_keys
.get(vkey)
.map(|(y, _)| get_fake_key_coords(*y))
.expect("vk exists, checked earlier"),
);
}
sequences.insert(
keycode_seq,
s.virtual_keys
.get(vkey)
.map(|(y, _)| get_fake_key_coords(*y))
.expect("vk exists, checked earlier"),
);
}
}
Ok(sequences)
}

fn parse_sequence_keys(exprs: &[SExpr], s: &ParserState) -> Result<Vec<u16>> {
use crate::sequences::*;
use SequenceEvent::*;

// Reuse macro parsing but do some other processing since sequences don't support everything
Expand Down Expand Up @@ -3065,9 +3131,23 @@ fn parse_sequence_keys(exprs: &[SExpr], s: &ParserState) -> Result<Vec<u16>> {
for modk in mods_currently_held.iter().copied() {
seq_num |= mod_mask_for_keycode(modk);
}
seq.push(seq_num);
if seq_num & KEY_OVERLAP_MARKER == KEY_OVERLAP_MARKER
&& seq_num & MASK_MODDED != KEY_OVERLAP_MARKER
{
bail_expr!(
&exprs_remaining[0],
"O-(...) lists cannot be combined with other modifiers."
);
}
if *pressed != KEY_OVERLAP {
// Note: key overlap item is special and goes at the end, not the beginning
seq.push(seq_num);
}
}
Release(released) => {
if *released == KEY_OVERLAP {
seq.push(KEY_OVERLAP_MARKER);
}
if do_release_mod {
mods_currently_held.remove(
mods_currently_held
Expand Down
Loading

0 comments on commit cdd524d

Please sign in to comment.