diff --git a/app/dts/bindings/behaviors/zmk,behavior-sensor-rotate-key-press.yaml b/app/dts/bindings/behaviors/zmk,behavior-sensor-rotate-key-press.yaml index 1fc60fcf..41ff368d 100644 --- a/app/dts/bindings/behaviors/zmk,behavior-sensor-rotate-key-press.yaml +++ b/app/dts/bindings/behaviors/zmk,behavior-sensor-rotate-key-press.yaml @@ -13,6 +13,12 @@ properties: type: int required: true const: 2 + modifier-key: + type: int + default: -1 + mod-timeout-ms: + type: int + default: -1 sensor-binding-cells: - param1 diff --git a/app/src/behaviors/behavior_sensor_rotate_key_press.c b/app/src/behaviors/behavior_sensor_rotate_key_press.c index c5b5a3f0..35953102 100644 --- a/app/src/behaviors/behavior_sensor_rotate_key_press.c +++ b/app/src/behaviors/behavior_sensor_rotate_key_press.c @@ -18,12 +18,54 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); #if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) -static int behavior_sensor_rotate_key_press_init(const struct device *dev) { return 0; }; +#define ZMK_BHV_SENSOR_MAX_MODS 2 + +struct behavior_sensor_rotate_key_press_config { + int modifier_key; + int mod_timeout_ms; +}; + +struct active_mod_press { + struct k_delayed_work work; + const struct behavior_sensor_rotate_key_press_config *config; + int64_t last_timestamp; +}; + +struct active_mod_press active_mod_presses[ZMK_BHV_SENSOR_MAX_MODS] = {}; + +void behavior_sensor_rotate_key_press_work_handler(struct k_work *item) { + struct active_mod_press *c = CONTAINER_OF(item, struct active_mod_press, work); + const struct behavior_sensor_rotate_key_press_config *cfg = c->config; + + /* timeout expired, release modifier */ + ZMK_EVENT_RAISE( + zmk_keycode_state_changed_from_encoded(cfg->modifier_key, false, k_uptime_get())); + c->config = NULL; + c->last_timestamp = 0; +} + +static int behavior_sensor_rotate_key_press_init(const struct device *dev) { + static bool init_first_run = true; + + if (init_first_run) { + init_first_run = false; + for (int i = 0; i < ZMK_BHV_SENSOR_MAX_MODS; i++) { + k_delayed_work_init(&active_mod_presses[i].work, + behavior_sensor_rotate_key_press_work_handler); + active_mod_presses[i].config = NULL; + active_mod_presses[i].last_timestamp = 0; + } + } + return 0; +} static int on_sensor_binding_triggered(struct zmk_behavior_binding *binding, const struct device *sensor, int64_t timestamp) { struct sensor_value value; int err; + const struct device *dev = device_get_binding(binding->behavior_dev); + const struct behavior_sensor_rotate_key_press_config *cfg = dev->config; + uint32_t keycode; LOG_DBG("inc keycode 0x%02X dec keycode 0x%02X", binding->param1, binding->param2); @@ -47,8 +89,52 @@ static int on_sensor_binding_triggered(struct zmk_behavior_binding *binding, LOG_DBG("SEND %d", keycode); + bool mod_timeout_enabled = cfg->mod_timeout_ms != -1; + struct active_mod_press *c = NULL; + + if (mod_timeout_enabled) { + // lookup slot + for (int i = 0; i < ZMK_BHV_SENSOR_MAX_MODS; i++) { + if (active_mod_presses[i].config == cfg) { + c = &active_mod_presses[i]; + c->config = cfg; + } + } + + // nothing found, take a slot + if (c == NULL) { + for (int i = 0; i < ZMK_BHV_SENSOR_MAX_MODS; i++) { + if (active_mod_presses[i].config == NULL) { + c = &active_mod_presses[i]; + c->config = cfg; + } + } + } + + if (c == NULL) { + LOG_WRN("increase ZMK_BHV_SENSOR_MAX_MODS"); + } + + if ((timestamp - c->last_timestamp) < cfg->mod_timeout_ms) { + /* another input, restart timeout */ + k_delayed_work_cancel(&c->work); + } else { + /* first time, activate modifier key */ + ZMK_EVENT_RAISE( + zmk_keycode_state_changed_from_encoded(cfg->modifier_key, true, timestamp)); + + // TODO: Better way to do this? + k_msleep(5); + } + } + ZMK_EVENT_RAISE(zmk_keycode_state_changed_from_encoded(keycode, true, timestamp)); + if (mod_timeout_enabled) { + c->last_timestamp = timestamp; + k_delayed_work_submit(&c->work, K_MSEC(cfg->mod_timeout_ms)); + } + // TODO: Better way to do this? k_msleep(5); @@ -56,11 +142,18 @@ static int on_sensor_binding_triggered(struct zmk_behavior_binding *binding, } static const struct behavior_driver_api behavior_sensor_rotate_key_press_driver_api = { - .sensor_binding_triggered = on_sensor_binding_triggered}; + .sensor_binding_triggered = on_sensor_binding_triggered, +}; #define KP_INST(n) \ + const struct behavior_sensor_rotate_key_press_config \ + behavior_sensor_rotate_key_press_config_##n = { \ + .modifier_key = DT_INST_PROP(n, modifier_key), \ + .mod_timeout_ms = DT_INST_PROP(n, mod_timeout_ms), \ + }; \ DEVICE_DT_INST_DEFINE(n, behavior_sensor_rotate_key_press_init, device_pm_control_nop, NULL, \ - NULL, APPLICATION, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, \ + &behavior_sensor_rotate_key_press_config_##n, APPLICATION, \ + CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, \ &behavior_sensor_rotate_key_press_driver_api); DT_INST_FOREACH_STATUS_OKAY(KP_INST) diff --git a/docs/docs/assets/features/encoders/highlighter.gif b/docs/docs/assets/features/encoders/highlighter.gif new file mode 100644 index 00000000..0cda8ea2 Binary files /dev/null and b/docs/docs/assets/features/encoders/highlighter.gif differ diff --git a/docs/docs/assets/features/encoders/supertab.gif b/docs/docs/assets/features/encoders/supertab.gif new file mode 100644 index 00000000..68409920 Binary files /dev/null and b/docs/docs/assets/features/encoders/supertab.gif differ diff --git a/docs/docs/features/encoders.md b/docs/docs/features/encoders.md index 225ee6f3..6dcfae4c 100644 --- a/docs/docs/features/encoders.md +++ b/docs/docs/features/encoders.md @@ -43,6 +43,46 @@ sensor-bindings = <&inc_dec_kp C_VOL_UP C_VOL_DN &inc_dec_kp PG_UP PG_DN>; Here, the left encoder is configured to control volume up and down while the right encoder sends either Page Up or Page Down. +### Modifier with built-in delay + +In addition to the keycodes above a modifier (`modifier-key`) can be configured that is pressed simultaneously with the configured keycodes, and then held until another keycode is send via an encoder action or a timeout (`mod-timeout-ms`) is reached. + +Example for application switching: + +``` +sm: supermods { + compatible = "zmk,behavior-sensor-rotate-key-press"; + label = "encoder supermod"; + #sensor-binding-cells = <2>; + modifier-key = ; + mod-timeout-ms = <1200>; +}; + +[...] + +sensor-bindings = <&sm TAB LS(TAB)>; +``` + +![Application switching with rotary encoder](../assets/features/encoders/supertab.gif) + +Example for selecting characters in an text editor: + +``` +hlm: highlightmods { + compatible = "zmk,behavior-sensor-rotate-key-press"; + label = "encoder highlighter"; + #sensor-binding-cells = <2>; + modifier-key = ; + mod-timeout-ms = <500>; +}; + +[...] + +sensor-bindings = <&hlm RIGHT LEFT>; +``` + +![Select characters with rotary encoder](../assets/features/encoders/highlighter.gif) + ## Adding Encoder Support See the [New Keyboard Shield](../development/new-shield.md#encoders) documentation for how to add or modify additional encoders to your shield.