rmk icon indicating copy to clipboard operation
rmk copied to clipboard

RMK: Thoughts on Design, Direction, and Customization

Open IniterWorker opened this issue 8 months ago • 13 comments

Hey folks,

I've recently started working with RMK, writing firmware for my Lily58. Like any good dev with questionable boundaries, I immediately went off the beaten path and started customizing things.

This post is mostly thoughts and feelings about where RMK is now and where it could go. It’s a bit philosophical, a bit technical, and definitely coming from someone who is new to keyboards' firmware and more into Rust. Let’s roll.


The Setup: My Custom Lily58

So here’s my setup: the Lily58 has 70 LEDs—58 under keys and 12 on the back panel. What I want is to create lighting effects based on events: user interactions, keyboard states, layer switches, whatever I can hook into.

In RMK today, the lighting system is fairly new, but it feels like it was designed around a “one-size-fits-all” model. For example:

pub(crate) struct LightService<'d, P: OutputPin, R: HidReaderTrait<ReportType = LedIndicator>> {
    pub(crate) enabled: bool,
    light_controller: &'d mut LightController<P>,
    reader: R,
}

It’s pin-based, non-async, and hard-wired into how RMK expects you to do lighting. Want to drive a bunch of WS2812s with an async external service? Not really in the cards.

I’m aiming for is something like this for a more Event Driven way with handler(s) that will allow me to write my own integration of the keyboard behaviour based on my hardware.

I want my LED logic to live outside RMK, in my keyboard integration repo—not inside the RMK core. And that brings me to the bigger topic…


RMK vs QMK: A Shift in Philosophy

QMK is a beast. It’s taught us all how to get keyboards working, fast. But it’s built around you configure what features you want, not you implement what your keyboard needs. You dig through headers, tweak defines, enable stuff like you’re turning on modules in a spaceship.

And that works—until it doesn’t. Want something QMK doesn’t support? Time to fork and patch.

What I’d love to see from RMK is the opposite mindset: you start with a clean core and plug in what you want. Traits over globals. Events over conditionals. Modular crates instead of monolithic code.


Where RMK Could Go (In My Ideal World)

Right now, RMK puts things like LED and display logic directly into the core. That’s fine if you’re building one universal firmware—but it makes real customization tricky.

Instead, imagine this layout:

// rmk
rmk          ← matrix scanning, debouncing, traits
rmk-macro         ← reusable keyboard concepts

// custom integrations / external projects
rmk-lily58         ← my implementation with 63 LEDs
rmk-display     ← example crate for display integration

Then from your keyboard repo, you implement the pieces you need. Want async WS2812 control? You bring your own. Want to skip LEDs altogether? No problem. Your build only depends on what you include.

This makes it easier to:

  • Write event-driven LED effects
  • Hook into external input/output (like a host-side daemon)
  • Split responsibilities across crates cleanly
  • Build a real library of reusable keyboard logic

TL;DR

  • RMK is already great—but could be even better by leaning more into modularity and flexibility.
  • Instead of “you configure what’s built-in,” we could aim for “you implement what you need.”
  • QMK showed us the way. RMK could take us further—leveraging Rust’s ecosystem, async power, and clean abstractions.

Would love to hear what others think. If this resonates, or if you’ve got ideas (or warnings 😅) from your own builds, I’m all ears.

That said, maybe I’m out of my depth here—and if I am, feel free to highlight where I’m missing the mark. I’m here to learn just as much as I’m here to build.

Cheers!

IniterWorker avatar Apr 24 '25 01:04 IniterWorker

Interesting thoughts, I really dislike the "fork and patch" approach, we currently have very little room for user customized code. I like the separate crates for LED and display, maybe we could do something similar for input devices as well?

Another idea is to have separate crates for each service, so rmk-vial would provide a VialService, rmk-display would provide a DisplayService.

pcasotti avatar Apr 24 '25 02:04 pcasotti

Thanks a lot for the thoughts!

Now my idea is like having something like Controller trait, which controls the other hardwares on the board, like LED/RGB/display, etc.

All external devices should implement Controller trait and it could be easily integrated to the project(yeah, it's just like InputDevice). The RMK core should expose the keyboard states and hooks for Controller to use. RMK could also provide common Controller implementations such as LEDs, WS2812, etc.

Both InputDevice and Controller need to be user-customizable, which is not implemented right now.

That's my current thoughts about it.

HaoboGu avatar Apr 24 '25 02:04 HaoboGu

That's my current thoughts about it.

Controller looks like it works with function calls.

  • Do you have thoughts about using a more event-driven approach instead?

Another point: for example, I would like to change the display or LED on the peripheral based on something from the central (e.g layer level, or caps). I see it's not really possible to talk both ways right now by design.

  • Do you have any thoughts about that?

IniterWorker avatar Apr 28 '25 03:04 IniterWorker

Do you have thoughts about using a more event-driven approach instead?

Yes, it's event-driven I think. It's similar as how InputDevice and InputProcessor work: the Controller waits for a channel, say CONTROL_EVENT_CHANNEL, if there's ControlEvent coming, the all Controllers process it one by one until the event is consumed.

HaoboGu avatar Apr 28 '25 03:04 HaoboGu

the Controller waits for a channel, say CONTROL_EVENT_CHANNEL, if there's ControlEvent coming, the all Controllers process it one by one until the event is consumed.

Another approach is to use a Watch for each "event", so controllers don't need to listen for all possible ControlEvents comming in the channel.

pcasotti avatar Apr 28 '25 04:04 pcasotti

Or a PubSubChannel? Because Watch overwrites the previous value by default.

HaoboGu avatar Apr 28 '25 05:04 HaoboGu

the Controller waits for a channel, say CONTROL_EVENT_CHANNEL, if there's ControlEvent coming, the all Controllers process it one by one until the event is consumed.

Another approach is to use a Watch for each "event", so controllers don't need to listen for all possible ControlEvents comming in the channel.

Hmmm, watch sounds a bit like the listener concept from Java and co, but maybe you meant that differently. RMK uses channels from embassy, which are implemented similar to tokio channels. These are Mutexes, which synchronize access to buffers. These are more similar to channels in go than listeners in java.

In keyboard.rs, line 54 you can see the main loop, which is trying to read KeyEvent from the KEY_EVENT_CHANNEL. If there is no event in the buffer the thread is suspended, until an event is written into the buffer. The loop stops execution, it doesn't run, like e.g. the javascript execution loop.

A received event is examined in a switch, which leads to different code.

Tl;dr: It is already event driven, using asynchronous channels. A watcher mechanism is not needed.

Please excuse, if I misunderstood you. I hope my explanation helps!

patmuk avatar May 06 '25 15:05 patmuk

Good idea to split the implementation into modules. I think that QMKs "re-define" approach comes from the language (C++) - and indeed, rust offers much better possibilities. The code is already modular in some areas. Have a look into the fork, combo and keyboard_macro implementation: These are modules, which are "turned on" by setting them to a BehaviorConfig - in a way a InversionOfControl mechanism. This could be enhanced by using traits (like ForkConfig_trait) for the BehaviorConfig fields, so that a user could use rmk's standard implementation or another alternative from another crate.

However, the functionality of fork, combo and keyboard macro is implemented in keyboard.rs. I cannot imagine how to do this in a different module or even crate ... but that might be possible. The hard point is the switch, which decides according to the KeyEvent which code to execute further. So one would have to dynamically add enum variants of other crates to the KeyEvent enum ... or more probably implement this differently.

I personally would prefer implementing extended or alternative functionality into rmk directly and choosing the desired behavior via configuration - so that all users can benefit from this. Really really unique and for others not useful things could be implemented in the own rust configuration project (which is indeed limited).

Lastly one goal of rmk is an easy configuration via a toml file. It would be hard to add custom crates here as well - but I guess it is ok to require a rust based configuration for this.

patmuk avatar May 06 '25 15:05 patmuk

Hmmm, watch sounds a bit like the listener concept from Java and co, but maybe you meant that differently. RMK uses channels from embassy, which are implemented similar to tokio channels.

I mean watch from embassy, my point is that we might not need previous events for the controller, so the overwriting of the previous value is actually what I intended.

pcasotti avatar May 06 '25 16:05 pcasotti

Hmmm, watch sounds a bit like the listener concept from Java and co, but maybe you meant that differently. RMK uses channels from embassy, which are implemented similar to tokio channels.

I mean watch from embassy, my point is that we might not need previous events for the controller, so the overwriting of the previous value is actually what I intended.

Ah, good - I could have saved my long explanations :) Do you have in mind that multiple receivers could connect to a KeyEvent using watch, so they could execute code and be in another crate? That might work. There is a danger that subsequent button presses might be overwritten before code is executed - but maybe just theoretically (slow processor, demanding code and fast typer).

patmuk avatar May 07 '25 07:05 patmuk

“you configure what features you want, not you implement what your keyboad”

I have some different opinions. QMK actually opens up a large number of callback functions and user-defined functions that can be overridden. In fact, users who are willing to use C language to write code can enjoy the freedom brought by this API system. Personally, I think the user space API for rmk should be richer. QMK is a worthy object to learn, excluding chibios.😊

hitsmaxft avatar May 17 '25 02:05 hitsmaxft

They are not conflicted I think. From my perspective, I would like RMK to be easily extended and customized, but also provide great built-in components that users can use directly. I know it's very difficult tho, but with Rust's typesystem I believe it can be implemented with a more elegant way than callbacks.

HaoboGu avatar May 17 '25 11:05 HaoboGu

They are not conflicted I think. From my perspective, I would like RMK to be easily extended and customized, but also provide great built-in components that users can use directly. I know it's very difficult tho, but with Rust's typesystem I believe it can be implemented with a more elegant way than callbacks.

I couldn't agree more with you.

For an asynchronous API design, callback is always the simplest, but also the most primitive, rough, and difficult to maintain on statusful things。

However, it is not an easy choice to provide a better method; both pure functions and actors have their own problems.😂

hitsmaxft avatar May 18 '25 05:05 hitsmaxft