diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index b12d04742ee..03cdaafae35 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -45,7 +45,7 @@ if ((NOT CONFIG_ZMK_SPLIT) OR CONFIG_ZMK_SPLIT_ROLE_CENTRAL) target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_KEY_TOGGLE app PRIVATE src/behaviors/behavior_key_toggle.c) target_sources(app PRIVATE src/behaviors/behavior_hold_tap.c) target_sources(app PRIVATE src/behaviors/behavior_sticky_key.c) - target_sources(app PRIVATE src/behaviors/behavior_caps_word.c) + target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_CAPS_WORD app PRIVATE src/behaviors/behavior_caps_word.c) target_sources(app PRIVATE src/behaviors/behavior_key_repeat.c) target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_MACRO app PRIVATE src/behaviors/behavior_macro.c) target_sources(app PRIVATE src/behaviors/behavior_momentary_layer.c) diff --git a/app/Kconfig.behaviors b/app/Kconfig.behaviors index 7a1e44f6db5..a58a8aebcda 100644 --- a/app/Kconfig.behaviors +++ b/app/Kconfig.behaviors @@ -1,6 +1,11 @@ # Copyright (c) 2023 The ZMK Contributors # SPDX-License-Identifier: MIT +config ZMK_BEHAVIOR_CAPS_WORD + bool + default y + depends on DT_HAS_ZMK_BEHAVIOR_CAPS_WORD_ENABLED + config ZMK_BEHAVIOR_KEY_TOGGLE bool default y diff --git a/app/dts/behaviors/caps_word.dtsi b/app/dts/behaviors/caps_word.dtsi index 795fbc08439..74a8c56e4b1 100644 --- a/app/dts/behaviors/caps_word.dtsi +++ b/app/dts/behaviors/caps_word.dtsi @@ -13,6 +13,13 @@ #binding-cells = <0>; continue-list = ; }; + + /omit-if-no-ref/ prog_word: prog_word { + compatible = "zmk,behavior-caps-word"; + #binding-cells = <0>; + continue-list = ; + shift-list = ; + }; }; }; diff --git a/app/dts/bindings/behaviors/zmk,behavior-caps-word.yaml b/app/dts/bindings/behaviors/zmk,behavior-caps-word.yaml index cc1dda01370..8429c5b8028 100644 --- a/app/dts/bindings/behaviors/zmk,behavior-caps-word.yaml +++ b/app/dts/bindings/behaviors/zmk,behavior-caps-word.yaml @@ -11,5 +11,18 @@ properties: continue-list: type: array required: true + description: | + List of key codes which should continue the caps word behavior but not have + modifiers applied when caps word is active. + Alphanumeric keys are included automatically unless no-default-keys is set. + shift-list: + type: array + description: | + List of key codes which should have modifiers applied when caps word is active. + Alpha keys are included automatically unless no-default-keys is set. mods: type: int + description: Bitmask of modifiers to apply. Default is MOD_LSFT. + no-default-keys: + type: boolean + description: Do not automatically include any keys in continue-list or shift-list. diff --git a/app/include/zmk/events/keycode_state_changed.h b/app/include/zmk/events/keycode_state_changed.h index 60ffcfc89e2..ba9a31a5fa8 100644 --- a/app/include/zmk/events/keycode_state_changed.h +++ b/app/include/zmk/events/keycode_state_changed.h @@ -11,35 +11,34 @@ #include struct zmk_keycode_state_changed { + int64_t timestamp; + zmk_key_t keycode; uint16_t usage_page; - uint32_t keycode; - uint8_t implicit_modifiers; - uint8_t explicit_modifiers; + zmk_mod_flags_t implicit_modifiers; + zmk_mod_flags_t explicit_modifiers; bool state; - int64_t timestamp; }; ZMK_EVENT_DECLARE(zmk_keycode_state_changed); static inline struct zmk_keycode_state_changed zmk_keycode_state_changed_from_encoded(uint32_t encoded, bool pressed, int64_t timestamp) { - uint16_t page = ZMK_HID_USAGE_PAGE(encoded); - uint16_t id = ZMK_HID_USAGE_ID(encoded); - uint8_t implicit_modifiers = 0x00; - uint8_t explicit_modifiers = 0x00; + struct zmk_key_param key = ZMK_KEY_PARAM_DECODE(encoded); + zmk_mod_flags_t implicit_modifiers = 0x00; + zmk_mod_flags_t explicit_modifiers = 0x00; - if (!page) { - page = HID_USAGE_KEY; + if (!key.page) { + key.page = HID_USAGE_KEY; } - if (is_mod(page, id)) { - explicit_modifiers = SELECT_MODS(encoded); + if (is_mod(key.page, key.id)) { + explicit_modifiers = key.modifiers; } else { - implicit_modifiers = SELECT_MODS(encoded); + implicit_modifiers = key.modifiers; } - return (struct zmk_keycode_state_changed){.usage_page = page, - .keycode = id, + return (struct zmk_keycode_state_changed){.usage_page = key.page, + .keycode = key.id, .implicit_modifiers = implicit_modifiers, .explicit_modifiers = explicit_modifiers, .state = pressed, diff --git a/app/include/zmk/keys.h b/app/include/zmk/keys.h index fa6e7cfe693..adfa70be3d3 100644 --- a/app/include/zmk/keys.h +++ b/app/include/zmk/keys.h @@ -20,6 +20,24 @@ struct zmk_key_event { bool pressed; }; +/** + * Key data from a devicetree key code parameter. + */ +struct zmk_key_param { + zmk_mod_flags_t modifiers; + uint8_t page; + uint16_t id; +}; + +/** + * Decode a uint32_t devicetree key code parameter to a struct zmk_key_param. + */ +#define ZMK_KEY_PARAM_DECODE(param) \ + (struct zmk_key_param) { \ + .modifiers = SELECT_MODS(param), .page = ZMK_HID_USAGE_PAGE(param), \ + .id = ZMK_HID_USAGE_ID(param), \ + } + static inline bool is_mod(uint8_t usage_page, uint32_t keycode) { return (keycode >= HID_USAGE_KEY_KEYBOARD_LEFTCONTROL && keycode <= HID_USAGE_KEY_KEYBOARD_RIGHT_GUI && usage_page == HID_USAGE_KEY); diff --git a/app/src/behaviors/behavior_caps_word.c b/app/src/behaviors/behavior_caps_word.c index d9b3f24ec7d..83b5ba06e07 100644 --- a/app/src/behaviors/behavior_caps_word.c +++ b/app/src/behaviors/behavior_caps_word.c @@ -22,19 +22,16 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); -#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) - -struct caps_word_continue_item { - uint16_t page; - uint32_t id; - uint8_t implicit_modifiers; +struct key_list { + size_t size; + struct zmk_key_param keys[]; }; struct behavior_caps_word_config { + const struct key_list *continue_keys; + const struct key_list *shift_keys; zmk_mod_flags_t mods; - uint8_t index; - uint8_t continuations_count; - struct caps_word_continue_item continuations[]; + bool no_default_keys; }; struct behavior_caps_word_data { @@ -82,22 +79,18 @@ static int caps_word_keycode_state_changed_listener(const zmk_event_t *eh); ZMK_LISTENER(behavior_caps_word, caps_word_keycode_state_changed_listener); ZMK_SUBSCRIPTION(behavior_caps_word, zmk_keycode_state_changed); -static const struct device *devs[DT_NUM_INST_STATUS_OKAY(DT_DRV_COMPAT)]; - -static bool caps_word_is_caps_includelist(const struct behavior_caps_word_config *config, - uint16_t usage_page, uint8_t usage_id, - uint8_t implicit_modifiers) { - for (int i = 0; i < config->continuations_count; i++) { - const struct caps_word_continue_item *continuation = &config->continuations[i]; - LOG_DBG("Comparing with 0x%02X - 0x%02X (with implicit mods: 0x%02X)", continuation->page, - continuation->id, continuation->implicit_modifiers); - - if (continuation->page == usage_page && continuation->id == usage_id && - (continuation->implicit_modifiers & - (implicit_modifiers | zmk_hid_get_explicit_mods())) == - continuation->implicit_modifiers) { - LOG_DBG("Continuing capsword, found included usage: 0x%02X - 0x%02X", usage_page, - usage_id); +#define DEVICE_COUNT DT_NUM_INST_STATUS_OKAY(DT_DRV_COMPAT) +#define DEVICE_INST(n, _) DEVICE_DT_GET(DT_DRV_INST(n)) + +static const struct device *devs[] = {LISTIFY(DEVICE_COUNT, DEVICE_INST, (, ))}; + +static bool key_list_contains(const struct key_list *list, uint16_t usage_page, zmk_key_t usage_id, + zmk_mod_flags_t modifiers) { + for (int i = 0; i < list->size; i++) { + const struct zmk_key_param *key = &list->keys[i]; + + if (key->page == usage_page && key->id == usage_id && + (key->modifiers & modifiers) == key->modifiers) { return true; } } @@ -105,23 +98,53 @@ static bool caps_word_is_caps_includelist(const struct behavior_caps_word_config return false; } -static bool caps_word_is_alpha(uint8_t usage_id) { - return (usage_id >= HID_USAGE_KEY_KEYBOARD_A && usage_id <= HID_USAGE_KEY_KEYBOARD_Z); +static bool caps_word_is_alpha(uint16_t usage_page, zmk_key_t usage_id) { + if (usage_page != HID_USAGE_KEY) { + return false; + } + + return usage_id >= HID_USAGE_KEY_KEYBOARD_A && usage_id <= HID_USAGE_KEY_KEYBOARD_Z; +} + +static bool caps_word_is_numeric(uint16_t usage_page, zmk_key_t usage_id) { + if (usage_page != HID_USAGE_KEY) { + return false; + } + + return ((usage_id >= HID_USAGE_KEY_KEYBOARD_1_AND_EXCLAMATION && + usage_id <= HID_USAGE_KEY_KEYBOARD_0_AND_RIGHT_PARENTHESIS) || + (usage_id >= HID_USAGE_KEY_KEYPAD_1_AND_END && + usage_id <= HID_USAGE_KEY_KEYPAD_0_AND_INSERT)) || + usage_id == HID_USAGE_KEY_KEYPAD_00 || usage_id == HID_USAGE_KEY_KEYPAD_000; } -static bool caps_word_is_numeric(uint8_t usage_id) { - return (usage_id >= HID_USAGE_KEY_KEYBOARD_1_AND_EXCLAMATION && - usage_id <= HID_USAGE_KEY_KEYBOARD_0_AND_RIGHT_PARENTHESIS); +static bool caps_word_should_enhance(const struct behavior_caps_word_config *config, + struct zmk_keycode_state_changed *ev) { + // Unless no-default-keys is set, alpha keys are enhanced. + if (!config->no_default_keys && caps_word_is_alpha(ev->usage_page, ev->keycode)) { + return true; + } + + zmk_mod_flags_t modifiers = ev->implicit_modifiers | zmk_hid_get_explicit_mods(); + + return key_list_contains(config->shift_keys, ev->usage_page, ev->keycode, modifiers); } -static void caps_word_enhance_usage(const struct behavior_caps_word_config *config, - struct zmk_keycode_state_changed *ev) { - if (ev->usage_page != HID_USAGE_KEY || !caps_word_is_alpha(ev->keycode)) { - return; +static bool caps_word_should_continue(const struct behavior_caps_word_config *config, + struct zmk_keycode_state_changed *ev) { + // Modifiers do not break a word, nor does any key that is enhanced. + if (is_mod(ev->usage_page, ev->keycode) || caps_word_should_enhance(config, ev)) { + return true; + } + + // Unless no-default-keys is set, number keys do not break a word. + if (!config->no_default_keys && caps_word_is_numeric(ev->usage_page, ev->keycode)) { + return true; } - LOG_DBG("Enhancing usage 0x%02X with modifiers: 0x%02X", ev->keycode, config->mods); - ev->implicit_modifiers |= config->mods; + zmk_mod_flags_t modifiers = ev->implicit_modifiers | zmk_hid_get_explicit_mods(); + + return key_list_contains(config->continue_keys, ev->usage_page, ev->keycode, modifiers); } static int caps_word_keycode_state_changed_listener(const zmk_event_t *eh) { @@ -130,25 +153,21 @@ static int caps_word_keycode_state_changed_listener(const zmk_event_t *eh) { return ZMK_EV_EVENT_BUBBLE; } - for (int i = 0; i < DT_NUM_INST_STATUS_OKAY(DT_DRV_COMPAT); i++) { + for (int i = 0; i < ARRAY_SIZE(devs); i++) { const struct device *dev = devs[i]; - if (dev == NULL) { - continue; - } - - struct behavior_caps_word_data *data = dev->data; + const struct behavior_caps_word_data *data = dev->data; if (!data->active) { continue; } const struct behavior_caps_word_config *config = dev->config; - caps_word_enhance_usage(config, ev); + if (caps_word_should_enhance(config, ev)) { + LOG_DBG("Enhancing usage 0x%02X with modifiers: 0x%02X", ev->keycode, config->mods); + ev->implicit_modifiers |= config->mods; + } - if (!caps_word_is_alpha(ev->keycode) && !caps_word_is_numeric(ev->keycode) && - !is_mod(ev->usage_page, ev->keycode) && - !caps_word_is_caps_includelist(config, ev->usage_page, ev->keycode, - ev->implicit_modifiers)) { + if (!caps_word_should_continue(config, ev)) { LOG_DBG("Deactivating caps_word for 0x%02X - 0x%02X", ev->usage_page, ev->keycode); deactivate_caps_word(dev); } @@ -157,34 +176,31 @@ static int caps_word_keycode_state_changed_listener(const zmk_event_t *eh) { return ZMK_EV_EVENT_BUBBLE; } -static int behavior_caps_word_init(const struct device *dev) { - const struct behavior_caps_word_config *config = dev->config; - devs[config->index] = dev; - return 0; -} - -#define CAPS_WORD_LABEL(i, _n) DT_INST_LABEL(i) +static int behavior_caps_word_init(const struct device *dev) { return 0; } -#define PARSE_BREAK(i) \ - { \ - .page = ZMK_HID_USAGE_PAGE(i), .id = ZMK_HID_USAGE_ID(i), \ - .implicit_modifiers = SELECT_MODS(i) \ - } +#define KEY_LIST_ITEM(i, n, prop) ZMK_KEY_PARAM_DECODE(DT_INST_PROP_BY_IDX(n, prop, i)) -#define BREAK_ITEM(i, n) PARSE_BREAK(DT_INST_PROP_BY_IDX(n, continue_list, i)) +#define PROP_KEY_LIST(n, prop) \ + COND_CODE_1(DT_NODE_HAS_PROP(DT_DRV_INST(n), prop), \ + ({ \ + .size = DT_INST_PROP_LEN(n, prop), \ + .keys = {LISTIFY(DT_INST_PROP_LEN(n, prop), KEY_LIST_ITEM, (, ), n, prop)}, \ + }), \ + ({.size = 0})) #define KP_INST(n) \ + static const struct key_list caps_word_continue_list_##n = PROP_KEY_LIST(n, continue_list); \ + static const struct key_list caps_word_shift_list_##n = PROP_KEY_LIST(n, shift_list); \ + \ static struct behavior_caps_word_data behavior_caps_word_data_##n = {.active = false}; \ static struct behavior_caps_word_config behavior_caps_word_config_##n = { \ - .index = n, \ .mods = DT_INST_PROP_OR(n, mods, MOD_LSFT), \ - .continuations = {LISTIFY(DT_INST_PROP_LEN(n, continue_list), BREAK_ITEM, (, ), n)}, \ - .continuations_count = DT_INST_PROP_LEN(n, continue_list), \ + .continue_keys = &caps_word_continue_list_##n, \ + .shift_keys = &caps_word_shift_list_##n, \ + .no_default_keys = DT_INST_PROP(n, no_default_keys), \ }; \ BEHAVIOR_DT_INST_DEFINE(n, behavior_caps_word_init, NULL, &behavior_caps_word_data_##n, \ &behavior_caps_word_config_##n, POST_KERNEL, \ CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, &behavior_caps_word_driver_api); DT_INST_FOREACH_STATUS_OKAY(KP_INST) - -#endif diff --git a/app/tests/caps-word/continue-with-modifiers/events.patterns b/app/tests/caps-word/continue-with-modifiers/events.patterns index dd4d3d3f700..0998941dfd0 100644 --- a/app/tests/caps-word/continue-with-modifiers/events.patterns +++ b/app/tests/caps-word/continue-with-modifiers/events.patterns @@ -1,4 +1,3 @@ s/.*hid_listener_keycode_//p s/.*hid_implicit_modifiers_//p -s/.*caps_word_enhance_usage/enhance_usage/p -s/.*caps_word_is_caps_includelist/caps_includelist/p \ No newline at end of file +s/.*caps_word_keycode_state_changed_listener/state_changed/p \ No newline at end of file diff --git a/app/tests/caps-word/continue-with-modifiers/keycode_events.snapshot b/app/tests/caps-word/continue-with-modifiers/keycode_events.snapshot index b4752fd416e..f40ec50120c 100644 --- a/app/tests/caps-word/continue-with-modifiers/keycode_events.snapshot +++ b/app/tests/caps-word/continue-with-modifiers/keycode_events.snapshot @@ -1,19 +1,17 @@ -enhance_usage: Enhancing usage 0x04 with modifiers: 0x02 +state_changed: Enhancing usage 0x04 with modifiers: 0x02 pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x02 explicit_mods 0x00 press: Modifiers set to 0x02 released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 release: Modifiers set to 0x00 pressed: usage_page 0x07 keycode 0xE1 implicit_mods 0x00 explicit_mods 0x00 press: Modifiers set to 0x02 -caps_includelist: Comparing with 0x07 - 0x2D (with implicit mods: 0x02) -caps_includelist: Continuing capsword, found included usage: 0x07 - 0x2D pressed: usage_page 0x07 keycode 0x2D implicit_mods 0x00 explicit_mods 0x00 press: Modifiers set to 0x02 released: usage_page 0x07 keycode 0x2D implicit_mods 0x00 explicit_mods 0x00 release: Modifiers set to 0x02 released: usage_page 0x07 keycode 0xE1 implicit_mods 0x00 explicit_mods 0x00 release: Modifiers set to 0x00 -enhance_usage: Enhancing usage 0x04 with modifiers: 0x02 +state_changed: Enhancing usage 0x04 with modifiers: 0x02 pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x02 explicit_mods 0x00 press: Modifiers set to 0x02 released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 diff --git a/app/tests/caps-word/continue-with-non-alpha-continue-list-item/events.patterns b/app/tests/caps-word/continue-with-non-alpha-continue-list-item/events.patterns index dd4d3d3f700..0998941dfd0 100644 --- a/app/tests/caps-word/continue-with-non-alpha-continue-list-item/events.patterns +++ b/app/tests/caps-word/continue-with-non-alpha-continue-list-item/events.patterns @@ -1,4 +1,3 @@ s/.*hid_listener_keycode_//p s/.*hid_implicit_modifiers_//p -s/.*caps_word_enhance_usage/enhance_usage/p -s/.*caps_word_is_caps_includelist/caps_includelist/p \ No newline at end of file +s/.*caps_word_keycode_state_changed_listener/state_changed/p \ No newline at end of file diff --git a/app/tests/caps-word/continue-with-non-alpha-continue-list-item/keycode_events.snapshot b/app/tests/caps-word/continue-with-non-alpha-continue-list-item/keycode_events.snapshot index 8910db99690..8dd0c6f0498 100644 --- a/app/tests/caps-word/continue-with-non-alpha-continue-list-item/keycode_events.snapshot +++ b/app/tests/caps-word/continue-with-non-alpha-continue-list-item/keycode_events.snapshot @@ -1,16 +1,13 @@ -enhance_usage: Enhancing usage 0x04 with modifiers: 0x02 +state_changed: Enhancing usage 0x04 with modifiers: 0x02 pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x02 explicit_mods 0x00 press: Modifiers set to 0x02 released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 release: Modifiers set to 0x00 -caps_includelist: Comparing with 0x07 - 0x2D (with implicit mods: 0x02) -caps_includelist: Comparing with 0x07 - 0x2D (with implicit mods: 0x00) -caps_includelist: Continuing capsword, found included usage: 0x07 - 0x2D pressed: usage_page 0x07 keycode 0x2D implicit_mods 0x00 explicit_mods 0x00 press: Modifiers set to 0x00 released: usage_page 0x07 keycode 0x2D implicit_mods 0x00 explicit_mods 0x00 release: Modifiers set to 0x00 -enhance_usage: Enhancing usage 0x04 with modifiers: 0x02 +state_changed: Enhancing usage 0x04 with modifiers: 0x02 pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x02 explicit_mods 0x00 press: Modifiers set to 0x02 released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 diff --git a/app/tests/caps-word/continue-with-non-modified-numeric-usage-id/events.patterns b/app/tests/caps-word/continue-with-non-modified-numeric-usage-id/events.patterns index dd4d3d3f700..0998941dfd0 100644 --- a/app/tests/caps-word/continue-with-non-modified-numeric-usage-id/events.patterns +++ b/app/tests/caps-word/continue-with-non-modified-numeric-usage-id/events.patterns @@ -1,4 +1,3 @@ s/.*hid_listener_keycode_//p s/.*hid_implicit_modifiers_//p -s/.*caps_word_enhance_usage/enhance_usage/p -s/.*caps_word_is_caps_includelist/caps_includelist/p \ No newline at end of file +s/.*caps_word_keycode_state_changed_listener/state_changed/p \ No newline at end of file diff --git a/app/tests/caps-word/continue-with-non-modified-numeric-usage-id/keycode_events.snapshot b/app/tests/caps-word/continue-with-non-modified-numeric-usage-id/keycode_events.snapshot index 23ddbe1b190..1c89fc117e4 100644 --- a/app/tests/caps-word/continue-with-non-modified-numeric-usage-id/keycode_events.snapshot +++ b/app/tests/caps-word/continue-with-non-modified-numeric-usage-id/keycode_events.snapshot @@ -1,4 +1,4 @@ -enhance_usage: Enhancing usage 0x04 with modifiers: 0x02 +state_changed: Enhancing usage 0x04 with modifiers: 0x02 pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x02 explicit_mods 0x00 press: Modifiers set to 0x02 released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 @@ -7,7 +7,7 @@ pressed: usage_page 0x07 keycode 0x23 implicit_mods 0x00 explicit_mods 0x00 press: Modifiers set to 0x00 released: usage_page 0x07 keycode 0x23 implicit_mods 0x00 explicit_mods 0x00 release: Modifiers set to 0x00 -enhance_usage: Enhancing usage 0x04 with modifiers: 0x02 +state_changed: Enhancing usage 0x04 with modifiers: 0x02 pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x02 explicit_mods 0x00 press: Modifiers set to 0x02 released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 diff --git a/app/tests/caps-word/continue-with-shifted-key/events.patterns b/app/tests/caps-word/continue-with-shifted-key/events.patterns new file mode 100644 index 00000000000..0998941dfd0 --- /dev/null +++ b/app/tests/caps-word/continue-with-shifted-key/events.patterns @@ -0,0 +1,3 @@ +s/.*hid_listener_keycode_//p +s/.*hid_implicit_modifiers_//p +s/.*caps_word_keycode_state_changed_listener/state_changed/p \ No newline at end of file diff --git a/app/tests/caps-word/continue-with-shifted-key/keycode_events.snapshot b/app/tests/caps-word/continue-with-shifted-key/keycode_events.snapshot new file mode 100644 index 00000000000..10d9ee00e51 --- /dev/null +++ b/app/tests/caps-word/continue-with-shifted-key/keycode_events.snapshot @@ -0,0 +1,15 @@ +state_changed: Enhancing usage 0x04 with modifiers: 0x02 +pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x02 explicit_mods 0x00 +press: Modifiers set to 0x02 +released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +release: Modifiers set to 0x00 +state_changed: Enhancing usage 0x2D with modifiers: 0x02 +pressed: usage_page 0x07 keycode 0x2D implicit_mods 0x02 explicit_mods 0x00 +press: Modifiers set to 0x02 +released: usage_page 0x07 keycode 0x2D implicit_mods 0x00 explicit_mods 0x00 +release: Modifiers set to 0x00 +state_changed: Enhancing usage 0x04 with modifiers: 0x02 +pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x02 explicit_mods 0x00 +press: Modifiers set to 0x02 +released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +release: Modifiers set to 0x00 diff --git a/app/tests/caps-word/continue-with-shifted-key/native_posix_64.keymap b/app/tests/caps-word/continue-with-shifted-key/native_posix_64.keymap new file mode 100644 index 00000000000..98e26ef87e1 --- /dev/null +++ b/app/tests/caps-word/continue-with-shifted-key/native_posix_64.keymap @@ -0,0 +1,22 @@ +#include +#include +#include +#include "../behavior_keymap.dtsi" + +&caps_word { + continue-list = <>; + shift-list = ; +}; + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,1,10) + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_RELEASE(1,1,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,1,10) + >; +}; \ No newline at end of file diff --git a/app/tests/caps-word/deactivate-by-alpha-with-no-default-keys/events.patterns b/app/tests/caps-word/deactivate-by-alpha-with-no-default-keys/events.patterns new file mode 100644 index 00000000000..0998941dfd0 --- /dev/null +++ b/app/tests/caps-word/deactivate-by-alpha-with-no-default-keys/events.patterns @@ -0,0 +1,3 @@ +s/.*hid_listener_keycode_//p +s/.*hid_implicit_modifiers_//p +s/.*caps_word_keycode_state_changed_listener/state_changed/p \ No newline at end of file diff --git a/app/tests/caps-word/deactivate-by-alpha-with-no-default-keys/keycode_events.snapshot b/app/tests/caps-word/deactivate-by-alpha-with-no-default-keys/keycode_events.snapshot new file mode 100644 index 00000000000..06abf4ef70b --- /dev/null +++ b/app/tests/caps-word/deactivate-by-alpha-with-no-default-keys/keycode_events.snapshot @@ -0,0 +1,14 @@ +state_changed: Enhancing usage 0x2D with modifiers: 0x02 +pressed: usage_page 0x07 keycode 0x2D implicit_mods 0x02 explicit_mods 0x00 +press: Modifiers set to 0x02 +released: usage_page 0x07 keycode 0x2D implicit_mods 0x00 explicit_mods 0x00 +release: Modifiers set to 0x00 +state_changed: Deactivating caps_word for 0x07 - 0x04 +pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +press: Modifiers set to 0x00 +released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +release: Modifiers set to 0x00 +pressed: usage_page 0x07 keycode 0x2D implicit_mods 0x00 explicit_mods 0x00 +press: Modifiers set to 0x00 +released: usage_page 0x07 keycode 0x2D implicit_mods 0x00 explicit_mods 0x00 +release: Modifiers set to 0x00 diff --git a/app/tests/caps-word/deactivate-by-alpha-with-no-default-keys/native_posix_64.keymap b/app/tests/caps-word/deactivate-by-alpha-with-no-default-keys/native_posix_64.keymap new file mode 100644 index 00000000000..13f75333d03 --- /dev/null +++ b/app/tests/caps-word/deactivate-by-alpha-with-no-default-keys/native_posix_64.keymap @@ -0,0 +1,22 @@ +#include +#include +#include +#include "../behavior_keymap.dtsi" + +&caps_word { + no-default-keys; + shift-list = ; +}; + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_RELEASE(1,1,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,1,10) + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_RELEASE(1,1,10) + >; +}; \ No newline at end of file diff --git a/app/tests/caps-word/deactivate-by-non-alpha-non-continuation/events.patterns b/app/tests/caps-word/deactivate-by-non-alpha-non-continuation/events.patterns index fa75ab0c7df..0998941dfd0 100644 --- a/app/tests/caps-word/deactivate-by-non-alpha-non-continuation/events.patterns +++ b/app/tests/caps-word/deactivate-by-non-alpha-non-continuation/events.patterns @@ -1,3 +1,3 @@ s/.*hid_listener_keycode_//p s/.*hid_implicit_modifiers_//p -s/.*caps_word_enhance_usage/enhance_usage/p \ No newline at end of file +s/.*caps_word_keycode_state_changed_listener/state_changed/p \ No newline at end of file diff --git a/app/tests/caps-word/deactivate-by-non-alpha-non-continuation/keycode_events.snapshot b/app/tests/caps-word/deactivate-by-non-alpha-non-continuation/keycode_events.snapshot index f479db1219a..b3f4aa6ce49 100644 --- a/app/tests/caps-word/deactivate-by-non-alpha-non-continuation/keycode_events.snapshot +++ b/app/tests/caps-word/deactivate-by-non-alpha-non-continuation/keycode_events.snapshot @@ -1,8 +1,9 @@ -enhance_usage: Enhancing usage 0x04 with modifiers: 0x02 +state_changed: Enhancing usage 0x04 with modifiers: 0x02 pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x02 explicit_mods 0x00 press: Modifiers set to 0x02 released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 release: Modifiers set to 0x00 +state_changed: Deactivating caps_word for 0x07 - 0x2D pressed: usage_page 0x07 keycode 0x2D implicit_mods 0x00 explicit_mods 0x00 press: Modifiers set to 0x00 released: usage_page 0x07 keycode 0x2D implicit_mods 0x00 explicit_mods 0x00 diff --git a/app/tests/caps-word/deactivate-by-second-press/events.patterns b/app/tests/caps-word/deactivate-by-second-press/events.patterns index fa75ab0c7df..0998941dfd0 100644 --- a/app/tests/caps-word/deactivate-by-second-press/events.patterns +++ b/app/tests/caps-word/deactivate-by-second-press/events.patterns @@ -1,3 +1,3 @@ s/.*hid_listener_keycode_//p s/.*hid_implicit_modifiers_//p -s/.*caps_word_enhance_usage/enhance_usage/p \ No newline at end of file +s/.*caps_word_keycode_state_changed_listener/state_changed/p \ No newline at end of file diff --git a/app/tests/caps-word/deactivate-by-second-press/keycode_events.snapshot b/app/tests/caps-word/deactivate-by-second-press/keycode_events.snapshot index 5181f75b1f8..3c8ffb33ff2 100644 --- a/app/tests/caps-word/deactivate-by-second-press/keycode_events.snapshot +++ b/app/tests/caps-word/deactivate-by-second-press/keycode_events.snapshot @@ -1,4 +1,4 @@ -enhance_usage: Enhancing usage 0x04 with modifiers: 0x02 +state_changed: Enhancing usage 0x04 with modifiers: 0x02 pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x02 explicit_mods 0x00 press: Modifiers set to 0x02 released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 diff --git a/docs/docs/behaviors/caps-word.md b/docs/docs/behaviors/caps-word.md index 77c8fd20d2a..d755b599a02 100644 --- a/docs/docs/behaviors/caps-word.md +++ b/docs/docs/behaviors/caps-word.md @@ -7,7 +7,9 @@ sidebar_label: Caps Word The caps word behavior behaves similar to a caps lock, but will automatically deactivate when any key not in a continue list is pressed, or if the caps word key is pressed again. For smaller keyboards using [mod-taps](/docs/behaviors/mod-tap), this can help avoid repeated alternating holds when typing words in all caps. -The modifiers are applied only to to the alphabetic (`A` to `Z`) keycodes, to avoid automatically applying them to numeric values, etc. +## Caps Word + +Applies shift to capitalize alphabetic keys. Remains active but does not apply shift when numeric keys, underscore, backspace, or delete are pressed. ### Behavior Binding @@ -19,15 +21,34 @@ Example: &caps_word ``` -### Configuration +## Programmer Word + +This is identical to `&caps_word` except that shift is also applied to `MINUS`. This can be useful when programming to type certain symbols. For example, enabling `&prog_word` and typing `some-constant` results in `SOME_CONSTANT`. + +### Behavior Binding + +- Reference: `&prog_word` + +Example: + +```dts +&prog_word +``` + +## Configuration + +### Shift List -#### Continue List +Caps word will apply shift to the following: -By default, the caps word will remain active when any alphanumeric character or underscore (`UNDERSCORE`), backspace (`BACKSPACE`), or delete (`DELETE`) characters are pressed. Any other non-modifier keycode sent will turn off caps word. If you would like to override this, you can set a new array of keys in the `continue-list` property in your keymap: +- Alphabetic keys (`A` through `Z`). +- Keys in the `shift-list` property (defaults to empty for `&caps_word` and `MINUS` for `&prog_word`). + +You can add more key codes to be shifted by overriding the `shift-list` property in your keymap: ```dts &caps_word { - continue-list = ; + shift-list = ; }; / { @@ -37,32 +58,66 @@ By default, the caps word will remain active when any alphanumeric character or }; ``` -#### Applied Modifier(s) +### Continue List -In addition, if you would like _multiple_ modifiers, instead of just `MOD_LSFT`, you can override the `mods` property: +Caps word will remain active when any of the following are pressed: + +- Alphabetic keys and keys in `shift-list`. +- Numeric keys (`N0` through `N9` and `KP_N0` through `KP_N9`). +- Modifier keys. +- Keys which do not send a key code (such as layer keys). +- Keys in the `continue-list` property (defaults to `UNDERSCORE`, `BACKSPACE`, and `DELETE`). + +Any other key code sent will turn off caps word. + +You can add more key codes to continue caps word by setting a `continue-list` property in your keymap. ```dts &caps_word { - mods = <(MOD_LSFT | MOD_LALT)>; + continue-list = ; }; +``` -/ { - keymap { - ... - }; +### No Default Keys + +By setting a `no-default-keys` property in your keymap, caps word will not automatically include alphabetic keys in `shift-list` or numeric keys in `continue-list`. This lets you explicitly control which keys are shifted and which continue a word, which may be useful in cases such as when your operating system is set to a non-US keyboard layout. + +```dts +&caps_word { + no-default-keys; + shift-list = ; + continue-list = ; +}; +``` + +### Applied Modifier(s) + +If you would like different or multiple modifiers instead of just `MOD_LSFT`, you can override the `mods` property: + +```dts +&caps_word { + mods = <(MOD_LSFT | MOD_LALT)>; }; ``` ### Multiple Caps Breaks -If you want to use multiple caps breaks with different codes to break the caps, you can add additional caps words instances to use in your keymap: +You can add multiple caps words instances with different sets of properties in your keymap: ```dts / { - prog_caps: prog_caps { - compatible = "zmk,behavior-caps-word"; - #binding-cells = <0>; - continue-list = ; + behaviors { + caps_sentence: caps_sentence { + compatible = "zmk,behavior-caps-word"; + #binding-cells = <0>; + continue-list = ; + }; + + ctrl_word: ctrl_word { + compatible = "zmk,behavior-caps-word"; + #binding-cells = <0>; + mods = ; + }; }; keymap { diff --git a/docs/docs/config/behaviors.md b/docs/docs/config/behaviors.md index 1a5b899ab97..90a078fbd59 100644 --- a/docs/docs/config/behaviors.md +++ b/docs/docs/config/behaviors.md @@ -29,21 +29,24 @@ Definition file: [zmk/app/dts/bindings/behaviors/zmk,behavior-caps-word.yaml](ht Applies to: `compatible = "zmk,behavior-caps-word"` -| Property | Type | Description | Default | -| ---------------- | ----- | ------------------------------------------------------------------ | ------------------------------- | -| `#binding-cells` | int | Must be `<0>` | | -| `continue-list` | array | List of [key codes](/docs/codes) which do not deactivate caps lock | `` | -| `mods` | int | A bit field of modifiers to apply | `` | +| Property | Type | Description | Default | +| ----------------- | ----- | -------------------------------------------------------------------- | ------------------------------- | +| `#binding-cells` | int | Must be `<0>` | | +| `continue-list` | array | List of [key codes](/docs/codes) which do not deactivate caps word | `` | +| `mods` | int | A bit field of modifiers to apply | `` | +| `no-default-keys` | bool | Do not add any keys to `continue-list` or `shift-list` by default | false | +| `shift-list` | array | List of [key codes](/docs/codes) which should have modifiers applied | `<>` | -`continue-list` is treated as if it always includes alphanumeric characters (A-Z, 0-9). +By default, `shift-list` is treated as if it always includes alphabetic keys (A-Z) and `continue-list` as if it always includes numeric keys (0-9 and numpad digits). Setting `no-default-keys` disables this behavior. See [dt-bindings/zmk/modifiers.h](https://github.com/zmkfirmware/zmk/blob/main/app/include/dt-bindings/zmk/modifiers.h) for a list of modifiers. You can use the following nodes to tweak the default behaviors: -| Node | Behavior | -| ------------ | -------------------------------------- | -| `&caps_word` | [Caps Word](../behaviors/caps-word.md) | +| Node | Behavior | +| ------------ | ------------------------------------------------------------ | +| `&caps_word` | [Caps Word](../behaviors/caps-word.md#caps-word) | +| `&prog_word` | [Programmer Word](../behaviors/caps-word.md#programmer-word) | ## Hold-Tap