diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 41892915..017dfea7 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -69,6 +69,7 @@ if ((NOT CONFIG_ZMK_SPLIT) OR CONFIG_ZMK_SPLIT_ROLE_CENTRAL) target_sources(app PRIVATE src/events/modifiers_state_changed.c) target_sources(app PRIVATE src/events/keycode_state_changed.c) target_sources_ifdef(CONFIG_ZMK_HID_INDICATORS app PRIVATE src/hid_indicators.c) + target_sources(app PRIVATE src/behaviors/behavior_capslock.c) if (CONFIG_ZMK_BLE) target_sources(app PRIVATE src/events/ble_active_profile_changed.c) diff --git a/app/dts/behaviors.dtsi b/app/dts/behaviors.dtsi index 23f2fee2..0fd9c40c 100644 --- a/app/dts/behaviors.dtsi +++ b/app/dts/behaviors.dtsi @@ -20,3 +20,4 @@ #include #include #include +#include diff --git a/app/dts/behaviors/capslock.dtsi b/app/dts/behaviors/capslock.dtsi new file mode 100644 index 00000000..aab98a87 --- /dev/null +++ b/app/dts/behaviors/capslock.dtsi @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2023 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include + +/ { + behaviors { + /omit-if-no-ref/ capslock_on: behavior_capslock_on { + compatible = "zmk,behavior-capslock"; + label = "CAPSLOCK_ON"; + #binding-cells = <0>; + capslock-press-duration = <5>; + enable-on-press; + }; + + /omit-if-no-ref/ capslock_off: behavior_capslock_off { + compatible = "zmk,behavior-capslock"; + label = "CAPSLOCK_OFF"; + #binding-cells = <0>; + capslock-press-duration = <5>; + disable-on-release; + }; + + /omit-if-no-ref/ capslock_hold: behavior_capslock_hold { + compatible = "zmk,behavior-capslock"; + label = "CAPSLOCK_HOLD"; + #binding-cells = <0>; + capslock-press-duration = <5>; + enable-on-press; + disable-on-release; + }; + + /omit-if-no-ref/ capslock_word: behavior_capslock_word { + compatible = "zmk,behavior-capslock"; + label = "CAPSLOCK_WORD"; + #binding-cells = <0>; + capslock-press-duration = <5>; + enable-on-press; + disable-on-next-release; + disable-on-keys = ; + }; + + /* MacOS compatibility */ + + /omit-if-no-ref/ capslock_on_mac: behavior_capslock_on_mac { + compatible = "zmk,behavior-capslock"; + label = "CAPSLOCK_ON_MAC"; + #binding-cells = <0>; + capslock-press-duration = <95>; + enable-on-press; + }; + + /omit-if-no-ref/ capslock_off_mac: behavior_capslock_off_mac { + compatible = "zmk,behavior-capslock"; + label = "CAPSLOCK_OFF_MAC"; + #binding-cells = <0>; + capslock-press-duration = <95>; + disable-on-release; + }; + + /omit-if-no-ref/ capslock_hold_mac: behavior_capslock_hold_mac { + compatible = "zmk,behavior-capslock"; + label = "CAPSLOCK_HOLD_MAC"; + #binding-cells = <0>; + capslock-press-duration = <95>; + enable-on-press; + disable-on-release; + }; + + /omit-if-no-ref/ capslock_word_mac: behavior_capslock_word_mac { + compatible = "zmk,behavior-capslock"; + label = "CAPSLOCK_WORD_MAC"; + #binding-cells = <0>; + capslock-press-duration = <95>; + enable-on-press; + disable-on-next-release; + disable-on-keys = ; + }; + + }; +}; \ No newline at end of file diff --git a/app/dts/bindings/behaviors/zmk,behavior-capslock.yaml b/app/dts/bindings/behaviors/zmk,behavior-capslock.yaml new file mode 100644 index 00000000..17229559 --- /dev/null +++ b/app/dts/bindings/behaviors/zmk,behavior-capslock.yaml @@ -0,0 +1,27 @@ +# Copyright (c) 2023 The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: Caps word behavior + +compatible: "zmk,behavior-capslock" + +include: zero_param.yaml + +properties: + enable-on-press: + type: boolean + disable-on-release: + type: boolean + disable-on-next-release: + type: boolean + disable-on-keys: + type: array + required: false + default: [] + capslock-press-keycode: + type: int + default: 0 # will default to `CAPSLOCK` + capslock-press-duration: + type: int + default: 95 # seems to be the shortest reliable delay on Mac + diff --git a/app/src/behaviors/behavior_capslock.c b/app/src/behaviors/behavior_capslock.c new file mode 100644 index 00000000..358a820c --- /dev/null +++ b/app/src/behaviors/behavior_capslock.c @@ -0,0 +1,233 @@ +/* + * Copyright (c) 2023 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_behavior_capslock + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#define HID_INDICATORS_CAPSLOCK_BIT BIT(1) + +#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) + +struct capslock_key_item { + uint16_t page; + uint32_t id; + uint8_t implicit_modifiers; +}; + +struct behavior_capslock_config { + uint8_t index; + uint32_t capslock_press_keycode; + uint32_t capslock_press_duration; + bool enable_on_press; + bool disable_on_release; + bool disable_on_next_release; + uint8_t disable_on_keys_count; + struct capslock_key_item disable_on_keys[]; +}; + +uint32_t config_capslock_press_keycode(const struct behavior_capslock_config *config) { + return config->capslock_press_keycode > 0 ? config->capslock_press_keycode : CAPSLOCK; +} + +struct behavior_capslock_data { + uint32_t position; + bool active; + bool just_activated; +}; + +static void toggle_capslock(const struct device *dev) { + const struct behavior_capslock_config *config = dev->config; + const struct behavior_capslock_data *data = dev->data; + + const int32_t keycode = config_capslock_press_keycode(config); + const struct zmk_behavior_binding kp_capslock = { + .behavior_dev = "KEY_PRESS", + .param1 = keycode, + }; + + LOG_DBG("queueing %dms capslock press (usage_page 0x%02X keycode 0x%02X)", + config->capslock_press_duration, ZMK_HID_USAGE_PAGE(keycode), + ZMK_HID_USAGE_ID(keycode)); + zmk_behavior_queue_add(data->position, kp_capslock, true, config->capslock_press_duration); + zmk_behavior_queue_add(data->position, kp_capslock, false, 0); +} + +static bool get_capslock_state(void) { + return zmk_hid_indicators_get_current_profile() & HID_INDICATORS_CAPSLOCK_BIT; +} + +static void set_capslock_state(const struct device *dev, const bool target_state) { + const bool current_state = get_capslock_state(); + + if (current_state != target_state) { + LOG_DBG("toggling capslock state from %d to %d", current_state, target_state); + toggle_capslock(dev); + } else { + LOG_DBG("capslock state is already %d", target_state); + } +} + +static void activate_capslock(const struct device *dev) { + struct behavior_capslock_data *data = dev->data; + + set_capslock_state(dev, true); + + data->just_activated |= !data->active; // gets reset in `on_capslock_binding_released` + data->active = true; +} + +static void deactivate_capslock(const struct device *dev) { + struct behavior_capslock_data *data = dev->data; + + set_capslock_state(dev, false); + data->active = false; +} + +static int on_capslock_binding_pressed(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + const struct device *dev = device_get_binding(binding->behavior_dev); + const struct behavior_capslock_config *config = dev->config; + struct behavior_capslock_data *data = dev->data; + + data->position = event.position; + if (config->enable_on_press) { + LOG_DBG("activating capslock (enable-on-press)"); + activate_capslock(dev); + } + + return ZMK_BEHAVIOR_OPAQUE; +} + +static int on_capslock_binding_released(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + const struct device *dev = device_get_binding(binding->behavior_dev); + const struct behavior_capslock_config *config = dev->config; + struct behavior_capslock_data *data = dev->data; + + if (config->disable_on_release) { + LOG_DBG("deactivating capslock (disable-on-release)"); + deactivate_capslock(dev); + } else if (config->disable_on_next_release && !data->just_activated) { + LOG_DBG("deactivating capslock (disable-on-next-release)"); + deactivate_capslock(dev); + } + + data->just_activated = false; + + return ZMK_BEHAVIOR_OPAQUE; +} + +static bool capslock_match_key_item(const struct capslock_key_item *key_items, + const size_t key_items_count, uint16_t usage_page, + uint8_t usage_id, uint8_t implicit_modifiers) { + for (int i = 0; i < key_items_count; ++i) { + const struct capslock_key_item *break_item = &key_items[i]; + LOG_DBG("checking disable-on-keys: usage_page 0x%02X keycode 0x%02X", break_item->page, + break_item->id); + if (break_item->page == usage_page && break_item->id == usage_id && + (break_item->implicit_modifiers & (implicit_modifiers | zmk_hid_get_explicit_mods())) == + break_item->implicit_modifiers) { + return true; + } + } + + return false; +} + +static const struct device *devs[DT_NUM_INST_STATUS_OKAY(DT_DRV_COMPAT)]; + +static int capslock_keycode_state_changed_listener(const zmk_event_t *eh) { + const struct zmk_keycode_state_changed *ev = as_zmk_keycode_state_changed(eh); + if (ev == NULL || !ev->state) { + return ZMK_EV_EVENT_BUBBLE; + } + + for (int i = 0; i < DT_NUM_INST_STATUS_OKAY(DT_DRV_COMPAT); i++) { + const struct device *dev = devs[i]; + if (dev == NULL) { + continue; + } + + struct behavior_capslock_data *data = dev->data; + if (!data->active) { + continue; + } + + const struct behavior_capslock_config *config = dev->config; + + if (ZMK_HID_USAGE(ev->usage_page, ev->keycode) == config_capslock_press_keycode(config)) { + if (get_capslock_state()) { + LOG_DBG("capslock being toggled off (capslock key event: usage_page 0x%02X keycode " + "0x%02X)", + ev->usage_page, ev->keycode); + data->active = false; + } + } else if (capslock_match_key_item(config->disable_on_keys, config->disable_on_keys_count, + ev->usage_page, ev->keycode, ev->implicit_modifiers)) { + LOG_DBG("deactivating capslock (disable-on-keys: usage_page 0x%02X keycode 0x%02X)", + ev->usage_page, ev->keycode); + deactivate_capslock(dev); + } + } + + return ZMK_EV_EVENT_BUBBLE; +} + +static const struct behavior_driver_api behavior_capslock_driver_api = { + .binding_pressed = on_capslock_binding_pressed, + .binding_released = on_capslock_binding_released, +}; + +ZMK_LISTENER(behavior_capslock, capslock_keycode_state_changed_listener); +ZMK_SUBSCRIPTION(behavior_capslock, zmk_keycode_state_changed); + +static int behavior_capslock_init(const struct device *dev) { + const struct behavior_capslock_config *config = dev->config; + devs[config->index] = dev; + return 0; +} + +#define PARSE_KEY_ITEM(i) \ + {.page = ZMK_HID_USAGE_PAGE(i), \ + .id = ZMK_HID_USAGE_ID(i), \ + .implicit_modifiers = SELECT_MODS(i)}, + +#define BREAK_ITEM(i, n) PARSE_KEY_ITEM(DT_INST_PROP_BY_IDX(n, disable_on_keys, i)) + +#define CAPSLOCK_INST(n) \ + static struct behavior_capslock_data behavior_capslock_data_##n = {.active = false}; \ + static struct behavior_capslock_config behavior_capslock_config_##n = { \ + .index = n, \ + .capslock_press_keycode = DT_INST_PROP(n, capslock_press_keycode), \ + .capslock_press_duration = DT_INST_PROP(n, capslock_press_duration), \ + .enable_on_press = DT_INST_PROP(n, enable_on_press), \ + .disable_on_release = DT_INST_PROP(n, disable_on_release), \ + .disable_on_next_release = DT_INST_PROP(n, disable_on_next_release), \ + .disable_on_keys = {UTIL_LISTIFY(DT_INST_PROP_LEN(n, disable_on_keys), BREAK_ITEM, n)}, \ + .disable_on_keys_count = DT_INST_PROP_LEN(n, disable_on_keys), \ + }; \ + DEVICE_DT_INST_DEFINE(n, behavior_capslock_init, NULL, &behavior_capslock_data_##n, \ + &behavior_capslock_config_##n, APPLICATION, \ + CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, &behavior_capslock_driver_api); + +DT_INST_FOREACH_STATUS_OKAY(CAPSLOCK_INST) + +#endif diff --git a/docs/docs/behaviors/capslock.md b/docs/docs/behaviors/capslock.md new file mode 100644 index 00000000..533330e4 --- /dev/null +++ b/docs/docs/behaviors/capslock.md @@ -0,0 +1,65 @@ +--- +title: Capslock Behavior +sidebar_label: Capslock +--- + +## Summary + +The capslock behavior provides ways to create improved, configurable Caps Lock keys. +Pressing the regular Caps Lock key _toggles_ the state of caps lock on the host machine, making its effect dependent of said state. +This behavior can account for the current state to offer more control on how and when caps lock get activated and deactivated, regardless of the current status on the connected device. + +### Behavior Binding + +Four pre-configured instances are provided: + +- `&capslock_on` enables caps lock +- `&capslock_off` disables caps lock +- `&capslock_hold` enables caps lock while held, and disables it when released +- `&capslock_word` enables caps lock, and disables it when a word separator is typed or the key is pressed again + +In order to ensure compatibility with MacOS (which ignores short capslock presses by design), `&capslock_on_mac`, `&capslock_off_mac`, `&capslock_hold_mac`, `&capslock_word_mac` versions using a longer press duration of 95ms are also available. + +### Configuration + +The following options allow to customize the capslock behavior: + +- `enable-on-press`: enable caps lock when the key is pressed +- `disable-on-release`: disable caps lock when the key is released +- `disable-on-next-release`: disable caps lock when the key is released a second time +- `disable-on-keys`: list of keys after which to disable capslock +- `capslock-press-duration`: duration of the capslock key press to generate (in milliseconds) + +### Examples + +The pre-configured `capslock_word` (which enables caps lock when pressed, and disables it when `SPACE`, `TAB`, `ENTER`, or `ESC` is pressed, or the key is pressed again) is defined as follow: + +``` +capslock_word: behavior_capslock_word { + compatible = "zmk,behavior-capslock"; + label = "CAPSLOCK_WORD"; + #binding-cells = <0>; + capslock-press-ms = <5>; + enable-on-press; + disable-on-next-release; + disable-on-keys = ; +}; +``` + +A key to activate caps lock and disable it only after typing a whole line can be defined as follow: + +``` +/ { + capslock_line: capslock_line { + compatible = "zmk,behavior-capslock"; + label = "CAPSLOCK_LINE"; + #binding-cells = <0>; + enable-on-press; + disable-on-keys = ; + }; + + keymap { + ... + }; +}; +```