zmkfirmware / zmk

ZMK Firmware Repository
https://zmk.dev/
MIT License
2.82k stars 2.85k forks source link

Feature request: keep-mods-combined or check-mods-exclusively? #2576

Open tomdl89 opened 1 month ago

tomdl89 commented 1 month ago

What's missing?

mod-morph behaviors can list mods (which trigger the modified binding) and keep-mods (which are passed on along with the modified binding) but there's no way to specify a mod as needing to be not kept when pressed alone, but kept when pressed along with other mod(s).

What could fix this?

Two ideas. Firstly, mod-morphs could include a new property keep-mods-combined which lists mods that should be "kept" when they are pressed with others (listed in mods), but defer to keep-mods when they are the only mod. Second idea would be to have a boolean property check-mods-exclusively?; when true, mods not listed under keep-mods are kept nonetheless when combined with a mod listed under keep-mods. The former is more granular, and the latter is quicker, and possibly simpler to understand.

Why would this be useful?

There may be other use cases, but for me, I "invert" my number row by assigning to each key &inv_1, &inv_2 etc. which are mod-morphs that I have made, swapping the unshifted and shifted versions of each number key. For each mod-morph, I have all the mods listed under mods and all mods except for MOD_LSFT and MOD_RSFT listed under keep-mods. For single modifiers, this works great. E.g. LCTL+1 gives me LCTL+1 just as I want. But for multiple modifiers where one of them is a shift key, it swallows the shift key (i.e. doesn't "keep" it) because MOD_LSFT and MOD_RSFT are (intentionally) not listed under keep-mods. E.g. LCTL+LSFT+1 just gives LCTL+1. I want the shift keys to be "not kept" when a shift key is the only modifier (otherwise I couldn't get a 1 when I need it) but when combined with another, "kept".

urob commented 1 month ago

IIRC implicit mods are always passed through. So you might be able to achieve this by nesting two mod-morphs and binding an implicit mod to the inner one.

That is, something like: outer mod-morph activated by shift binds inner morph activated by ctrl on the morphed slot. The inner binds LS(LC(N1) on the morphed slot.

tomdl89 commented 1 month ago

@urob can you be more specific? I have tried a few things in this direction already with no success, but want to make sure I'm not overlooking something. If outer mod is only activated by shift, then LC+1 would give LC+LS+1 which is not what I'd want. If inner mod binds LS(LC(N1)) then LGUI(LS(N1)) would give LS(LC(N1)) which is also not want I'd want. Maybe I'm misunderstanding here. If you have the time/patience, could you show what mods should be listed as mods, what mods should be listed at keep-mods, and what keys should be in the normal & modified slots in each nesting level in your proposed solution?

urob commented 1 month ago

It might be more economically to activate the outer morph with LCTL|LGUI|LALT, to avoid having to nest all combinations of mods:

outer_morph
- mods = LCTL|LGUI|LALT
- keep-mods = LCTL|LGUI|LALT
- binding (unmorphed) = inner_morph
- binding (morphed) = N1              # at least one mod other than shift is active,
                                      # all mods including shift are passed through

Then define the inner morph as follows:

inner_morph
- mods = LSFT
- binding (unmorphed) = LS(N1)        # no mods are active
- binding (morphed) = N1              # only shift is active (which is masked)

(You can wrap both behavior definitions into a single pre-processor macro to efficiently generate the entire numbers row)