rmk icon indicating copy to clipboard operation
rmk copied to clipboard

Q: adding chording keyboard support

Open akavel opened this issue 9 months ago • 11 comments

Hi! I'm developing a chording keyboard, with firmware written in Rust (current code at: https://github.com/akavel/clawtype; some photos of the current version at: https://www.printables.com/model/1231156-clawtype). I wonder, would you be open for me to try and add features of such a keyboard to RMK? If yes, would you have some guidelines on where and how to do this?

FWIW, I think it might make sense to make it some kind of a separate "major mode" or something; or maybe a layer mode, hard to say; the point is, in such a chording keyboard, chords are a primary means of operation. On the other hand, I do have one layer for use with a (gyroscope-based) mouse, where I "disconnect" two keys from the chording mode, to make them work as "normal" (non-chorded) mouse buttons. Notably, in this keyboard the keys must be non-matrix, each one needs to be connected to a separate GPIO, as I must be able to properly handle "full NKRO".

I'm writing to ask about your thoughts on whether this might be a useful thing to try to add to RMK, and you could have some suggestions how I could try to do that - or if you don't think so.

akavel avatar Mar 30 '25 00:03 akavel

I'm quite open to this, RMK provides direct pin matrix and combo support which might be useful for your case. But as far as I can tell, there are still work to do, such as NKRO, configurable number of combos. etc.

I have to say, this could be hard to implement all those features, but if you want to try, I'd be happy to help.

HaoboGu avatar Mar 30 '25 03:03 HaoboGu

Thanks for the reply and openness!

So, my first important question would be: does rmk currently have any support for NON-matrix keys? Sorry if I misunderstood your reply, I just need to clearly know the status of this first, it is quite foundational for my case 😅

akavel avatar Mar 30 '25 20:03 akavel

Yes, there's direct_pin just for it. The direct pin is organized to matrix, but you can use a 1*N matrix.

HaoboGu avatar Mar 31 '25 02:03 HaoboGu

That sounds great - I would like to implement chording as well for my sweep. It looks to me like there is only a macro KeyAction missing - meaning outputting several keys when triggering a combo.

For example: Holding the "chording-key" and pressing H E would output "hello". This could be something like

Combo::new([k!(H), k!(E)], KeyAction::Macro([k!(H), k!(E), k!(L), k!(L), k!(O)], Some(2));

where the "chording key" activates the layer 2. KeyAction::Macro would need to be implemented - but that might not be hard?

patmuk avatar Apr 01 '25 21:04 patmuk

Can you show a preview, how the config file will look like?

tib888 avatar Apr 15 '25 10:04 tib888

Can you show a preview, how the config file will look like?

Sure: In the example I will define two Macros:

In your keymap.rs:

pub(crate) fn get_macro_sequences() -> [u8; MACRO_SPACE_SIZE] {
    define_macro_sequences(&[
        Vec::from_slice(&[
            MacroOperation::Text(KeyCode::H, true),
            MacroOperation::Text(KeyCode::E, false),
            MacroOperation::Text(KeyCode::L, false),
            MacroOperation::Text(KeyCode::L, false),
            MacroOperation::Text(KeyCode::O, false),
        ])
        .expect("too many elements"),
        Vec::from_slice(&[
            MacroOperation::Press(KeyCode::LShift),
            MacroOperation::Tap(KeyCode::W),
            MacroOperation::Release(KeyCode::LShift),
            MacroOperation::Tap(KeyCode::O),
            MacroOperation::Tap(KeyCode::R),
            MacroOperation::Tap(KeyCode::L),
            MacroOperation::Tap(KeyCode::D),
        ])
        .expect("too many elements"),
    ])
}

    CombosConfig {
        combos: Vec::from_slice(&[
            Combo::new([k!(H), k!(E)], k!(Macro0), Some(1)),
            Combo::new([k!(W), k!(D)], KeyAction::Single(Action::TriggerMacro(1), Some(1)),
        ])
        .expect("too many combo definitions!"),
        timeout: Duration::from_millis(50),
    }

Layer 1 in this example is a layer for chords only ... I did that so that I have a shift-key to intentionally trigger the chords and separate them from other combos.

The macros are defined in an array - and thus automatically get their index assigned. Thus the first array element is bound to KeyCode::Macro0, the second to KeyCode::Macro1, etc. This is how Vial does it.

This already works in the PR :)

I am currently working on the second combo definition: Using an Action::TriggerMacro(idx) as an alternative to the KeyCode::MacroX. The idea is that for the same indexes (0 = 31) either can be used interchangeable. But if one needs more than 32 macro tiggers (up to 256), one can use TriggerMacro instead.

patmuk avatar Apr 15 '25 10:04 patmuk

In central.rs, within BehaviorConfig there is a new field:

    let behavior_config = BehaviorConfig {
        combo: keymap::get_combos(),
        tap_hold: TapHoldConfig {enable_hrm: true, ..Default::default()},
        fork: keymap::get_forks(),
        macros: KeyboardMacrosConfig::new(keymap::get_macro_sequences()),
        ..Default::default()
    };

patmuk avatar Apr 15 '25 10:04 patmuk

Can you show a preview, how the config file will look like?

To not confuse: In the comment you replied to I wrote my idea before implementing, now I am nearly done in the way I described above :)

patmuk avatar Apr 15 '25 10:04 patmuk

Nice! And do you want to edit these macros later with Vial? Or is it possible at all to use Vial for this if you have so many of them?

If the answer is "No", then an alternative implementation may make sense, where these predefined macros would sit only in the program code as compiled constant strings (in the EEPROM, no need for any RAM space). And a simple code would "play" them just like currently the text macros... - so firmware rebuild would be needed if you want to change them. At least this is just how I would do it.

But even if you accept this idea, please finish first the macro configuration part, because I need that. :)

tib888 avatar Apr 15 '25 13:04 tib888

Thanks :)

Good idea to save RAM space ... yes, with the current implementation KeyCode::Macro0 - 31 can trigger the macros as described above. And they show up cleanly in Vial, and can be changed or configured from there as well :) So this is basically done.

Yes, as I plan to trigger potentially all macros via combos they don't need to map to real KeyCodes. So they could be implemented with another mechanism and stored differently ...

I am close to finish now :) Triggering KeyAction::Single(Action::TriggerMacro(x)) works assigned from a combo, as I need it!

It doesn't work when assigned in the keymap directly - but assigning KeyCode::MacroX there works. I will implement to_via_keycode to map to that keycode for the first 32 macros - so that should be ok. The limitation remaining is that Action::TriggerMacro(x) will not work when assigned to a key directly. I guess this is rarly needed ... and even if one could use another key and reassign it to that macro with fork or combo :)

Wrapping up now and documenting the PR. Will be finished today :)

patmuk avatar Apr 15 '25 14:04 patmuk

@tib888 check out https://github.com/HaoboGu/rmk/pull/337, all done :)

patmuk avatar Apr 15 '25 19:04 patmuk

I think this issue can be closed, now RMK's combo and macro support would solve this issue.

HaoboGu avatar Jul 15 '25 14:07 HaoboGu