From 5eeb310b2f0ccf1adf91b448573001559696a1d7 Mon Sep 17 00:00:00 2001
From: Okke Formsma <okke@formsma.nl>
Date: Sat, 26 Dec 2020 14:46:56 +0100
Subject: [PATCH] feat(grave-escape): implement grave-escape

closes #85
---
 app/CMakeLists.txt                            |  1 +
 app/dts/behaviors.dtsi                        |  1 +
 app/dts/behaviors/gresc.dtsi                  | 19 ++++
 .../behaviors/zmk,behavior-mod-morph.yaml     | 16 +++
 app/include/zmk/hid.h                         |  1 +
 app/src/behaviors/behavior_mod_morph.c        | 99 +++++++++++++++++++
 app/src/hid.c                                 |  2 +
 .../gresc/gresc-press-release/events.patterns |  1 +
 .../keycode_events.snapshot                   | 18 ++++
 .../gresc-press-release/native_posix.keymap   | 49 +++++++++
 .../gresc/gresc-two-instances/events.patterns |  2 +
 .../keycode_events.snapshot                   |  6 ++
 .../gresc-two-instances/native_posix.keymap   | 43 ++++++++
 13 files changed, 258 insertions(+)
 create mode 100644 app/dts/behaviors/gresc.dtsi
 create mode 100644 app/dts/bindings/behaviors/zmk,behavior-mod-morph.yaml
 create mode 100644 app/src/behaviors/behavior_mod_morph.c
 create mode 100644 app/tests/gresc/gresc-press-release/events.patterns
 create mode 100644 app/tests/gresc/gresc-press-release/keycode_events.snapshot
 create mode 100644 app/tests/gresc/gresc-press-release/native_posix.keymap
 create mode 100644 app/tests/gresc/gresc-two-instances/events.patterns
 create mode 100644 app/tests/gresc/gresc-two-instances/keycode_events.snapshot
 create mode 100644 app/tests/gresc/gresc-two-instances/native_posix.keymap

diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt
index b217a1a1..83cf1bb8 100644
--- a/app/CMakeLists.txt
+++ b/app/CMakeLists.txt
@@ -45,6 +45,7 @@ if ((NOT CONFIG_ZMK_SPLIT) OR CONFIG_ZMK_SPLIT_BLE_ROLE_CENTRAL)
   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_momentary_layer.c)
+  target_sources(app PRIVATE src/behaviors/behavior_mod_morph.c)
   target_sources(app PRIVATE src/behaviors/behavior_outputs.c)
   target_sources(app PRIVATE src/behaviors/behavior_toggle_layer.c)
   target_sources(app PRIVATE src/behaviors/behavior_to_layer.c)
diff --git a/app/dts/behaviors.dtsi b/app/dts/behaviors.dtsi
index daa073f1..4333ceea 100644
--- a/app/dts/behaviors.dtsi
+++ b/app/dts/behaviors.dtsi
@@ -3,6 +3,7 @@
 #include <behaviors/none.dtsi>
 #include <behaviors/mod_tap.dtsi>
 #include <behaviors/layer_tap.dtsi>
+#include <behaviors/gresc.dtsi>
 #include <behaviors/sticky_key.dtsi>
 #include <behaviors/momentary_layer.dtsi>
 #include <behaviors/toggle_layer.dtsi>
diff --git a/app/dts/behaviors/gresc.dtsi b/app/dts/behaviors/gresc.dtsi
new file mode 100644
index 00000000..29593880
--- /dev/null
+++ b/app/dts/behaviors/gresc.dtsi
@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) 2020 The ZMK Contributors
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <dt-bindings/zmk/keys.h>
+
+/ {
+	behaviors {
+		/omit-if-no-ref/ gresc: grave_escape {
+			compatible = "zmk,behavior-mod-morph";
+			label = "GRAVE_ESCAPE";
+			#binding-cells = <0>;
+			bindings = <&kp ESC>, <&kp GRAVE>;
+            mods = <(MOD_LGUI|MOD_LSFT|MOD_RGUI|MOD_RSFT)>;
+		};
+	};
+};
diff --git a/app/dts/bindings/behaviors/zmk,behavior-mod-morph.yaml b/app/dts/bindings/behaviors/zmk,behavior-mod-morph.yaml
new file mode 100644
index 00000000..66b452a5
--- /dev/null
+++ b/app/dts/bindings/behaviors/zmk,behavior-mod-morph.yaml
@@ -0,0 +1,16 @@
+# Copyright (c) 2020 The ZMK Contributors
+# SPDX-License-Identifier: MIT
+
+description: Keyboard Reset Behavior
+
+compatible: "zmk,behavior-mod-morph"
+
+include: zero_param.yaml
+
+properties:
+  bindings:
+    type: phandle-array
+    required: true
+  mods:
+    type: int
+    required: true
diff --git a/app/include/zmk/hid.h b/app/include/zmk/hid.h
index 1ec51262..aca3cc46 100644
--- a/app/include/zmk/hid.h
+++ b/app/include/zmk/hid.h
@@ -166,6 +166,7 @@ struct zmk_hid_consumer_report {
     struct zmk_hid_consumer_report_body body;
 } __packed;
 
+zmk_mod_flags_t zmk_hid_get_explicit_mods();
 int zmk_hid_register_mod(zmk_mod_t modifier);
 int zmk_hid_unregister_mod(zmk_mod_t modifier);
 int zmk_hid_implicit_modifiers_press(zmk_mod_flags_t implicit_modifiers);
diff --git a/app/src/behaviors/behavior_mod_morph.c b/app/src/behaviors/behavior_mod_morph.c
new file mode 100644
index 00000000..36d109b8
--- /dev/null
+++ b/app/src/behaviors/behavior_mod_morph.c
@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2020 The ZMK Contributors
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#define DT_DRV_COMPAT zmk_behavior_mod_morph
+
+#include <device.h>
+#include <drivers/behavior.h>
+#include <logging/log.h>
+#include <zmk/behavior.h>
+
+#include <zmk/matrix.h>
+#include <zmk/endpoints.h>
+#include <zmk/event_manager.h>
+#include <zmk/events/position_state_changed.h>
+#include <zmk/events/keycode_state_changed.h>
+#include <zmk/events/modifiers_state_changed.h>
+#include <zmk/hid.h>
+
+LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
+
+#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT)
+
+struct behavior_mod_morph_config {
+    struct zmk_behavior_binding normal_binding;
+    struct zmk_behavior_binding morph_binding;
+    zmk_mod_flags_t mods;
+};
+
+struct behavior_mod_morph_data {
+    struct zmk_behavior_binding *pressed_binding;
+};
+
+static int on_mod_morph_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_mod_morph_config *cfg = dev->config;
+    struct behavior_mod_morph_data *data = dev->data;
+
+    if (data->pressed_binding != NULL) {
+        LOG_ERR("Can't press the same mod-morph twice");
+        return -ENOTSUP;
+    }
+
+    if (zmk_hid_get_explicit_mods() & cfg->mods) {
+        data->pressed_binding = (struct zmk_behavior_binding *)&cfg->morph_binding;
+    } else {
+        data->pressed_binding = (struct zmk_behavior_binding *)&cfg->normal_binding;
+    }
+    return behavior_keymap_binding_pressed(data->pressed_binding, event);
+}
+
+static int on_mod_morph_binding_released(struct zmk_behavior_binding *binding,
+                                         struct zmk_behavior_binding_event event) {
+    const struct device *dev = device_get_binding(binding->behavior_dev);
+    struct behavior_mod_morph_data *data = dev->data;
+
+    if (data->pressed_binding == NULL) {
+        LOG_ERR("Mod-morph already released");
+        return -ENOTSUP;
+    }
+
+    struct zmk_behavior_binding *pressed_binding = data->pressed_binding;
+    data->pressed_binding = NULL;
+    return behavior_keymap_binding_released(pressed_binding, event);
+}
+
+static const struct behavior_driver_api behavior_mod_morph_driver_api = {
+    .binding_pressed = on_mod_morph_binding_pressed,
+    .binding_released = on_mod_morph_binding_released,
+};
+
+static int behavior_mod_morph_init(const struct device *dev) { return 0; }
+
+#define _TRANSFORM_ENTRY(idx, node)                                                                \
+    {                                                                                              \
+        .behavior_dev = DT_LABEL(DT_INST_PHANDLE_BY_IDX(node, bindings, idx)),                     \
+        .param1 = COND_CODE_0(DT_INST_PHA_HAS_CELL_AT_IDX(node, bindings, idx, param1), (0),       \
+                              (DT_INST_PHA_BY_IDX(node, bindings, idx, param1))),                  \
+        .param2 = COND_CODE_0(DT_INST_PHA_HAS_CELL_AT_IDX(node, bindings, idx, param2), (0),       \
+                              (DT_INST_PHA_BY_IDX(node, bindings, idx, param2))),                  \
+    }
+
+#define KP_INST(n)                                                                                 \
+    static struct behavior_mod_morph_config behavior_mod_morph_config_##n = {                      \
+        .normal_binding = _TRANSFORM_ENTRY(0, n),                                                  \
+        .morph_binding = _TRANSFORM_ENTRY(1, n),                                                   \
+        .mods = DT_INST_PROP(n, mods),                                                             \
+    };                                                                                             \
+    static struct behavior_mod_morph_data behavior_mod_morph_data_##n = {};                        \
+    DEVICE_AND_API_INIT(behavior_mod_morph_##n, DT_INST_LABEL(n), behavior_mod_morph_init,         \
+                        &behavior_mod_morph_data_##n, &behavior_mod_morph_config_##n, APPLICATION, \
+                        CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, &behavior_mod_morph_driver_api);
+
+DT_INST_FOREACH_STATUS_OKAY(KP_INST)
+
+#endif
\ No newline at end of file
diff --git a/app/src/hid.c b/app/src/hid.c
index b9ddfc52..65eabd9c 100644
--- a/app/src/hid.c
+++ b/app/src/hid.c
@@ -26,6 +26,8 @@ static zmk_mod_flags_t explicit_modifiers = 0;
         LOG_DBG("Modifiers set to 0x%02X", keyboard_report.body.modifiers);                        \
     }
 
+zmk_mod_flags_t zmk_hid_get_explicit_mods() { return explicit_modifiers; }
+
 int zmk_hid_register_mod(zmk_mod_t modifier) {
     explicit_modifier_counts[modifier]++;
     LOG_DBG("Modifier %d count %d", modifier, explicit_modifier_counts[modifier]);
diff --git a/app/tests/gresc/gresc-press-release/events.patterns b/app/tests/gresc/gresc-press-release/events.patterns
new file mode 100644
index 00000000..b1342af4
--- /dev/null
+++ b/app/tests/gresc/gresc-press-release/events.patterns
@@ -0,0 +1 @@
+s/.*hid_listener_keycode_//p
diff --git a/app/tests/gresc/gresc-press-release/keycode_events.snapshot b/app/tests/gresc/gresc-press-release/keycode_events.snapshot
new file mode 100644
index 00000000..ebeba596
--- /dev/null
+++ b/app/tests/gresc/gresc-press-release/keycode_events.snapshot
@@ -0,0 +1,18 @@
+pressed: usage_page 0x07 keycode 0x29 mods 0x00
+released: usage_page 0x07 keycode 0x29 mods 0x00
+pressed: usage_page 0x07 keycode 0xe1 mods 0x00
+pressed: usage_page 0x07 keycode 0x35 mods 0x00
+released: usage_page 0x07 keycode 0x35 mods 0x00
+released: usage_page 0x07 keycode 0xe1 mods 0x00
+pressed: usage_page 0x07 keycode 0xe3 mods 0x00
+pressed: usage_page 0x07 keycode 0x35 mods 0x00
+released: usage_page 0x07 keycode 0x35 mods 0x00
+released: usage_page 0x07 keycode 0xe3 mods 0x00
+pressed: usage_page 0x07 keycode 0xe1 mods 0x00
+pressed: usage_page 0x07 keycode 0x35 mods 0x00
+released: usage_page 0x07 keycode 0xe1 mods 0x00
+released: usage_page 0x07 keycode 0x35 mods 0x00
+pressed: usage_page 0x07 keycode 0xe3 mods 0x00
+pressed: usage_page 0x07 keycode 0x35 mods 0x00
+released: usage_page 0x07 keycode 0xe3 mods 0x00
+released: usage_page 0x07 keycode 0x35 mods 0x00
diff --git a/app/tests/gresc/gresc-press-release/native_posix.keymap b/app/tests/gresc/gresc-press-release/native_posix.keymap
new file mode 100644
index 00000000..7ca3d77d
--- /dev/null
+++ b/app/tests/gresc/gresc-press-release/native_posix.keymap
@@ -0,0 +1,49 @@
+#include <dt-bindings/zmk/keys.h>
+#include <behaviors.dtsi>
+#include <dt-bindings/zmk/kscan-mock.h>
+
+/ {
+	keymap {
+		compatible = "zmk,keymap";
+		label ="Default keymap";
+
+		default_layer {
+			bindings = <
+				&gresc         &none
+				&kp LEFT_SHIFT &kp LEFT_GUI
+			>;
+		};
+	};
+};
+
+&kscan {
+	events = <
+		/* esc */
+		ZMK_MOCK_PRESS(0,0,10) 
+		ZMK_MOCK_RELEASE(0,0,10)
+
+		/* ~ */
+		ZMK_MOCK_PRESS(1,0,10) 
+		ZMK_MOCK_PRESS(0,0,10) 
+		ZMK_MOCK_RELEASE(0,0,10)
+		ZMK_MOCK_RELEASE(1,0,10)
+
+		/* LGUI+` */
+		ZMK_MOCK_PRESS(1,1,10) 
+		ZMK_MOCK_PRESS(0,0,10) 
+		ZMK_MOCK_RELEASE(0,0,10)
+		ZMK_MOCK_RELEASE(1,1,10)
+
+		/* ~ */
+		ZMK_MOCK_PRESS(1,0,10) 
+		ZMK_MOCK_PRESS(0,0,10) 
+		ZMK_MOCK_RELEASE(1,0,10)
+		ZMK_MOCK_RELEASE(0,0,10)
+
+		/* LGUI+` */
+		ZMK_MOCK_PRESS(1,1,10) 
+		ZMK_MOCK_PRESS(0,0,10) 
+		ZMK_MOCK_RELEASE(1,1,10)
+		ZMK_MOCK_RELEASE(0,0,10)
+	>;
+};
\ No newline at end of file
diff --git a/app/tests/gresc/gresc-two-instances/events.patterns b/app/tests/gresc/gresc-two-instances/events.patterns
new file mode 100644
index 00000000..ef7b7955
--- /dev/null
+++ b/app/tests/gresc/gresc-two-instances/events.patterns
@@ -0,0 +1,2 @@
+s/.*hid_listener_keycode_//p
+s/.*on_mod_morph_binding_/morph_binding_/p
\ No newline at end of file
diff --git a/app/tests/gresc/gresc-two-instances/keycode_events.snapshot b/app/tests/gresc/gresc-two-instances/keycode_events.snapshot
new file mode 100644
index 00000000..170e0cb1
--- /dev/null
+++ b/app/tests/gresc/gresc-two-instances/keycode_events.snapshot
@@ -0,0 +1,6 @@
+pressed: usage_page 0x07 keycode 0x29 mods 0x00
+released: usage_page 0x07 keycode 0x29 mods 0x00
+pressed: usage_page 0x07 keycode 0xe1 mods 0x00
+pressed: usage_page 0x07 keycode 0x35 mods 0x00
+released: usage_page 0x07 keycode 0xe1 mods 0x00
+released: usage_page 0x07 keycode 0x35 mods 0x00
diff --git a/app/tests/gresc/gresc-two-instances/native_posix.keymap b/app/tests/gresc/gresc-two-instances/native_posix.keymap
new file mode 100644
index 00000000..0c38721d
--- /dev/null
+++ b/app/tests/gresc/gresc-two-instances/native_posix.keymap
@@ -0,0 +1,43 @@
+#include <dt-bindings/zmk/keys.h>
+#include <behaviors.dtsi>
+#include <dt-bindings/zmk/kscan-mock.h>
+
+/*
+This test checks nothing breaks if two grave-escapes are pressed at the same time.
+If someone ever really needs two, they can make a second behavior definition.
+
+The second gresc that is pressed is ignored.
+The first gresc that is released releases the key.
+*/
+
+/ {
+	keymap {
+		compatible = "zmk,keymap";
+		label ="Default keymap";
+
+		default_layer {
+			bindings = <
+				&gresc         &gresc
+				&kp LEFT_SHIFT &kp LEFT_GUI
+			>;
+		};
+	};
+};
+
+&kscan {
+	events = <
+		/* esc */
+		ZMK_MOCK_PRESS(0,0,10) 
+		ZMK_MOCK_PRESS(0,1,10) /* the second gresc is ignored */
+		ZMK_MOCK_RELEASE(0,0,10)
+		ZMK_MOCK_RELEASE(0,1,10) /* the second gresc is ignored */
+
+		/* ~ */
+		ZMK_MOCK_PRESS(1,0,10) 
+		ZMK_MOCK_PRESS(0,1,10)
+		ZMK_MOCK_PRESS(0,0,10)  /* the second gresc is ignored */ 
+		ZMK_MOCK_RELEASE(1,0,10)
+		ZMK_MOCK_RELEASE(0,1,10)
+		ZMK_MOCK_RELEASE(0,0,10)  /* the second gresc is ignored */
+	>;
+};
\ No newline at end of file