Compare commits

..

4 commits
v1.0.0 ... main

Author SHA1 Message Date
618aaea115 README: clarify v1.0 vs v1.1 API for separate module repos 2026-07-01 16:08:01 +08:00
e7b543c501 README: add separate module repo guide 2026-07-01 15:49:32 +08:00
e800222842 Add README with setup, usage, and custom effect guide 2026-07-01 15:46:36 +08:00
e2a7af15f4 feat: implement all 6 fork parity behaviors
- ZMK_RGB_EFFECT_PERSISTENT flag: layer effect survives RGB toggle
- on_idle callback: wake re-renders layer, sleep turns off without
  clearing layer_effect_active
- is_active callback: ext power stays on for layer RGB
- Wakeup filtering: battery events (wakeup=false) don't wake sleeping
  RGB, layer changes (wakeup=true) do
- Layer filtering: only refresh when changed layer matches active top
  layer, skip unnecessary re-renders
- Fade delay: calls zmk_rgb_set_tick_delay() with per-layer DT
  fade-delay prop after rendering static pixels

Requires: zmk fork rgb-modular-v1.1 branch
2026-07-01 15:17:06 +08:00
2 changed files with 53 additions and 15 deletions

View file

@ -2,7 +2,7 @@
Modular RGB effects for ZMK keyboards. Provides per-key per-layer RGB indicators and a framework for adding custom effects without forking ZMK.
Requires: [ZMK fork](https://github.com/afiqzudinhadi/zmk) branch `rgb-modular-v1.0` (modular effect registration API).
Requires: [ZMK fork](https://github.com/afiqzudinhadi/zmk) branch `rgb-modular-v1.1` (modular effect registration API).
## Setup
@ -16,11 +16,11 @@ manifest:
projects:
- name: zmk
remote: afiqzudinhadi
revision: rgb-modular-v1.0
revision: rgb-modular-v1.1
import: app/west.yml
- name: zmk-rgb-effects
remote: afiqzudinhadi
revision: v1.0.0
revision: v1.1.1
```
Enable in your `.conf`:
@ -55,6 +55,7 @@ Define per-key colors in your keymap/overlay using devicetree:
layer1 {
layer-id = <1>;
fade-delay = <15>;
bindings = <
&ugc RGB_BLUE &ugc RGB_BLUE &ugc RGB_OFF ...
>;
@ -67,6 +68,7 @@ Define per-key colors in your keymap/overlay using devicetree:
- `pixel-lookup` — maps LED chain index to key position
- `layer-id` — which keymap layer this color map applies to
- `fade-delay` — seconds before reverting to animated effect (-1 = never)
- `bindings` — color per key using behaviors below
### Behaviors
@ -94,7 +96,7 @@ static void my_render(struct zmk_rgb_effect_ctx *ctx) {
}
ZMK_RGB_EFFECT_DEFINE(my_effect, "My Effect",
my_render, 0, NULL, NULL);
my_render, 0, NULL, NULL, NULL, NULL);
```
Add to `CMakeLists.txt`:
@ -150,7 +152,12 @@ Add to your `west.yml`:
revision: main
```
Requires the ZMK fork with the modular effect API (`rgb-modular-v1.0` or later). No dependency on this module.
Requires the ZMK fork with the modular effect API. No dependency on this module — the linker section collects effect registrations across all modules.
- `rgb-modular-v1.0` — basic effect API (`ZMK_RGB_EFFECT_DEFINE` with 6 params: render, flags, on_select, on_deselect). Sufficient for simple animated or static effects.
- `rgb-modular-v1.1` — extended API (adds `on_idle`, `is_active` callbacks, `ZMK_RGB_EFFECT_PERSISTENT` flag, `zmk_rgb_request_refresh_wakeup()`, `zmk_rgb_set_tick_delay()`, `zmk_rgb_is_on()`). Needed for effects that persist across RGB toggle/idle or use fade-delay.
Start with `rgb-modular-v1.0` if you want to explore the API from scratch.
### Effect flags
@ -158,6 +165,7 @@ Requires the ZMK fork with the modular effect API (`rgb-modular-v1.0` or later).
|------|-------------|
| `0` | Animated — core runs render at 25ms tick |
| `ZMK_RGB_EFFECT_STATIC` | No periodic tick — render called on demand via `zmk_rgb_request_refresh()` |
| `ZMK_RGB_EFFECT_PERSISTENT` | Survives RGB toggle and idle sleep |
### Effect callbacks
@ -166,6 +174,8 @@ Requires the ZMK fork with the modular effect API (`rgb-modular-v1.0` or later).
| `render` | Each tick (animated) or on refresh (static) |
| `on_select` | Effect becomes active |
| `on_deselect` | User cycles to different effect |
| `on_idle(bool awake)` | Keyboard sleeps/wakes (persistent effects only) |
| `is_active()` | Queried for ext power gating — return `true` to keep LEDs powered |
### Render context
@ -183,6 +193,9 @@ Requires the ZMK fork with the modular effect API (`rgb-modular-v1.0` or later).
- `zmk_rgb_hsb_scale_min_max(hsb)` — scale brightness to configured min/max
- `zmk_rgb_hsb_scale_zero_max(hsb)` — scale brightness from 0 to max
- `zmk_rgb_request_refresh()` — trigger re-render for static effects
- `zmk_rgb_request_refresh_wakeup(bool)` — refresh with conditional wake from sleep
- `zmk_rgb_set_tick_delay(int seconds)` — delayed tick restart (for fade effects)
- `zmk_rgb_is_on()` — query RGB on/off state
## Versioning

View file

@ -90,11 +90,16 @@ static void layer_effect_render(struct zmk_rgb_effect_ctx *ctx) {
uint8_t layer = rgb_underglow_top_layer();
const struct zmk_behavior_binding *rgbmap = rgb_underglow_get_bindings(layer);
if (rgbmap != NULL) {
apply_rgbmap(ctx->pixels, ctx->num_pixels, rgbmap, ZMK_KEYMAP_LEN,
layer, ctx->base_color.b);
if (apply_rgbmap(ctx->pixels, ctx->num_pixels, rgbmap, ZMK_KEYMAP_LEN,
layer, ctx->base_color.b)) {
int fade_delay = zmk_rgbmap_fade_delay(layer);
if (fade_delay >= 0) {
zmk_rgb_set_tick_delay(fade_delay);
}
}
} else {
for (int i = 0; i < ctx->num_pixels; i++) {
ctx->pixels[i] = (struct led_rgb){r : 0, g : 0, b : 0};
if (zmk_rgb_is_on()) {
zmk_rgb_underglow_transient_off();
}
}
}
@ -107,11 +112,27 @@ static void layer_effect_on_deselect(void) {
layer_effect_active = false;
}
ZMK_RGB_EFFECT_DEFINE(effect_layer, "Layer Indicators",
layer_effect_render, ZMK_RGB_EFFECT_STATIC,
layer_effect_on_select, layer_effect_on_deselect);
static void layer_effect_on_idle(bool awake) {
if (awake) {
if (layer_effect_active) {
zmk_rgb_request_refresh_wakeup(true);
}
} else {
zmk_rgb_underglow_transient_off();
}
}
/* Event listeners — refresh pixels on layer/color change */
static bool layer_effect_is_active(void) {
return layer_effect_active;
}
ZMK_RGB_EFFECT_DEFINE(effect_layer, "Layer Indicators",
layer_effect_render,
ZMK_RGB_EFFECT_STATIC | ZMK_RGB_EFFECT_PERSISTENT,
layer_effect_on_select, layer_effect_on_deselect,
layer_effect_on_idle, layer_effect_is_active);
/* Event listeners */
static int layer_effect_event_listener(const zmk_event_t *eh) {
if (!layer_effect_active) {
@ -126,13 +147,17 @@ static int layer_effect_event_listener(const zmk_event_t *eh) {
#if !IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
set_peripheral_layers_state(ev->layers);
#endif
zmk_rgb_request_refresh();
zmk_rgb_request_refresh_wakeup(true);
return 0;
}
#endif
if (as_zmk_underglow_color_changed(eh)) {
zmk_rgb_request_refresh();
const struct zmk_underglow_color_changed *ev = as_zmk_underglow_color_changed(eh);
uint8_t layer = rgb_underglow_top_layer();
if ((ev->layers & BIT(layer)) == BIT(layer)) {
zmk_rgb_request_refresh_wakeup(ev->wakeup);
}
return 0;
}