diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 60c502fcd..3bb262ca8 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -52,6 +52,7 @@ if ((NOT CONFIG_ZMK_SPLIT) OR CONFIG_ZMK_SPLIT_ROLE_CENTRAL) target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_STICKY_KEY app PRIVATE src/behaviors/behavior_sticky_key.c) target_sources(app PRIVATE src/behaviors/behavior_caps_word.c) target_sources(app PRIVATE src/behaviors/behavior_key_repeat.c) + target_sources(app PRIVATE src/behaviors/behavior_snap_tap.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) target_sources(app PRIVATE src/behaviors/behavior_mod_morph.c) diff --git a/app/dts/behaviors.dtsi b/app/dts/behaviors.dtsi index 653b085d5..176dc583c 100644 --- a/app/dts/behaviors.dtsi +++ b/app/dts/behaviors.dtsi @@ -28,3 +28,4 @@ #include #include #include +#include diff --git a/app/dts/behaviors/snap_tap.dtsi b/app/dts/behaviors/snap_tap.dtsi new file mode 100644 index 000000000..ffae15930 --- /dev/null +++ b/app/dts/behaviors/snap_tap.dtsi @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include + +/ { + behaviors { +#if ZMK_BEHAVIOR_OMIT(SNAP_TAP) + /omit-if-no-ref/ +#endif + // Universal snap tap behavior: &st + // Example: &st A D (A key with D as opposing key) + st: snap_tap { + compatible = "zmk,behavior-snap-tap"; + #binding-cells = <2>; + display-name = "Snap Tap"; + }; + + }; +}; \ No newline at end of file diff --git a/app/dts/bindings/behaviors/zmk,behavior-snap-tap.yaml b/app/dts/bindings/behaviors/zmk,behavior-snap-tap.yaml new file mode 100644 index 000000000..13c181f2e --- /dev/null +++ b/app/dts/bindings/behaviors/zmk,behavior-snap-tap.yaml @@ -0,0 +1,8 @@ +# Copyright (c) 2024 The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: Snap Tap behavior for SOCD (Simultaneous Opposing Cardinal Directions) + +compatible: "zmk,behavior-snap-tap" + +include: two_param.yaml \ No newline at end of file diff --git a/app/src/behaviors/behavior_snap_tap.c b/app/src/behaviors/behavior_snap_tap.c new file mode 100644 index 000000000..e264b747c --- /dev/null +++ b/app/src/behaviors/behavior_snap_tap.c @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_behavior_snap_tap + +#include +#include +#include + +#include +#include +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) + +struct behavior_snap_tap_config { + uint8_t index; +}; + +// Global state storage for snap tap keys +#define MAX_SNAP_TAP_KEYS 16 +struct snap_tap_state { + uint32_t keycode; + uint32_t opposing_keycode; + bool is_physically_pressed; + bool is_logically_pressed; + uint32_t position; +}; + +static struct snap_tap_state snap_tap_states[MAX_SNAP_TAP_KEYS]; +static int snap_tap_count = 0; + +// Find state for given keycode combination +static struct snap_tap_state *find_snap_tap_state(uint32_t keycode, uint32_t opposing_keycode) { + for (int i = 0; i < snap_tap_count; i++) { + if (snap_tap_states[i].keycode == keycode && + snap_tap_states[i].opposing_keycode == opposing_keycode) { + return &snap_tap_states[i]; + } + } + return NULL; +} + +// Find state for opposing key +static struct snap_tap_state *find_opposing_state(uint32_t my_keycode) { + for (int i = 0; i < snap_tap_count; i++) { + if (snap_tap_states[i].opposing_keycode == my_keycode) { + return &snap_tap_states[i]; + } + } + return NULL; +} + +// Create or get state for keycode pair +static struct snap_tap_state *get_or_create_state(uint32_t keycode, uint32_t opposing_keycode, + uint32_t position) { + struct snap_tap_state *state = find_snap_tap_state(keycode, opposing_keycode); + if (state) { + return state; + } + + if (snap_tap_count >= MAX_SNAP_TAP_KEYS) { + LOG_ERR("Maximum snap tap keys exceeded"); + return NULL; + } + + state = &snap_tap_states[snap_tap_count++]; + state->keycode = keycode; + state->opposing_keycode = opposing_keycode; + state->is_physically_pressed = false; + state->is_logically_pressed = false; + state->position = position; + + LOG_DBG("Created snap tap state for keycode 0x%02X opposing 0x%02X", keycode, opposing_keycode); + return state; +} + +static int behavior_snap_tap_init(const struct device *dev) { return 0; } + +static int on_snap_tap_binding_pressed(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + uint32_t keycode = binding->param1; + uint32_t opposing_keycode = binding->param2; + + LOG_DBG("Snap tap pressed: keycode 0x%02X, opposing: 0x%02X", keycode, opposing_keycode); + + struct snap_tap_state *state = get_or_create_state(keycode, opposing_keycode, event.position); + if (!state) { + return ZMK_BEHAVIOR_OPAQUE; + } + + state->is_physically_pressed = true; + + // Find opposing state and suppress it if needed + struct snap_tap_state *opposing_state = find_opposing_state(keycode); + if (opposing_state && opposing_state->is_physically_pressed && + opposing_state->is_logically_pressed) { + LOG_DBG("Suppressing opposing key: 0x%02X", opposing_keycode); + opposing_state->is_logically_pressed = false; + raise_zmk_keycode_state_changed_from_encoded(opposing_keycode, false, event.timestamp); + } + + // Press our key logically + state->is_logically_pressed = true; + return raise_zmk_keycode_state_changed_from_encoded(keycode, true, event.timestamp); +} + +static int on_snap_tap_binding_released(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + uint32_t keycode = binding->param1; + uint32_t opposing_keycode = binding->param2; + + LOG_DBG("Snap tap released: keycode 0x%02X", keycode); + + struct snap_tap_state *state = find_snap_tap_state(keycode, opposing_keycode); + if (!state) { + return ZMK_BEHAVIOR_OPAQUE; + } + + state->is_physically_pressed = false; + + // Always release our key logically + if (state->is_logically_pressed) { + state->is_logically_pressed = false; + raise_zmk_keycode_state_changed_from_encoded(keycode, false, event.timestamp); + } + + // Check if opposing key should be restored + struct snap_tap_state *opposing_state = find_opposing_state(keycode); + if (opposing_state && opposing_state->is_physically_pressed && + !opposing_state->is_logically_pressed) { + LOG_DBG("Restoring opposing key: 0x%02X", opposing_keycode); + opposing_state->is_logically_pressed = true; + raise_zmk_keycode_state_changed_from_encoded(opposing_keycode, true, event.timestamp); + } + + return ZMK_BEHAVIOR_OPAQUE; +} + +static const struct behavior_driver_api behavior_snap_tap_driver_api = { + .binding_pressed = on_snap_tap_binding_pressed, + .binding_released = on_snap_tap_binding_released, +#if IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) + .parameter_metadata = &metadata, +#endif // IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) +}; + +#if IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) + +static const struct behavior_parameter_value_metadata param_values[] = { + { + .display_name = "Key", + .type = BEHAVIOR_PARAMETER_VALUE_TYPE_HID_USAGE, + }, + { + .display_name = "Opposing Key", + .type = BEHAVIOR_PARAMETER_VALUE_TYPE_HID_USAGE, + }, +}; + +static const struct behavior_parameter_metadata_set param_metadata_set[] = {{ + .param1_values = param_values, + .param1_values_len = 1, + .param2_values = param_values, + .param2_values_len = 1, +}}; + +static const struct behavior_parameter_metadata metadata = { + .sets_len = ARRAY_SIZE(param_metadata_set), + .sets = param_metadata_set, +}; + +#endif + +#define ST_INST(n) \ + static const struct behavior_snap_tap_config behavior_snap_tap_config_##n = { \ + .index = n, \ + }; \ + BEHAVIOR_DT_INST_DEFINE(n, behavior_snap_tap_init, NULL, NULL, &behavior_snap_tap_config_##n, \ + POST_KERNEL, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, \ + &behavior_snap_tap_driver_api); + +DT_INST_FOREACH_STATUS_OKAY(ST_INST) + +#endif \ No newline at end of file