jtroo / kanata

Improve keyboard comfort and usability with advanced customization
GNU Lesser General Public License v3.0
2.92k stars 125 forks source link

Feature Request: No-Op Virtual-Keys in Sequence Key-Lists #950

Closed antler5 closed 6 months ago

antler5 commented 6 months ago

Is your feature request related to a problem? Please describe.

There are a few compounding factors. I'd like:

Because of fall-through, sequence-position, and distinguishment, these keys can't be bound to sldr. A key-list can not contain XX, which is also not distinguishable. They can't be macro sldr [a-z], because [a-z] would fall-through (we want a no-op) and also are already bound (don't want existing keys to interact with the sequence).

Describe the solution you'd like.

Ideally I would be able to bind multiple virtual-keys to (unique, if necessary) no-ops and use either the virtual-key names or the unique no-ops in sequence key-lists.

Describe alternatives you've considered.

If any substantial portion of arbitrary-code values are no-ops, it may be possible to bind to those and augment key-lists to be able to pick them up. I'm not sure how to figure out how they'll be interpreted though.

Additional context

This is an itch I've scratched before by hacking on ZMK, and there's a group of alt-keyboard nerds working on a new implementation for QMK (see: qmk_sequence_transform). I was looking to KMonad for a software implementation, but just don't know where to begin with Haskell. I don't know any Rust either, but spent some time poking at Kanata by cloning existing CustomActions before stumbling into the trie-based Sequences which are already very close to the level of expressiveness I'm looking for. They're just missing the dedicated, distinguishable keys I've described, and the ability to back-space back up the trie, which idk that I want anyways (Traversal state is incremental / maintained until we reach a match, right?).

Bed now and idk when I'll be back, but I'd be happy in theory to learn enough Rust to hack something awful together, just for my own personal use, if you could spare a thought on what's feasible and how best to go about it -- though I expect there's some distance between the kind of patch I'd settle for and something worthy of mainline. I understand that the delegation to keyberon complicates this, hence my focus on no-ops -- it fits into my current mental model without too much risk of issues.

Any aid appreciated, and thank you for creating kanata as it is.

rszyma commented 6 months ago

Because of fall-through, sequence-position, and distinguishment, these keys can't be bound to sldr. A key-list can not contain XX, which is also not distinguishable. They can't be macro sldr [a-z], because [a-z] would fall-through (we want a no-op) and also are already bound (don't want existing keys to interact with the sequence).

Keys with sequence with fallthough triggering sldr at the same time are quite possible with existing kanata features. E.g. autocorrect implementation:

(defcfg
    process-unmapped-keys true
    sequence-input-mode visible-backspaced
)
(defsrc)
(defvar seq-reset-keys (spc bspc enter))
(deflayermap (base)
    ___ (multi _ (fork XX sldr $seq-reset-keys))
)
(deftemplate seq (vk-name in out)
    (defseq $vk-name $in)
    (defvirtualkeys $vk-name $out)
)
;; autocorrect implementation
(template-expand seq seq001 (g u a g e) (macro g a u g e))
(template-expand seq seq002 (t h i e r) (macro t h e i r))
(template-expand seq seq003 (t u r e) (macro t r u e))
;; so on
jtroo commented 6 months ago

Backspacing back up the trie should be doable, I don't know of any major blockers for it. I considered it before for my own uses but decided it wasn't worthwhile, I just tell myself not to make typing mistakes 😉

jtroo commented 6 months ago

Regarding the no-op virtual keys, it seems Windows doesn't have any VK codes greater than 255, and Linux has a large range here that doesn't seem to have a key name:

        OsCode::KEY_ONSCREEN_KEYBOARD => KeyCode::K632,
        OsCode::KEY_633 => KeyCode::K633,
    ...
        OsCode::KEY_703 => KeyCode::K703,
        OsCode::BTN_TRIGGER_HAPPY1 => KeyCode::K704,

Perhaps we could special-case these and:

antler5 commented 6 months ago

I haven't given any example sequences and was worried I hadn't explained what I was going for well enough, but we seem to be on the same page :p

@rszyma That's beautiful, thank you! An excellent example, just still limited to real keys (can't have XX /in/ the sequence). I should learn how that fork works.

@jtroo

  • make sure visible-backspaced doesn't treat these as backspace-required

I've got a toy implementation, but am still, uh, learning Rust, and haven't fixed this specifically. Edit: Added OsCodes::KEY_UNKNOWN to the list of non-visual key-codes.

Ideally we don't need to bind them to real OsCode's. I've added a single key, second-to-last in enum KeyCode*, and only code that touches sequences can differentiate specific instances of it (i need two) through some creative, non-production-quality data-wrangling. This will work for my use-case, but is heinous: I wonder if a more general and elegant solution exists as a super-set of the existing virtual-keys model. Code's been a pleasure to work with.


Edit: Now that I have it working how I wanted, I've collected my scattered "???" comments -- I don't necessarily need answers.

jtroo commented 6 months ago

I wonder if a more general and elegant solution exists as a super-set of the existing virtual-keys model.

Extending the existing virtual keys mechanism doesn't seem promising to me. At the implementation level a virtual key is "merely" an input position in keyberon's layout and has no representation in the output keys that sequences operate on.

Certainly a new mechanism, e.g. virtual outputs could be created, but I don't imagine it would have any links in the implementation to virtual keys.

antler5 commented 6 months ago

Extending the existing virtual keys mechanism [...] Certainly a new mechanism, e.g. virtual outputs could be created, but I don't imagine it would have any links in the implementation to virtual keys.

I've had a good think about it and understood what I'm doing (funny how we don't always know :p). These Virtual Output Keys (vout's?) are unique, non-fungible localkeys with optional OsCode bindings, so that's (architecturally) what they're an extension of. In configuration these unbound local-keys could be in separate def forms, be represented by splitting localkey definitions from their OsCode bindings forms, or be inferred from local-key bindings which are omitted or given a sentinel value (expanded from XX?). I currently resolve unbound localkeys into OsCode::KEY_UNKNOWN, and have maintained non-fungibility by adjusting parsing & Sequences to encode and interpret an additional <u16> as a discriminating value.

Current the discriminator is always present (doubling the lengths of sequence paths), but it only needs to appear immediately after dedicated OsCodes for unbound localkeys (am not so sure about using OsCode::KEY_UNKNOWN for this). This still being my only exposure to Rust, I'm curious about whether this constraint can be encoded in the type-system (on either end) in a way meaningfully contains the other: make a Vec<Either<KeyCode, VirtualOutputIdentity>> or something and maybe the type-checker can lead me to anything other than Sequences that needs to tell them apart (eg. chords, modified keys are separate, and currently messier, but same theory. I expect it generalizes pretty broadly.).

Functional criteria for scratching-my-own-itch ends at parity with #956 (no multi-vout chords or handling outside of Sequences), but I think the approach would be reasonable to merge if the above considerations are addressed and the type-system makes it prettier and/or is worth unwrapping everywhere else-- but i still don't have a great feel for whether that sounds solid (tests and docs aside). I definitely think that virtualoutputkeys (which subsume localkeys) and virtualinputkeys (née fakekeys) form a clear dichotomy through their names in a way that localkeys, fakekeys / virtualkeys, and my magic keys don't, and that they may be easier to present to new users as two related, flexible concepts in juxtaposition rather than as two-three distinct ones.

jtroo commented 6 months ago

The PR #961 implements a "worse-is-better"-style implementation that I think meets the use case

jtroo commented 6 months ago

I question the value that virtualoutputkeys-type feature can add, for it to be worth implementing and handling everywhere. The places I can think off the top of my head where it would be usable are:

But with the switch|fork use case, I don't currently imagine that virtualoutputkeys makes new use cases possible that aren't already possible with virtual input keys today.

Well, I suppose since virtual input keys aren't usable with fork, it would help there. But switch is a superset of all fork functionality anyway - though it does have the quirk of not behaving the same as fork does when called with rpt-any.

antler5 commented 6 months ago

Not digging in atm, but checking the docs, I see you're refering to the fork argument right-trigger-keys and the per-case switch argument keys check. I haven't got a use for 'em there either, and this does make my config expressable. Look like they're just more call-site changes away though. What happens if we put one in a switch anyway, just an errant KeyMacroN emission? Should that be an error, and would that require refactoring the rest of the call-sites anyway? But however you wanna do it, sure; the prefix definitions make me chuckle.

I'd just highlight that accepting the accidental complexity of repurposing unused keycodes resolves the type complexity without necessarily ruling out unification with localkeys. Require users to assign these ignored OsCodes to localkey atoms themselves and we preserve a minimal, customizable representation on the keymap, as well as making it clear how closely related they are. Again, talking without digging in to back this up, but as far as I can tell the only difference between localkeys and Seq0-9 (as long as we're making the "selected keycodes" compromise) is whether the assigned OsCode is one of those reserved no-ops. I'd leave the OpCodes unassigned by default, give them generic names (like, teach localkeys to expand NoOp0-NoOp9 into their #'s), and consider whether they behave appropriately when bound to localkeys and used in Sequences (and, perhaps incidentally, in switches) as-is. (I don't know that the prefix hack would as... straightforward, if that's what it is now :p)

Just sounds sleek to have unicode on the keymap and the ability to (re-)configure them like any other localkeys, that's the "vkey/vout" split right there, and I think you can still do that while dodging type changes by reserving OpCodes. But I could be missing something obv, and like I said, solves any use-case I can defend either way~

jtroo commented 6 months ago

I'd leave the OpCodes unassigned by default, give them generic names (like, teach localkeys to expand NoOp0-NoOp9 into their #'s), and consider whether they behave appropriately when bound to localkeys and used in Sequences (and, perhaps incidentally, in switches)

Hm yea I think the nop0-nop9 naming (shortening to 4 chars to match other key lengths) is good. As you mentioned these are indeed usable in switch|fork in addition to being useful in sequences. I personally prefer to leave these set by default though, so there's less for users to configure. In the code as it is today, it doesn't really matter if they are in use; if kanata has processed the key then the outputs will be suppressed.

There certainly could be features added in the future to manually configure which OsCode values are ignored and create one's own nopX keys with custom numbers. But for now I'll defer to YAGNI - the PR seems good enough as-is (once names are changed).

antler5 commented 6 months ago

The selection of opcodes is fine, I was only thinking about custom unicodes atoms-- but that's trivial to customize in src and you can get pretty close if aliases can include unicode anyway.

Thanks so much for hashin' it out with me c:

gerhard-h commented 6 months ago

There are cases where I do (multi @anykey F13) just to use F13 in a switch later on. If I can do the same with noop1 in the future I would prefer that, but I'm non sure I understood the concept.

antler5 commented 6 months ago

It matters most for Sequences with visible-backspace enabled because (IIRC) otherwise the Sequence or Switch eats the captured input anyways, but yes, as of the above merge it should Just Work to swap F13 out for any of the explicitly-arbitrary nop0-9 values. I agree that this is preferable because it reflects intent more clearly, but maybe high-value function keys were the real Worse is Better all along.

non sure I understood the concept

While I'm happy with and support the PR as-is (it works! :p), nop's could have been expressed as (deflocalkeys-linux nop0 XX [..]). I argued above that this would make nop examples double as additional supporting documentation for localkeys, and would love to know if you think requiring users to write that binding explicitly would have been more helpful or confusing for your understanding.

I'm specifically curious whether your uncertainty about the feature stems from how, while nop0-9 are better names then seq0-9, these keys -- called no-ops because they ""output"" suppressed keycodes -- aren't really "no-ops" from the user's perspective as soon as they're being used in a Seq. or Switch statement. Requiring users to define these bindings would not only make their conceptual nature as no-op localkeys explicit (clarifying where they can be used once defined), but allow users to define whatever terms feel natural to them, and hence avoid introducing (in the long-term, supporting) these terms that are defined by their OS-facing (lack of) outputs rather than their role in user-facing configuration and the intent of their user-defined behavior. You can still write an alias that resolves to nop0-9, so, do you have a term or unicode symbol in mind that you would use for such an alias if it were required? If so, perhaps it would have been clearer have users write (deflocalkeys-linux 🦆 XX [..]) ( *quack* >u<) and not expose nop0-9 in the first place.

(Though now that I'm thinking about it again, a user might reasonably expect every XX on their base later to be synonymous with such a localkey -- not so, because multiple localkeys can't have the same binding and XX would be a specially-handled exception that expends into nop0-9 keycodes sequentially, but I do realize there's a plot-hole of potential confusion in the solution I've fixated on)

jtroo commented 6 months ago

There are cases where I do (multi @anykey F13) just to use F13 in a switch later on. If I can do the same with noop1 in the future I would prefer that, but I'm non sure I understood the concept.

Yea the new nopX keys should be able to fulfill the old case of high-value F-keys in a (hopefully) better way

maybe high-value function keys were the real Worse is Better all along.

This has been the recommended solution for a long time 😆. But there has been at least one report that I can recall where someone was actually using the f13-f24 keys for another purpose, so they needed a different solution. The solution then was to add more logic to switch to allow processing on inputs.