diff --git a/quantum/action_tapping.c b/quantum/action_tapping.c
index 8f238490f2a..af7b8b5ce1c 100644
--- a/quantum/action_tapping.c
+++ b/quantum/action_tapping.c
@@ -49,6 +49,47 @@ __attribute__((weak)) bool get_permissive_hold(uint16_t keycode, keyrecord_t *re
}
# endif
+# ifdef CHORDAL_HOLD
+__attribute__((weak)) bool get_chordal_hold(
+ uint16_t tap_hold_keycode, keyrecord_t* tap_hold_record,
+ uint16_t other_keycode, keyrecord_t* other_record) {
+ return get_chordal_hold_default(tap_hold_record, other_record);
+}
+
+bool get_chordal_hold_default(
+ keyrecord_t* tap_hold_record, keyrecord_t* other_record) {
+ uint8_t tap_hold_hand = chordal_hold_handedness_user(tap_hold_record->event.key);
+ if (tap_hold_hand == 0) {
+ return true;
+ }
+ uint8_t other_hand = chordal_hold_handedness_user(other_record->event.key);
+ return other_hand == 0 || tap_hold_hand != other_hand;
+}
+
+__attribute__((weak)) uint8_t chordal_hold_handedness_kb(keypos_t key) {
+# if defined(SPLIT_KEYBOARD) || ((MATRIX_ROWS) > (MATRIX_COLS))
+#pragma message "Inferred handedness rows"
+ // If the keyboard is split or if MATRIX_ROWS > MATRIX_COLS, assume that the
+ // first half of the rows are left and the latter half are right.
+ return (key.row < (MATRIX_ROWS) / 2) ? /*left*/ 1 : /*right*/ 2;
+# else
+#pragma message "Inferred handedness cols"
+ // Otherwise, assume the first half of the cols are left, others are right.
+ return (key.col < (MATRIX_COLS) / 2) ? /*left*/ 1 : /*right*/ 2;
+# endif
+}
+
+__attribute__((weak)) uint8_t chordal_hold_handedness_user(keypos_t key) {
+# if defined(CHORDAL_HOLD_LAYOUT)
+# pragma message "Using chordal_hold_layout"
+ // If given, read handedness from `chordal_hold_layout` array.
+ return pgm_read_byte(&chordal_hold_layout[key.row][key.col]);
+# else
+ return chordal_hold_handedness_kb(key);
+# endif
+}
+# endif // CHORDAL_HOLD
+
# ifdef HOLD_ON_OTHER_KEY_PRESS_PER_KEY
__attribute__((weak)) bool get_hold_on_other_key_press(uint16_t keycode, keyrecord_t *record) {
return false;
@@ -188,7 +229,7 @@ bool process_tapping(keyrecord_t *keyp) {
return true;
}
-# if (defined(AUTO_SHIFT_ENABLE) && defined(RETRO_SHIFT)) || defined(PERMISSIVE_HOLD_PER_KEY) || defined(HOLD_ON_OTHER_KEY_PRESS_PER_KEY)
+# if (defined(AUTO_SHIFT_ENABLE) && defined(RETRO_SHIFT)) || defined(PERMISSIVE_HOLD_PER_KEY) || defined(CHORDAL_HOLD) || defined(HOLD_ON_OTHER_KEY_PRESS_PER_KEY)
TAP_DEFINE_KEYCODE;
# endif
@@ -271,6 +312,22 @@ bool process_tapping(keyrecord_t *keyp) {
// set interrupted flag when other key pressed during tapping
if (event.pressed) {
tapping_key.tap.interrupted = true;
+
+# if defined(CHORDAL_HOLD)
+ if (!is_tap_record(keyp) &&
+ !get_chordal_hold(tapping_keycode, &tapping_key,
+ get_record_keycode(keyp, true), keyp)) {
+ // Settle the tapping key as *tapped*, since it is
+ // not considered a held chord with keyp.
+ ac_dprintf("Tapping: End. Tap in non-chord\n");
+ tapping_key.tap.count = 1;
+ // In process_action(), HOLD_ON_OTHER_KEY_PRESS will
+ // revert interrupted events to holds, so this needs
+ // to be set false.
+ tapping_key.tap.interrupted = false;
+ process_record(&tapping_key);
+ } else
+# endif
if (TAP_GET_HOLD_ON_OTHER_KEY_PRESS
# if defined(AUTO_SHIFT_ENABLE) && defined(RETRO_SHIFT)
// Auto Shift cannot evaluate this early
@@ -278,6 +335,8 @@ bool process_tapping(keyrecord_t *keyp) {
&& !(MAYBE_RETRO_SHIFTING(event, keyp) && get_auto_shifted_key(get_record_keycode(keyp, false), keyp))
# endif
) {
+ // Settle the tapping key as *held*, since
+ // HOLD_ON_OTHER_KEY_PRESS is enabled for this key.
ac_dprintf("Tapping: End. No tap. Interfered by pressed key\n");
process_record(&tapping_key);
tapping_key = (keyrecord_t){0};
diff --git a/quantum/action_tapping.h b/quantum/action_tapping.h
index 6b518b82988..0e7721b5008 100644
--- a/quantum/action_tapping.h
+++ b/quantum/action_tapping.h
@@ -46,6 +46,82 @@ bool get_permissive_hold(uint16_t keycode, keyrecord_t *record);
bool get_retro_tapping(uint16_t keycode, keyrecord_t *record);
bool get_hold_on_other_key_press(uint16_t keycode, keyrecord_t *record);
+#ifdef CHORDAL_HOLD
+/**
+ * Callback to say when a key chord before the tapping term is considered held.
+ *
+ * In keymap.c, define the callback
+ *
+ * bool get_chordal_hold(uint16_t tap_hold_keycode,
+ * keyrecord_t* tap_hold_record,
+ * uint16_t other_keycode,
+ * keyrecord_t* other_record) {
+ * // Conditions...
+ * }
+ *
+ * This callback is called when:
+ *
+ * 1. `tap_hold_keycode` is pressed.
+ * 2. `other_keycode` is pressed while `tap_hold_keycode` is still held,
+ * provided `other_keycode` is *not* also a tap-hold key and it is pressed
+ * before the tapping term.
+ *
+ * Returning true indicates that the tap-hold key should be considered held, or
+ * false to consider it tapped.
+ *
+ * @param tap_hold_keycode Keycode of the tap-hold key.
+ * @param tap_hold_record Record from the tap-hold press event.
+ * @param other_keycode Keycode of the other key.
+ * @param other_record Record from the other key's press event.
+ * @return True if the tap-hold key is considered held; false if tapped.
+ */
+bool get_chordal_hold(
+ uint16_t tap_hold_keycode, keyrecord_t* tap_hold_record,
+ uint16_t other_keycode, keyrecord_t* other_record);
+
+/**
+ * Default "opposite hands rule" for whether a key chord should settle as held.
+ *
+ * This function returns true when the tap-hold key and other key are on
+ * "opposite hands." In detail, handedness of the two keys are compared. If
+ * handedness values differ, or if either handedness is zero, the function
+ * returns true, indicating a hold. Otherwise, it returns false, indicating that
+ * the tap-hold key should settle as tapped.
+ *
+ * "Handedness" is determined as follows, in order of decending precedence:
+ * 1. `chordal_hold_handedness_user()`, if defined.
+ * 2. `chordal_hold_layout`, if CHORDAL_HOLD_LAYOUT is defined.
+ * 3. `chordal_hold_handedness_kb()`, if defined.
+ * 4. fallback assumption based on keyboard matrix dimensions.
+ *
+ * @param tap_hold_record Record of the active tap-hold key press.
+ * @param other_record Record of the other, interrupting key press.
+ * @return True if the tap-hold key is considered held; false if tapped.
+ */
+bool get_chordal_hold_default(
+ keyrecord_t* tap_hold_record, keyrecord_t* other_record);
+
+/**
+ * Keyboard-level callback to determine handedness of a key.
+ *
+ * This function should return:
+ * 1 for keys pressed by the left hand,
+ * 2 for keys on the right hand,
+ * 0 for keys exempt from the "opposite hands rule." This could be used
+ * perhaps on thumb keys or keys that might be pressed by either hand.
+ *
+ * @param key A key matrix position.
+ * @return Handedness value.
+ */
+uint8_t chordal_hold_handedness_kb(keypos_t key);
+/** User callback to determine handedness of a key. */
+uint8_t chordal_hold_handedness_user(keypos_t key);
+
+# ifdef CHORDAL_HOLD_LAYOUT
+extern const uint8_t chordal_hold_layout[MATRIX_ROWS][MATRIX_COLS] PROGMEM;
+# endif
+#endif
+
#ifdef DYNAMIC_TAPPING_TERM_ENABLE
extern uint16_t g_tapping_term;
#endif
diff --git a/tests/tap_hold_configurations/chordal_hold/config.h b/tests/tap_hold_configurations/chordal_hold/config.h
new file mode 100644
index 00000000000..510b6a035f2
--- /dev/null
+++ b/tests/tap_hold_configurations/chordal_hold/config.h
@@ -0,0 +1,21 @@
+/* Copyright 2022 Vladislav Kucheriavykh
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#pragma once
+
+#include "test_common.h"
+#define CHORDAL_HOLD
+
diff --git a/tests/tap_hold_configurations/chordal_hold/test.mk b/tests/tap_hold_configurations/chordal_hold/test.mk
new file mode 100644
index 00000000000..6b5968df16f
--- /dev/null
+++ b/tests/tap_hold_configurations/chordal_hold/test.mk
@@ -0,0 +1,18 @@
+# Copyright 2022 Vladislav Kucheriavykh
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+# --------------------------------------------------------------------------------
+# Keep this file, even if it is empty, as a marker that this folder contains tests
+# --------------------------------------------------------------------------------
diff --git a/tests/tap_hold_configurations/chordal_hold/test_tap_hold.cpp b/tests/tap_hold_configurations/chordal_hold/test_tap_hold.cpp
new file mode 100644
index 00000000000..cbe8e4e1c20
--- /dev/null
+++ b/tests/tap_hold_configurations/chordal_hold/test_tap_hold.cpp
@@ -0,0 +1,167 @@
+/* Copyright 2022 Vladislav Kucheriavykh
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "keyboard_report_util.hpp"
+#include "keycode.h"
+#include "test_common.hpp"
+#include "action_tapping.h"
+#include "test_fixture.hpp"
+#include "test_keymap_key.hpp"
+
+using testing::_;
+using testing::InSequence;
+
+class ChordalHold : public TestFixture {};
+
+TEST_F(ChordalHold, chord_with_mod_tap_settled_as_hold) {
+ TestDriver driver;
+ InSequence s;
+ // Mod-tap key on the left hand.
+ auto mod_tap_hold_key = KeymapKey(0, 1, 0, SFT_T(KC_P));
+ // Regular key on the right hand.
+ auto regular_key = KeymapKey(0, MATRIX_COLS - 1, 0, KC_A);
+
+ set_keymap({mod_tap_hold_key, regular_key});
+
+ /* Press mod-tap-hold key. */
+ EXPECT_NO_REPORT(driver);
+ mod_tap_hold_key.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ /* Press regular key. */
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT));
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_A));
+ regular_key.press();
+ idle_for(TAPPING_TERM);
+ VERIFY_AND_CLEAR(driver);
+
+ /* Release regular key. */
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT));
+ regular_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ /* Release mod-tap-hold key. */
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_hold_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(ChordalHold, non_chord_with_mod_tap_settled_as_tap) {
+ TestDriver driver;
+ InSequence s;
+ // Mod-tap key and regular key both on the left hand.
+ auto mod_tap_hold_key = KeymapKey(0, 1, 0, SFT_T(KC_P));
+ auto regular_key = KeymapKey(0, 2, 0, KC_A);
+
+ set_keymap({mod_tap_hold_key, regular_key});
+
+ /* Press mod-tap-hold key. */
+ EXPECT_NO_REPORT(driver);
+ mod_tap_hold_key.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ /* Press regular key. */
+ EXPECT_REPORT(driver, (KC_P));
+ EXPECT_REPORT(driver, (KC_P, KC_A));
+ regular_key.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ /* Release regular key. */
+ EXPECT_REPORT(driver, (KC_P));
+ regular_key.release();
+ idle_for(TAPPING_TERM);
+ VERIFY_AND_CLEAR(driver);
+
+ /* Release mod-tap-hold key. */
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_hold_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(ChordalHold, tap_mod_tap_key) {
+ TestDriver driver;
+ InSequence s;
+ auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_P));
+
+ set_keymap({mod_tap_key});
+
+ EXPECT_NO_REPORT(driver);
+ mod_tap_key.press();
+ idle_for(TAPPING_TERM - 1);
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_REPORT(driver, (KC_P));
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(ChordalHold, hold_mod_tap_key) {
+ TestDriver driver;
+ InSequence s;
+ auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_P));
+
+ set_keymap({mod_tap_key});
+
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT));
+ mod_tap_key.press();
+ idle_for(TAPPING_TERM + 1);
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(ChordalHold, chordal_hold_ignores_multiple_mod_taps) {
+ TestDriver driver;
+ InSequence s;
+ auto mod_tap_key1 = KeymapKey(0, 1, 0, SFT_T(KC_A));
+ auto mod_tap_key2 = KeymapKey(0, 2, 0, RSFT_T(KC_B));
+
+ set_keymap({mod_tap_key1, mod_tap_key2});
+
+ /* Press mod-tap-hold key. */
+ EXPECT_NO_REPORT(driver);
+ mod_tap_key1.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ /* Press second mod-tap key. */
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT));
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_RIGHT_SHIFT));
+ mod_tap_key2.press();
+ idle_for(TAPPING_TERM + 1);
+ VERIFY_AND_CLEAR(driver);
+
+ /* Release keys. */
+ EXPECT_REPORT(driver, (KC_RIGHT_SHIFT));
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_key1.release();
+ run_one_scan_loop();
+ mod_tap_key2.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
diff --git a/tests/tap_hold_configurations/chordal_hold_and_hold_on_other_key_press/config.h b/tests/tap_hold_configurations/chordal_hold_and_hold_on_other_key_press/config.h
new file mode 100644
index 00000000000..b1d37b1e3f9
--- /dev/null
+++ b/tests/tap_hold_configurations/chordal_hold_and_hold_on_other_key_press/config.h
@@ -0,0 +1,22 @@
+/* Copyright 2022 Vladislav Kucheriavykh
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#pragma once
+
+#include "test_common.h"
+#define CHORDAL_HOLD
+#define HOLD_ON_OTHER_KEY_PRESS
+
diff --git a/tests/tap_hold_configurations/chordal_hold_and_hold_on_other_key_press/test.mk b/tests/tap_hold_configurations/chordal_hold_and_hold_on_other_key_press/test.mk
new file mode 100644
index 00000000000..6b5968df16f
--- /dev/null
+++ b/tests/tap_hold_configurations/chordal_hold_and_hold_on_other_key_press/test.mk
@@ -0,0 +1,18 @@
+# Copyright 2022 Vladislav Kucheriavykh
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+# --------------------------------------------------------------------------------
+# Keep this file, even if it is empty, as a marker that this folder contains tests
+# --------------------------------------------------------------------------------
diff --git a/tests/tap_hold_configurations/chordal_hold_and_hold_on_other_key_press/test_tap_hold.cpp b/tests/tap_hold_configurations/chordal_hold_and_hold_on_other_key_press/test_tap_hold.cpp
new file mode 100644
index 00000000000..68ee607ac8f
--- /dev/null
+++ b/tests/tap_hold_configurations/chordal_hold_and_hold_on_other_key_press/test_tap_hold.cpp
@@ -0,0 +1,167 @@
+/* Copyright 2022 Vladislav Kucheriavykh
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "keyboard_report_util.hpp"
+#include "keycode.h"
+#include "test_common.hpp"
+#include "action_tapping.h"
+#include "test_fixture.hpp"
+#include "test_keymap_key.hpp"
+
+using testing::_;
+using testing::InSequence;
+
+class ChordalHoldAndHoldOnOtherKeypress : public TestFixture {};
+
+TEST_F(ChordalHoldAndHoldOnOtherKeypress, chord_with_mod_tap_settled_as_hold) {
+ TestDriver driver;
+ InSequence s;
+ // Mod-tap key on the left hand.
+ auto mod_tap_hold_key = KeymapKey(0, 1, 0, SFT_T(KC_P));
+ // Regular key on the right hand.
+ auto regular_key = KeymapKey(0, MATRIX_COLS - 1, 0, KC_A);
+
+ set_keymap({mod_tap_hold_key, regular_key});
+
+ /* Press mod-tap-hold key. */
+ EXPECT_NO_REPORT(driver);
+ mod_tap_hold_key.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ /* Press regular key. */
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT));
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_A));
+ regular_key.press();
+ idle_for(TAPPING_TERM);
+ VERIFY_AND_CLEAR(driver);
+
+ /* Release regular key. */
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT));
+ regular_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ /* Release mod-tap-hold key. */
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_hold_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(ChordalHoldAndHoldOnOtherKeypress, non_chord_with_mod_tap_settled_as_tap) {
+ TestDriver driver;
+ InSequence s;
+ // Mod-tap key and regular key both on the left hand.
+ auto mod_tap_hold_key = KeymapKey(0, 1, 0, SFT_T(KC_P));
+ auto regular_key = KeymapKey(0, 2, 0, KC_A);
+
+ set_keymap({mod_tap_hold_key, regular_key});
+
+ /* Press mod-tap-hold key. */
+ EXPECT_NO_REPORT(driver);
+ mod_tap_hold_key.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ /* Press regular key. */
+ EXPECT_REPORT(driver, (KC_P));
+ EXPECT_REPORT(driver, (KC_P, KC_A));
+ regular_key.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ /* Release regular key. */
+ EXPECT_REPORT(driver, (KC_P));
+ regular_key.release();
+ idle_for(TAPPING_TERM);
+ VERIFY_AND_CLEAR(driver);
+
+ /* Release mod-tap-hold key. */
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_hold_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(ChordalHoldAndHoldOnOtherKeypress, tap_mod_tap_key) {
+ TestDriver driver;
+ InSequence s;
+ auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_P));
+
+ set_keymap({mod_tap_key});
+
+ EXPECT_NO_REPORT(driver);
+ mod_tap_key.press();
+ idle_for(TAPPING_TERM - 1);
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_REPORT(driver, (KC_P));
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(ChordalHoldAndHoldOnOtherKeypress, hold_mod_tap_key) {
+ TestDriver driver;
+ InSequence s;
+ auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_P));
+
+ set_keymap({mod_tap_key});
+
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT));
+ mod_tap_key.press();
+ idle_for(TAPPING_TERM + 1);
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(ChordalHoldAndHoldOnOtherKeypress, chordal_hold_ignores_multiple_mod_taps) {
+ TestDriver driver;
+ InSequence s;
+ auto mod_tap_key1 = KeymapKey(0, 1, 0, SFT_T(KC_A));
+ auto mod_tap_key2 = KeymapKey(0, 2, 0, RSFT_T(KC_B));
+
+ set_keymap({mod_tap_key1, mod_tap_key2});
+
+ /* Press mod-tap-hold key. */
+ EXPECT_NO_REPORT(driver);
+ mod_tap_key1.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ /* Press second mod-tap key. */
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT));
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_RIGHT_SHIFT));
+ mod_tap_key2.press();
+ idle_for(TAPPING_TERM + 1);
+ VERIFY_AND_CLEAR(driver);
+
+ /* Release keys. */
+ EXPECT_REPORT(driver, (KC_RIGHT_SHIFT));
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_key1.release();
+ run_one_scan_loop();
+ mod_tap_key2.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
diff --git a/tests/tap_hold_configurations/chordal_hold_and_permissive_hold/config.h b/tests/tap_hold_configurations/chordal_hold_and_permissive_hold/config.h
new file mode 100644
index 00000000000..7f06b679a85
--- /dev/null
+++ b/tests/tap_hold_configurations/chordal_hold_and_permissive_hold/config.h
@@ -0,0 +1,22 @@
+/* Copyright 2022 Vladislav Kucheriavykh
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#pragma once
+
+#include "test_common.h"
+#define CHORDAL_HOLD
+#define PERMISSIVE_HOLD
+
diff --git a/tests/tap_hold_configurations/chordal_hold_and_permissive_hold/test.mk b/tests/tap_hold_configurations/chordal_hold_and_permissive_hold/test.mk
new file mode 100644
index 00000000000..6b5968df16f
--- /dev/null
+++ b/tests/tap_hold_configurations/chordal_hold_and_permissive_hold/test.mk
@@ -0,0 +1,18 @@
+# Copyright 2022 Vladislav Kucheriavykh
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+# --------------------------------------------------------------------------------
+# Keep this file, even if it is empty, as a marker that this folder contains tests
+# --------------------------------------------------------------------------------
diff --git a/tests/tap_hold_configurations/chordal_hold_and_permissive_hold/test_tap_hold.cpp b/tests/tap_hold_configurations/chordal_hold_and_permissive_hold/test_tap_hold.cpp
new file mode 100644
index 00000000000..fdb053a5b21
--- /dev/null
+++ b/tests/tap_hold_configurations/chordal_hold_and_permissive_hold/test_tap_hold.cpp
@@ -0,0 +1,167 @@
+/* Copyright 2022 Vladislav Kucheriavykh
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "keyboard_report_util.hpp"
+#include "keycode.h"
+#include "test_common.hpp"
+#include "action_tapping.h"
+#include "test_fixture.hpp"
+#include "test_keymap_key.hpp"
+
+using testing::_;
+using testing::InSequence;
+
+class ChordalHoldAndPermissiveHold : public TestFixture {};
+
+TEST_F(ChordalHoldAndPermissiveHold, chord_with_mod_tap_settled_as_hold) {
+ TestDriver driver;
+ InSequence s;
+ // Mod-tap key on the left hand.
+ auto mod_tap_hold_key = KeymapKey(0, 1, 0, SFT_T(KC_P));
+ // Regular key on the right hand.
+ auto regular_key = KeymapKey(0, MATRIX_COLS - 1, 0, KC_A);
+
+ set_keymap({mod_tap_hold_key, regular_key});
+
+ /* Press mod-tap-hold key. */
+ EXPECT_NO_REPORT(driver);
+ mod_tap_hold_key.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ /* Press regular key. */
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT));
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_A));
+ regular_key.press();
+ idle_for(TAPPING_TERM);
+ VERIFY_AND_CLEAR(driver);
+
+ /* Release regular key. */
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT));
+ regular_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ /* Release mod-tap-hold key. */
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_hold_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(ChordalHoldAndPermissiveHold, non_chord_with_mod_tap_settled_as_tap) {
+ TestDriver driver;
+ InSequence s;
+ // Mod-tap key and regular key both on the left hand.
+ auto mod_tap_hold_key = KeymapKey(0, 1, 0, SFT_T(KC_P));
+ auto regular_key = KeymapKey(0, 2, 0, KC_A);
+
+ set_keymap({mod_tap_hold_key, regular_key});
+
+ /* Press mod-tap-hold key. */
+ EXPECT_NO_REPORT(driver);
+ mod_tap_hold_key.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ /* Press regular key. */
+ EXPECT_REPORT(driver, (KC_P));
+ EXPECT_REPORT(driver, (KC_P, KC_A));
+ regular_key.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ /* Release regular key. */
+ EXPECT_REPORT(driver, (KC_P));
+ regular_key.release();
+ idle_for(TAPPING_TERM);
+ VERIFY_AND_CLEAR(driver);
+
+ /* Release mod-tap-hold key. */
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_hold_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(ChordalHoldAndPermissiveHold, tap_mod_tap_key) {
+ TestDriver driver;
+ InSequence s;
+ auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_P));
+
+ set_keymap({mod_tap_key});
+
+ EXPECT_NO_REPORT(driver);
+ mod_tap_key.press();
+ idle_for(TAPPING_TERM - 1);
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_REPORT(driver, (KC_P));
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(ChordalHoldAndPermissiveHold, hold_mod_tap_key) {
+ TestDriver driver;
+ InSequence s;
+ auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_P));
+
+ set_keymap({mod_tap_key});
+
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT));
+ mod_tap_key.press();
+ idle_for(TAPPING_TERM + 1);
+ VERIFY_AND_CLEAR(driver);
+
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_key.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(ChordalHoldAndPermissiveHold, chordal_hold_ignores_multiple_mod_taps) {
+ TestDriver driver;
+ InSequence s;
+ auto mod_tap_key1 = KeymapKey(0, 1, 0, SFT_T(KC_A));
+ auto mod_tap_key2 = KeymapKey(0, 2, 0, RSFT_T(KC_B));
+
+ set_keymap({mod_tap_key1, mod_tap_key2});
+
+ /* Press mod-tap-hold key. */
+ EXPECT_NO_REPORT(driver);
+ mod_tap_key1.press();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+
+ /* Press second mod-tap key. */
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT));
+ EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_RIGHT_SHIFT));
+ mod_tap_key2.press();
+ idle_for(TAPPING_TERM + 1);
+ VERIFY_AND_CLEAR(driver);
+
+ /* Release keys. */
+ EXPECT_REPORT(driver, (KC_RIGHT_SHIFT));
+ EXPECT_EMPTY_REPORT(driver);
+ mod_tap_key1.release();
+ run_one_scan_loop();
+ mod_tap_key2.release();
+ run_one_scan_loop();
+ VERIFY_AND_CLEAR(driver);
+}
+