KMKfw / kmk_firmware

Clackety Keyboards Powered by Python
https://kmkfw.zulipchat.com
Other
1.32k stars 458 forks source link

[Enhancement] Conditional module/extension #956

Open p3lim opened 3 months ago

p3lim commented 3 months ago

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

I use a lot of keys that depend on other keys being pressed, and the closest solution to this in KMK that I've found is to create a a bunch of custom key like this:

from kmk.keys import make_key

def on_pressed(key, keyboard, KC, *args, **kwargs):
  SPACE = {KC.SPACE}
  if SPACE.intersection(keyboard.keys_pressed):
    keyboard.keys_pressed.add(KC.A)
  else:
    keyboard.keys_pressed.add(KC.B)
  keyboard.hid_pending = True
  return keyboard

def on_released(key, keyboard, KC, *args, **kwargs):
  keyboard.keys_pressed.discard(KC.A)
  keyboard.keys_pressed.discard(KC.B)
  keyboard.hid_pending = True
  return keyboard

make_key(names=('SPAB'), on_press=on_pressed, on_release=on_released)

Describe the solution you'd like

A module/extension that makes this easier. Using the same example keys as above:

# syntax:
#  if   key(s)     then  else
KC.CK((KC.SPACE,), KC.A, KC.B)

This might be very specific to my usecase, so it's understandable if it gets denied, I just want to clean up my messy main.py :smile:.

p3lim commented 3 months ago

Thinking maybe something as simple as this?

from kmk.keys import make_argumented_key
from kmk.modules import Module

class ConditionalKeyMeta:
    def __init__(self, mods, key1, key2): # could use some type checks
        self.mods = set(mods)
        self.key1 = key1
        self.key2 = key2

class ConditionalKey(Module):
    def __init__(self):
        make_argumented_key(
            names=('CK',),
            validator=ConditionalKeyMeta,
        )

    def during_bootup(self, keyboard):
        return

    def before_matrix_scan(self, keyboard):
        return

    def after_matrix_scan(self, keyboard):
        return

    def process_key(self, keyboard):
        if is_pressed:
            if self.meta.mods.intersection(keyboard.keys_pressed):
                key = self.meta.key1
            else:
                key = self.meta.key2
        return key

    def before_hid_send(self, keyboard):
        return

    def after_hid_send(self, keyboard):
        return

    def on_powersave_enable(self, keyboard):
        return

    def on_powersave_disable(self, keyboard):
        return

Haven't made a module before, my knowledge of KMK is still new, and I'm currently unable to test if it works as my new keyboard I will put KMK on hasn't arrived just yet.

xs5871 commented 3 months ago

You're basically asking for a "ModMorph" or "KeyOverride" kind of module. I had that in my pipeline for a year and been using an mvp similar to yours. But when I got to thinking about unit tests and all the edge cases it got burried in the maybe-later pile.

b2ox commented 1 month ago

I have modified the code to make it work.

from kmk.keys import make_argumented_key
from kmk.modules import Module

class ConditionalKeyMeta:
    def __init__(self, mods, keyThen, keyElse):
        self.mods = set(mods)
        self.keyThen = keyThen
        self.keyElse = keyElse

class ConditionalKey(Module):
    def __init__(self):
        make_argumented_key(
            names=('CK',),
            validator=ConditionalKeyMeta,
        )

    def during_bootup(self, keyboard):
        return

    def before_matrix_scan(self, keyboard):
        return

    def after_matrix_scan(self, keyboard):
        return

    def process_key(self, keyboard, key, is_pressed, int_coord):
        if not isinstance(key.meta, ConditionalKeyMeta):
            return key

        if key.meta.mods.issubset(keyboard.keys_pressed):
            return key.meta.keyThen

        return key.meta.keyElse

    def before_hid_send(self, keyboard):
        return

    def after_hid_send(self, keyboard):
        return

    def on_powersave_enable(self, keyboard):
        return

    def on_powersave_disable(self, keyboard):
        return