Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow customization of which keys caps_word modifies #1742

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions app/Kconfig.behaviors
Original file line number Diff line number Diff line change
@@ -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
Expand Down
7 changes: 7 additions & 0 deletions app/dts/behaviors/caps_word.dtsi
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@
#binding-cells = <0>;
continue-list = <UNDERSCORE BACKSPACE DELETE>;
};

/omit-if-no-ref/ prog_word: prog_word {
compatible = "zmk,behavior-caps-word";
#binding-cells = <0>;
continue-list = <UNDERSCORE BACKSPACE DELETE>;
shift-list = <MINUS>;
};
};
};

13 changes: 13 additions & 0 deletions app/dts/bindings/behaviors/zmk,behavior-caps-word.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
29 changes: 14 additions & 15 deletions app/include/zmk/events/keycode_state_changed.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,35 +11,34 @@
#include <zmk/keys.h>

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,
Expand Down
18 changes: 18 additions & 0 deletions app/include/zmk/keys.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
146 changes: 81 additions & 65 deletions app/src/behaviors/behavior_caps_word.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -82,46 +79,72 @@ 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;
}
}

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) {
Expand All @@ -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);
}
Expand All @@ -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
Original file line number Diff line number Diff line change
@@ -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
s/.*caps_word_keycode_state_changed_listener/state_changed/p
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
s/.*caps_word_keycode_state_changed_listener/state_changed/p
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Loading
Loading