Skip to content
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
1 change: 1 addition & 0 deletions app/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions app/dts/behaviors.dtsi
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@
#include <behaviors/soft_off.dtsi>
#include <behaviors/studio_unlock.dtsi>
#include <behaviors/mouse_keys.dtsi>
#include <behaviors/snap_tap.dtsi>
26 changes: 26 additions & 0 deletions app/dts/behaviors/snap_tap.dtsi
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright (c) 2024 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/

#include <dt-bindings/zmk/behaviors.h>
#include <dt-bindings/zmk/keys.h>
#include <dt-bindings/zmk/hid_usage.h>
#include <dt-bindings/zmk/hid_usage_pages.h>

/ {
behaviors {
#if ZMK_BEHAVIOR_OMIT(SNAP_TAP)
/omit-if-no-ref/
#endif
// Universal snap tap behavior: &st <key> <opposing_key>
// 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";
};

};
};
8 changes: 8 additions & 0 deletions app/dts/bindings/behaviors/zmk,behavior-snap-tap.yaml
Original file line number Diff line number Diff line change
@@ -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
191 changes: 191 additions & 0 deletions app/src/behaviors/behavior_snap_tap.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
/*
* Copyright (c) 2024 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/

#define DT_DRV_COMPAT zmk_behavior_snap_tap

#include <zephyr/device.h>
#include <drivers/behavior.h>
#include <zephyr/logging/log.h>

#include <zmk/event_manager.h>
#include <zmk/events/keycode_state_changed.h>
#include <zmk/behavior.h>
#include <zmk/hid.h>

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