From 0f3993b9a3cd3a50b2c9e5d920d6bdb40a9d78be Mon Sep 17 00:00:00 2001 From: Ryan Date: Mon, 16 Dec 2024 06:06:05 +1100 Subject: default_keyboard.h generation tweaks (#24715) --- lib/python/qmk/cli/generate/keyboard_h.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) (limited to 'lib/python/qmk/cli/generate') diff --git a/lib/python/qmk/cli/generate/keyboard_h.py b/lib/python/qmk/cli/generate/keyboard_h.py index 5863a0983a..cb9528d96b 100755 --- a/lib/python/qmk/cli/generate/keyboard_h.py +++ b/lib/python/qmk/cli/generate/keyboard_h.py @@ -22,6 +22,11 @@ def _generate_layouts(keyboard, kb_info_json): row_num = kb_info_json['matrix_size']['rows'] lines = [] + lines.append('') + lines.append('// Layout content') + lines.append('') + lines.append('#define XXX KC_NO') + for layout_name, layout_data in kb_info_json['layouts'].items(): if layout_data['c_macro']: continue @@ -31,7 +36,7 @@ def _generate_layouts(keyboard, kb_info_json): continue layout_keys = [] - layout_matrix = [['KC_NO'] * col_num for _ in range(row_num)] + layout_matrix = [['XXX'] * col_num for _ in range(row_num)] for key_data in layout_data['layout']: row, col = key_data['matrix'] @@ -46,7 +51,7 @@ def _generate_layouts(keyboard, kb_info_json): lines.append('') lines.append(f'#define {layout_name}({", ".join(layout_keys)}) {{ \\') - rows = ', \\\n'.join(['\t {' + ', '.join(row) + '}' for row in layout_matrix]) + rows = ', \\\n'.join([' { ' + ', '.join(row) + ' }' for row in layout_matrix]) rows += ' \\' lines.append(rows) lines.append('}') @@ -67,6 +72,9 @@ def _generate_keycodes(kb_info_json): return [] lines = [] + lines.append('') + lines.append('// Keycode content') + lines.append('') lines.append('enum keyboard_keycodes {') for index, item in enumerate(kb_info_json.get('keycodes')): @@ -103,17 +111,14 @@ def generate_keyboard_h(cli): valid_config = dd_layouts or keyboard_h # Build the layouts.h file. - keyboard_h_lines = [GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE, '#pragma once', '#include "quantum.h"'] + keyboard_h_lines = [GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE, '#pragma once', '', '#include "quantum.h"'] - keyboard_h_lines.append('') - keyboard_h_lines.append('// Layout content') if dd_layouts: keyboard_h_lines.extend(dd_layouts) + if keyboard_h: keyboard_h_lines.append(f'#include "{Path(keyboard_h).name}"') - keyboard_h_lines.append('') - keyboard_h_lines.append('// Keycode content') if dd_keycodes: keyboard_h_lines.extend(dd_keycodes) -- cgit v1.2.3 From 544ddde113657a932275399f577a422a809d2880 Mon Sep 17 00:00:00 2001 From: Pascal Getreuer Date: Mon, 27 Jan 2025 03:32:23 -0800 Subject: [Core] Add Chordal Hold, an "opposite hands rule" tap-hold option similar to Achordion, Bilateral Combinations. (#24560) * Chordal Hold: restrict what chords settle as hold * Chordal Hold: docs and further improvements * Fix formatting. * Doc rewording and minor edit. * Support Chordal Hold of multiple tap-hold keys. * Fix formatting. * Simplification and additional test. * Fix formatting. * Tighten tests. * Add test two_mod_taps_same_hand_hold_til_timeout. * Revise handing of pairs of tap-hold keys. * Generate a default chordal_hold_layout. * Document chordal_hold_handedness(). * Add license notice to new and branched files in PR. * Add `tapping.chordal_hold` property for info.json. * Update docs/reference_info_json.md * Revise "hand" jsonschema. * Chordal Hold: Improved layout handedness heuristic. This commit improves the heuristic used in generate-keyboard-c for inferring key handedness from keyboard.json geometry data. Heuristic summary: 1. If the layout is symmetric (e.g. most split keyboards), guess the handedness based on the sign of (x - layout_x_midpoint). 2. Otherwise, if the layout has a key of >=6u width, it is probably the spacebar. Form a dividing line through the spacebar, nearly vertical but with a slight angle to follow typical row stagger. 3. Otherwise, assume handedness based on the widest horizontal separation. I have tested this strategy on a couple dozen keyboards and found it to work reliably. * Use Optional instead of `| None`. * Refactor to avoid lambdas. * Remove trailing comma in chordal_hold_layout. * Minor docs edits. * Revise to allow combining multiple same-hand mods. This commit revises Chordal Hold as described in discussion in https://github.com/qmk/qmk_firmware/pull/24560#discussion_r1894655238 1. In "RCTL_T(KC_A)↓, RSFT_T(KC_C)↓, RCTL_T(KC_A)↑" before the tapping term, RCTL_T(KC_A) is settled as tapped. 2. In "RCTL_T(KC_A)↓, RSFT_T(KC_C)↓, RSFT_T(KC_C)↑", both RCTL_T(KC_A) and RSFT_T(KC_C) are settled as tapped. 3. In "RCTL_T(KC_A)↓, RSFT_T(KC_C)↓, KC_U↓" (all keys on the same side), both RCTL_T(KC_A) and RSFT_T(KC_C) are settled as tapped. 4. In "RCTL_T(KC_A)↓, RSFT_T(KC_C)↓, LSFT_T(KC_T)↓", with the third key on the other side, we allow Permissive Hold or Hold On Other Keypress to decide how/when to settle the keys. 5. In "RCTL_T(KC_A)↓, RSFT_T(KC_C)↓" held until the tapping term, the keys are settled as held. 1–3 provide same-hand roll protection. 4–5 are for combining multiple same-hand modifiers. I've updated the unit tests and have been running it on my keyboard, for a few hours so far, and all seems good. I really like this scheme. It allows combining same-side mods, yet it also has roll protection on streaks. For me, this feels like Achordion, but clearly better streak handling and improved responsiveness. * Fix formatting. * Add a couple tests with LT keys. * Remove stale use of CHORDAL_HOLD_LAYOUT. * Fix misspelling lastest -> latest * Handling tweak for LTs and tests. * Fix formatting. * More tests with LT keys. * Fix formatting.--- data/mappings/info_config.hjson | 1 + data/schemas/keyboard.jsonschema | 7 +- docs/reference_info_json.md | 4 + docs/tap_hold.md | 163 ++++ lib/python/qmk/cli/generate/keyboard_c.py | 138 +++- quantum/action_tapping.c | 274 ++++++- quantum/action_tapping.h | 65 ++ .../chordal_hold/default/config.h | 21 + .../chordal_hold/default/test.mk | 17 + .../chordal_hold/default/test_keymap.c | 22 + .../chordal_hold/default/test_one_shot_keys.cpp | 168 ++++ .../chordal_hold/default/test_tap_hold.cpp | 264 ++++++ .../chordal_hold/hold_on_other_key_press/config.h | 22 + .../chordal_hold/hold_on_other_key_press/test.mk | 17 + .../hold_on_other_key_press/test_keymap.c | 22 + .../hold_on_other_key_press/test_tap_hold.cpp | 807 ++++++++++++++++++ .../chordal_hold/permissive_hold/config.h | 22 + .../chordal_hold/permissive_hold/test.mk | 17 + .../chordal_hold/permissive_hold/test_keymap.c | 22 + .../permissive_hold/test_one_shot_keys.cpp | 174 ++++ .../chordal_hold/permissive_hold/test_tap_hold.cpp | 898 +++++++++++++++++++++ .../retro_shift_permissive_hold/config.h | 28 + .../retro_shift_permissive_hold/test.mk | 18 + .../retro_shift_permissive_hold/test_keymap.c | 22 + .../test_retro_shift.cpp | 420 ++++++++++ 25 files changed, 3607 insertions(+), 26 deletions(-) create mode 100644 tests/tap_hold_configurations/chordal_hold/default/config.h create mode 100644 tests/tap_hold_configurations/chordal_hold/default/test.mk create mode 100644 tests/tap_hold_configurations/chordal_hold/default/test_keymap.c create mode 100644 tests/tap_hold_configurations/chordal_hold/default/test_one_shot_keys.cpp create mode 100644 tests/tap_hold_configurations/chordal_hold/default/test_tap_hold.cpp create mode 100644 tests/tap_hold_configurations/chordal_hold/hold_on_other_key_press/config.h create mode 100644 tests/tap_hold_configurations/chordal_hold/hold_on_other_key_press/test.mk create mode 100644 tests/tap_hold_configurations/chordal_hold/hold_on_other_key_press/test_keymap.c create mode 100644 tests/tap_hold_configurations/chordal_hold/hold_on_other_key_press/test_tap_hold.cpp create mode 100644 tests/tap_hold_configurations/chordal_hold/permissive_hold/config.h create mode 100644 tests/tap_hold_configurations/chordal_hold/permissive_hold/test.mk create mode 100644 tests/tap_hold_configurations/chordal_hold/permissive_hold/test_keymap.c create mode 100644 tests/tap_hold_configurations/chordal_hold/permissive_hold/test_one_shot_keys.cpp create mode 100644 tests/tap_hold_configurations/chordal_hold/permissive_hold/test_tap_hold.cpp create mode 100644 tests/tap_hold_configurations/chordal_hold/retro_shift_permissive_hold/config.h create mode 100644 tests/tap_hold_configurations/chordal_hold/retro_shift_permissive_hold/test.mk create mode 100644 tests/tap_hold_configurations/chordal_hold/retro_shift_permissive_hold/test_keymap.c create mode 100644 tests/tap_hold_configurations/chordal_hold/retro_shift_permissive_hold/test_retro_shift.cpp (limited to 'lib/python/qmk/cli/generate') diff --git a/data/mappings/info_config.hjson b/data/mappings/info_config.hjson index 8d3e3c7f0e..b643553b52 100644 --- a/data/mappings/info_config.hjson +++ b/data/mappings/info_config.hjson @@ -200,6 +200,7 @@ "SPLIT_WPM_ENABLE": {"info_key": "split.transport.sync.wpm", "value_type": "flag"}, // Tapping + "CHORDAL_HOLD": {"info_key": "tapping.chordal_hold", "value_type": "flag"}, "HOLD_ON_OTHER_KEY_PRESS": {"info_key": "tapping.hold_on_other_key_press", "value_type": "flag"}, "HOLD_ON_OTHER_KEY_PRESS_PER_KEY": {"info_key": "tapping.hold_on_other_key_press_per_key", "value_type": "flag"}, "PERMISSIVE_HOLD": {"info_key": "tapping.permissive_hold", "value_type": "flag"}, diff --git a/data/schemas/keyboard.jsonschema b/data/schemas/keyboard.jsonschema index ec87680fa0..8b6cc7032b 100644 --- a/data/schemas/keyboard.jsonschema +++ b/data/schemas/keyboard.jsonschema @@ -422,7 +422,11 @@ "h": {"$ref": "qmk.definitions.v1#/key_unit"}, "w": {"$ref": "qmk.definitions.v1#/key_unit"}, "x": {"$ref": "qmk.definitions.v1#/key_unit"}, - "y": {"$ref": "qmk.definitions.v1#/key_unit"} + "y": {"$ref": "qmk.definitions.v1#/key_unit"}, + "hand": { + "type": "string", + "enum": ["L", "R", "*"] + } } } } @@ -915,6 +919,7 @@ "tapping": { "type": "object", "properties": { + "chordal_hold": {"type": "boolean"}, "force_hold": {"type": "boolean"}, "force_hold_per_key": {"type": "boolean"}, "ignore_mod_tap_interrupt": {"type": "boolean"}, diff --git a/docs/reference_info_json.md b/docs/reference_info_json.md index 99ff7b1f7a..29b999c32e 100644 --- a/docs/reference_info_json.md +++ b/docs/reference_info_json.md @@ -74,6 +74,8 @@ You can create `info.json` files at every level under `qmk_firmware/keyboards/Boolean + * Default: `false` * `hold_on_other_key_press` Boolean * Default: `false` * `hold_on_other_key_press_per_key` Boolean @@ -328,6 +330,8 @@ The ISO enter key is represented by a 1.25u×2uh key. Renderers which utilize in * `h` KeyUnit * The height of the key, in key units. * Default: `1` (1u) + * `hand` String + * The handedness of the key for Chordal Hold, either `"L"` (left hand), `"R"` (right hand), or `"*"` (either or exempted handedness). * `label` String * What to name the key. This is *not* a key assignment as in the keymap, but should usually correspond to the keycode for the first layer of the default keymap. * Example: `"Escape"` diff --git a/docs/tap_hold.md b/docs/tap_hold.md index 9b7f6552cb..254d5de5ec 100644 --- a/docs/tap_hold.md +++ b/docs/tap_hold.md @@ -425,6 +425,169 @@ uint16_t get_quick_tap_term(uint16_t keycode, keyrecord_t *record) { If `QUICK_TAP_TERM` is set higher than `TAPPING_TERM`, it will default to `TAPPING_TERM`. ::: +## Chordal Hold + +Chordal Hold is intended to be used together with either Permissive Hold or Hold +On Other Key Press. Chordal Hold is enabled by adding to your `config.h`: + +```c +#define CHORDAL_HOLD +``` + +Chordal Hold implements, by default, an "opposite hands" rule. Suppose a +tap-hold key is pressed and then, before the tapping term, another key is +pressed. With Chordal Hold, the tap-hold key is settled as tapped if the two +keys are on the same hand. + +Otherwise, if the keys are on opposite hands, Chordal Hold introduces no new +behavior. Hold On Other Key Press or Permissive Hold may be used together with +Chordal Hold to configure the behavior in the opposite hands case. With Hold On +Other Key Press, an opposite hands chord is settled immediately as held. Or with +Permissive Hold, an opposite hands chord is settled as held provided the other +key is pressed and released (nested press) before releasing the tap-hold key. + +Chordal Hold may be useful to avoid accidental modifier activation with +mod-taps, particularly in rolled keypresses when using home row mods. + +Notes: + +* Chordal Hold has no effect after the tapping term. + +* Combos are exempt from the opposite hands rule, since "handedness" is + ill-defined in this case. Even so, Chordal Hold's behavior involving combos + may be customized through the `get_chordal_hold()` callback. + +An example of a sequence that is affected by “chordal hold”: + +- `SFT_T(KC_A)` Down +- `KC_C` Down +- `KC_C` Up +- `SFT_T(KC_A)` Up + +``` + TAPPING_TERM + +---------------------------|--------+ + | +----------------------+ | | + | | SFT_T(KC_A) | | | + | +----------------------+ | | + | +--------------+ | | + | | KC_C | | | + | +--------------+ | | + +---------------------------|--------+ +``` + +If the two keys are on the same hand, then this will produce `ac` with +`SFT_T(KC_A)` settled as tapped the moment that `KC_C` is pressed. + +If the two keys are on opposite hands and the `HOLD_ON_OTHER_KEY_PRESS` option +enabled, this will produce `C` with `SFT_T(KC_A)` settled as held when `KC_C` is +pressed. + +Or if the two keys are on opposite hands and the `PERMISSIVE_HOLD` option is +enabled, this will produce `C` with `SFT_T(KC_A)` settled as held when that +`KC_C` is released. + +### Chordal Hold Handedness + +Determining whether keys are on the same or opposite hands involves defining the +"handedness" of each key position. By default, if nothing is specified, +handedness is guessed based on keyboard geometry. + +Handedness may be specified with `chordal_hold_layout`. In keymap.c, define +`chordal_hold_layout` in the following form: + +```c +const char chordal_hold_layout[MATRIX_ROWS][MATRIX_COLS] PROGMEM = + LAYOUT( + 'L', 'L', 'L', 'L', 'L', 'L', 'R', 'R', 'R', 'R', 'R', 'R', + 'L', 'L', 'L', 'L', 'L', 'L', 'R', 'R', 'R', 'R', 'R', 'R', + 'L', 'L', 'L', 'L', 'L', 'L', 'R', 'R', 'R', 'R', 'R', 'R', + 'L', 'L', 'L', 'R', 'R', 'R' + ); +``` + +Use the same `LAYOUT` macro as used to define your keymap layers. Each entry is +a character indicating the handedness of one key, either `'L'` for left, `'R'` +for right, or `'*'` to exempt keys from the "opposite hands rule." A key with +`'*'` handedness may settle as held in chords with any other key. This could be +used perhaps on thumb keys or other places where you want to allow same-hand +chords. + +Keyboard makers may specify handedness in keyboard.json. Under `"layouts"`, +specify the handedness of a key by adding a `"hand"` field with a value of +either `"L"`, `"R"`, or `"*"`. Note that if `"layouts"` contains multiple +layouts, only the first one is read. For example: + +```json +{"matrix": [5, 6], "x": 0, "y": 5.5, "w": 1.25, "hand", "*"}, +``` + +Alternatively, handedness may be defined functionally with +`chordal_hold_handedness()`. For example, in keymap.c define: + +```c +char chordal_hold_handedness(keypos_t key) { + if (key.col == 0 || key.col == MATRIX_COLS - 1) { + return '*'; // Exempt the outer columns. + } + // On split keyboards, typically, the first half of the rows are on the + // left, and the other half are on the right. + return key.row < MATRIX_ROWS / 2 ? 'L' : 'R'; +} +``` + +Given the matrix position of a key, the function should return `'L'`, `'R'`, or +`'*'`. Adapt the logic in this function according to the keyboard's matrix. + +::: warning +Note the matrix may have irregularities around larger keys, around the edges of +the board, and around thumb clusters. You may find it helpful to use [this +debugging example](faq_debug#which-matrix-position-is-this-keypress) to +correspond physical keys to matrix positions. +::: + +::: tip If you define both `chordal_hold_layout[MATRIX_ROWS][MATRIX_COLS]` and +`chordal_hold_handedness(keypos_t key)` for handedness, the latter takes +precedence. +::: + + +### Per-chord customization + +Beyond the per-key configuration possible through handedness, Chordal Hold may +be configured at a *per-chord* granularity for detailed tuning. In keymap.c, +define `get_chordal_hold()`. Returning `true` allows the chord to be held, while +returning `false` settles as tapped. + +For example: + +```c +bool get_chordal_hold(uint16_t tap_hold_keycode, keyrecord_t* tap_hold_record, + uint16_t other_keycode, keyrecord_t* other_record) { + // Exceptionally allow some one-handed chords for hotkeys. + switch (tap_hold_keycode) { + case LCTL_T(KC_Z): + if (other_keycode == KC_C || other_keycode == KC_V) { + return true; + } + break; + + case RCTL_T(KC_SLSH): + if (other_keycode == KC_N) { + return true; + } + break; + } + // Otherwise defer to the opposite hands rule. + return get_chordal_hold_default(tap_hold_record, other_record); +} +``` + +As shown in the last line above, you may use +`get_chordal_hold_default(tap_hold_record, other_record)` to get the default tap +vs. hold decision according to the opposite hands rule. + + ## Retro Tapping To enable `retro tapping`, add the following to your `config.h`: diff --git a/lib/python/qmk/cli/generate/keyboard_c.py b/lib/python/qmk/cli/generate/keyboard_c.py index 228b320942..1978de4a22 100755 --- a/lib/python/qmk/cli/generate/keyboard_c.py +++ b/lib/python/qmk/cli/generate/keyboard_c.py @@ -1,5 +1,9 @@ """Used by the make system to generate keyboard.c from info.json. """ +import bisect +import dataclasses +from typing import Optional + from milc import cli from qmk.info import info_json @@ -87,6 +91,7 @@ def _gen_matrix_mask(info_data): lines.append(f' 0b{"".join(reversed(mask[i]))},') lines.append('};') lines.append('#endif') + lines.append('') return lines @@ -122,6 +127,128 @@ def _gen_joystick_axes(info_data): lines.append('};') lines.append('#endif') + lines.append('') + + return lines + + +@dataclasses.dataclass +class LayoutKey: + """Geometric info for one key in a layout.""" + row: int + col: int + x: float + y: float + w: float = 1.0 + h: float = 1.0 + hand: Optional[str] = None + + @staticmethod + def from_json(key_json): + row, col = key_json['matrix'] + return LayoutKey( + row=row, + col=col, + x=key_json['x'], + y=key_json['y'], + w=key_json.get('w', 1.0), + h=key_json.get('h', 1.0), + hand=key_json.get('hand', None), + ) + + @property + def cx(self): + """Center x coordinate of the key.""" + return self.x + self.w / 2.0 + + @property + def cy(self): + """Center y coordinate of the key.""" + return self.y + self.h / 2.0 + + +class Layout: + """Geometric info of a layout.""" + def __init__(self, layout_json): + self.keys = [LayoutKey.from_json(key_json) for key_json in layout_json['layout']] + self.x_min = min(key.cx for key in self.keys) + self.x_max = max(key.cx for key in self.keys) + self.x_mid = (self.x_min + self.x_max) / 2 + # If there is one key with width >= 6u, it is probably the spacebar. + i = [i for i, key in enumerate(self.keys) if key.w >= 6.0] + self.spacebar = self.keys[i[0]] if len(i) == 1 else None + + def is_symmetric(self, tol: float = 0.02): + """Whether the key positions are symmetric about x_mid.""" + x = sorted([key.cx for key in self.keys]) + for i in range(len(x)): + x_i_mirrored = 2.0 * self.x_mid - x[i] + # Find leftmost x element greater than or equal to (x_i_mirrored - tol). + j = bisect.bisect_left(x, x_i_mirrored - tol) + if j == len(x) or abs(x[j] - x_i_mirrored) > tol: + return False + + return True + + def widest_horizontal_gap(self): + """Finds the x midpoint of the widest horizontal gap between keys.""" + x = sorted([key.cx for key in self.keys]) + x_mid = self.x_mid + max_sep = 0 + for i in range(len(x) - 1): + sep = x[i + 1] - x[i] + if sep > max_sep: + max_sep = sep + x_mid = (x[i + 1] + x[i]) / 2 + + return x_mid + + +def _gen_chordal_hold_layout(info_data): + """Convert info.json content to chordal_hold_layout + """ + # NOTE: If there are multiple layouts, only the first is read. + for layout_name, layout_json in info_data['layouts'].items(): + layout = Layout(layout_json) + break + + if layout.is_symmetric(): + # If the layout is symmetric (e.g. most split keyboards), guess the + # handedness based on the sign of (x - layout.x_mid). + hand_signs = [key.x - layout.x_mid for key in layout.keys] + elif layout.spacebar is not None: + # If the layout has a spacebar, form a dividing line through the spacebar, + # nearly vertical but with a slight angle to follow typical row stagger. + x0 = layout.spacebar.cx - 0.05 + y0 = layout.spacebar.cy - 1.0 + hand_signs = [(key.x - x0) - (key.y - y0) / 3.0 for key in layout.keys] + else: + # Fallback: assume handedness based on the widest horizontal separation. + x_mid = layout.widest_horizontal_gap() + hand_signs = [key.x - x_mid for key in layout.keys] + + for key, hand_sign in zip(layout.keys, hand_signs): + if key.hand is None: + if key == layout.spacebar or abs(hand_sign) <= 0.02: + key.hand = '*' + else: + key.hand = 'L' if hand_sign < 0.0 else 'R' + + lines = [] + lines.append('#ifdef CHORDAL_HOLD') + line = ('__attribute__((weak)) const char chordal_hold_layout[MATRIX_ROWS][MATRIX_COLS] PROGMEM = ' + layout_name + '(') + + x_prev = None + for key in layout.keys: + if x_prev is None or key.x < x_prev: + lines.append(line) + line = ' ' + line += f"'{key.hand}', " + x_prev = key.x + + lines.append(line[:-2]) + lines.append(');') + lines.append('#endif') return lines @@ -136,11 +263,12 @@ def generate_keyboard_c(cli): kb_info_json = info_json(cli.args.keyboard) # Build the layouts.h file. - keyboard_h_lines = [GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE, '#include QMK_KEYBOARD_H', ''] + keyboard_c_lines = [GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE, '#include QMK_KEYBOARD_H', ''] - keyboard_h_lines.extend(_gen_led_configs(kb_info_json)) - keyboard_h_lines.extend(_gen_matrix_mask(kb_info_json)) - keyboard_h_lines.extend(_gen_joystick_axes(kb_info_json)) + keyboard_c_lines.extend(_gen_led_configs(kb_info_json)) + keyboard_c_lines.extend(_gen_matrix_mask(kb_info_json)) + keyboard_c_lines.extend(_gen_joystick_axes(kb_info_json)) + keyboard_c_lines.extend(_gen_chordal_hold_layout(kb_info_json)) # Show the results - dump_lines(cli.args.output, keyboard_h_lines, cli.args.quiet) + dump_lines(cli.args.output, keyboard_c_lines, cli.args.quiet) diff --git a/quantum/action_tapping.c b/quantum/action_tapping.c index 8f238490f2..9171970702 100644 --- a/quantum/action_tapping.c +++ b/quantum/action_tapping.c @@ -49,6 +49,45 @@ __attribute__((weak)) bool get_permissive_hold(uint16_t keycode, keyrecord_t *re } # endif +# if defined(CHORDAL_HOLD) +extern const char chordal_hold_layout[MATRIX_ROWS][MATRIX_COLS] PROGMEM; + +# define REGISTERED_TAPS_SIZE 8 +// Array of tap-hold keys that have been settled as tapped but not yet released. +static keypos_t registered_taps[REGISTERED_TAPS_SIZE] = {}; +static uint8_t num_registered_taps = 0; + +/** Adds `key` to the registered_taps array. */ +static void registered_taps_add(keypos_t key); +/** Returns the index of `key` in registered_taps, or -1 if not found. */ +static int8_t registered_tap_find(keypos_t key); +/** Removes index `i` from the registered_taps array. */ +static void registered_taps_del_index(uint8_t i); +/** Logs the registered_taps array for debugging. */ +static void debug_registered_taps(void); + +/** \brief Finds which queued events should be held according to Chordal Hold. + * + * In a situation with multiple unsettled tap-hold key presses, scan the queue + * up until the first release, non-tap-hold, or one-shot event and find the + * latest event in the queue that settles as held according to + * get_chordal_hold(). + * + * \return Index of the first tap, or equivalently, one past the latest hold. + */ +static uint8_t waiting_buffer_find_chordal_hold_tap(void); + +/** Processes queued events up to and including `key` as tapped. */ +static void waiting_buffer_chordal_hold_taps_until(keypos_t key); + +/** \brief Processes and pops buffered events until the first tap-hold event. */ +static void waiting_buffer_process_regular(void); + +static bool is_mt_or_lt(uint16_t keycode) { + return IS_QK_MOD_TAP(keycode) || IS_QK_LAYER_TAP(keycode); +} +# 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; @@ -166,6 +205,20 @@ void action_tapping_process(keyrecord_t record) { bool process_tapping(keyrecord_t *keyp) { const keyevent_t event = keyp->event; +# if defined(CHORDAL_HOLD) + if (!event.pressed) { + const int8_t i = registered_tap_find(event.key); + if (i != -1) { + // If a tap-hold key was previously settled as tapped, set its + // tap.count correspondingly on release. + keyp->tap.count = 1; + registered_taps_del_index(i); + ac_dprintf("Found tap release for [%d]\n", i); + debug_registered_taps(); + } + } +# endif // CHORDAL_HOLD + // state machine is in the "reset" state, no tapping key is to be // processed if (IS_NOEVENT(tapping_key.event)) { @@ -188,7 +241,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 @@ -199,6 +252,7 @@ bool process_tapping(keyrecord_t *keyp) { // early return for tick events return true; } + if (tapping_key.tap.count == 0) { if (IS_TAPPING_RECORD(keyp) && !event.pressed) { // first tap! @@ -212,6 +266,25 @@ bool process_tapping(keyrecord_t *keyp) { // enqueue return false; } +# if defined(CHORDAL_HOLD) + else if (is_mt_or_lt(tapping_keycode) && !event.pressed && waiting_buffer_typed(event) && !get_chordal_hold(tapping_keycode, &tapping_key, get_record_keycode(keyp, false), keyp)) { + // Key release that is not a chord with the tapping key. + // Settle the tapping key and any other pending tap-hold + // keys preceding the press of this key as tapped. + + ac_dprintf("Tapping: End. Chord considered a tap\n"); + tapping_key.tap.count = 1; + registered_taps_add(tapping_key.event.key); + process_record(&tapping_key); + tapping_key = (keyrecord_t){0}; + + waiting_buffer_chordal_hold_taps_until(event.key); + debug_registered_taps(); + debug_waiting_buffer(); + // enqueue + return false; + } +# endif // CHORDAL_HOLD /* Process a key typed within TAPPING_TERM * This can register the key before settlement of tapping, * useful for long TAPPING_TERM but may prevent fast typing. @@ -229,6 +302,22 @@ bool process_tapping(keyrecord_t *keyp) { // clang-format on ac_dprintf("Tapping: End. No tap. Interfered by typing key\n"); process_record(&tapping_key); + +# if defined(CHORDAL_HOLD) + uint8_t first_tap = waiting_buffer_find_chordal_hold_tap(); + ac_dprintf("first_tap = %u\n", first_tap); + if (first_tap < WAITING_BUFFER_SIZE) { + for (; waiting_buffer_tail != first_tap; waiting_buffer_tail = (waiting_buffer_tail + 1) % WAITING_BUFFER_SIZE) { + ac_dprintf("Processing [%u]\n", waiting_buffer_tail); + process_record(&waiting_buffer[waiting_buffer_tail]); + } + } + + waiting_buffer_chordal_hold_taps_until(event.key); + debug_registered_taps(); + debug_waiting_buffer(); +# endif // CHORDAL_HOLD + tapping_key = (keyrecord_t){0}; debug_tapping_key(); // enqueue @@ -237,6 +326,19 @@ bool process_tapping(keyrecord_t *keyp) { /* Process release event of a key pressed before tapping starts * Without this unexpected repeating will occur with having fast repeating setting * https://github.com/tmk/tmk_keyboard/issues/60 + * + * NOTE: This workaround causes events to process out of order, + * e.g. in a rolled press of three tap-hold keys like + * + * "A down, B down, C down, A up, B up, C up" + * + * events are processed as + * + * "A down, B down, A up, B up, C down, C up" + * + * It seems incorrect to process keyp before the tapping key. + * This workaround is old, from 2013. This might no longer + * be needed for the original problem it was meant to address. */ else if (!event.pressed && !waiting_buffer_typed(event)) { // Modifier/Layer should be retained till end of this tapping. @@ -271,19 +373,52 @@ bool process_tapping(keyrecord_t *keyp) { // set interrupted flag when other key pressed during tapping if (event.pressed) { tapping_key.tap.interrupted = true; - if (TAP_GET_HOLD_ON_OTHER_KEY_PRESS + +# if defined(CHORDAL_HOLD) + if (is_mt_or_lt(tapping_keycode) && !get_chordal_hold(tapping_keycode, &tapping_key, get_record_keycode(keyp, false), keyp)) { + // 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; + + if (!is_tap_record(keyp)) { + ac_dprintf("Tapping: End. Chord considered a tap\n"); + tapping_key.tap.count = 1; + registered_taps_add(tapping_key.event.key); + debug_registered_taps(); + process_record(&tapping_key); + tapping_key = (keyrecord_t){0}; + } + } else +# endif // CHORDAL_HOLD + if (TAP_GET_HOLD_ON_OTHER_KEY_PRESS # if defined(AUTO_SHIFT_ENABLE) && defined(RETRO_SHIFT) - // Auto Shift cannot evaluate this early - // Retro Shift uses the hold action for all nested taps even without HOLD_ON_OTHER_KEY_PRESS, so this is fine to skip - && !(MAYBE_RETRO_SHIFTING(event, keyp) && get_auto_shifted_key(get_record_keycode(keyp, false), keyp)) + // Auto Shift cannot evaluate this early + // Retro Shift uses the hold action for all nested taps even without HOLD_ON_OTHER_KEY_PRESS, so this is fine to skip + && !(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}; + +# if defined(CHORDAL_HOLD) + if (waiting_buffer_tail != waiting_buffer_head && is_tap_record(&waiting_buffer[waiting_buffer_tail])) { + tapping_key = waiting_buffer[waiting_buffer_tail]; + // Pop tail from the queue. + waiting_buffer_tail = (waiting_buffer_tail + 1) % WAITING_BUFFER_SIZE; + debug_waiting_buffer(); + } else +# endif // CHORDAL_HOLD + { + tapping_key = (keyrecord_t){0}; + } debug_tapping_key(); - // enqueue - return false; + +# if defined(CHORDAL_HOLD) + waiting_buffer_process_regular(); +# endif // CHORDAL_HOLD } } // enqueue @@ -520,26 +655,125 @@ void waiting_buffer_scan_tap(void) { } } -/** \brief Tapping key debug print - * - * FIXME: Needs docs - */ +# 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) { + if (tap_hold_record->event.type != KEY_EVENT || other_record->event.type != KEY_EVENT) { + return true; // Return true on combos or other non-key events. + } + + char tap_hold_hand = chordal_hold_handedness(tap_hold_record->event.key); + if (tap_hold_hand == '*') { + return true; + } + char other_hand = chordal_hold_handedness(other_record->event.key); + return other_hand == '*' || tap_hold_hand != other_hand; +} + +__attribute__((weak)) char chordal_hold_handedness(keypos_t key) { + return (char)pgm_read_byte(&chordal_hold_layout[key.row][key.col]); +} + +static void registered_taps_add(keypos_t key) { + if (num_registered_taps >= REGISTERED_TAPS_SIZE) { + ac_dprintf("TAPS OVERFLOW: CLEAR ALL STATES\n"); + clear_keyboard(); + num_registered_taps = 0; + } + + registered_taps[num_registered_taps] = key; + ++num_registered_taps; +} + +static int8_t registered_tap_find(keypos_t key) { + for (int8_t i = 0; i < num_registered_taps; ++i) { + if (KEYEQ(registered_taps[i], key)) { + return i; + } + } + return -1; +} + +static void registered_taps_del_index(uint8_t i) { + if (i < num_registered_taps) { + --num_registered_taps; + if (i < num_registered_taps) { + registered_taps[i] = registered_taps[num_registered_taps]; + } + } +} + +static void debug_registered_taps(void) { + ac_dprintf("registered_taps = { "); + for (int8_t i = 0; i < num_registered_taps; ++i) { + ac_dprintf("%02X%02X ", registered_taps[i].row, registered_taps[i].col); + } + ac_dprintf("}\n"); +} + +static uint8_t waiting_buffer_find_chordal_hold_tap(void) { + keyrecord_t *prev = &tapping_key; + uint16_t prev_keycode = get_record_keycode(&tapping_key, false); + uint8_t first_tap = WAITING_BUFFER_SIZE; + for (uint8_t i = waiting_buffer_tail; i != waiting_buffer_head; i = (i + 1) % WAITING_BUFFER_SIZE) { + keyrecord_t * cur = &waiting_buffer[i]; + const uint16_t cur_keycode = get_record_keycode(cur, false); + if (!cur->event.pressed || !is_mt_or_lt(prev_keycode)) { + break; + } else if (get_chordal_hold(prev_keycode, prev, cur_keycode, cur)) { + first_tap = i; // Track one index past the latest hold. + } + prev = cur; + prev_keycode = cur_keycode; + } + return first_tap; +} + +static void waiting_buffer_chordal_hold_taps_until(keypos_t key) { + while (waiting_buffer_tail != waiting_buffer_head) { + keyrecord_t *record = &waiting_buffer[waiting_buffer_tail]; + ac_dprintf("waiting_buffer_chordal_hold_taps_until: processing [%u]\n", waiting_buffer_tail); + if (is_tap_record(record)) { + record->tap.count = 1; + registered_taps_add(record->event.key); + } + process_record(record); + waiting_buffer_tail = (waiting_buffer_tail + 1) % WAITING_BUFFER_SIZE; + + if (KEYEQ(key, record->event.key) && record->event.pressed) { + break; + } + } +} + +static void waiting_buffer_process_regular(void) { + for (; waiting_buffer_tail != waiting_buffer_head; waiting_buffer_tail = (waiting_buffer_tail + 1) % WAITING_BUFFER_SIZE) { + if (is_tap_record(&waiting_buffer[waiting_buffer_tail])) { + break; // Stop once a tap-hold key event is reached. + } + ac_dprintf("waiting_buffer_process_regular: processing [%u]\n", waiting_buffer_tail); + process_record(&waiting_buffer[waiting_buffer_tail]); + } + debug_waiting_buffer(); +} +# endif // CHORDAL_HOLD + +/** \brief Logs tapping key if ACTION_DEBUG is enabled. */ static void debug_tapping_key(void) { ac_dprintf("TAPPING_KEY="); debug_record(tapping_key); ac_dprintf("\n"); } -/** \brief Waiting buffer debug print - * - * FIXME: Needs docs - */ +/** \brief Logs waiting buffer if ACTION_DEBUG is enabled. */ static void debug_waiting_buffer(void) { - ac_dprintf("{ "); + ac_dprintf("{"); for (uint8_t i = waiting_buffer_tail; i != waiting_buffer_head; i = (i + 1) % WAITING_BUFFER_SIZE) { - ac_dprintf("[%u]=", i); + ac_dprintf(" [%u]=", i); debug_record(waiting_buffer[i]); - ac_dprintf(" "); } ac_dprintf("}\n"); } diff --git a/quantum/action_tapping.h b/quantum/action_tapping.h index 6b518b8298..c3c7b999ec 100644 --- a/quantum/action_tapping.h +++ b/quantum/action_tapping.h @@ -46,6 +46,71 @@ 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 may be 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. + * + * If false is returned, this has the effect of immediately settling the + * tap-hold key as tapped. If true is returned, the tap-hold key is still + * unsettled, and may be settled as held depending on configuration and + * subsequent events. + * + * @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 may be 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 may 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 '*', the function + * returns true, indicating that it may be held. Otherwise, it returns false, + * in which case the tap-hold key is immediately settled at tapped. + * + * @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 may be considered held; false if tapped. + */ +bool get_chordal_hold_default(keyrecord_t *tap_hold_record, keyrecord_t *other_record); + +/** + * Gets the handedness of a key. + * + * This function returns: + * 'L' for keys pressed by the left hand, + * 'R' for keys on the right hand, + * '*' 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. + */ +char chordal_hold_handedness(keypos_t key); + +extern const char chordal_hold_layout[MATRIX_ROWS][MATRIX_COLS] PROGMEM; +#endif + #ifdef DYNAMIC_TAPPING_TERM_ENABLE extern uint16_t g_tapping_term; #endif diff --git a/tests/tap_hold_configurations/chordal_hold/default/config.h b/tests/tap_hold_configurations/chordal_hold/default/config.h new file mode 100644 index 0000000000..2ba155df73 --- /dev/null +++ b/tests/tap_hold_configurations/chordal_hold/default/config.h @@ -0,0 +1,21 @@ +/* Copyright 2022 Vladislav Kucheriavykh + * Copyright 2024 Google LLC + * + * 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/default/test.mk b/tests/tap_hold_configurations/chordal_hold/default/test.mk new file mode 100644 index 0000000000..2b049cea3b --- /dev/null +++ b/tests/tap_hold_configurations/chordal_hold/default/test.mk @@ -0,0 +1,17 @@ +# Copyright 2022 Vladislav Kucheriavykh +# Copyright 2024 Google LLC +# +# 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 . + +INTROSPECTION_KEYMAP_C = test_keymap.c diff --git a/tests/tap_hold_configurations/chordal_hold/default/test_keymap.c b/tests/tap_hold_configurations/chordal_hold/default/test_keymap.c new file mode 100644 index 0000000000..8a6a2c59b0 --- /dev/null +++ b/tests/tap_hold_configurations/chordal_hold/default/test_keymap.c @@ -0,0 +1,22 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "quantum.h" + +const char chordal_hold_layout[MATRIX_ROWS][MATRIX_COLS] PROGMEM = { + {'L', 'L', 'L', 'L', 'L', 'R', 'R', 'R', 'R', 'R'}, + {'L', 'L', 'L', 'L', 'L', 'R', 'R', 'R', 'R', 'R'}, + {'*', 'L', 'L', 'L', 'L', 'R', 'R', 'R', 'R', 'R'}, + {'L', 'L', 'L', 'L', 'L', 'R', 'R', 'R', 'R', 'R'}, +}; diff --git a/tests/tap_hold_configurations/chordal_hold/default/test_one_shot_keys.cpp b/tests/tap_hold_configurations/chordal_hold/default/test_one_shot_keys.cpp new file mode 100644 index 0000000000..ac4edd08b2 --- /dev/null +++ b/tests/tap_hold_configurations/chordal_hold/default/test_one_shot_keys.cpp @@ -0,0 +1,168 @@ +/* Copyright 2021 Stefan Kerkmann + * Copyright 2024 Google LLC + * + * 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 "action_util.h" +#include "keyboard_report_util.hpp" +#include "test_common.hpp" + +using testing::_; +using testing::InSequence; + +class OneShot : public TestFixture {}; +class OneShotParametrizedTestFixture : public ::testing::WithParamInterface>, public OneShot {}; + +TEST_P(OneShotParametrizedTestFixture, OSMWithAdditionalKeypress) { + TestDriver driver; + KeymapKey osm_key = GetParam().first; + KeymapKey regular_key = GetParam().second; + + set_keymap({osm_key, regular_key}); + + // Press and release OSM. + EXPECT_NO_REPORT(driver); + tap_key(osm_key); + VERIFY_AND_CLEAR(driver); + + // Press regular key. + EXPECT_REPORT(driver, (osm_key.report_code, regular_key.report_code)); + regular_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release regular key. + EXPECT_EMPTY_REPORT(driver); + regular_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_P(OneShotParametrizedTestFixture, OSMAsRegularModifierWithAdditionalKeypress) { + TestDriver driver; + KeymapKey osm_key = GetParam().first; + KeymapKey regular_key = GetParam().second; + + set_keymap({osm_key, regular_key}); + + // Press OSM. + EXPECT_NO_REPORT(driver); + osm_key.press(); + run_one_scan_loop(); + // Press regular key. + regular_key.press(); + run_one_scan_loop(); + // Release regular key. + regular_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release OSM. + EXPECT_REPORT(driver, (regular_key.report_code, osm_key.report_code)); + EXPECT_EMPTY_REPORT(driver); + osm_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +// clang-format off + +INSTANTIATE_TEST_CASE_P( + OneShotModifierTests, + OneShotParametrizedTestFixture, + ::testing::Values( + // First is osm key, second is regular key. + std::make_pair(KeymapKey{0, 0, 0, OSM(MOD_LSFT), KC_LSFT}, KeymapKey{0, 1, 1, KC_A}), + std::make_pair(KeymapKey{0, 0, 0, OSM(MOD_LCTL), KC_LCTL}, KeymapKey{0, 1, 1, KC_A}), + std::make_pair(KeymapKey{0, 0, 0, OSM(MOD_LALT), KC_LALT}, KeymapKey{0, 1, 1, KC_A}), + std::make_pair(KeymapKey{0, 0, 0, OSM(MOD_LGUI), KC_LGUI}, KeymapKey{0, 1, 1, KC_A}), + std::make_pair(KeymapKey{0, 0, 0, OSM(MOD_RCTL), KC_RCTL}, KeymapKey{0, 1, 1, KC_A}), + std::make_pair(KeymapKey{0, 0, 0, OSM(MOD_RSFT), KC_RSFT}, KeymapKey{0, 1, 1, KC_A}), + std::make_pair(KeymapKey{0, 0, 0, OSM(MOD_RALT), KC_RALT}, KeymapKey{0, 1, 1, KC_A}), + std::make_pair(KeymapKey{0, 0, 0, OSM(MOD_RGUI), KC_RGUI}, KeymapKey{0, 1, 1, KC_A}) + )); +// clang-format on + +TEST_F(OneShot, OSLWithAdditionalKeypress) { + TestDriver driver; + InSequence s; + KeymapKey osl_key = KeymapKey{0, 0, 0, OSL(1)}; + KeymapKey osl_key1 = KeymapKey{1, 0, 0, KC_X}; + KeymapKey regular_key0 = KeymapKey{0, 1, 0, KC_Y}; + KeymapKey regular_key1 = KeymapKey{1, 1, 0, KC_A}; + + set_keymap({osl_key, osl_key1, regular_key0, regular_key1}); + + // Press OSL key. + EXPECT_NO_REPORT(driver); + osl_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release OSL key. + EXPECT_NO_REPORT(driver); + osl_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Press regular key. + EXPECT_REPORT(driver, (regular_key1.report_code)); + EXPECT_EMPTY_REPORT(driver); + regular_key1.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release regular key. + EXPECT_NO_REPORT(driver); + regular_key1.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(OneShot, OSLWithOsmAndAdditionalKeypress) { + TestDriver driver; + InSequence s; + KeymapKey osl_key = KeymapKey{0, 0, 0, OSL(1)}; + KeymapKey osm_key = KeymapKey{1, 1, 0, OSM(MOD_LSFT), KC_LSFT}; + KeymapKey regular_key = KeymapKey{1, 1, 1, KC_A}; + KeymapKey blank_key = KeymapKey{1, 0, 0, KC_NO}; + + set_keymap({osl_key, osm_key, regular_key, blank_key}); + + // Press OSL key. + EXPECT_NO_REPORT(driver); + osl_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release OSL key. + EXPECT_NO_REPORT(driver); + osl_key.release(); + run_one_scan_loop(); + EXPECT_TRUE(layer_state_is(1)); + VERIFY_AND_CLEAR(driver); + + // Press and release OSM. + EXPECT_NO_REPORT(driver); + tap_key(osm_key); + EXPECT_TRUE(layer_state_is(1)); + VERIFY_AND_CLEAR(driver); + + // Tap regular key. + EXPECT_REPORT(driver, (osm_key.report_code, regular_key.report_code)); + EXPECT_EMPTY_REPORT(driver); + tap_key(regular_key); + VERIFY_AND_CLEAR(driver); +} diff --git a/tests/tap_hold_configurations/chordal_hold/default/test_tap_hold.cpp b/tests/tap_hold_configurations/chordal_hold/default/test_tap_hold.cpp new file mode 100644 index 0000000000..70949e218c --- /dev/null +++ b/tests/tap_hold_configurations/chordal_hold/default/test_tap_hold.cpp @@ -0,0 +1,264 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#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 ChordalHoldDefault : public TestFixture {}; + +TEST_F(ChordalHoldDefault, chord_nested_press_settled_as_tap) { + TestDriver driver; + InSequence s; + // Mod-tap key on the left hand. + auto mod_tap_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_key, regular_key}); + + // Press mod-tap key. + EXPECT_NO_REPORT(driver); + mod_tap_key.press(); + run_one_scan_loop(); + // Tap regular key. + tap_key(regular_key); + VERIFY_AND_CLEAR(driver); + + // Release mod-tap key. + EXPECT_REPORT(driver, (KC_P)); + EXPECT_REPORT(driver, (KC_P, KC_A)); + EXPECT_REPORT(driver, (KC_P)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHoldDefault, chord_rolled_press_settled_as_tap) { + TestDriver driver; + InSequence s; + // Mod-tap key on the left hand. + auto mod_tap_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_key, regular_key}); + + // Press mod-tap key and regular key. + EXPECT_NO_REPORT(driver); + mod_tap_key.press(); + run_one_scan_loop(); + regular_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release mod-tap key. + EXPECT_REPORT(driver, (KC_P)); + EXPECT_REPORT(driver, (KC_P, KC_A)); + EXPECT_REPORT(driver, (KC_A)); + mod_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release regular key. + EXPECT_EMPTY_REPORT(driver); + regular_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHoldDefault, 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_key = KeymapKey(0, 1, 0, SFT_T(KC_P)); + auto regular_key = KeymapKey(0, 2, 0, KC_A); + + set_keymap({mod_tap_key, regular_key}); + + // Press mod-tap-hold key. + EXPECT_NO_REPORT(driver); + mod_tap_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(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release mod-tap-hold key. + EXPECT_EMPTY_REPORT(driver); + mod_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHoldDefault, 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(ChordalHoldDefault, 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(ChordalHoldDefault, two_mod_taps_same_hand_hold_til_timeout) { + TestDriver driver; + InSequence s; + auto mod_tap_key1 = KeymapKey(0, MATRIX_COLS - 2, 0, RCTL_T(KC_A)); + auto mod_tap_key2 = KeymapKey(0, MATRIX_COLS - 1, 0, RSFT_T(KC_B)); + + set_keymap({mod_tap_key1, mod_tap_key2}); + + // Press mod-tap keys. + EXPECT_NO_REPORT(driver); + mod_tap_key1.press(); + run_one_scan_loop(); + mod_tap_key2.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Continue holding til the tapping term. + EXPECT_REPORT(driver, (KC_RIGHT_CTRL)); + EXPECT_REPORT(driver, (KC_RIGHT_CTRL, KC_RIGHT_SHIFT)); + idle_for(TAPPING_TERM); + VERIFY_AND_CLEAR(driver); + + // Release mod-tap keys. + EXPECT_REPORT(driver, (KC_RIGHT_SHIFT)); + mod_tap_key1.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_EMPTY_REPORT(driver); + mod_tap_key2.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHoldDefault, three_mod_taps_same_hand_streak_roll) { + TestDriver driver; + InSequence s; + auto mod_tap_key1 = KeymapKey(0, 1, 0, SFT_T(KC_A)); + auto mod_tap_key2 = KeymapKey(0, 2, 0, CTL_T(KC_B)); + auto mod_tap_key3 = KeymapKey(0, 3, 0, RSFT_T(KC_C)); + + set_keymap({mod_tap_key1, mod_tap_key2, mod_tap_key3}); + + // Press mod-tap keys. + EXPECT_NO_REPORT(driver); + mod_tap_key1.press(); + run_one_scan_loop(); + mod_tap_key2.press(); + run_one_scan_loop(); + mod_tap_key3.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release keys 1, 2, 3. + // + // NOTE: The correct order of events should be + // EXPECT_REPORT(driver, (KC_A, KC_B, KC_C)); + // EXPECT_REPORT(driver, (KC_B, KC_C)); + // EXPECT_REPORT(driver, (KC_C)); + // EXPECT_EMPTY_REPORT(driver); + // + // However, due to a workaround for https://github.com/tmk/tmk_keyboard/issues/60, + // the events are processed out of order, with the first two keys released + // before pressing KC_C. + EXPECT_REPORT(driver, (KC_A)); + EXPECT_REPORT(driver, (KC_A, KC_B)); + EXPECT_REPORT(driver, (KC_B)); + EXPECT_EMPTY_REPORT(driver); + EXPECT_REPORT(driver, (KC_C)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_key1.release(); + run_one_scan_loop(); + mod_tap_key2.release(); + run_one_scan_loop(); + mod_tap_key3.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHoldDefault, tap_regular_key_while_layer_tap_key_is_held) { + TestDriver driver; + InSequence s; + auto layer_tap_hold_key = KeymapKey(0, 1, 0, LT(1, KC_P)); + auto regular_key = KeymapKey(0, MATRIX_COLS - 1, 0, KC_A); + auto layer_key = KeymapKey(1, MATRIX_COLS - 1, 0, KC_B); + + set_keymap({layer_tap_hold_key, regular_key, layer_key}); + + EXPECT_NO_REPORT(driver); + layer_tap_hold_key.press(); // Press layer-tap-hold key. + run_one_scan_loop(); + regular_key.press(); // Press regular key. + run_one_scan_loop(); + regular_key.release(); // Release regular key. + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_P)); + EXPECT_REPORT(driver, (KC_P, KC_A)); + EXPECT_REPORT(driver, (KC_P)); + EXPECT_EMPTY_REPORT(driver); + layer_tap_hold_key.release(); // Release layer-tap-hold key. + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} diff --git a/tests/tap_hold_configurations/chordal_hold/hold_on_other_key_press/config.h b/tests/tap_hold_configurations/chordal_hold/hold_on_other_key_press/config.h new file mode 100644 index 0000000000..87094b2fac --- /dev/null +++ b/tests/tap_hold_configurations/chordal_hold/hold_on_other_key_press/config.h @@ -0,0 +1,22 @@ +/* Copyright 2022 Vladislav Kucheriavykh + * Copyright 2024 Google LLC + * + * 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/hold_on_other_key_press/test.mk b/tests/tap_hold_configurations/chordal_hold/hold_on_other_key_press/test.mk new file mode 100644 index 0000000000..2b049cea3b --- /dev/null +++ b/tests/tap_hold_configurations/chordal_hold/hold_on_other_key_press/test.mk @@ -0,0 +1,17 @@ +# Copyright 2022 Vladislav Kucheriavykh +# Copyright 2024 Google LLC +# +# 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 . + +INTROSPECTION_KEYMAP_C = test_keymap.c diff --git a/tests/tap_hold_configurations/chordal_hold/hold_on_other_key_press/test_keymap.c b/tests/tap_hold_configurations/chordal_hold/hold_on_other_key_press/test_keymap.c new file mode 100644 index 0000000000..8a6a2c59b0 --- /dev/null +++ b/tests/tap_hold_configurations/chordal_hold/hold_on_other_key_press/test_keymap.c @@ -0,0 +1,22 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "quantum.h" + +const char chordal_hold_layout[MATRIX_ROWS][MATRIX_COLS] PROGMEM = { + {'L', 'L', 'L', 'L', 'L', 'R', 'R', 'R', 'R', 'R'}, + {'L', 'L', 'L', 'L', 'L', 'R', 'R', 'R', 'R', 'R'}, + {'*', 'L', 'L', 'L', 'L', 'R', 'R', 'R', 'R', 'R'}, + {'L', 'L', 'L', 'L', 'L', 'R', 'R', 'R', 'R', 'R'}, +}; diff --git a/tests/tap_hold_configurations/chordal_hold/hold_on_other_key_press/test_tap_hold.cpp b/tests/tap_hold_configurations/chordal_hold/hold_on_other_key_press/test_tap_hold.cpp new file mode 100644 index 0000000000..e6852bbbb1 --- /dev/null +++ b/tests/tap_hold_configurations/chordal_hold/hold_on_other_key_press/test_tap_hold.cpp @@ -0,0 +1,807 @@ +// Copyright 2024-2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#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 ChordalHoldHoldOnOtherKeyPress : public TestFixture {}; + +TEST_F(ChordalHoldHoldOnOtherKeyPress, chord_with_mod_tap_settled_as_hold) { + TestDriver driver; + InSequence s; + // Mod-tap key on the left hand. + auto mod_tap_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_key, regular_key}); + + // Press mod-tap-hold key. + EXPECT_NO_REPORT(driver); + mod_tap_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(); + run_one_scan_loop(); + 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_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHoldHoldOnOtherKeyPress, chord_nested_press_settled_as_hold) { + TestDriver driver; + InSequence s; + // Mod-tap key on the left hand. + auto mod_tap_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_key, regular_key}); + + // Press mod-tap-hold key. + EXPECT_NO_REPORT(driver); + mod_tap_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Tap regular key. + EXPECT_REPORT(driver, (KC_LEFT_SHIFT)); + EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_A)); + EXPECT_REPORT(driver, (KC_LEFT_SHIFT)); + tap_key(regular_key); + VERIFY_AND_CLEAR(driver); + + // Release mod-tap-hold key. + EXPECT_EMPTY_REPORT(driver); + mod_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHoldHoldOnOtherKeyPress, chord_rolled_press_settled_as_hold) { + TestDriver driver; + InSequence s; + // Mod-tap key on the left hand. + auto mod_tap_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_key, regular_key}); + + // Press mod-tap key. + EXPECT_NO_REPORT(driver); + mod_tap_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(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release mod-tap key. + EXPECT_REPORT(driver, (KC_A)); + mod_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release regular key. + EXPECT_EMPTY_REPORT(driver); + regular_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHoldHoldOnOtherKeyPress, 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_key = KeymapKey(0, 1, 0, SFT_T(KC_P)); + auto regular_key = KeymapKey(0, 2, 0, KC_A); + + set_keymap({mod_tap_key, regular_key}); + + // Press mod-tap-hold key. + EXPECT_NO_REPORT(driver); + mod_tap_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(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release mod-tap-hold key. + EXPECT_EMPTY_REPORT(driver); + mod_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHoldHoldOnOtherKeyPress, 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(ChordalHoldHoldOnOtherKeyPress, 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(ChordalHoldHoldOnOtherKeyPress, two_mod_taps_same_hand_hold_til_timeout) { + TestDriver driver; + InSequence s; + auto mod_tap_key1 = KeymapKey(0, MATRIX_COLS - 2, 0, RCTL_T(KC_A)); + auto mod_tap_key2 = KeymapKey(0, MATRIX_COLS - 1, 0, RSFT_T(KC_B)); + + set_keymap({mod_tap_key1, mod_tap_key2}); + + // Press mod-tap keys. + EXPECT_NO_REPORT(driver); + mod_tap_key1.press(); + run_one_scan_loop(); + mod_tap_key2.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Continue holding til the tapping term. + EXPECT_REPORT(driver, (KC_RIGHT_CTRL)); + EXPECT_REPORT(driver, (KC_RIGHT_CTRL, KC_RIGHT_SHIFT)); + idle_for(TAPPING_TERM); + VERIFY_AND_CLEAR(driver); + + // Release mod-tap keys. + EXPECT_REPORT(driver, (KC_RIGHT_SHIFT)); + mod_tap_key1.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_EMPTY_REPORT(driver); + mod_tap_key2.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHoldHoldOnOtherKeyPress, two_mod_taps_nested_press_opposite_hands) { + TestDriver driver; + InSequence s; + auto mod_tap_key1 = KeymapKey(0, 1, 0, SFT_T(KC_A)); + auto mod_tap_key2 = KeymapKey(0, MATRIX_COLS - 1, 0, RSFT_T(KC_B)); + + set_keymap({mod_tap_key1, mod_tap_key2}); + + // Press first mod-tap 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)); + mod_tap_key2.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release second mod-tap key. + EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_B)); + EXPECT_REPORT(driver, (KC_LEFT_SHIFT)); + mod_tap_key2.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release first mod-tap key. + EXPECT_EMPTY_REPORT(driver); + mod_tap_key1.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHoldHoldOnOtherKeyPress, two_mod_taps_nested_press_same_hand) { + 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 keys. + EXPECT_NO_REPORT(driver); + mod_tap_key1.press(); + run_one_scan_loop(); + mod_tap_key2.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release mod-tap keys. + EXPECT_REPORT(driver, (KC_A)); + EXPECT_REPORT(driver, (KC_A, KC_B)); + EXPECT_REPORT(driver, (KC_A)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_key2.release(); + run_one_scan_loop(); + mod_tap_key1.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHoldHoldOnOtherKeyPress, three_mod_taps_same_hand_streak_roll) { + TestDriver driver; + InSequence s; + auto mod_tap_key1 = KeymapKey(0, 1, 0, SFT_T(KC_A)); + auto mod_tap_key2 = KeymapKey(0, 2, 0, CTL_T(KC_B)); + auto mod_tap_key3 = KeymapKey(0, 3, 0, RSFT_T(KC_C)); + + set_keymap({mod_tap_key1, mod_tap_key2, mod_tap_key3}); + + // Press mod-tap keys. + EXPECT_NO_REPORT(driver); + mod_tap_key1.press(); + run_one_scan_loop(); + mod_tap_key2.press(); + run_one_scan_loop(); + mod_tap_key3.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release keys 1, 2, 3. + // + // NOTE: The correct order of events should be + // EXPECT_REPORT(driver, (KC_A)); + // EXPECT_REPORT(driver, (KC_A, KC_B)); + // EXPECT_REPORT(driver, (KC_A, KC_B, KC_C)); + // EXPECT_REPORT(driver, (KC_B, KC_C)); + // EXPECT_REPORT(driver, (KC_C)); + // EXPECT_EMPTY_REPORT(driver); + // + // However, due to a workaround for https://github.com/tmk/tmk_keyboard/issues/60, + // the events are processed out of order, with the first two keys released + // before pressing KC_C. + EXPECT_REPORT(driver, (KC_A)); + EXPECT_REPORT(driver, (KC_A, KC_B)); + EXPECT_REPORT(driver, (KC_B)); + EXPECT_EMPTY_REPORT(driver); + EXPECT_REPORT(driver, (KC_C)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_key1.release(); + run_one_scan_loop(); + mod_tap_key2.release(); + run_one_scan_loop(); + mod_tap_key3.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_NO_REPORT(driver); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHoldHoldOnOtherKeyPress, three_mod_taps_same_hand_streak_orders) { + TestDriver driver; + InSequence s; + auto mod_tap_key1 = KeymapKey(0, 1, 0, SFT_T(KC_A)); + auto mod_tap_key2 = KeymapKey(0, 2, 0, CTL_T(KC_B)); + auto mod_tap_key3 = KeymapKey(0, 3, 0, RSFT_T(KC_C)); + + set_keymap({mod_tap_key1, mod_tap_key2, mod_tap_key3}); + + EXPECT_REPORT(driver, (KC_A)); + EXPECT_REPORT(driver, (KC_A, KC_B)); + EXPECT_REPORT(driver, (KC_A, KC_B, KC_C)); + EXPECT_REPORT(driver, (KC_A, KC_B)); + EXPECT_REPORT(driver, (KC_A)); + EXPECT_EMPTY_REPORT(driver); + // Press mod-tap keys. + mod_tap_key1.press(); + run_one_scan_loop(); + mod_tap_key2.press(); + run_one_scan_loop(); + mod_tap_key3.press(); + run_one_scan_loop(); + // Release keys 3, 2, 1. + mod_tap_key3.release(); + run_one_scan_loop(); + mod_tap_key2.release(); + run_one_scan_loop(); + mod_tap_key1.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_A)); + EXPECT_REPORT(driver, (KC_A, KC_B)); + EXPECT_REPORT(driver, (KC_A, KC_B, KC_C)); + EXPECT_REPORT(driver, (KC_A, KC_B)); + EXPECT_REPORT(driver, (KC_B)); + EXPECT_EMPTY_REPORT(driver); + idle_for(TAPPING_TERM); + // Press mod-tap keys. + mod_tap_key1.press(); + run_one_scan_loop(); + mod_tap_key2.press(); + run_one_scan_loop(); + mod_tap_key3.press(); + run_one_scan_loop(); + // Release keys 3, 1, 2. + mod_tap_key3.release(); + run_one_scan_loop(); + mod_tap_key1.release(); + run_one_scan_loop(); + mod_tap_key2.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // NOTE: The correct order of events should be + // EXPECT_REPORT(driver, (KC_A)); + // EXPECT_REPORT(driver, (KC_A, KC_B)); + // EXPECT_REPORT(driver, (KC_A, KC_B, KC_C)); + // EXPECT_REPORT(driver, (KC_A, KC_C)); + // EXPECT_REPORT(driver, (KC_A)); + // EXPECT_EMPTY_REPORT(driver); + // + // However, due to a workaround for https://github.com/tmk/tmk_keyboard/issues/60, + // the events are processed out of order, with the first two keys released + // before pressing KC_C. + EXPECT_REPORT(driver, (KC_A)); + EXPECT_REPORT(driver, (KC_A, KC_B)); + EXPECT_REPORT(driver, (KC_A)); + EXPECT_REPORT(driver, (KC_A, KC_C)); + EXPECT_REPORT(driver, (KC_A)); + EXPECT_EMPTY_REPORT(driver); + idle_for(TAPPING_TERM); + // Press mod-tap keys. + mod_tap_key1.press(); + run_one_scan_loop(); + mod_tap_key2.press(); + run_one_scan_loop(); + mod_tap_key3.press(); + run_one_scan_loop(); + // Release keys 2, 3, 1. + mod_tap_key2.release(); + run_one_scan_loop(); + mod_tap_key3.release(); + run_one_scan_loop(); + mod_tap_key1.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHoldHoldOnOtherKeyPress, three_mod_taps_two_left_one_right) { + TestDriver driver; + InSequence s; + auto mod_tap_key1 = KeymapKey(0, 1, 0, SFT_T(KC_A)); + auto mod_tap_key2 = KeymapKey(0, 2, 0, CTL_T(KC_B)); + auto mod_tap_key3 = KeymapKey(0, MATRIX_COLS - 1, 0, RSFT_T(KC_C)); + + set_keymap({mod_tap_key1, mod_tap_key2, mod_tap_key3}); + + // Press mod-tap keys. + EXPECT_NO_REPORT(driver); + mod_tap_key1.press(); + run_one_scan_loop(); + mod_tap_key2.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_LEFT_SHIFT)); + EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_LEFT_CTRL)); + mod_tap_key3.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release key 3. + EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_LEFT_CTRL, KC_C)); + EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_LEFT_CTRL)); + mod_tap_key3.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release key 2, then key 1. + EXPECT_REPORT(driver, (KC_LEFT_SHIFT)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_key2.release(); + run_one_scan_loop(); + mod_tap_key1.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Press mod-tap keys. + EXPECT_NO_REPORT(driver); + mod_tap_key1.press(); + run_one_scan_loop(); + mod_tap_key2.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_LEFT_SHIFT)); + EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_LEFT_CTRL)); + mod_tap_key3.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release key 3. + EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_LEFT_CTRL, KC_C)); + EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_LEFT_CTRL)); + mod_tap_key3.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release key 1, then key 2. + EXPECT_REPORT(driver, (KC_LEFT_CTRL)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_key1.release(); + run_one_scan_loop(); + mod_tap_key2.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHoldHoldOnOtherKeyPress, three_mod_taps_one_held_two_tapped) { + TestDriver driver; + InSequence s; + auto mod_tap_key1 = KeymapKey(0, 1, 0, SFT_T(KC_A)); + auto mod_tap_key2 = KeymapKey(0, MATRIX_COLS - 2, 0, CTL_T(KC_B)); + auto mod_tap_key3 = KeymapKey(0, MATRIX_COLS - 1, 0, RSFT_T(KC_C)); + + set_keymap({mod_tap_key1, mod_tap_key2, mod_tap_key3}); + + // Press mod-tap keys. + EXPECT_NO_REPORT(driver); + mod_tap_key1.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_LEFT_SHIFT)); + mod_tap_key2.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release keys 3, 2, 1. + EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_B)); + EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_B, KC_C)); + EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_B)); + EXPECT_REPORT(driver, (KC_LEFT_SHIFT)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_key3.press(); + run_one_scan_loop(); + mod_tap_key3.release(); + run_one_scan_loop(); + mod_tap_key2.release(); + run_one_scan_loop(); + mod_tap_key1.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Press mod-tap keys. + EXPECT_NO_REPORT(driver); + idle_for(TAPPING_TERM); + mod_tap_key1.press(); + run_one_scan_loop(); + + EXPECT_REPORT(driver, (KC_LEFT_SHIFT)); + mod_tap_key2.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release keys 3, 1, 2. + EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_B)); + EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_B, KC_C)); + EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_B)); + EXPECT_REPORT(driver, (KC_B)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_key3.press(); + run_one_scan_loop(); + mod_tap_key3.release(); + run_one_scan_loop(); + mod_tap_key1.release(); + run_one_scan_loop(); + mod_tap_key2.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHoldHoldOnOtherKeyPress, tap_regular_key_while_layer_tap_key_is_held) { + TestDriver driver; + InSequence s; + auto layer_tap_hold_key = KeymapKey(0, 1, 0, LT(1, KC_P)); + auto regular_key = KeymapKey(0, MATRIX_COLS - 1, 0, KC_A); + auto no_key = KeymapKey(1, 1, 0, XXXXXXX); + auto layer_key = KeymapKey(1, MATRIX_COLS - 1, 0, KC_B); + + set_keymap({layer_tap_hold_key, regular_key, no_key, layer_key}); + + // Press layer-tap-hold key. + EXPECT_NO_REPORT(driver); + layer_tap_hold_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Press regular key. + EXPECT_REPORT(driver, (KC_B)); + regular_key.press(); + run_one_scan_loop(); + EXPECT_EQ(layer_state, 2); + VERIFY_AND_CLEAR(driver); + + // Release regular key. + EXPECT_EMPTY_REPORT(driver); + regular_key.release(); + run_one_scan_loop(); + EXPECT_EQ(layer_state, 2); + VERIFY_AND_CLEAR(driver); + + // Release layer-tap-hold key. + EXPECT_NO_REPORT(driver); + layer_tap_hold_key.release(); + run_one_scan_loop(); + EXPECT_EQ(layer_state, 0); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHoldHoldOnOtherKeyPress, long_distinct_taps_of_layer_tap_key_and_regular_key) { + TestDriver driver; + InSequence s; + auto layer_tap_hold_key = KeymapKey(0, 1, 0, LT(1, KC_P)); + auto regular_key = KeymapKey(0, MATRIX_COLS - 1, 0, KC_A); + auto layer_key = KeymapKey(0, MATRIX_COLS - 1, 0, KC_B); + + set_keymap({layer_tap_hold_key, regular_key}); + + // Press layer-tap-hold key. + EXPECT_NO_REPORT(driver); + layer_tap_hold_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Idle for tapping term of layer tap hold key. + EXPECT_NO_REPORT(driver); + idle_for(TAPPING_TERM + 1); + EXPECT_EQ(layer_state, 2); + VERIFY_AND_CLEAR(driver); + + // Release layer-tap-hold key. + EXPECT_NO_REPORT(driver); + layer_tap_hold_key.release(); + run_one_scan_loop(); + EXPECT_EQ(layer_state, 0); + VERIFY_AND_CLEAR(driver); + + // Press regular key. + EXPECT_REPORT(driver, (KC_A)); + regular_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release regular key. + EXPECT_EMPTY_REPORT(driver); + regular_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHoldHoldOnOtherKeyPress, nested_tap_of_layer_0_layer_tap_keys) { + TestDriver driver; + InSequence s; + // The keys are layer-taps on layer 0 but regular keys on layer 1. + auto first_layer_tap_key = KeymapKey(0, 1, 0, LT(1, KC_A)); + auto second_layer_tap_key = KeymapKey(0, MATRIX_COLS - 1, 0, LT(1, KC_P)); + auto first_key_on_layer = KeymapKey(1, 1, 0, KC_B); + auto second_key_on_layer = KeymapKey(1, MATRIX_COLS - 1, 0, KC_Q); + + set_keymap({first_layer_tap_key, second_layer_tap_key, first_key_on_layer, second_key_on_layer}); + + // Press first layer-tap key. + EXPECT_NO_REPORT(driver); + first_layer_tap_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Press second layer-tap key. + EXPECT_REPORT(driver, (KC_Q)); + second_layer_tap_key.press(); + run_one_scan_loop(); + EXPECT_EQ(layer_state, 2); + VERIFY_AND_CLEAR(driver); + + // Release second layer-tap key. + EXPECT_EMPTY_REPORT(driver); + second_layer_tap_key.release(); + run_one_scan_loop(); + EXPECT_EQ(layer_state, 2); + VERIFY_AND_CLEAR(driver); + + // Release first layer-tap key. + EXPECT_NO_REPORT(driver); + first_layer_tap_key.release(); + run_one_scan_loop(); + EXPECT_EQ(layer_state, 0); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHoldHoldOnOtherKeyPress, lt_mt_one_regular_key) { + TestDriver driver; + InSequence s; + auto lt_key = KeymapKey(0, 1, 0, LT(1, KC_A)); + auto mt_key0 = KeymapKey(0, 2, 0, SFT_T(KC_B)); + auto mt_key1 = KeymapKey(1, 2, 0, CTL_T(KC_C)); + auto regular_key = KeymapKey(1, MATRIX_COLS - 1, 0, KC_X); + auto no_key0 = KeymapKey(0, MATRIX_COLS - 1, 0, XXXXXXX); + auto no_key1 = KeymapKey(1, 1, 0, XXXXXXX); + + set_keymap({lt_key, mt_key0, mt_key1, regular_key, no_key0, no_key1}); + + // Press LT, MT. + EXPECT_NO_REPORT(driver); + lt_key.press(); + run_one_scan_loop(); + mt_key1.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Press regular key. + EXPECT_REPORT(driver, (KC_LCTL)); + EXPECT_REPORT(driver, (KC_LCTL, KC_X)); + regular_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release the regular key. + EXPECT_REPORT(driver, (KC_LCTL)); + regular_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release MT key. + EXPECT_EMPTY_REPORT(driver); + mt_key1.release(); + run_one_scan_loop(); + EXPECT_EQ(get_mods(), 0); + VERIFY_AND_CLEAR(driver); + + // Release LT key. + lt_key.release(); + run_one_scan_loop(); + EXPECT_EQ(layer_state, 0); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHoldHoldOnOtherKeyPress, nested_tap_of_layer_tap_keys) { + TestDriver driver; + InSequence s; + // The keys are layer-taps on all layers. + auto first_key_layer_0 = KeymapKey(0, 1, 0, LT(1, KC_A)); + auto second_key_layer_0 = KeymapKey(0, MATRIX_COLS - 1, 0, LT(1, KC_P)); + auto first_key_layer_1 = KeymapKey(1, 1, 0, LT(2, KC_B)); + auto second_key_layer_1 = KeymapKey(1, MATRIX_COLS - 1, 0, LT(2, KC_Q)); + auto first_key_layer_2 = KeymapKey(2, 1, 0, KC_TRNS); + auto second_key_layer_2 = KeymapKey(2, MATRIX_COLS - 1, 0, KC_TRNS); + + set_keymap({first_key_layer_0, second_key_layer_0, first_key_layer_1, second_key_layer_1, first_key_layer_2, second_key_layer_2}); + + // Press first layer-tap key. + EXPECT_NO_REPORT(driver); + first_key_layer_0.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Press second layer-tap key. + EXPECT_NO_REPORT(driver); + second_key_layer_0.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release second layer-tap key. + EXPECT_REPORT(driver, (KC_Q)); + EXPECT_EMPTY_REPORT(driver); + second_key_layer_0.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release first layer-tap key. + EXPECT_NO_REPORT(driver); + first_key_layer_0.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHoldHoldOnOtherKeyPress, roll_layer_tap_key_with_regular_key) { + TestDriver driver; + InSequence s; + + auto layer_tap_hold_key = KeymapKey(0, 1, 0, LT(1, KC_P)); + auto regular_key = KeymapKey(0, MATRIX_COLS - 1, 0, KC_A); + auto layer_key = KeymapKey(1, MATRIX_COLS - 1, 0, KC_B); + + set_keymap({layer_tap_hold_key, regular_key, layer_key}); + + // Press layer-tap-hold key. + EXPECT_NO_REPORT(driver); + layer_tap_hold_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Press regular key. + EXPECT_REPORT(driver, (KC_B)); + regular_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release layer-tap-hold key. + EXPECT_NO_REPORT(driver); + layer_tap_hold_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release regular key. + EXPECT_EMPTY_REPORT(driver); + regular_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} diff --git a/tests/tap_hold_configurations/chordal_hold/permissive_hold/config.h b/tests/tap_hold_configurations/chordal_hold/permissive_hold/config.h new file mode 100644 index 0000000000..f7bb7ab0ec --- /dev/null +++ b/tests/tap_hold_configurations/chordal_hold/permissive_hold/config.h @@ -0,0 +1,22 @@ +/* Copyright 2022 Vladislav Kucheriavykh + * Copyright 2024 Google LLC + * + * 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/permissive_hold/test.mk b/tests/tap_hold_configurations/chordal_hold/permissive_hold/test.mk new file mode 100644 index 0000000000..2b049cea3b --- /dev/null +++ b/tests/tap_hold_configurations/chordal_hold/permissive_hold/test.mk @@ -0,0 +1,17 @@ +# Copyright 2022 Vladislav Kucheriavykh +# Copyright 2024 Google LLC +# +# 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 . + +INTROSPECTION_KEYMAP_C = test_keymap.c diff --git a/tests/tap_hold_configurations/chordal_hold/permissive_hold/test_keymap.c b/tests/tap_hold_configurations/chordal_hold/permissive_hold/test_keymap.c new file mode 100644 index 0000000000..8a6a2c59b0 --- /dev/null +++ b/tests/tap_hold_configurations/chordal_hold/permissive_hold/test_keymap.c @@ -0,0 +1,22 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "quantum.h" + +const char chordal_hold_layout[MATRIX_ROWS][MATRIX_COLS] PROGMEM = { + {'L', 'L', 'L', 'L', 'L', 'R', 'R', 'R', 'R', 'R'}, + {'L', 'L', 'L', 'L', 'L', 'R', 'R', 'R', 'R', 'R'}, + {'*', 'L', 'L', 'L', 'L', 'R', 'R', 'R', 'R', 'R'}, + {'L', 'L', 'L', 'L', 'L', 'R', 'R', 'R', 'R', 'R'}, +}; diff --git a/tests/tap_hold_configurations/chordal_hold/permissive_hold/test_one_shot_keys.cpp b/tests/tap_hold_configurations/chordal_hold/permissive_hold/test_one_shot_keys.cpp new file mode 100644 index 0000000000..e48cba73e2 --- /dev/null +++ b/tests/tap_hold_configurations/chordal_hold/permissive_hold/test_one_shot_keys.cpp @@ -0,0 +1,174 @@ +/* Copyright 2021 Stefan Kerkmann + * Copyright 2024 Google LLC + * + * 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 "action_util.h" +#include "keyboard_report_util.hpp" +#include "test_common.hpp" + +using testing::_; +using testing::InSequence; + +class OneShot : public TestFixture {}; +class OneShotParametrizedTestFixture : public ::testing::WithParamInterface>, public OneShot {}; + +TEST_P(OneShotParametrizedTestFixture, OSMWithAdditionalKeypress) { + TestDriver driver; + KeymapKey osm_key = GetParam().first; + KeymapKey regular_key = GetParam().second; + + set_keymap({osm_key, regular_key}); + + // Press and release OSM. + EXPECT_NO_REPORT(driver); + tap_key(osm_key); + VERIFY_AND_CLEAR(driver); + + // Press regular key. + EXPECT_REPORT(driver, (osm_key.report_code, regular_key.report_code)); + regular_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release regular key. + EXPECT_EMPTY_REPORT(driver); + regular_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_P(OneShotParametrizedTestFixture, OSMAsRegularModifierWithAdditionalKeypress) { + TestDriver driver; + KeymapKey osm_key = GetParam().first; + KeymapKey regular_key = GetParam().second; + + set_keymap({osm_key, regular_key}); + + // Press OSM. + EXPECT_NO_REPORT(driver); + osm_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Press regular key. + EXPECT_NO_REPORT(driver); + regular_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release regular key. + EXPECT_REPORT(driver, (osm_key.report_code)).Times(2); + EXPECT_REPORT(driver, (regular_key.report_code, osm_key.report_code)); + regular_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release OSM. + EXPECT_EMPTY_REPORT(driver); + osm_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +// clang-format off + +INSTANTIATE_TEST_CASE_P( + OneShotModifierTests, + OneShotParametrizedTestFixture, + ::testing::Values( + // First is osm key, second is regular key. + std::make_pair(KeymapKey{0, 0, 0, OSM(MOD_LSFT), KC_LSFT}, KeymapKey{0, 1, 1, KC_A}), + std::make_pair(KeymapKey{0, 0, 0, OSM(MOD_LCTL), KC_LCTL}, KeymapKey{0, 1, 1, KC_A}), + std::make_pair(KeymapKey{0, 0, 0, OSM(MOD_LALT), KC_LALT}, KeymapKey{0, 1, 1, KC_A}), + std::make_pair(KeymapKey{0, 0, 0, OSM(MOD_LGUI), KC_LGUI}, KeymapKey{0, 1, 1, KC_A}), + std::make_pair(KeymapKey{0, 0, 0, OSM(MOD_RCTL), KC_RCTL}, KeymapKey{0, 1, 1, KC_A}), + std::make_pair(KeymapKey{0, 0, 0, OSM(MOD_RSFT), KC_RSFT}, KeymapKey{0, 1, 1, KC_A}), + std::make_pair(KeymapKey{0, 0, 0, OSM(MOD_RALT), KC_RALT}, KeymapKey{0, 1, 1, KC_A}), + std::make_pair(KeymapKey{0, 0, 0, OSM(MOD_RGUI), KC_RGUI}, KeymapKey{0, 1, 1, KC_A}) + )); +// clang-format on + +TEST_F(OneShot, OSLWithAdditionalKeypress) { + TestDriver driver; + InSequence s; + KeymapKey osl_key = KeymapKey{0, 0, 0, OSL(1)}; + KeymapKey osl_key1 = KeymapKey{1, 0, 0, KC_X}; + KeymapKey regular_key0 = KeymapKey{0, 1, 0, KC_Y}; + KeymapKey regular_key1 = KeymapKey{1, 1, 0, KC_A}; + + set_keymap({osl_key, osl_key1, regular_key0, regular_key1}); + + // Press OSL key. + EXPECT_NO_REPORT(driver); + osl_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release OSL key. + EXPECT_NO_REPORT(driver); + osl_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Press regular key. + EXPECT_REPORT(driver, (regular_key1.report_code)); + EXPECT_EMPTY_REPORT(driver); + regular_key1.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release regular key. + EXPECT_NO_REPORT(driver); + regular_key1.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(OneShot, OSLWithOsmAndAdditionalKeypress) { + TestDriver driver; + InSequence s; + KeymapKey osl_key = KeymapKey{0, 0, 0, OSL(1)}; + KeymapKey osm_key = KeymapKey{1, 1, 0, OSM(MOD_LSFT), KC_LSFT}; + KeymapKey regular_key = KeymapKey{1, 1, 1, KC_A}; + KeymapKey blank_key = KeymapKey{1, 0, 0, KC_NO}; + + set_keymap({osl_key, osm_key, regular_key, blank_key}); + + // Press OSL key. + EXPECT_NO_REPORT(driver); + osl_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release OSL key. + EXPECT_NO_REPORT(driver); + osl_key.release(); + run_one_scan_loop(); + EXPECT_TRUE(layer_state_is(1)); + VERIFY_AND_CLEAR(driver); + + // Press and release OSM. + EXPECT_NO_REPORT(driver); + tap_key(osm_key); + EXPECT_TRUE(layer_state_is(1)); + VERIFY_AND_CLEAR(driver); + + // Tap regular key. + EXPECT_REPORT(driver, (osm_key.report_code, regular_key.report_code)); + EXPECT_EMPTY_REPORT(driver); + tap_key(regular_key); + VERIFY_AND_CLEAR(driver); +} diff --git a/tests/tap_hold_configurations/chordal_hold/permissive_hold/test_tap_hold.cpp b/tests/tap_hold_configurations/chordal_hold/permissive_hold/test_tap_hold.cpp new file mode 100644 index 0000000000..1f89ab3eae --- /dev/null +++ b/tests/tap_hold_configurations/chordal_hold/permissive_hold/test_tap_hold.cpp @@ -0,0 +1,898 @@ +// Copyright 2024-2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#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 ChordalHoldPermissiveHold : public TestFixture {}; + +TEST_F(ChordalHoldPermissiveHold, chordal_hold_handedness) { + EXPECT_EQ(chordal_hold_handedness({.col = 0, .row = 0}), 'L'); + EXPECT_EQ(chordal_hold_handedness({.col = MATRIX_COLS - 1, .row = 0}), 'R'); + EXPECT_EQ(chordal_hold_handedness({.col = 0, .row = 2}), '*'); +} + +TEST_F(ChordalHoldPermissiveHold, get_chordal_hold_default) { + auto make_record = [](uint8_t row, uint8_t col, keyevent_type_t type = KEY_EVENT) { + return keyrecord_t{ + .event = + { + .key = {.col = col, .row = row}, + .type = type, + .pressed = true, + }, + }; + }; + // Create two records on the left hand. + keyrecord_t record_l0 = make_record(0, 0); + keyrecord_t record_l1 = make_record(1, 0); + // Create a record on the right hand. + keyrecord_t record_r = make_record(0, MATRIX_COLS - 1); + + // Function should return true when records are on opposite hands. + EXPECT_TRUE(get_chordal_hold_default(&record_l0, &record_r)); + EXPECT_TRUE(get_chordal_hold_default(&record_r, &record_l0)); + // ... and false when on the same hand. + EXPECT_FALSE(get_chordal_hold_default(&record_l0, &record_l1)); + EXPECT_FALSE(get_chordal_hold_default(&record_l1, &record_l0)); + // But (2, 0) has handedness '*', for which true is returned for chords + // with either hand. + keyrecord_t record_l2 = make_record(2, 0); + EXPECT_TRUE(get_chordal_hold_default(&record_l2, &record_l0)); + EXPECT_TRUE(get_chordal_hold_default(&record_l2, &record_r)); + + // Create a record resulting from a combo. + keyrecord_t record_combo = make_record(0, 0, COMBO_EVENT); + // Function returns true in all cases. + EXPECT_TRUE(get_chordal_hold_default(&record_l0, &record_combo)); + EXPECT_TRUE(get_chordal_hold_default(&record_r, &record_combo)); + EXPECT_TRUE(get_chordal_hold_default(&record_combo, &record_l0)); + EXPECT_TRUE(get_chordal_hold_default(&record_combo, &record_r)); +} + +TEST_F(ChordalHoldPermissiveHold, chord_nested_press_settled_as_hold) { + TestDriver driver; + InSequence s; + // Mod-tap key on the left hand. + auto mod_tap_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_key, regular_key}); + + // Press mod-tap key. + EXPECT_NO_REPORT(driver); + mod_tap_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Tap regular key. + EXPECT_REPORT(driver, (KC_LEFT_SHIFT)); + EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_A)); + EXPECT_REPORT(driver, (KC_LEFT_SHIFT)); + tap_key(regular_key); + VERIFY_AND_CLEAR(driver); + + // Release mod-tap key. + EXPECT_EMPTY_REPORT(driver); + mod_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHoldPermissiveHold, chord_rolled_press_settled_as_tap) { + TestDriver driver; + InSequence s; + // Mod-tap key on the left hand. + auto mod_tap_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_key, regular_key}); + + // Press mod-tap key and regular key. + EXPECT_NO_REPORT(driver); + mod_tap_key.press(); + run_one_scan_loop(); + regular_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release mod-tap key. + EXPECT_REPORT(driver, (KC_P)); + EXPECT_REPORT(driver, (KC_P, KC_A)); + EXPECT_REPORT(driver, (KC_A)); + mod_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release regular key. + EXPECT_EMPTY_REPORT(driver); + regular_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHoldPermissiveHold, 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_key = KeymapKey(0, 1, 0, SFT_T(KC_P)); + auto regular_key = KeymapKey(0, 2, 0, KC_A); + + set_keymap({mod_tap_key, regular_key}); + + // Press mod-tap-hold key. + EXPECT_NO_REPORT(driver); + mod_tap_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(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release mod-tap-hold key. + EXPECT_EMPTY_REPORT(driver); + mod_tap_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHoldPermissiveHold, 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(ChordalHoldPermissiveHold, 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(ChordalHoldPermissiveHold, two_mod_taps_same_hand_hold_til_timeout) { + TestDriver driver; + InSequence s; + auto mod_tap_key1 = KeymapKey(0, MATRIX_COLS - 2, 0, RCTL_T(KC_A)); + auto mod_tap_key2 = KeymapKey(0, MATRIX_COLS - 1, 0, RSFT_T(KC_B)); + + set_keymap({mod_tap_key1, mod_tap_key2}); + + // Press mod-tap keys. + EXPECT_NO_REPORT(driver); + mod_tap_key1.press(); + run_one_scan_loop(); + mod_tap_key2.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Continue holding til the tapping term. + EXPECT_REPORT(driver, (KC_RIGHT_CTRL)); + EXPECT_REPORT(driver, (KC_RIGHT_CTRL, KC_RIGHT_SHIFT)); + idle_for(TAPPING_TERM); + VERIFY_AND_CLEAR(driver); + + // Release mod-tap keys. + EXPECT_REPORT(driver, (KC_RIGHT_SHIFT)); + mod_tap_key1.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_EMPTY_REPORT(driver); + mod_tap_key2.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHoldPermissiveHold, two_mod_taps_nested_press_opposite_hands) { + TestDriver driver; + InSequence s; + auto mod_tap_key1 = KeymapKey(0, 1, 0, SFT_T(KC_A)); + auto mod_tap_key2 = KeymapKey(0, MATRIX_COLS - 1, 0, RSFT_T(KC_B)); + + set_keymap({mod_tap_key1, mod_tap_key2}); + + // Press mod-tap keys. + EXPECT_NO_REPORT(driver); + mod_tap_key1.press(); + run_one_scan_loop(); + mod_tap_key2.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release mod-tap keys. + EXPECT_REPORT(driver, (KC_LEFT_SHIFT)); + EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_B)); + EXPECT_REPORT(driver, (KC_LEFT_SHIFT)); + mod_tap_key2.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_EMPTY_REPORT(driver); + mod_tap_key1.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHoldPermissiveHold, two_mod_taps_nested_press_same_hand) { + 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 keys. + EXPECT_NO_REPORT(driver); + mod_tap_key1.press(); + run_one_scan_loop(); + mod_tap_key2.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release mod-tap keys. + EXPECT_REPORT(driver, (KC_A)); + EXPECT_REPORT(driver, (KC_A, KC_B)); + EXPECT_REPORT(driver, (KC_A)); + mod_tap_key2.release(); + run_one_scan_loop(); + + EXPECT_EMPTY_REPORT(driver); + mod_tap_key1.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHoldPermissiveHold, three_mod_taps_same_hand_streak_roll) { + TestDriver driver; + InSequence s; + auto mod_tap_key1 = KeymapKey(0, 1, 0, SFT_T(KC_A)); + auto mod_tap_key2 = KeymapKey(0, 2, 0, CTL_T(KC_B)); + auto mod_tap_key3 = KeymapKey(0, 3, 0, RSFT_T(KC_C)); + + set_keymap({mod_tap_key1, mod_tap_key2, mod_tap_key3}); + + // Press mod-tap keys. + EXPECT_NO_REPORT(driver); + mod_tap_key1.press(); + run_one_scan_loop(); + mod_tap_key2.press(); + run_one_scan_loop(); + mod_tap_key3.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release keys 1, 2, 3. + // + // NOTE: The correct order of events should be + // EXPECT_REPORT(driver, (KC_A, KC_B, KC_C)); + // EXPECT_REPORT(driver, (KC_B, KC_C)); + // EXPECT_REPORT(driver, (KC_C)); + // EXPECT_EMPTY_REPORT(driver); + // + // However, due to a workaround for https://github.com/tmk/tmk_keyboard/issues/60, + // the events are processed out of order, with the first two keys released + // before pressing KC_C. + EXPECT_REPORT(driver, (KC_A)); + EXPECT_REPORT(driver, (KC_A, KC_B)); + EXPECT_REPORT(driver, (KC_B)); + EXPECT_EMPTY_REPORT(driver); + EXPECT_REPORT(driver, (KC_C)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_key1.release(); + run_one_scan_loop(); + mod_tap_key2.release(); + run_one_scan_loop(); + mod_tap_key3.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHoldPermissiveHold, three_mod_taps_same_hand_streak_orders) { + TestDriver driver; + InSequence s; + auto mod_tap_key1 = KeymapKey(0, 1, 0, SFT_T(KC_A)); + auto mod_tap_key2 = KeymapKey(0, 2, 0, CTL_T(KC_B)); + auto mod_tap_key3 = KeymapKey(0, 3, 0, RSFT_T(KC_C)); + + set_keymap({mod_tap_key1, mod_tap_key2, mod_tap_key3}); + + // Press mod-tap keys. + EXPECT_NO_REPORT(driver); + mod_tap_key1.press(); + run_one_scan_loop(); + mod_tap_key2.press(); + run_one_scan_loop(); + mod_tap_key3.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release keys 3, 2, 1. + EXPECT_REPORT(driver, (KC_A)); + EXPECT_REPORT(driver, (KC_A, KC_B)); + EXPECT_REPORT(driver, (KC_A, KC_B, KC_C)); + EXPECT_REPORT(driver, (KC_A, KC_B)); + mod_tap_key3.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_A)); + mod_tap_key2.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_EMPTY_REPORT(driver); + mod_tap_key1.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Press mod-tap keys. + EXPECT_NO_REPORT(driver); + idle_for(TAPPING_TERM); + mod_tap_key1.press(); + run_one_scan_loop(); + mod_tap_key2.press(); + run_one_scan_loop(); + mod_tap_key3.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release keys 3, 1, 2. + EXPECT_REPORT(driver, (KC_A)); + EXPECT_REPORT(driver, (KC_A, KC_B)); + EXPECT_REPORT(driver, (KC_A, KC_B, KC_C)); + EXPECT_REPORT(driver, (KC_A, KC_B)); + mod_tap_key3.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_B)); + mod_tap_key1.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_EMPTY_REPORT(driver); + mod_tap_key2.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Press mod-tap keys. + EXPECT_NO_REPORT(driver); + idle_for(TAPPING_TERM); + mod_tap_key1.press(); + run_one_scan_loop(); + mod_tap_key2.press(); + run_one_scan_loop(); + mod_tap_key3.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release keys 2, 3, 1. + // + // NOTE: The correct order of events should be + // EXPECT_REPORT(driver, (KC_A, KC_B, KC_C)); + // EXPECT_REPORT(driver, (KC_A, KC_C)); + // EXPECT_REPORT(driver, (KC_A)); + // EXPECT_EMPTY_REPORT(driver); + // + // However, due to a workaround for https://github.com/tmk/tmk_keyboard/issues/60, + // the events are processed out of order. + EXPECT_REPORT(driver, (KC_A)); + EXPECT_REPORT(driver, (KC_A, KC_B)); + EXPECT_REPORT(driver, (KC_A)); + EXPECT_REPORT(driver, (KC_A, KC_C)); + EXPECT_REPORT(driver, (KC_A)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_key2.release(); + run_one_scan_loop(); + mod_tap_key3.release(); + run_one_scan_loop(); + mod_tap_key1.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_NO_REPORT(driver); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHoldPermissiveHold, three_mod_taps_opposite_hands_roll) { + TestDriver driver; + InSequence s; + auto mod_tap_key1 = KeymapKey(0, 1, 0, SFT_T(KC_A)); + auto mod_tap_key2 = KeymapKey(0, 2, 0, CTL_T(KC_B)); + auto mod_tap_key3 = KeymapKey(0, MATRIX_COLS - 1, 0, RSFT_T(KC_C)); + + set_keymap({mod_tap_key1, mod_tap_key2, mod_tap_key3}); + + // Press mod-tap keys. + EXPECT_NO_REPORT(driver); + mod_tap_key1.press(); + run_one_scan_loop(); + mod_tap_key2.press(); + run_one_scan_loop(); + mod_tap_key3.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release keys 1, 2, 3. + // + // NOTE: The correct order of events should be + // EXPECT_REPORT(driver, (KC_A, KC_B)); + // EXPECT_REPORT(driver, (KC_A, KC_B, KC_C)); + // EXPECT_REPORT(driver, (KC_B, KC_C)); + // EXPECT_REPORT(driver, (KC_C)); + // EXPECT_EMPTY_REPORT(driver); + // + // However, due to a workaround for https://github.com/tmk/tmk_keyboard/issues/60, + // the events are processed out of order, with the first two keys released + // before pressing KC_C. + EXPECT_REPORT(driver, (KC_A)); + EXPECT_REPORT(driver, (KC_A, KC_B)); + EXPECT_REPORT(driver, (KC_B)); + EXPECT_EMPTY_REPORT(driver); + EXPECT_REPORT(driver, (KC_C)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_key1.release(); + run_one_scan_loop(); + mod_tap_key2.release(); + run_one_scan_loop(); + mod_tap_key3.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHoldPermissiveHold, three_mod_taps_two_left_one_right) { + TestDriver driver; + InSequence s; + auto mod_tap_key1 = KeymapKey(0, 1, 0, SFT_T(KC_A)); + auto mod_tap_key2 = KeymapKey(0, 2, 0, CTL_T(KC_B)); + auto mod_tap_key3 = KeymapKey(0, MATRIX_COLS - 1, 0, RSFT_T(KC_C)); + + set_keymap({mod_tap_key1, mod_tap_key2, mod_tap_key3}); + + // Press mod-tap keys. + EXPECT_NO_REPORT(driver); + mod_tap_key1.press(); + run_one_scan_loop(); + mod_tap_key2.press(); + run_one_scan_loop(); + mod_tap_key3.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release key 3. + EXPECT_REPORT(driver, (KC_LEFT_SHIFT)); + EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_LEFT_CTRL)); + EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_LEFT_CTRL, KC_C)); + EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_LEFT_CTRL)); + mod_tap_key3.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release key 2, then key 1. + EXPECT_REPORT(driver, (KC_LEFT_SHIFT)); + mod_tap_key2.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_EMPTY_REPORT(driver); + mod_tap_key1.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Press mod-tap keys. + EXPECT_NO_REPORT(driver); + idle_for(TAPPING_TERM); + mod_tap_key1.press(); + run_one_scan_loop(); + mod_tap_key2.press(); + run_one_scan_loop(); + mod_tap_key3.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release key 3. + EXPECT_REPORT(driver, (KC_LEFT_SHIFT)); + EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_LEFT_CTRL)); + EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_LEFT_CTRL, KC_C)); + EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_LEFT_CTRL)); + mod_tap_key3.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release key 1, then key 2. + EXPECT_REPORT(driver, (KC_LEFT_CTRL)); + mod_tap_key1.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_EMPTY_REPORT(driver); + mod_tap_key2.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHoldPermissiveHold, three_mod_taps_one_held_two_tapped) { + TestDriver driver; + InSequence s; + auto mod_tap_key1 = KeymapKey(0, 1, 0, SFT_T(KC_A)); + auto mod_tap_key2 = KeymapKey(0, MATRIX_COLS - 2, 0, CTL_T(KC_B)); + auto mod_tap_key3 = KeymapKey(0, MATRIX_COLS - 1, 0, RSFT_T(KC_C)); + + set_keymap({mod_tap_key1, mod_tap_key2, mod_tap_key3}); + + // Press mod-tap keys. + EXPECT_NO_REPORT(driver); + mod_tap_key1.press(); + run_one_scan_loop(); + mod_tap_key2.press(); + run_one_scan_loop(); + mod_tap_key3.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release keys 3, 2, 1. + EXPECT_REPORT(driver, (KC_LEFT_SHIFT)); + EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_B)); + EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_B, KC_C)); + EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_B)); + mod_tap_key3.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_LEFT_SHIFT)); + mod_tap_key2.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_EMPTY_REPORT(driver); + mod_tap_key1.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Press mod-tap keys. + EXPECT_NO_REPORT(driver); + idle_for(TAPPING_TERM); + mod_tap_key1.press(); + run_one_scan_loop(); + mod_tap_key2.press(); + run_one_scan_loop(); + mod_tap_key3.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release keys 3, 1, 2. + EXPECT_REPORT(driver, (KC_LEFT_SHIFT)); + EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_B)); + EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_B, KC_C)); + EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_B)); + mod_tap_key3.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_B)); + mod_tap_key1.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_EMPTY_REPORT(driver); + mod_tap_key2.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHoldPermissiveHold, two_mod_taps_one_regular_key) { + TestDriver driver; + InSequence s; + auto mod_tap_key1 = KeymapKey(0, 1, 0, SFT_T(KC_A)); + auto mod_tap_key2 = KeymapKey(0, MATRIX_COLS - 2, 0, CTL_T(KC_B)); + auto regular_key = KeymapKey(0, MATRIX_COLS - 1, 0, KC_C); + + set_keymap({mod_tap_key1, mod_tap_key2, regular_key}); + + // Press keys. + EXPECT_NO_REPORT(driver); + mod_tap_key1.press(); + run_one_scan_loop(); + mod_tap_key2.press(); + run_one_scan_loop(); + regular_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release keys. + EXPECT_REPORT(driver, (KC_LEFT_SHIFT)); + EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_B)); + EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_B, KC_C)); + EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_C)); + mod_tap_key2.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_LEFT_SHIFT)); + regular_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_EMPTY_REPORT(driver); + mod_tap_key1.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Press mod-tap keys. + EXPECT_NO_REPORT(driver); + idle_for(TAPPING_TERM); + mod_tap_key1.press(); + run_one_scan_loop(); + mod_tap_key2.press(); + run_one_scan_loop(); + regular_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release keys. + EXPECT_REPORT(driver, (KC_LEFT_SHIFT)); + EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_B)); + EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_B, KC_C)); + EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_B)); + regular_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_REPORT(driver, (KC_B)); + mod_tap_key1.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + EXPECT_EMPTY_REPORT(driver); + mod_tap_key2.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHoldPermissiveHold, tap_regular_key_while_layer_tap_key_is_held) { + TestDriver driver; + InSequence s; + auto layer_tap_hold_key = KeymapKey(0, 1, 0, LT(1, KC_P)); + auto regular_key = KeymapKey(0, MATRIX_COLS - 1, 0, KC_A); + auto no_key = KeymapKey(1, 1, 0, XXXXXXX); + auto layer_key = KeymapKey(1, MATRIX_COLS - 1, 0, KC_B); + + set_keymap({layer_tap_hold_key, regular_key, no_key, layer_key}); + + // Press layer-tap-hold key. + EXPECT_NO_REPORT(driver); + layer_tap_hold_key.press(); + run_one_scan_loop(); + // Press regular key. + regular_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release regular key. + EXPECT_REPORT(driver, (KC_B)); + EXPECT_EMPTY_REPORT(driver); + regular_key.release(); + run_one_scan_loop(); + EXPECT_EQ(layer_state, 2); + VERIFY_AND_CLEAR(driver); + + // Release layer-tap-hold key. + EXPECT_NO_REPORT(driver); + layer_tap_hold_key.release(); + run_one_scan_loop(); + EXPECT_EQ(layer_state, 0); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHoldPermissiveHold, nested_tap_of_layer_0_layer_tap_keys) { + TestDriver driver; + InSequence s; + // The keys are layer-taps on layer 2 but regular keys on layer 1. + auto first_layer_tap_key = KeymapKey(0, 1, 0, LT(1, KC_A)); + auto second_layer_tap_key = KeymapKey(0, MATRIX_COLS - 1, 0, LT(1, KC_P)); + auto first_key_on_layer = KeymapKey(1, 1, 0, KC_B); + auto second_key_on_layer = KeymapKey(1, MATRIX_COLS - 1, 0, KC_Q); + + set_keymap({first_layer_tap_key, second_layer_tap_key, first_key_on_layer, second_key_on_layer}); + + // Press first layer-tap key. + EXPECT_NO_REPORT(driver); + first_layer_tap_key.press(); + run_one_scan_loop(); + // Press second layer-tap key. + second_layer_tap_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release second layer-tap key. + EXPECT_REPORT(driver, (KC_Q)); + EXPECT_EMPTY_REPORT(driver); + second_layer_tap_key.release(); + run_one_scan_loop(); + EXPECT_EQ(layer_state, 2); + VERIFY_AND_CLEAR(driver); + + // Release first layer-tap key. + EXPECT_NO_REPORT(driver); + first_layer_tap_key.release(); + run_one_scan_loop(); + EXPECT_EQ(layer_state, 0); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHoldPermissiveHold, lt_mt_one_regular_key) { + TestDriver driver; + InSequence s; + auto lt_key = KeymapKey(0, 1, 0, LT(1, KC_A)); + auto mt_key0 = KeymapKey(0, 2, 0, SFT_T(KC_B)); + auto mt_key1 = KeymapKey(1, 2, 0, CTL_T(KC_C)); + auto regular_key = KeymapKey(1, MATRIX_COLS - 1, 0, KC_X); + auto no_key0 = KeymapKey(0, MATRIX_COLS - 1, 0, XXXXXXX); + auto no_key1 = KeymapKey(1, 1, 0, XXXXXXX); + + set_keymap({lt_key, mt_key0, mt_key1, regular_key, no_key0, no_key1}); + + // Press LT, MT, and regular key. + EXPECT_NO_REPORT(driver); + lt_key.press(); + run_one_scan_loop(); + mt_key1.press(); + run_one_scan_loop(); + regular_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release the regular key. + EXPECT_REPORT(driver, (KC_LCTL)); + EXPECT_REPORT(driver, (KC_LCTL, KC_X)); + EXPECT_REPORT(driver, (KC_LCTL)); + regular_key.release(); + run_one_scan_loop(); + EXPECT_EQ(get_mods(), MOD_BIT_LCTRL); + EXPECT_EQ(layer_state, 2); + VERIFY_AND_CLEAR(driver); + + // Release MT key. + EXPECT_EMPTY_REPORT(driver); + mt_key1.release(); + run_one_scan_loop(); + EXPECT_EQ(get_mods(), 0); + VERIFY_AND_CLEAR(driver); + + // Release LT key. + EXPECT_NO_REPORT(driver); + lt_key.release(); + run_one_scan_loop(); + EXPECT_EQ(layer_state, 0); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHoldPermissiveHold, nested_tap_of_layer_tap_keys) { + TestDriver driver; + InSequence s; + // The keys are layer-taps on all layers. + auto first_key_layer_0 = KeymapKey(0, 1, 0, LT(1, KC_A)); + auto second_key_layer_0 = KeymapKey(0, MATRIX_COLS - 1, 0, LT(1, KC_P)); + auto first_key_layer_1 = KeymapKey(1, 1, 0, LT(2, KC_B)); + auto second_key_layer_1 = KeymapKey(1, MATRIX_COLS - 1, 0, LT(2, KC_Q)); + auto first_key_layer_2 = KeymapKey(2, 1, 0, KC_TRNS); + auto second_key_layer_2 = KeymapKey(2, MATRIX_COLS - 1, 0, KC_TRNS); + + set_keymap({first_key_layer_0, second_key_layer_0, first_key_layer_1, second_key_layer_1, first_key_layer_2, second_key_layer_2}); + + // Press first layer-tap key. + EXPECT_NO_REPORT(driver); + first_key_layer_0.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Press second layer-tap key. + EXPECT_NO_REPORT(driver); + second_key_layer_0.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release second layer-tap key. + EXPECT_REPORT(driver, (KC_Q)); + EXPECT_EMPTY_REPORT(driver); + second_key_layer_0.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release first layer-tap key. + EXPECT_NO_REPORT(driver); + first_key_layer_0.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} + +TEST_F(ChordalHoldPermissiveHold, roll_layer_tap_key_with_regular_key) { + TestDriver driver; + InSequence s; + + auto layer_tap_hold_key = KeymapKey(0, 1, 0, LT(1, KC_P)); + auto regular_key = KeymapKey(0, MATRIX_COLS - 1, 0, KC_A); + auto layer_key = KeymapKey(1, MATRIX_COLS - 1, 0, KC_B); + + set_keymap({layer_tap_hold_key, regular_key, layer_key}); + + // Press layer-tap-hold key. + EXPECT_NO_REPORT(driver); + layer_tap_hold_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Press regular key. + regular_key.press(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release layer-tap-hold key. + EXPECT_REPORT(driver, (KC_P)); + EXPECT_REPORT(driver, (KC_P, KC_A)); + EXPECT_REPORT(driver, (KC_A)); + EXPECT_NO_REPORT(driver); + layer_tap_hold_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); + + // Release regular key. + EXPECT_EMPTY_REPORT(driver); + regular_key.release(); + run_one_scan_loop(); + VERIFY_AND_CLEAR(driver); +} diff --git a/tests/tap_hold_configurations/chordal_hold/retro_shift_permissive_hold/config.h b/tests/tap_hold_configurations/chordal_hold/retro_shift_permissive_hold/config.h new file mode 100644 index 0000000000..4d704c5978 --- /dev/null +++ b/tests/tap_hold_configurations/chordal_hold/retro_shift_permissive_hold/config.h @@ -0,0 +1,28 @@ +/* Copyright 2022 Isaac Elenbaas + * Copyright 2024 Google LLC + * + * 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 + +#define RETRO_SHIFT 2 * TAPPING_TERM +// releases between AUTO_SHIFT_TIMEOUT and TAPPING_TERM are not tested +#define AUTO_SHIFT_TIMEOUT TAPPING_TERM +#define AUTO_SHIFT_MODIFIERS diff --git a/tests/tap_hold_configurations/chordal_hold/retro_shift_permissive_hold/test.mk b/tests/tap_hold_configurations/chordal_hold/retro_shift_permissive_hold/test.mk new file mode 100644 index 0000000000..c39dfa9cb1 --- /dev/null +++ b/tests/tap_hold_configurations/chordal_hold/retro_shift_permissive_hold/test.mk @@ -0,0 +1,18 @@ +# Copyright 2022 Isaac Elenbaas +# Copyright 2024 Google LLC +# +# 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 . + +AUTO_SHIFT_ENABLE = yes +INTROSPECTION_KEYMAP_C = test_keymap.c diff --git a/tests/tap_hold_configurations/chordal_hold/retro_shift_permissive_hold/test_keymap.c b/tests/tap_hold_configurations/chordal_hold/retro_shift_permissive_hold/test_keymap.c new file mode 100644 index 0000000000..8a6a2c59b0 --- /dev/null +++ b/tests/tap_hold_configurations/chordal_hold/retro_shift_permissive_hold/test_keymap.c @@ -0,0 +1,22 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "quantum.h" + +const char chordal_hold_layout[MATRIX_ROWS][MATRIX_COLS] PROGMEM = { + {'L', 'L', 'L', 'L', 'L', 'R', 'R', 'R', 'R', 'R'}, + {'L', 'L', 'L', 'L', 'L', 'R', 'R', 'R', 'R', 'R'}, + {'*', 'L', 'L', 'L', 'L', 'R', 'R', 'R', 'R', 'R'}, + {'L', 'L', 'L', 'L', 'L', 'R', 'R', 'R', 'R', 'R'}, +}; diff --git a/tests/tap_hold_configurations/chordal_hold/retro_shift_permissive_hold/test_retro_shift.cpp b/tests/tap_hold_configurations/chordal_hold/retro_shift_permissive_hold/test_retro_shift.cpp new file mode 100644 index 0000000000..16fbabbd8a --- /dev/null +++ b/tests/tap_hold_configurations/chordal_hold/retro_shift_permissive_hold/test_retro_shift.cpp @@ -0,0 +1,420 @@ +/* Copyright 2022 Isaac Elenbaas + * Copyright 2024 Google LLC + * + * 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" + +bool get_auto_shifted_key(uint16_t keycode, keyrecord_t *record) { + return true; +} + +using testing::_; +using testing::AnyNumber; +using testing::AnyOf; +using testing::InSequence; + +class RetroShiftPermissiveHold : public TestFixture {}; + +TEST_F(RetroShiftPermissiveHold, tap_regular_key_while_mod_tap_key_is_held_under_tapping_term) { + TestDriver driver; + InSequence s; + auto mod_tap_hold_key = KeymapKey(0, 0, 0, CTL_T(KC_P)); + 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(); + testing::Mock::VerifyAndClearExpectations(&driver); + + /* Press regular key. */ + EXPECT_NO_REPORT(driver); + regular_key.press(); + run_one_scan_loop(); + testing::Mock::VerifyAndClearExpectations(&driver); + + /* Release regular key. */ + EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LCTL))).Times(AnyNumber()); + EXPECT_REPORT(driver, (KC_LCTL, KC_A)); + EXPECT_REPORT(driver, (KC_LCTL)); + regular_key.release(); + run_one_scan_loop(); + testing::Mock::VerifyAndClearExpectations(&driver); + + /* Release mod-tap-hold key. */ + EXPECT_EMPTY_REPORT(driver); + mod_tap_hold_key.release(); + run_one_scan_loop(); + testing::Mock::VerifyAndClearExpectations(&driver); +} + +TEST_F(RetroShiftPermissiveHold, tap_mod_tap_key_while_mod_tap_key_is_held_under_tapping_term) { + TestDriver driver; + InSequence s; + auto mod_tap_hold_key = KeymapKey(0, 0, 0, CTL_T(KC_P)); + auto mod_tap_regular_key = KeymapKey(0, MATRIX_COLS - 1, 0, ALT_T(KC_A)); + + set_keymap({mod_tap_hold_key, mod_tap_regular_key}); + + /* Press mod-tap-hold key. */ + EXPECT_NO_REPORT(driver); + mod_tap_hold_key.press(); + run_one_scan_loop(); + testing::Mock::VerifyAndClearExpectations(&driver); + + /* Press mod-tap-regular key. */ + EXPECT_NO_REPORT(driver); + mod_tap_regular_key.press(); + run_one_scan_loop(); + testing::Mock::VerifyAndClearExpectations(&driver); + + /* Release mod-tap-regular key. */ + EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LCTL))).Times(AnyNumber()); + EXPECT_REPORT(driver, (KC_LCTL, KC_A)); + EXPECT_REPORT(driver, (KC_LCTL)); + mod_tap_regular_key.release(); + run_one_scan_loop(); + testing::Mock::VerifyAndClearExpectations(&driver); + + /* Release mod-tap-hold key. */ + EXPECT_EMPTY_REPORT(driver); + mod_tap_hold_key.release(); + run_one_scan_loop(); + testing::Mock::VerifyAndClearExpectations(&driver); +} + +TEST_F(RetroShiftPermissiveHold, tap_regular_key_while_mod_tap_key_is_held_over_tapping_term) { + TestDriver driver; + InSequence s; + auto mod_tap_hold_key = KeymapKey(0, 0, 0, CTL_T(KC_P)); + 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(); + testing::Mock::VerifyAndClearExpectations(&driver); + + /* Press regular key. */ + EXPECT_NO_REPORT(driver); + regular_key.press(); + run_one_scan_loop(); + testing::Mock::VerifyAndClearExpectations(&driver); + + /* Release regular key. */ + EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LCTL))).Times(AnyNumber()); + EXPECT_REPORT(driver, (KC_LCTL, KC_A)); + EXPECT_REPORT(driver, (KC_LCTL)); + regular_key.release(); + run_one_scan_loop(); + idle_for(TAPPING_TERM); + testing::Mock::VerifyAndClearExpectations(&driver); + + /* Release mod-tap-hold key. */ + EXPECT_EMPTY_REPORT(driver); + mod_tap_hold_key.release(); + run_one_scan_loop(); + testing::Mock::VerifyAndClearExpectations(&driver); +} + +TEST_F(RetroShiftPermissiveHold, tap_mod_tap_key_while_mod_tap_key_is_held_over_tapping_term) { + TestDriver driver; + InSequence s; + auto mod_tap_hold_key = KeymapKey(0, 0, 0, CTL_T(KC_P)); + auto mod_tap_regular_key = KeymapKey(0, MATRIX_COLS - 1, 0, ALT_T(KC_A)); + + set_keymap({mod_tap_hold_key, mod_tap_regular_key}); + + /* Press mod-tap-hold key. */ + EXPECT_NO_REPORT(driver); + mod_tap_hold_key.press(); + run_one_scan_loop(); + testing::Mock::VerifyAndClearExpectations(&driver); + + /* Press mod-tap-regular key. */ + EXPECT_NO_REPORT(driver); + mod_tap_regular_key.press(); + run_one_scan_loop(); + testing::Mock::VerifyAndClearExpectations(&driver); + + /* Release mod-tap-regular key. */ + EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LCTL))).Times(AnyNumber()); + EXPECT_REPORT(driver, (KC_LCTL, KC_A)); + EXPECT_REPORT(driver, (KC_LCTL)); + mod_tap_regular_key.release(); + run_one_scan_loop(); + idle_for(TAPPING_TERM); + testing::Mock::VerifyAndClearExpectations(&driver); + + /* Release mod-tap-hold key. */ + EXPECT_EMPTY_REPORT(driver); + mod_tap_hold_key.release(); + run_one_scan_loop(); + testing::Mock::VerifyAndClearExpectations(&driver); +} + +TEST_F(RetroShiftPermissiveHold, hold_regular_key_while_mod_tap_key_is_held_over_tapping_term) { + TestDriver driver; + InSequence s; + auto mod_tap_hold_key = KeymapKey(0, 0, 0, CTL_T(KC_P)); + 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(); + testing::Mock::VerifyAndClearExpectations(&driver); + + /* Press regular key. */ + EXPECT_NO_REPORT(driver); + regular_key.press(); + run_one_scan_loop(); + idle_for(AUTO_SHIFT_TIMEOUT); + testing::Mock::VerifyAndClearExpectations(&driver); + + /* Release regular key. */ + // clang-format off + EXPECT_CALL(driver, send_keyboard_mock(AnyOf( + KeyboardReport(KC_LCTL, KC_LSFT), + KeyboardReport(KC_LSFT), + KeyboardReport(KC_LCTL)))) + .Times(AnyNumber()); + // clang-format on + EXPECT_REPORT(driver, (KC_LCTL, KC_LSFT, KC_A)); + // clang-format off + EXPECT_CALL(driver, send_keyboard_mock(AnyOf( + KeyboardReport(KC_LCTL, KC_LSFT), + KeyboardReport(KC_LSFT)))) + .Times(AnyNumber()); + // clang-format on + EXPECT_REPORT(driver, (KC_LCTL)); + regular_key.release(); + run_one_scan_loop(); + testing::Mock::VerifyAndClearExpectations(&driver); + + /* Release mod-tap-hold key. */ + EXPECT_EMPTY_REPORT(driver); + mod_tap_hold_key.release(); + run_one_scan_loop(); + testing::Mock::VerifyAndClearExpectations(&driver); +} + +TEST_F(RetroShiftPermissiveHold, hold_mod_tap_key_while_mod_tap_key_is_held_over_tapping_term) { + TestDriver driver; + InSequence s; + auto mod_tap_hold_key = KeymapKey(0, 0, 0, CTL_T(KC_P)); + auto mod_tap_regular_key = KeymapKey(0, MATRIX_COLS - 1, 0, ALT_T(KC_A)); + + set_keymap({mod_tap_hold_key, mod_tap_regular_key}); + + /* Press mod-tap-hold key. */ + EXPECT_NO_REPORT(driver); + mod_tap_hold_key.press(); + run_one_scan_loop(); + testing::Mock::VerifyAndClearExpectations(&driver); + + /* Press mod-tap-regular key. */ + EXPECT_NO_REPORT(driver); + mod_tap_regular_key.press(); + run_one_scan_loop(); + idle_for(AUTO_SHIFT_TIMEOUT); + testing::Mock::VerifyAndClearExpectations(&driver); + + /* Release mod-tap-regular key. */ + // clang-format off + EXPECT_CALL(driver, send_keyboard_mock(AnyOf( + KeyboardReport(KC_LCTL, KC_LSFT), + KeyboardReport(KC_LSFT), + KeyboardReport(KC_LCTL)))) + .Times(AnyNumber()); + // clang-format on + EXPECT_REPORT(driver, (KC_LCTL, KC_LSFT, KC_A)); + // clang-format off + EXPECT_CALL(driver, send_keyboard_mock(AnyOf( + KeyboardReport(KC_LCTL, KC_LSFT), + KeyboardReport(KC_LSFT)))) + .Times(AnyNumber()); + // clang-format on + EXPECT_REPORT(driver, (KC_LCTL)); + mod_tap_regular_key.release(); + run_one_scan_loop(); + idle_for(TAPPING_TERM); + testing::Mock::VerifyAndClearExpectations(&driver); + + /* Release mod-tap-hold key. */ + EXPECT_EMPTY_REPORT(driver); + mod_tap_hold_key.release(); + run_one_scan_loop(); + testing::Mock::VerifyAndClearExpectations(&driver); +} + +TEST_F(RetroShiftPermissiveHold, roll_tap_regular_key_while_mod_tap_key_is_held_under_tapping_term) { + TestDriver driver; + InSequence s; + auto mod_tap_hold_key = KeymapKey(0, 0, 0, CTL_T(KC_P)); + 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(); + testing::Mock::VerifyAndClearExpectations(&driver); + + /* Press regular key. */ + EXPECT_NO_REPORT(driver); + regular_key.press(); + run_one_scan_loop(); + testing::Mock::VerifyAndClearExpectations(&driver); + + /* Release mod-tap-hold key. */ + EXPECT_REPORT(driver, (KC_P)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_hold_key.release(); + run_one_scan_loop(); + testing::Mock::VerifyAndClearExpectations(&driver); + + /* Release regular key. */ + EXPECT_REPORT(driver, (KC_A)); + EXPECT_EMPTY_REPORT(driver); + regular_key.release(); + run_one_scan_loop(); + testing::Mock::VerifyAndClearExpectations(&driver); +} + +TEST_F(RetroShiftPermissiveHold, roll_tap_mod_tap_key_while_mod_tap_key_is_held_under_tapping_term) { + TestDriver driver; + InSequence s; + auto mod_tap_hold_key = KeymapKey(0, 0, 0, CTL_T(KC_P)); + auto mod_tap_regular_key = KeymapKey(0, MATRIX_COLS - 1, 0, ALT_T(KC_A)); + + set_keymap({mod_tap_hold_key, mod_tap_regular_key}); + + /* Press mod-tap-hold key. */ + EXPECT_NO_REPORT(driver); + mod_tap_hold_key.press(); + run_one_scan_loop(); + testing::Mock::VerifyAndClearExpectations(&driver); + + /* Press mod-tap-regular key. */ + EXPECT_NO_REPORT(driver); + mod_tap_regular_key.press(); + run_one_scan_loop(); + testing::Mock::VerifyAndClearExpectations(&driver); + + /* Release mod-tap-hold key. */ + EXPECT_REPORT(driver, (KC_P)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_hold_key.release(); + run_one_scan_loop(); + testing::Mock::VerifyAndClearExpectations(&driver); + + /* Release mod-tap-regular key. */ + EXPECT_REPORT(driver, (KC_A)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_regular_key.release(); + run_one_scan_loop(); + testing::Mock::VerifyAndClearExpectations(&driver); +} + +TEST_F(RetroShiftPermissiveHold, roll_hold_regular_key_while_mod_tap_key_is_held_under_tapping_term) { + TestDriver driver; + InSequence s; + auto mod_tap_hold_key = KeymapKey(0, 0, 0, CTL_T(KC_P)); + 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(); + testing::Mock::VerifyAndClearExpectations(&driver); + + /* Press regular key. */ + EXPECT_NO_REPORT(driver); + regular_key.press(); + run_one_scan_loop(); + testing::Mock::VerifyAndClearExpectations(&driver); + + /* Release mod-tap-hold key. */ + EXPECT_REPORT(driver, (KC_P)); + EXPECT_EMPTY_REPORT(driver); + EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LSFT))).Times(AnyNumber()); + EXPECT_REPORT(driver, (KC_LSFT, KC_A)); + EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LSFT))).Times(AnyNumber()); + EXPECT_EMPTY_REPORT(driver); + mod_tap_hold_key.release(); + run_one_scan_loop(); + idle_for(AUTO_SHIFT_TIMEOUT); + testing::Mock::VerifyAndClearExpectations(&driver); + + /* Release regular key. */ + EXPECT_NO_REPORT(driver); + regular_key.release(); + run_one_scan_loop(); + testing::Mock::VerifyAndClearExpectations(&driver); +} + +TEST_F(RetroShiftPermissiveHold, roll_hold_mod_tap_key_while_mod_tap_key_is_held_under_tapping_term) { + TestDriver driver; + InSequence s; + auto mod_tap_hold_key = KeymapKey(0, 0, 0, CTL_T(KC_P)); + auto mod_tap_regular_key = KeymapKey(0, MATRIX_COLS - 1, 0, ALT_T(KC_A)); + + set_keymap({mod_tap_hold_key, mod_tap_regular_key}); + + /* Press mod-tap-hold key. */ + EXPECT_NO_REPORT(driver); + mod_tap_hold_key.press(); + run_one_scan_loop(); + testing::Mock::VerifyAndClearExpectations(&driver); + + /* Press mod-tap-regular key. */ + EXPECT_NO_REPORT(driver); + mod_tap_regular_key.press(); + run_one_scan_loop(); + testing::Mock::VerifyAndClearExpectations(&driver); + + /* Release mod-tap-hold key. */ + EXPECT_REPORT(driver, (KC_P)); + EXPECT_EMPTY_REPORT(driver); + mod_tap_hold_key.release(); + run_one_scan_loop(); + testing::Mock::VerifyAndClearExpectations(&driver); + + /* Release mod-tap-regular key. */ + EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LSFT))).Times(AnyNumber()); + EXPECT_REPORT(driver, (KC_LSFT, KC_A)); + EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LSFT))).Times(AnyNumber()); + EXPECT_EMPTY_REPORT(driver); + idle_for(AUTO_SHIFT_TIMEOUT); + mod_tap_regular_key.release(); + run_one_scan_loop(); + testing::Mock::VerifyAndClearExpectations(&driver); +} -- cgit v1.2.3 From 273d8d6a1a6badb8d9b473954f16a0fbbe671987 Mon Sep 17 00:00:00 2001 From: Ryan Date: Sat, 1 Feb 2025 21:19:30 +1100 Subject: `qmk docs`: restore `--port` and `--browser` arguments (#24623) * `qmk docs`: restore `--port` and `--browser` arguments * Make docs command args always a list--- docs/cli_commands.md | 15 +++++++++------ docs/contributing.md | 4 ++-- lib/python/qmk/cli/docs.py | 9 ++++++--- lib/python/qmk/cli/generate/docs.py | 6 ++---- lib/python/qmk/docs.py | 8 ++++---- 5 files changed, 23 insertions(+), 19 deletions(-) (limited to 'lib/python/qmk/cli/generate') diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 4cd5ae98c3..d17b0eda23 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -723,23 +723,26 @@ Now open your dev environment and live a squiggly-free life. ## `qmk docs` -This command starts a local HTTP server which you can use for browsing or improving the docs. Default port is 5173. +This command starts a local HTTP server which you can use for browsing or improving the docs, and provides live reload capability whilst editing. Default port is 8936. +Use the `-b`/`--browser` flag to automatically open the local webserver in your default browser. -This command requires `node` and `yarn` to be installed as prerequisites, and provides live reload capability whilst editing. +Requires `node` and `yarn` to be installed as prerequisites. **Usage**: ``` -usage: qmk docs [-h] +usage: qmk docs [-h] [-b] [-p PORT] options: - -h, --help show this help message and exit + -h, --help show this help message and exit + -b, --browser Open the docs in the default browser. + -p, --port PORT Port number to use. ``` ## `qmk generate-docs` -This command allows you to generate QMK documentation locally. It can be uses for general browsing or improving the docs. -Use the `-s`/`--serve` flag to also serve the static site once built. Default port is 4173. +This command generates QMK documentation for production. +Use the `-s`/`--serve` flag to also serve the static site on port 4173 once built. Note that this does not provide live reloading; use `qmk docs` instead for development purposes. This command requires `node` and `yarn` to be installed as prerequisites, and requires the operating system to support symlinks. diff --git a/docs/contributing.md b/docs/contributing.md index bbb1997a6f..70a00b706d 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -106,10 +106,10 @@ enum my_keycodes { Before opening a pull request, you can preview your changes if you have set up the development environment by running this command from the `qmk_firmware/` folder: ``` -qmk docs +qmk docs -b ``` -and navigating to `http://localhost:5173/`. +Which should automatically open your browser; otherwise, navigate to `http://localhost:8936/`. ## Keyboards diff --git a/lib/python/qmk/cli/docs.py b/lib/python/qmk/cli/docs.py index d28dddf194..da02ebf95e 100644 --- a/lib/python/qmk/cli/docs.py +++ b/lib/python/qmk/cli/docs.py @@ -6,6 +6,8 @@ from qmk.docs import prepare_docs_build_area, run_docs_command from milc import cli +@cli.argument('-p', '--port', default=8936, type=int, help='Port number to use.') +@cli.argument('-b', '--browser', action='store_true', help='Open the docs in the default browser.') @cli.subcommand('Run a local webserver for QMK documentation.', hidden=False if cli.config.user.developer else True) def docs(cli): """Spin up a local HTTP server for the QMK docs. @@ -22,6 +24,7 @@ def docs(cli): if not prepare_docs_build_area(is_production=False): return False - if not cli.config.general.verbose: - cli.log.info('Serving docs at http://localhost:5173/ (Ctrl+C to stop)') - run_docs_command('run', 'docs:dev') + cmd = ['docs:dev', '--port', f'{cli.args.port}'] + if cli.args.browser: + cmd.append('--open') + run_docs_command('run', cmd) diff --git a/lib/python/qmk/cli/generate/docs.py b/lib/python/qmk/cli/generate/docs.py index 5821d43b86..7abeca9d2a 100644 --- a/lib/python/qmk/cli/generate/docs.py +++ b/lib/python/qmk/cli/generate/docs.py @@ -27,10 +27,8 @@ def generate_docs(cli): return False cli.log.info('Building vitepress docs') - run_docs_command('run', 'docs:build') + run_docs_command('run', ['docs:build']) cli.log.info('Successfully generated docs to %s.', BUILD_DOCS_PATH) if cli.args.serve: - if not cli.config.general.verbose: - cli.log.info('Serving docs at http://localhost:4173/ (Ctrl+C to stop)') - run_docs_command('run', 'docs:preview') + run_docs_command('run', ['docs:preview']) diff --git a/lib/python/qmk/docs.py b/lib/python/qmk/docs.py index 56694cf6ae..75d2d60bda 100644 --- a/lib/python/qmk/docs.py +++ b/lib/python/qmk/docs.py @@ -17,18 +17,18 @@ BUILD_DOCS_PATH = BUILD_PATH / 'docs' DOXYGEN_PATH = BUILD_DOCS_PATH / 'static' / 'doxygen' -def run_docs_command(verb, cmd=None): +def run_docs_command(verb, cmd_args=None): environ['PATH'] += pathsep + str(NODE_MODULES_PATH / '.bin') - args = {'capture_output': False if cli.config.general.verbose else True, 'check': True, 'stdin': DEVNULL} + args = {'capture_output': False, 'check': True} docs_env = environ.copy() if cli.config.general.verbose: docs_env['DEBUG'] = 'vitepress:*,vite:*' args['env'] = docs_env arg_list = ['yarn', verb] - if cmd: - arg_list.append(cmd) + if cmd_args: + arg_list.extend(cmd_args) chdir(BUILDDEFS_PATH) cli.run(arg_list, **args) -- cgit v1.2.3 From 1efc82403bebe759272d1ba7a79d9dfa0d5df506 Mon Sep 17 00:00:00 2001 From: Nick Brassel Date: Wed, 26 Feb 2025 22:25:41 +1100 Subject: Community modules (#24848) --- .github/labeler.yml | 4 + .github/workflows/format.yml | 1 + builddefs/build_keyboard.mk | 122 +++++++--- data/constants/keycodes/keycodes_0.0.7.hjson | 7 + data/constants/module_hooks/0.1.0.hjson | 25 ++ data/constants/module_hooks/1.0.0.hjson | 26 ++ data/schemas/community_module.jsonschema | 17 ++ data/schemas/keyboard.jsonschema | 17 +- data/schemas/keymap.jsonschema | 6 + docs/_sidebar.json | 1 + docs/custom_quantum_functions.md | 11 +- docs/features/community_modules.md | 142 +++++++++++ .../onekey/keymaps/community_module/keymap.c | 7 + .../onekey/keymaps/community_module/keymap.json | 3 + lib/python/qmk/cli/__init__.py | 1 + lib/python/qmk/cli/format/c.py | 2 +- lib/python/qmk/cli/format/json.py | 13 +- lib/python/qmk/cli/generate/community_modules.py | 263 +++++++++++++++++++++ lib/python/qmk/cli/generate/rules_mk.py | 46 +++- lib/python/qmk/cli/info.py | 5 + lib/python/qmk/commands.py | 5 +- lib/python/qmk/community_modules.py | 100 ++++++++ lib/python/qmk/info.py | 27 +++ lib/python/qmk/json_encoders.py | 28 +++ lib/python/qmk/keymap.py | 27 --- modules/qmk/hello_world/hello_world.c | 33 +++ modules/qmk/hello_world/introspection.c | 10 + modules/qmk/hello_world/introspection.h | 10 + modules/qmk/hello_world/qmk_module.json | 13 + modules/qmk/hello_world/rules.mk | 2 + quantum/action.h | 2 +- quantum/keyboard.c | 44 +++- quantum/keycodes.h | 3 + quantum/keymap_introspection.c | 11 + quantum/os_detection.c | 7 + quantum/quantum.c | 26 +- quantum/quantum.h | 4 + 37 files changed, 987 insertions(+), 84 deletions(-) create mode 100644 data/constants/keycodes/keycodes_0.0.7.hjson create mode 100644 data/constants/module_hooks/0.1.0.hjson create mode 100644 data/constants/module_hooks/1.0.0.hjson create mode 100644 data/schemas/community_module.jsonschema create mode 100644 docs/features/community_modules.md create mode 100644 keyboards/handwired/onekey/keymaps/community_module/keymap.c create mode 100644 keyboards/handwired/onekey/keymaps/community_module/keymap.json create mode 100644 lib/python/qmk/cli/generate/community_modules.py create mode 100644 lib/python/qmk/community_modules.py create mode 100644 modules/qmk/hello_world/hello_world.c create mode 100644 modules/qmk/hello_world/introspection.c create mode 100644 modules/qmk/hello_world/introspection.h create mode 100644 modules/qmk/hello_world/qmk_module.json create mode 100644 modules/qmk/hello_world/rules.mk (limited to 'lib/python/qmk/cli/generate') diff --git a/.github/labeler.yml b/.github/labeler.yml index 270cd1a813..82f9672bb7 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -54,3 +54,7 @@ dd: - data/constants/** - data/mappings/** - data/schemas/** +community_module: + - changed-files: + - any-glob-to-any-file: + - modules/** diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 74c518fe05..b4e32f981b 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -10,6 +10,7 @@ on: - 'lib/arm_atsam/**' - 'lib/lib8tion/**' - 'lib/python/**' + - 'modules/**' - 'platforms/**' - 'quantum/**' - 'tests/**' diff --git a/builddefs/build_keyboard.mk b/builddefs/build_keyboard.mk index 7d58c29462..c5fc9cd25d 100644 --- a/builddefs/build_keyboard.mk +++ b/builddefs/build_keyboard.mk @@ -112,6 +112,39 @@ endif ifneq ("$(wildcard $(KEYBOARD_PATH_1)/rules.mk)","") include $(KEYBOARD_PATH_1)/rules.mk endif +# Create dependencies on DD keyboard config - structure validated elsewhere +DD_CONFIG_FILES := +ifneq ("$(wildcard $(KEYBOARD_PATH_1)/info.json)","") + DD_CONFIG_FILES += $(KEYBOARD_PATH_1)/info.json +endif +ifneq ("$(wildcard $(KEYBOARD_PATH_2)/info.json)","") + DD_CONFIG_FILES += $(KEYBOARD_PATH_2)/info.json +endif +ifneq ("$(wildcard $(KEYBOARD_PATH_3)/info.json)","") + DD_CONFIG_FILES += $(KEYBOARD_PATH_3)/info.json +endif +ifneq ("$(wildcard $(KEYBOARD_PATH_4)/info.json)","") + DD_CONFIG_FILES += $(KEYBOARD_PATH_4)/info.json +endif +ifneq ("$(wildcard $(KEYBOARD_PATH_5)/info.json)","") + DD_CONFIG_FILES += $(KEYBOARD_PATH_5)/info.json +endif + +ifneq ("$(wildcard $(KEYBOARD_PATH_1)/keyboard.json)","") + DD_CONFIG_FILES += $(KEYBOARD_PATH_1)/keyboard.json +endif +ifneq ("$(wildcard $(KEYBOARD_PATH_2)/keyboard.json)","") + DD_CONFIG_FILES += $(KEYBOARD_PATH_2)/keyboard.json +endif +ifneq ("$(wildcard $(KEYBOARD_PATH_3)/keyboard.json)","") + DD_CONFIG_FILES += $(KEYBOARD_PATH_3)/keyboard.json +endif +ifneq ("$(wildcard $(KEYBOARD_PATH_4)/keyboard.json)","") + DD_CONFIG_FILES += $(KEYBOARD_PATH_4)/keyboard.json +endif +ifneq ("$(wildcard $(KEYBOARD_PATH_5)/keyboard.json)","") + DD_CONFIG_FILES += $(KEYBOARD_PATH_5)/keyboard.json +endif MAIN_KEYMAP_PATH_1 := $(KEYBOARD_PATH_1)/keymaps/$(KEYMAP) MAIN_KEYMAP_PATH_2 := $(KEYBOARD_PATH_2)/keymaps/$(KEYMAP) @@ -207,17 +240,17 @@ ifneq ("$(wildcard $(KEYMAP_JSON))", "") include $(INFO_RULES_MK) # Add rules to generate the keymap files - indentation here is important -$(INTERMEDIATE_OUTPUT)/src/keymap.c: $(KEYMAP_JSON) +$(INTERMEDIATE_OUTPUT)/src/keymap.c: $(KEYMAP_JSON) $(DD_CONFIG_FILES) @$(SILENT) || printf "$(MSG_GENERATING) $@" | $(AWK_CMD) $(eval CMD=$(QMK_BIN) json2c --quiet --output $(KEYMAP_C) $(KEYMAP_JSON)) @$(BUILD_CMD) -$(INTERMEDIATE_OUTPUT)/src/config.h: $(KEYMAP_JSON) +$(INTERMEDIATE_OUTPUT)/src/config.h: $(KEYMAP_JSON) $(DD_CONFIG_FILES) @$(SILENT) || printf "$(MSG_GENERATING) $@" | $(AWK_CMD) $(eval CMD=$(QMK_BIN) generate-config-h --quiet --output $(KEYMAP_H) $(KEYMAP_JSON)) @$(BUILD_CMD) -$(INTERMEDIATE_OUTPUT)/src/keymap.h: $(KEYMAP_JSON) +$(INTERMEDIATE_OUTPUT)/src/keymap.h: $(KEYMAP_JSON) $(DD_CONFIG_FILES) @$(SILENT) || printf "$(MSG_GENERATING) $@" | $(AWK_CMD) $(eval CMD=$(QMK_BIN) generate-keymap-h --quiet --output $(INTERMEDIATE_OUTPUT)/src/keymap.h $(KEYMAP_JSON)) @$(BUILD_CMD) @@ -226,6 +259,32 @@ generated-files: $(INTERMEDIATE_OUTPUT)/src/config.h $(INTERMEDIATE_OUTPUT)/src/ endif +# Community modules +$(INTERMEDIATE_OUTPUT)/src/community_modules.h: $(KEYMAP_JSON) $(DD_CONFIG_FILES) + @$(SILENT) || printf "$(MSG_GENERATING) $@" | $(AWK_CMD) + $(eval CMD=$(QMK_BIN) generate-community-modules-h -kb $(KEYBOARD) --quiet --output $(INTERMEDIATE_OUTPUT)/src/community_modules.h $(KEYMAP_JSON)) + @$(BUILD_CMD) + +$(INTERMEDIATE_OUTPUT)/src/community_modules.c: $(KEYMAP_JSON) $(DD_CONFIG_FILES) + @$(SILENT) || printf "$(MSG_GENERATING) $@" | $(AWK_CMD) + $(eval CMD=$(QMK_BIN) generate-community-modules-c -kb $(KEYBOARD) --quiet --output $(INTERMEDIATE_OUTPUT)/src/community_modules.c $(KEYMAP_JSON)) + @$(BUILD_CMD) + +$(INTERMEDIATE_OUTPUT)/src/community_modules_introspection.c: $(KEYMAP_JSON) $(DD_CONFIG_FILES) + @$(SILENT) || printf "$(MSG_GENERATING) $@" | $(AWK_CMD) + $(eval CMD=$(QMK_BIN) generate-community-modules-introspection-c -kb $(KEYBOARD) --quiet --output $(INTERMEDIATE_OUTPUT)/src/community_modules_introspection.c $(KEYMAP_JSON)) + @$(BUILD_CMD) + +$(INTERMEDIATE_OUTPUT)/src/community_modules_introspection.h: $(KEYMAP_JSON) $(DD_CONFIG_FILES) + @$(SILENT) || printf "$(MSG_GENERATING) $@" | $(AWK_CMD) + $(eval CMD=$(QMK_BIN) generate-community-modules-introspection-h -kb $(KEYBOARD) --quiet --output $(INTERMEDIATE_OUTPUT)/src/community_modules_introspection.h $(KEYMAP_JSON)) + @$(BUILD_CMD) + +SRC += $(INTERMEDIATE_OUTPUT)/src/community_modules.c + +generated-files: $(INTERMEDIATE_OUTPUT)/src/community_modules.h $(INTERMEDIATE_OUTPUT)/src/community_modules.c $(INTERMEDIATE_OUTPUT)/src/community_modules_introspection.c $(INTERMEDIATE_OUTPUT)/src/community_modules_introspection.h + + include $(BUILDDEFS_PATH)/converters.mk # Generate the board's version.h file. @@ -315,6 +374,14 @@ endif # Find all of the config.h files and add them to our CONFIG_H define. CONFIG_H := + +define config_h_community_module_appender + ifneq ("$(wildcard $(1)/config.h)","") + CONFIG_H += $(1)/config.h + endif +endef +$(foreach module,$(COMMUNITY_MODULE_PATHS),$(eval $(call config_h_community_module_appender,$(module)))) + ifneq ("$(wildcard $(KEYBOARD_PATH_5)/config.h)","") CONFIG_H += $(KEYBOARD_PATH_5)/config.h endif @@ -332,6 +399,14 @@ ifneq ("$(wildcard $(KEYBOARD_PATH_1)/config.h)","") endif POST_CONFIG_H := + +define post_config_h_community_module_appender + ifneq ("$(wildcard $(1)/post_config.h)","") + POST_CONFIG_H += $(1)/post_config.h + endif +endef +$(foreach module,$(COMMUNITY_MODULE_PATHS),$(eval $(call post_config_h_community_module_appender,$(module)))) + ifneq ("$(wildcard $(KEYBOARD_PATH_1)/post_config.h)","") POST_CONFIG_H += $(KEYBOARD_PATH_1)/post_config.h endif @@ -348,40 +423,6 @@ ifneq ("$(wildcard $(KEYBOARD_PATH_5)/post_config.h)","") POST_CONFIG_H += $(KEYBOARD_PATH_5)/post_config.h endif -# Create dependencies on DD keyboard config - structure validated elsewhere -DD_CONFIG_FILES := -ifneq ("$(wildcard $(KEYBOARD_PATH_1)/info.json)","") - DD_CONFIG_FILES += $(KEYBOARD_PATH_1)/info.json -endif -ifneq ("$(wildcard $(KEYBOARD_PATH_2)/info.json)","") - DD_CONFIG_FILES += $(KEYBOARD_PATH_2)/info.json -endif -ifneq ("$(wildcard $(KEYBOARD_PATH_3)/info.json)","") - DD_CONFIG_FILES += $(KEYBOARD_PATH_3)/info.json -endif -ifneq ("$(wildcard $(KEYBOARD_PATH_4)/info.json)","") - DD_CONFIG_FILES += $(KEYBOARD_PATH_4)/info.json -endif -ifneq ("$(wildcard $(KEYBOARD_PATH_5)/info.json)","") - DD_CONFIG_FILES += $(KEYBOARD_PATH_5)/info.json -endif - -ifneq ("$(wildcard $(KEYBOARD_PATH_1)/keyboard.json)","") - DD_CONFIG_FILES += $(KEYBOARD_PATH_1)/keyboard.json -endif -ifneq ("$(wildcard $(KEYBOARD_PATH_2)/keyboard.json)","") - DD_CONFIG_FILES += $(KEYBOARD_PATH_2)/keyboard.json -endif -ifneq ("$(wildcard $(KEYBOARD_PATH_3)/keyboard.json)","") - DD_CONFIG_FILES += $(KEYBOARD_PATH_3)/keyboard.json -endif -ifneq ("$(wildcard $(KEYBOARD_PATH_4)/keyboard.json)","") - DD_CONFIG_FILES += $(KEYBOARD_PATH_4)/keyboard.json -endif -ifneq ("$(wildcard $(KEYBOARD_PATH_5)/keyboard.json)","") - DD_CONFIG_FILES += $(KEYBOARD_PATH_5)/keyboard.json -endif - CONFIG_H += $(INTERMEDIATE_OUTPUT)/src/info_config.h KEYBOARD_SRC += $(INTERMEDIATE_OUTPUT)/src/default_keyboard.c @@ -462,6 +503,13 @@ ifneq ("$(wildcard $(KEYBOARD_PATH_5)/post_rules.mk)","") include $(KEYBOARD_PATH_5)/post_rules.mk endif +define post_rules_mk_community_module_includer + ifneq ("$(wildcard $(1)/post_rules.mk)","") + include $(1)/post_rules.mk + endif +endef +$(foreach module,$(COMMUNITY_MODULE_PATHS),$(eval $(call post_rules_mk_community_module_includer,$(module)))) + ifneq ("$(wildcard $(KEYMAP_PATH)/config.h)","") CONFIG_H += $(KEYMAP_PATH)/config.h endif diff --git a/data/constants/keycodes/keycodes_0.0.7.hjson b/data/constants/keycodes/keycodes_0.0.7.hjson new file mode 100644 index 0000000000..52e1a50443 --- /dev/null +++ b/data/constants/keycodes/keycodes_0.0.7.hjson @@ -0,0 +1,7 @@ +{ + "ranges": { + "0x77C0/0x003F": { + "define": "QK_COMMUNITY_MODULE" + } + } +} diff --git a/data/constants/module_hooks/0.1.0.hjson b/data/constants/module_hooks/0.1.0.hjson new file mode 100644 index 0000000000..c77f4c297e --- /dev/null +++ b/data/constants/module_hooks/0.1.0.hjson @@ -0,0 +1,25 @@ +{ + keyboard_pre_init: { + ret_type: void + args: void + } + keyboard_post_init: { + ret_type: void + args: void + } + pre_process_record: { + ret_type: bool + args: uint16_t keycode, keyrecord_t *record + call_params: keycode, record + } + process_record: { + ret_type: bool + args: uint16_t keycode, keyrecord_t *record + call_params: keycode, record + } + post_process_record: { + ret_type: void + args: uint16_t keycode, keyrecord_t *record + call_params: keycode, record + } +} diff --git a/data/constants/module_hooks/1.0.0.hjson b/data/constants/module_hooks/1.0.0.hjson new file mode 100644 index 0000000000..4e7bf30412 --- /dev/null +++ b/data/constants/module_hooks/1.0.0.hjson @@ -0,0 +1,26 @@ +{ + housekeeping_task: { + ret_type: void + args: void + } + suspend_power_down: { + ret_type: void + args: void + } + suspend_wakeup_init: { + ret_type: void + args: void + } + shutdown: { + ret_type: bool + args: bool jump_to_bootloader + call_params: jump_to_bootloader + } + process_detected_host_os: { + ret_type: bool + args: os_variant_t os + call_params: os + guard: defined(OS_DETECTION_ENABLE) + header: os_detection.h + } +} diff --git a/data/schemas/community_module.jsonschema b/data/schemas/community_module.jsonschema new file mode 100644 index 0000000000..a3474476df --- /dev/null +++ b/data/schemas/community_module.jsonschema @@ -0,0 +1,17 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema#", + "$id": "qmk.community_module.v1", + "title": "Community Module Information", + "type": "object", + "required": ["module_name", "maintainer"] + "properties": { + "module_name": {"$ref": "qmk.definitions.v1#/text_identifier"}, + "maintainer": {"$ref": "qmk.definitions.v1#/text_identifier"}, + "url": { + "type": "string", + "format": "uri" + }, + "keycodes": {"$ref": "qmk.definitions.v1#/keycode_decl_array"}, + "features": {"$ref": "qmk.keyboard.v1#/definitions/features_config"}, + } +} diff --git a/data/schemas/keyboard.jsonschema b/data/schemas/keyboard.jsonschema index 8b6cc7032b..9b63f62d45 100644 --- a/data/schemas/keyboard.jsonschema +++ b/data/schemas/keyboard.jsonschema @@ -31,6 +31,11 @@ "pins": {"$ref": "qmk.definitions.v1#/mcu_pin_array"} } } + "features_config": { + "$ref": "qmk.definitions.v1#/boolean_array", + "propertyNames": {"$ref": "qmk.definitions.v1#/snake_case"}, + "not": {"required": ["lto"]} + }, }, "type": "object", "not": {"required": ["vendorId", "productId"]}, // reject via keys... @@ -328,11 +333,7 @@ "enabled": {"type": "boolean"} } }, - "features": { - "$ref": "qmk.definitions.v1#/boolean_array", - "propertyNames": {"$ref": "qmk.definitions.v1#/snake_case"}, - "not": {"required": ["lto"]} - }, + "features": { "$ref": "#/definitions/features_config" }, "indicators": { "type": "object", "properties": { @@ -467,6 +468,12 @@ "rows": {"$ref": "qmk.definitions.v1#/mcu_pin_array"} } }, + "modules": { + "type": "array", + "items": { + "type": "string" + } + }, "mouse_key": { "type": "object", "properties": { diff --git a/data/schemas/keymap.jsonschema b/data/schemas/keymap.jsonschema index e967e45c53..b92a536c2c 100644 --- a/data/schemas/keymap.jsonschema +++ b/data/schemas/keymap.jsonschema @@ -71,6 +71,12 @@ "config": {"$ref": "qmk.keyboard.v1"}, "notes": { "type": "string" + }, + "modules": { + "type": "array", + "items": { + "type": "string" + } } } } diff --git a/docs/_sidebar.json b/docs/_sidebar.json index 95601be7de..0b5f297018 100644 --- a/docs/_sidebar.json +++ b/docs/_sidebar.json @@ -60,6 +60,7 @@ "items": [ { "text": "Customizing Functionality", "link": "/custom_quantum_functions" }, { "text": "Driver Installation with Zadig", "link": "/driver_installation_zadig" }, + { "text": "Community Modules", "link": "/features/community_modules" }, { "text": "Keymap Overview", "link": "/keymap" }, { "text": "Development Environments", diff --git a/docs/custom_quantum_functions.md b/docs/custom_quantum_functions.md index 1479eb53f6..c69beb055e 100644 --- a/docs/custom_quantum_functions.md +++ b/docs/custom_quantum_functions.md @@ -9,12 +9,19 @@ This page does not assume any special knowledge about QMK, but reading [Understa We have structured QMK as a hierarchy: * Core (`_quantum`) + * Community Module (`_`) + * Community Module -> Keyboard/Revision (`__kb`) + * Community Module -> Keymap (`__user`) * Keyboard/Revision (`_kb`) * Keymap (`_user`) Each of the functions described below can be defined with a `_kb()` suffix or a `_user()` suffix. We intend for you to use the `_kb()` suffix at the Keyboard/Revision level, while the `_user()` suffix should be used at the Keymap level. -When defining functions at the Keyboard/Revision level it is important that your `_kb()` implementation call `_user()` before executing anything else- otherwise the keymap level function will never be called. +When defining functions at the Keyboard/Revision level it is important that your `_kb()` implementation call `_user()` at an appropriate location, otherwise the keymap level function will never be called. + +Functions at the `__xxx()` level are intended to allow keyboards or keymaps to override or enhance the processing associated with a [community module](/features/community_modules). + +When defining module overrides such as `process_record_()`, the same pattern should be used; the module must invoke `process_record__kb()` as appropriate. # Custom Keycodes @@ -99,7 +106,7 @@ These are the three main initialization functions, listed in the order that they * `keyboard_post_init_*` - Happens at the end of the firmware's startup process. This is where you'd want to put "customization" code, for the most part. ::: warning -For most people, the `keyboard_post_init_user` function is what you want to call. For instance, this is where you want to set up things for RGB Underglow. +For most people, the `keyboard_post_init_user` function is what you want to implement. For instance, this is where you want to set up things for RGB Underglow. ::: ## Keyboard Pre Initialization code diff --git a/docs/features/community_modules.md b/docs/features/community_modules.md new file mode 100644 index 0000000000..3a1a82e7bc --- /dev/null +++ b/docs/features/community_modules.md @@ -0,0 +1,142 @@ +# Community Modules + +Community Modules are a feature within QMK which allows code to be implemented by third parties, making it available for other people to import into their own builds. + +These modules can provide implementations which override or enhance normal QMK processing; initialization, key processing, suspend, and shutdown are some of the provided hooks which modules may implement. + +## Adding a Community Module to your build + +Community Modules have first-class support for [External Userspace](/newbs_external_userspace), and QMK strongly recommends using External Userspace for hosting keymaps and Community Modules together. + +Modules must live in either of two locations: + +* `/modules/` +* `/modules/` + +A basic module is provided within QMK itself -- `qmk/hello_world` -- which prints out a notification over [HID console](/faq_debug) after 10 seconds, and adds a new keycode, `COMMUNITY_MODULE_HELLO` (aliased to `CM_HELO`) which types `Hello there.` to the active application when the corresponding key is pressed. + +To add this module to your build, in your keymap's directory create a `keymap.json` with the following content: + +```json +{ + "modules": [ + "qmk/hello_world" + ] +} +``` + +If you already have a `keymap.json`, you'll need to manually merge the `modules` section into your keymap. + +::: warning +Community Modules are not supported by QMK Configurator. If you wish to use Community Modules, you must build your own firmware. +::: + +## Adding a Community Module to your External Userspace + +Module authors are encouraged to provide a git repository on GitHub which may be imported into a user's external userspace. If a user wishes to import a module repository, they can do the following: + +```sh +cd /path/to/your/external/userspace +mkdir -p modules +# Replace the following {user} and {repo} with the author's community module repository +git submodule add https://github.com/{user}/{repo}.git modules/{user} +git submdule update --init --recursive +``` + +This will ensure the copy of the module is made in your userspace. + +Add a new entry into your `keymap.json` with the desired modules, replacing `{user}` and `{module_name}` as appropriate: + +```json +{ + "modules": [ + "qmk/hello_world", + "{user}/{module_name}" + ] +} +``` + +::: info +The module listed in `keymap.json` is the relative path within the `modules/` directory. So long as the module is present _somewhere_ under `modules/`, then the `keymap.json` can refer to that path. +::: + +## Writing a QMK Community Module + +As stated earlier, Community Module authors are strongly encouraged to provide their modules through git, allowing users to leverage submodules to import functionality. + +### `qmk_module.json` + +A Community Module is denoted by a `qmk_module.json` file such as the following: + +```json +{ + "module_name": "Hello World", + "maintainer": "QMK Maintainers", + "features": { + "deferred_exec": true + }, + "keycodes": [ + { + "key": "COMMUNITY_MODULE_HELLO", + "aliases": ["CM_HELO"] + } + ] +} +``` + +At minimum, the module must provide the `module_name` and `maintainer` fields. + +The use of `features` matches the definition normally provided within `keyboard.json` and `info.json`, allowing a module to signal to the build system that it has its own dependencies. In the example above, it enables the _deferred executor_ feature whenever the above module is used in a build. + +The `keycodes` array allows a module to provide new keycodes (as well as corresponding aliases) to a keymap. + +### `rules.mk` / `post_rules.mk` + +These two files follows standard QMK build system logic, allowing for `Makefile`-style customisation as if it were present in the keyboard or keymap. + +### `.c` + +This file will be automatically added to the build if the filename matches the directory name. For example, the `qmk/hello_world` module contains a `hello_world.c` file, which is automatically added to the build. + +::: info +Other files intended to be included must use the normal method of `SRC += my_file.c` inside `rules.mk`. +::: + +::: tip +This file should use `ASSERT_COMMUNITY_MODULES_MIN_API_VERSION(1,0,0);` to enforce a minimum version of the API that it requires, ensuring the Community Module is built with a compatible version of QMK. The list of APIs and corresponding version is given at the bottom of this document. Note the use of commas instead of periods. +::: + +### `introspection.c` / `introspection.h` + +These two files hook into the keymap introspection logic -- the header is prepended before the user keymap, and the C source file is appended after the user keymap. + +The header may provide definitions which are useful to the user's `keymap.c`. + +The source file may provide functions which allow access to information specified in the user's `keymap.c`. + +::: warning +Introspection is a relatively advanced topic within QMK, and existing patterns should be followed. If you need help please [open an issue](https://github.com/qmk/qmk_firmware/issues/new) or [chat with us on Discord](https://discord.gg/qmk). +::: + +### Compatible APIs + +Community Modules may provide specializations for the following APIs: + +| Base API | API Format | Example (`hello_world` module) | API Version | +|----------------------------|-------------------------------------|----------------------------------------|-------------| +| `keyboard_pre_init` | `keyboard_pre_init_` | `keyboard_pre_init_hello_world` | `0.1.0` | +| `keyboard_post_init` | `keyboard_post_init_` | `keyboard_post_init_hello_world` | `0.1.0` | +| `pre_process_record` | `pre_process_record_` | `pre_process_record_hello_world` | `0.1.0` | +| `process_record` | `process_record_` | `process_record_hello_world` | `0.1.0` | +| `post_process_record` | `post_process_record_` | `post_process_record_hello_world` | `0.1.0` | +| `housekeeping_task` | `housekeeping_task_` | `housekeeping_task_hello_world` | `1.0.0` | +| `suspend_power_down` | `suspend_power_down_` | `suspend_power_down_hello_world` | `1.0.0` | +| `suspend_wakeup_init` | `suspend_wakeup_init_` | `suspend_wakeup_init_hello_world` | `1.0.0` | +| `shutdown` | `shutdown_` | `shutdown_hello_world` | `1.0.0` | +| `process_detected_host_os` | `process_detected_host_os_` | `process_detected_host_os_hello_world` | `1.0.0` | + +::: info +An unspecified API is disregarded if a Community Module does not provide a specialization for it. +::: + +Each API has an equivalent `__kb()` and `__user()` hook, as per the normal QMK [`_quantum`, `_kb`, and `_user` functions](/custom_quantum_functions#a-word-on-core-vs-keyboards-vs-keymap). diff --git a/keyboards/handwired/onekey/keymaps/community_module/keymap.c b/keyboards/handwired/onekey/keymaps/community_module/keymap.c new file mode 100644 index 0000000000..5115a9fea0 --- /dev/null +++ b/keyboards/handwired/onekey/keymaps/community_module/keymap.c @@ -0,0 +1,7 @@ +// Copyright 2025 Nick Brassel (@tzarc) +// SPDX-License-Identifier: GPL-2.0-or-later +#include QMK_KEYBOARD_H + +const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = { + LAYOUT_ortho_1x1(CM_HELO) +}; diff --git a/keyboards/handwired/onekey/keymaps/community_module/keymap.json b/keyboards/handwired/onekey/keymaps/community_module/keymap.json new file mode 100644 index 0000000000..e3b4051b9e --- /dev/null +++ b/keyboards/handwired/onekey/keymaps/community_module/keymap.json @@ -0,0 +1,3 @@ +{ + "modules": ["qmk/hello_world"] +} diff --git a/lib/python/qmk/cli/__init__.py b/lib/python/qmk/cli/__init__.py index bb47ea26ed..3f2ba9ce3c 100644 --- a/lib/python/qmk/cli/__init__.py +++ b/lib/python/qmk/cli/__init__.py @@ -49,6 +49,7 @@ subcommands = [ 'qmk.cli.generate.api', 'qmk.cli.generate.autocorrect_data', 'qmk.cli.generate.compilation_database', + 'qmk.cli.generate.community_modules', 'qmk.cli.generate.config_h', 'qmk.cli.generate.develop_pr_list', 'qmk.cli.generate.dfu_header', diff --git a/lib/python/qmk/cli/format/c.py b/lib/python/qmk/cli/format/c.py index a58aef3fbc..65818155b0 100644 --- a/lib/python/qmk/cli/format/c.py +++ b/lib/python/qmk/cli/format/c.py @@ -10,7 +10,7 @@ from qmk.path import normpath from qmk.c_parse import c_source_files c_file_suffixes = ('c', 'h', 'cpp', 'hpp') -core_dirs = ('drivers', 'quantum', 'tests', 'tmk_core', 'platforms') +core_dirs = ('drivers', 'quantum', 'tests', 'tmk_core', 'platforms', 'modules') ignored = ('tmk_core/protocol/usb_hid', 'platforms/chibios/boards') diff --git a/lib/python/qmk/cli/format/json.py b/lib/python/qmk/cli/format/json.py index 3670294434..61f5254184 100755 --- a/lib/python/qmk/cli/format/json.py +++ b/lib/python/qmk/cli/format/json.py @@ -9,7 +9,7 @@ from milc import cli from qmk.info import info_json from qmk.json_schema import json_load, validate -from qmk.json_encoders import InfoJSONEncoder, KeymapJSONEncoder, UserspaceJSONEncoder +from qmk.json_encoders import InfoJSONEncoder, KeymapJSONEncoder, UserspaceJSONEncoder, CommunityModuleJSONEncoder from qmk.path import normpath @@ -30,6 +30,13 @@ def _detect_json_format(file, json_data): except ValidationError: pass + if json_encoder is None: + try: + validate(json_data, 'qmk.community_module.v1') + json_encoder = CommunityModuleJSONEncoder + except ValidationError: + pass + if json_encoder is None: try: validate(json_data, 'qmk.keyboard.v1') @@ -54,6 +61,8 @@ def _get_json_encoder(file, json_data): json_encoder = KeymapJSONEncoder elif cli.args.format == 'userspace': json_encoder = UserspaceJSONEncoder + elif cli.args.format == 'community_module': + json_encoder = CommunityModuleJSONEncoder else: # This should be impossible cli.log.error('Unknown format: %s', cli.args.format) @@ -61,7 +70,7 @@ def _get_json_encoder(file, json_data): @cli.argument('json_file', arg_only=True, type=normpath, help='JSON file to format') -@cli.argument('-f', '--format', choices=['auto', 'keyboard', 'keymap', 'userspace'], default='auto', arg_only=True, help='JSON formatter to use (Default: autodetect)') +@cli.argument('-f', '--format', choices=['auto', 'keyboard', 'keymap', 'userspace', 'community_module'], default='auto', arg_only=True, help='JSON formatter to use (Default: autodetect)') @cli.argument('-i', '--inplace', action='store_true', arg_only=True, help='If set, will operate in-place on the input file') @cli.argument('-p', '--print', action='store_true', arg_only=True, help='If set, will print the formatted json to stdout ') @cli.subcommand('Generate an info.json file for a keyboard.', hidden=False if cli.config.user.developer else True) diff --git a/lib/python/qmk/cli/generate/community_modules.py b/lib/python/qmk/cli/generate/community_modules.py new file mode 100644 index 0000000000..23678a2fb5 --- /dev/null +++ b/lib/python/qmk/cli/generate/community_modules.py @@ -0,0 +1,263 @@ +import contextlib +from argcomplete.completers import FilesCompleter +from pathlib import Path + +from milc import cli + +import qmk.path +from qmk.info import get_modules +from qmk.keyboard import keyboard_completer, keyboard_folder +from qmk.commands import dump_lines +from qmk.constants import GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE +from qmk.community_modules import module_api_list, load_module_jsons, find_module_path + + +@contextlib.contextmanager +def _render_api_guard(lines, api): + if api.guard: + lines.append(f'#if {api.guard}') + yield + if api.guard: + lines.append(f'#endif // {api.guard}') + + +def _render_api_header(api): + lines = [] + if api.header: + lines.append('') + with _render_api_guard(lines, api): + lines.append(f'#include <{api.header}>') + return lines + + +def _render_keycodes(module_jsons): + lines = [] + lines.append('') + lines.append('enum {') + first = True + for module_json in module_jsons: + module_name = Path(module_json['module']).name + keycodes = module_json.get('keycodes', []) + if len(keycodes) > 0: + lines.append(f' // From module: {module_name}') + for keycode in keycodes: + key = keycode.get('key', None) + if first: + lines.append(f' {key} = QK_COMMUNITY_MODULE,') + first = False + else: + lines.append(f' {key},') + for alias in keycode.get('aliases', []): + lines.append(f' {alias} = {key},') + lines.append('') + lines.append(' LAST_COMMUNITY_MODULE_KEY') + lines.append('};') + lines.append('_Static_assert((int)LAST_COMMUNITY_MODULE_KEY <= (int)(QK_COMMUNITY_MODULE_MAX+1), "Too many community module keycodes");') + return lines + + +def _render_api_declarations(api, module, user_kb=True): + lines = [] + lines.append('') + with _render_api_guard(lines, api): + if user_kb: + lines.append(f'{api.ret_type} {api.name}_{module}_user({api.args});') + lines.append(f'{api.ret_type} {api.name}_{module}_kb({api.args});') + lines.append(f'{api.ret_type} {api.name}_{module}({api.args});') + return lines + + +def _render_api_implementations(api, module): + module_name = Path(module).name + lines = [] + lines.append('') + with _render_api_guard(lines, api): + # _user + lines.append(f'__attribute__((weak)) {api.ret_type} {api.name}_{module_name}_user({api.args}) {{') + if api.ret_type == 'bool': + lines.append(' return true;') + else: + pass + lines.append('}') + lines.append('') + + # _kb + lines.append(f'__attribute__((weak)) {api.ret_type} {api.name}_{module_name}_kb({api.args}) {{') + if api.ret_type == 'bool': + lines.append(f' if(!{api.name}_{module_name}_user({api.call_params})) {{ return false; }}') + lines.append(' return true;') + else: + lines.append(f' {api.name}_{module_name}_user({api.call_params});') + lines.append('}') + lines.append('') + + # module (non-suffixed) + lines.append(f'__attribute__((weak)) {api.ret_type} {api.name}_{module_name}({api.args}) {{') + if api.ret_type == 'bool': + lines.append(f' if(!{api.name}_{module_name}_kb({api.call_params})) {{ return false; }}') + lines.append(' return true;') + else: + lines.append(f' {api.name}_{module_name}_kb({api.call_params});') + lines.append('}') + return lines + + +def _render_core_implementation(api, modules): + lines = [] + lines.append('') + with _render_api_guard(lines, api): + lines.append(f'{api.ret_type} {api.name}_modules({api.args}) {{') + if api.ret_type == 'bool': + lines.append(' return true') + for module in modules: + module_name = Path(module).name + if api.ret_type == 'bool': + lines.append(f' && {api.name}_{module_name}({api.call_params})') + else: + lines.append(f' {api.name}_{module_name}({api.call_params});') + if api.ret_type == 'bool': + lines.append(' ;') + lines.append('}') + return lines + + +@cli.argument('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to') +@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") +@cli.argument('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate community_modules.h for.') +@cli.argument('filename', nargs='?', type=qmk.path.FileType('r'), arg_only=True, completer=FilesCompleter('.json'), help='Configurator JSON file') +@cli.subcommand('Creates a community_modules.h from a keymap.json file.') +def generate_community_modules_h(cli): + """Creates a community_modules.h from a keymap.json file + """ + if cli.args.output and cli.args.output.name == '-': + cli.args.output = None + + api_list, api_version, ver_major, ver_minor, ver_patch = module_api_list() + + lines = [ + GPL2_HEADER_C_LIKE, + GENERATED_HEADER_C_LIKE, + '#pragma once', + '#include ', + '#include ', + '#include ', + '', + '#define COMMUNITY_MODULES_API_VERSION_BUILDER(ver_major,ver_minor,ver_patch) (((((uint32_t)(ver_major))&0xFF) << 24) | ((((uint32_t)(ver_minor))&0xFF) << 16) | (((uint32_t)(ver_patch))&0xFF))', + f'#define COMMUNITY_MODULES_API_VERSION COMMUNITY_MODULES_API_VERSION_BUILDER({ver_major},{ver_minor},{ver_patch})', + f'#define ASSERT_COMMUNITY_MODULES_MIN_API_VERSION(ver_major,ver_minor,ver_patch) _Static_assert(COMMUNITY_MODULES_API_VERSION_BUILDER(ver_major,ver_minor,ver_patch) <= COMMUNITY_MODULES_API_VERSION, "Community module requires a newer version of QMK modules API -- needs: " #ver_major "." #ver_minor "." #ver_patch ", current: {api_version}.")', + '', + 'typedef struct keyrecord_t keyrecord_t; // forward declaration so we don\'t need to include quantum.h', + '', + ] + + modules = get_modules(cli.args.keyboard, cli.args.filename) + module_jsons = load_module_jsons(modules) + if len(modules) > 0: + lines.extend(_render_keycodes(module_jsons)) + + for api in api_list: + lines.extend(_render_api_header(api)) + + for module in modules: + lines.append('') + lines.append(f'// From module: {module}') + for api in api_list: + lines.extend(_render_api_declarations(api, Path(module).name)) + lines.append('') + + lines.append('// Core wrapper') + for api in api_list: + lines.extend(_render_api_declarations(api, 'modules', user_kb=False)) + + dump_lines(cli.args.output, lines, cli.args.quiet, remove_repeated_newlines=True) + + +@cli.argument('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to') +@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") +@cli.argument('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate community_modules.c for.') +@cli.argument('filename', nargs='?', type=qmk.path.FileType('r'), arg_only=True, completer=FilesCompleter('.json'), help='Configurator JSON file') +@cli.subcommand('Creates a community_modules.c from a keymap.json file.') +def generate_community_modules_c(cli): + """Creates a community_modules.c from a keymap.json file + """ + if cli.args.output and cli.args.output.name == '-': + cli.args.output = None + + api_list, _, _, _, _ = module_api_list() + + lines = [ + GPL2_HEADER_C_LIKE, + GENERATED_HEADER_C_LIKE, + '', + '#include "community_modules.h"', + ] + + modules = get_modules(cli.args.keyboard, cli.args.filename) + if len(modules) > 0: + + for module in modules: + for api in api_list: + lines.extend(_render_api_implementations(api, Path(module).name)) + + for api in api_list: + lines.extend(_render_core_implementation(api, modules)) + + dump_lines(cli.args.output, lines, cli.args.quiet, remove_repeated_newlines=True) + + +@cli.argument('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to') +@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") +@cli.argument('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate community_modules.c for.') +@cli.argument('filename', nargs='?', type=qmk.path.FileType('r'), arg_only=True, completer=FilesCompleter('.json'), help='Configurator JSON file') +@cli.subcommand('Creates a community_modules_introspection.h from a keymap.json file.') +def generate_community_modules_introspection_h(cli): + """Creates a community_modules_introspection.h from a keymap.json file + """ + if cli.args.output and cli.args.output.name == '-': + cli.args.output = None + + lines = [ + GPL2_HEADER_C_LIKE, + GENERATED_HEADER_C_LIKE, + '', + ] + + modules = get_modules(cli.args.keyboard, cli.args.filename) + if len(modules) > 0: + for module in modules: + module_path = find_module_path(module) + lines.append(f'#if __has_include("{module_path}/introspection.h")') + lines.append(f'#include "{module_path}/introspection.h"') + lines.append(f'#endif // __has_include("{module_path}/introspection.h")') + lines.append('') + + dump_lines(cli.args.output, lines, cli.args.quiet, remove_repeated_newlines=True) + + +@cli.argument('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to') +@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") +@cli.argument('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate community_modules.c for.') +@cli.argument('filename', nargs='?', type=qmk.path.FileType('r'), arg_only=True, completer=FilesCompleter('.json'), help='Configurator JSON file') +@cli.subcommand('Creates a community_modules_introspection.c from a keymap.json file.') +def generate_community_modules_introspection_c(cli): + """Creates a community_modules_introspection.c from a keymap.json file + """ + if cli.args.output and cli.args.output.name == '-': + cli.args.output = None + + lines = [ + GPL2_HEADER_C_LIKE, + GENERATED_HEADER_C_LIKE, + '', + ] + + modules = get_modules(cli.args.keyboard, cli.args.filename) + if len(modules) > 0: + for module in modules: + module_path = find_module_path(module) + lines.append(f'#if __has_include("{module_path}/introspection.c")') + lines.append(f'#include "{module_path}/introspection.c"') + lines.append(f'#endif // __has_include("{module_path}/introspection.c")') + lines.append('') + + dump_lines(cli.args.output, lines, cli.args.quiet, remove_repeated_newlines=True) diff --git a/lib/python/qmk/cli/generate/rules_mk.py b/lib/python/qmk/cli/generate/rules_mk.py index 5291556109..cae9b07c3e 100755 --- a/lib/python/qmk/cli/generate/rules_mk.py +++ b/lib/python/qmk/cli/generate/rules_mk.py @@ -6,12 +6,13 @@ from dotty_dict import dotty from argcomplete.completers import FilesCompleter from milc import cli -from qmk.info import info_json +from qmk.info import info_json, get_modules from qmk.json_schema import json_load from qmk.keyboard import keyboard_completer, keyboard_folder from qmk.commands import dump_lines, parse_configurator_json from qmk.path import normpath, FileType from qmk.constants import GPL2_HEADER_SH_LIKE, GENERATED_HEADER_SH_LIKE +from qmk.community_modules import find_module_path, load_module_jsons def generate_rule(rules_key, rules_value): @@ -46,6 +47,42 @@ def process_mapping_rule(kb_info_json, rules_key, info_dict): return generate_rule(rules_key, rules_value) +def generate_features_rules(features_dict): + lines = [] + for feature, enabled in features_dict.items(): + feature = feature.upper() + enabled = 'yes' if enabled else 'no' + lines.append(generate_rule(f'{feature}_ENABLE', enabled)) + return lines + + +def generate_modules_rules(keyboard, filename): + lines = [] + modules = get_modules(keyboard, filename) + if len(modules) > 0: + lines.append('') + lines.append('OPT_DEFS += -DCOMMUNITY_MODULES_ENABLE=TRUE') + for module in modules: + module_path = find_module_path(module) + if not module_path: + raise FileNotFoundError(f"Module '{module}' not found.") + lines.append('') + lines.append(f'COMMUNITY_MODULES += {module_path.name}') # use module_path here instead of module as it may be a subdirectory + lines.append(f'OPT_DEFS += -DCOMMUNITY_MODULE_{module_path.name.upper()}_ENABLE=TRUE') + lines.append(f'COMMUNITY_MODULE_PATHS += {module_path}') + lines.append(f'VPATH += {module_path}') + lines.append(f'SRC += $(wildcard {module_path}/{module_path.name}.c)') + lines.append(f'-include {module_path}/rules.mk') + + module_jsons = load_module_jsons(modules) + for module_json in module_jsons: + if 'features' in module_json: + lines.append('') + lines.append(f'# Module: {module_json["module_name"]}') + lines.extend(generate_features_rules(module_json['features'])) + return lines + + @cli.argument('filename', nargs='?', arg_only=True, type=FileType('r'), completer=FilesCompleter('.json'), help='A configurator export JSON to be compiled and flashed or a pre-compiled binary firmware file (bin/hex) to be flashed.') @cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to') @cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") @@ -80,10 +117,7 @@ def generate_rules_mk(cli): # Iterate through features to enable/disable them if 'features' in kb_info_json: - for feature, enabled in kb_info_json['features'].items(): - feature = feature.upper() - enabled = 'yes' if enabled else 'no' - rules_mk_lines.append(generate_rule(f'{feature}_ENABLE', enabled)) + rules_mk_lines.extend(generate_features_rules(kb_info_json['features'])) # Set SPLIT_TRANSPORT, if needed if kb_info_json.get('split', {}).get('transport', {}).get('protocol') == 'custom': @@ -99,6 +133,8 @@ def generate_rules_mk(cli): if converter: rules_mk_lines.append(generate_rule('CONVERT_TO', converter)) + rules_mk_lines.extend(generate_modules_rules(cli.args.keyboard, cli.args.filename)) + # Show the results dump_lines(cli.args.output, rules_mk_lines) diff --git a/lib/python/qmk/cli/info.py b/lib/python/qmk/cli/info.py index e662407474..5925b57258 100755 --- a/lib/python/qmk/cli/info.py +++ b/lib/python/qmk/cli/info.py @@ -52,6 +52,11 @@ def show_keymap(kb_info_json, title_caps=True): if keymap_path and keymap_path.suffix == '.json': keymap_data = json.load(keymap_path.open(encoding='utf-8')) + + # cater for layout-less keymap.json + if 'layout' not in keymap_data: + return + layout_name = keymap_data['layout'] layout_name = kb_info_json.get('layout_aliases', {}).get(layout_name, layout_name) # Resolve alias names diff --git a/lib/python/qmk/commands.py b/lib/python/qmk/commands.py index a05b3641b5..0e1876ca7a 100644 --- a/lib/python/qmk/commands.py +++ b/lib/python/qmk/commands.py @@ -98,11 +98,14 @@ def in_virtualenv(): return active_prefix != sys.prefix -def dump_lines(output_file, lines, quiet=True): +def dump_lines(output_file, lines, quiet=True, remove_repeated_newlines=False): """Handle dumping to stdout or file Creates parent folders if required """ generated = '\n'.join(lines) + '\n' + if remove_repeated_newlines: + while '\n\n\n' in generated: + generated = generated.replace('\n\n\n', '\n\n') if output_file and output_file.name != '-': output_file.parent.mkdir(parents=True, exist_ok=True) if output_file.exists(): diff --git a/lib/python/qmk/community_modules.py b/lib/python/qmk/community_modules.py new file mode 100644 index 0000000000..f7e96a6b93 --- /dev/null +++ b/lib/python/qmk/community_modules.py @@ -0,0 +1,100 @@ +import os + +from pathlib import Path +from functools import lru_cache + +from milc.attrdict import AttrDict + +from qmk.json_schema import json_load, validate, merge_ordered_dicts +from qmk.util import truthy +from qmk.constants import QMK_FIRMWARE, QMK_USERSPACE, HAS_QMK_USERSPACE +from qmk.path import under_qmk_firmware, under_qmk_userspace + +COMMUNITY_MODULE_JSON_FILENAME = 'qmk_module.json' + + +class ModuleAPI(AttrDict): + def __init__(self, **kwargs): + super().__init__() + for key, value in kwargs.items(): + self[key] = value + + +@lru_cache(maxsize=1) +def module_api_list(): + module_definition_files = sorted(set(QMK_FIRMWARE.glob('data/constants/module_hooks/*.hjson'))) + module_definition_jsons = [json_load(f) for f in module_definition_files] + module_definitions = merge_ordered_dicts(module_definition_jsons) + latest_module_version = module_definition_files[-1].stem + latest_module_version_parts = latest_module_version.split('.') + + api_list = [] + for name, mod in module_definitions.items(): + api_list.append(ModuleAPI( + ret_type=mod['ret_type'], + name=name, + args=mod['args'], + call_params=mod.get('call_params', ''), + guard=mod.get('guard', None), + header=mod.get('header', None), + )) + + return api_list, latest_module_version, latest_module_version_parts[0], latest_module_version_parts[1], latest_module_version_parts[2] + + +def find_available_module_paths(): + """Find all available modules. + """ + search_dirs = [] + if HAS_QMK_USERSPACE: + search_dirs.append(QMK_USERSPACE / 'modules') + search_dirs.append(QMK_FIRMWARE / 'modules') + + modules = [] + for search_dir in search_dirs: + for module_json_path in search_dir.rglob(COMMUNITY_MODULE_JSON_FILENAME): + modules.append(module_json_path.parent) + return modules + + +def find_module_path(module): + """Find a module by name. + """ + for module_path in find_available_module_paths(): + # Ensure the module directory is under QMK Firmware or QMK Userspace + relative_path = under_qmk_firmware(module_path) + if not relative_path: + relative_path = under_qmk_userspace(module_path) + if not relative_path: + continue + + lhs = str(relative_path.as_posix())[len('modules/'):] + rhs = str(Path(module).as_posix()) + + if relative_path and lhs == rhs: + return module_path + return None + + +def load_module_json(module): + """Load a module JSON file. + """ + module_path = find_module_path(module) + if not module_path: + raise FileNotFoundError(f'Module not found: {module}') + + module_json = json_load(module_path / COMMUNITY_MODULE_JSON_FILENAME) + + if not truthy(os.environ.get('SKIP_SCHEMA_VALIDATION'), False): + validate(module_json, 'qmk.community_module.v1') + + module_json['module'] = module + module_json['module_path'] = module_path + + return module_json + + +def load_module_jsons(modules): + """Load the module JSON files, matching the specified order. + """ + return list(map(load_module_json, modules)) diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py index d70e7ee1b3..93eba7376a 100644 --- a/lib/python/qmk/info.py +++ b/lib/python/qmk/info.py @@ -1059,3 +1059,30 @@ def keymap_json(keyboard, keymap, force_layout=None): _extract_config_h(kb_info_json, parse_config_h_file(keymap_config)) return kb_info_json + + +def get_modules(keyboard, keymap_filename): + """Get the modules for a keyboard/keymap. + """ + modules = [] + + if keymap_filename: + keymap_json = parse_configurator_json(keymap_filename) + + if keymap_json: + kb = keymap_json.get('keyboard', None) + if not kb: + kb = keyboard + + if kb: + kb_info_json = info_json(kb) + if kb_info_json: + modules.extend(kb_info_json.get('modules', [])) + + modules.extend(keymap_json.get('modules', [])) + + elif keyboard: + kb_info_json = info_json(keyboard) + modules.extend(kb_info_json.get('modules', [])) + + return list(dict.fromkeys(modules)) # remove dupes diff --git a/lib/python/qmk/json_encoders.py b/lib/python/qmk/json_encoders.py index 0e4ad1d220..e83a381d52 100755 --- a/lib/python/qmk/json_encoders.py +++ b/lib/python/qmk/json_encoders.py @@ -235,3 +235,31 @@ class UserspaceJSONEncoder(QMKJSONEncoder): return '01build_targets' return key + + +class CommunityModuleJSONEncoder(QMKJSONEncoder): + """Custom encoder to make qmk_module.json's a little nicer to work with. + """ + def sort_dict(self, item): + """Sorts the hashes in a nice way. + """ + key = item[0] + + if self.indentation_level == 1: + if key == 'module_name': + return '00module_name' + if key == 'maintainer': + return '01maintainer' + if key == 'url': + return '02url' + if key == 'features': + return '03features' + if key == 'keycodes': + return '04keycodes' + elif self.indentation_level == 3: # keycodes + if key == 'key': + return '00key' + if key == 'aliases': + return '01aliases' + + return key diff --git a/lib/python/qmk/keymap.py b/lib/python/qmk/keymap.py index 9dd043c4a8..8e36461722 100644 --- a/lib/python/qmk/keymap.py +++ b/lib/python/qmk/keymap.py @@ -334,33 +334,6 @@ def write_json(keyboard, keymap, layout, layers, macros=None): return write_file(keymap_file, keymap_content) -def write(keymap_json): - """Generate the `keymap.c` and write it to disk. - - Returns the filename written to. - - `keymap_json` should be a dict with the following keys: - keyboard - The name of the keyboard - - keymap - The name of the keymap - - layout - The LAYOUT macro this keymap uses. - - layers - An array of arrays describing the keymap. Each item in the inner array should be a string that is a valid QMK keycode. - - macros - A list of macros for this keymap. - """ - keymap_content = generate_c(keymap_json) - keymap_file = qmk.path.keymaps(keymap_json['keyboard'])[0] / keymap_json['keymap'] / 'keymap.c' - - return write_file(keymap_file, keymap_content) - - def locate_keymap(keyboard, keymap, force_layout=None): """Returns the path to a keymap for a specific keyboard. """ diff --git a/modules/qmk/hello_world/hello_world.c b/modules/qmk/hello_world/hello_world.c new file mode 100644 index 0000000000..d9dd366100 --- /dev/null +++ b/modules/qmk/hello_world/hello_world.c @@ -0,0 +1,33 @@ +// Copyright 2025 Nick Brassel (@tzarc) +// SPDX-License-Identifier: GPL-2.0-or-later +#include QMK_KEYBOARD_H + +#include "introspection.h" + +ASSERT_COMMUNITY_MODULES_MIN_API_VERSION(1, 0, 0); + +uint32_t delayed_hello_world(uint32_t trigger_time, void *cb_arg) { + printf("Hello, world! I'm a QMK based keyboard! The keymap array size is %d bytes.\n", (int)hello_world_introspection().total_size); + return 0; +} + +void keyboard_post_init_hello_world(void) { + keyboard_post_init_hello_world_kb(); + defer_exec(10000, delayed_hello_world, NULL); +} + +bool process_record_hello_world(uint16_t keycode, keyrecord_t *record) { + if (!process_record_hello_world_kb(keycode, record)) { + return false; + } + + switch (keycode) { + case COMMUNITY_MODULE_HELLO: + if (record->event.pressed) { + SEND_STRING("Hello there."); + break; + } + } + + return true; +} diff --git a/modules/qmk/hello_world/introspection.c b/modules/qmk/hello_world/introspection.c new file mode 100644 index 0000000000..2c32a074f5 --- /dev/null +++ b/modules/qmk/hello_world/introspection.c @@ -0,0 +1,10 @@ +// Copyright 2025 Nick Brassel (@tzarc) +// SPDX-License-Identifier: GPL-2.0-or-later + +hello_world_introspection_t hello_world_introspection(void) { + hello_world_introspection_t introspection = { + .total_size = sizeof(keymaps), + .layer_count = sizeof(keymaps) / sizeof(keymaps[0]), + }; + return introspection; +} diff --git a/modules/qmk/hello_world/introspection.h b/modules/qmk/hello_world/introspection.h new file mode 100644 index 0000000000..fd3d7f24a0 --- /dev/null +++ b/modules/qmk/hello_world/introspection.h @@ -0,0 +1,10 @@ +// Copyright 2025 Nick Brassel (@tzarc) +// SPDX-License-Identifier: GPL-2.0-or-later +#include QMK_KEYBOARD_H + +typedef struct hello_world_introspection_t { + int16_t total_size; + int16_t layer_count; +} hello_world_introspection_t; + +hello_world_introspection_t hello_world_introspection(void); diff --git a/modules/qmk/hello_world/qmk_module.json b/modules/qmk/hello_world/qmk_module.json new file mode 100644 index 0000000000..4f269cb4e9 --- /dev/null +++ b/modules/qmk/hello_world/qmk_module.json @@ -0,0 +1,13 @@ +{ + "module_name": "Hello World", + "maintainer": "QMK Maintainers", + "features": { + "deferred_exec": true + }, + "keycodes": [ + { + "key": "COMMUNITY_MODULE_HELLO", + "aliases": ["CM_HELO"] + } + ] +} diff --git a/modules/qmk/hello_world/rules.mk b/modules/qmk/hello_world/rules.mk new file mode 100644 index 0000000000..91806fb1e3 --- /dev/null +++ b/modules/qmk/hello_world/rules.mk @@ -0,0 +1,2 @@ +# Just a simple rules.mk which tests that they work from a community module. +$(shell $(QMK_BIN) hello -n "from QMK's hello world community module") diff --git a/quantum/action.h b/quantum/action.h index d5b15c6f17..7596688f31 100644 --- a/quantum/action.h +++ b/quantum/action.h @@ -45,7 +45,7 @@ typedef struct { } tap_t; /* Key event container for recording */ -typedef struct { +typedef struct keyrecord_t { keyevent_t event; #ifndef NO_ACTION_TAPPING tap_t tap; diff --git a/quantum/keyboard.c b/quantum/keyboard.c index d7836cf36e..ad740de4b3 100644 --- a/quantum/keyboard.c +++ b/quantum/keyboard.c @@ -289,6 +289,21 @@ __attribute__((weak)) void keyboard_pre_init_kb(void) { keyboard_pre_init_user(); } +/** \brief keyboard_pre_init_modules + * + * FIXME: needs doc + */ +__attribute__((weak)) void keyboard_pre_init_modules(void) {} + +/** \brief keyboard_pre_init_quantum + * + * FIXME: needs doc + */ +void keyboard_pre_init_quantum(void) { + keyboard_pre_init_modules(); + keyboard_pre_init_kb(); +} + /** \brief keyboard_post_init_user * * FIXME: needs doc @@ -305,6 +320,23 @@ __attribute__((weak)) void keyboard_post_init_kb(void) { keyboard_post_init_user(); } +/** \brief keyboard_post_init_modules + * + * FIXME: needs doc + */ + +__attribute__((weak)) void keyboard_post_init_modules(void) {} + +/** \brief keyboard_post_init_quantum + * + * FIXME: needs doc + */ + +void keyboard_post_init_quantum(void) { + keyboard_post_init_modules(); + keyboard_post_init_kb(); +} + /** \brief matrix_can_read * * Allows overriding when matrix scanning operations should be executed. @@ -323,7 +355,7 @@ void keyboard_setup(void) { eeprom_driver_init(); #endif matrix_setup(); - keyboard_pre_init_kb(); + keyboard_pre_init_quantum(); } #ifndef SPLIT_KEYBOARD @@ -355,6 +387,13 @@ __attribute__((weak)) bool should_process_keypress(void) { return is_keyboard_master(); } +/** \brief housekeeping_task_modules + * + * Codegen will override this if community modules are enabled. + * This is specific to keyboard-level functionality. + */ +__attribute__((weak)) void housekeeping_task_modules(void) {} + /** \brief housekeeping_task_kb * * Override this function if you have a need to execute code for every keyboard main loop iteration. @@ -374,6 +413,7 @@ __attribute__((weak)) void housekeeping_task_user(void) {} * Invokes hooks for executing code after QMK is done after each loop iteration. */ void housekeeping_task(void) { + housekeeping_task_modules(); housekeeping_task_kb(); housekeeping_task_user(); } @@ -493,7 +533,7 @@ void keyboard_init(void) { debug_enable = true; #endif - keyboard_post_init_kb(); /* Always keep this last */ + keyboard_post_init_quantum(); /* Always keep this last */ } /** \brief key_event_task diff --git a/quantum/keycodes.h b/quantum/keycodes.h index 5929e35687..b4fc38f5ff 100644 --- a/quantum/keycodes.h +++ b/quantum/keycodes.h @@ -76,6 +76,8 @@ enum qk_keycode_ranges { QK_MACRO_MAX = 0x777F, QK_CONNECTION = 0x7780, QK_CONNECTION_MAX = 0x77BF, + QK_COMMUNITY_MODULE = 0x77C0, + QK_COMMUNITY_MODULE_MAX = 0x77FF, QK_LIGHTING = 0x7800, QK_LIGHTING_MAX = 0x78FF, QK_QUANTUM = 0x7C00, @@ -1476,6 +1478,7 @@ enum qk_keycode_defines { #define IS_QK_STENO(code) ((code) >= QK_STENO && (code) <= QK_STENO_MAX) #define IS_QK_MACRO(code) ((code) >= QK_MACRO && (code) <= QK_MACRO_MAX) #define IS_QK_CONNECTION(code) ((code) >= QK_CONNECTION && (code) <= QK_CONNECTION_MAX) +#define IS_QK_COMMUNITY_MODULE(code) ((code) >= QK_COMMUNITY_MODULE && (code) <= QK_COMMUNITY_MODULE_MAX) #define IS_QK_LIGHTING(code) ((code) >= QK_LIGHTING && (code) <= QK_LIGHTING_MAX) #define IS_QK_QUANTUM(code) ((code) >= QK_QUANTUM && (code) <= QK_QUANTUM_MAX) #define IS_QK_KB(code) ((code) >= QK_KB && (code) <= QK_KB_MAX) diff --git a/quantum/keymap_introspection.c b/quantum/keymap_introspection.c index 236b54ce98..23e842353a 100644 --- a/quantum/keymap_introspection.c +++ b/quantum/keymap_introspection.c @@ -1,6 +1,10 @@ // Copyright 2022 Nick Brassel (@tzarc) // SPDX-License-Identifier: GPL-2.0-or-later +#if defined(COMMUNITY_MODULES_ENABLE) +# include "community_modules_introspection.h" +#endif // defined(COMMUNITY_MODULES_ENABLE) + // Pull the actual keymap code so that we can inspect stuff from it #include KEYMAP_C @@ -171,3 +175,10 @@ __attribute__((weak)) const key_override_t* key_override_get(uint16_t key_overri } #endif // defined(KEY_OVERRIDE_ENABLE) + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Community modules (must be last in this file!) + +#if defined(COMMUNITY_MODULES_ENABLE) +# include "community_modules_introspection.c" +#endif // defined(COMMUNITY_MODULES_ENABLE) diff --git a/quantum/os_detection.c b/quantum/os_detection.c index 84bbeeed54..9a9f9052f2 100644 --- a/quantum/os_detection.c +++ b/quantum/os_detection.c @@ -72,6 +72,8 @@ static volatile struct usb_device_state maxprev_usb_device_state = {.configure_s static volatile bool debouncing = false; static volatile fast_timer_t last_time = 0; +bool process_detected_host_os_modules(os_variant_t os); + void os_detection_task(void) { #ifdef OS_DETECTION_KEYBOARD_RESET // resetting the keyboard on the USB device state change callback results in instability, so delegate that to this task @@ -96,12 +98,17 @@ void os_detection_task(void) { if (detected_os != reported_os || first_report) { first_report = false; reported_os = detected_os; + process_detected_host_os_modules(detected_os); process_detected_host_os_kb(detected_os); } } } } +__attribute__((weak)) bool process_detected_host_os_modules(os_variant_t os) { + return true; +} + __attribute__((weak)) bool process_detected_host_os_kb(os_variant_t detected_os) { return process_detected_host_os_user(detected_os); } diff --git a/quantum/quantum.c b/quantum/quantum.c index b63300add8..adb14d64b6 100644 --- a/quantum/quantum.c +++ b/quantum/quantum.c @@ -162,6 +162,10 @@ __attribute__((weak)) void tap_code16(uint16_t code) { tap_code16_delay(code, code == KC_CAPS_LOCK ? TAP_HOLD_CAPS_DELAY : TAP_CODE_DELAY); } +__attribute__((weak)) bool pre_process_record_modules(uint16_t keycode, keyrecord_t *record) { + return true; +} + __attribute__((weak)) bool pre_process_record_kb(uint16_t keycode, keyrecord_t *record) { return pre_process_record_user(keycode, record); } @@ -174,6 +178,10 @@ __attribute__((weak)) bool process_action_kb(keyrecord_t *record) { return true; } +__attribute__((weak)) bool process_record_modules(uint16_t keycode, keyrecord_t *record) { + return true; +} + __attribute__((weak)) bool process_record_kb(uint16_t keycode, keyrecord_t *record) { return process_record_user(keycode, record); } @@ -182,12 +190,22 @@ __attribute__((weak)) bool process_record_user(uint16_t keycode, keyrecord_t *re return true; } +__attribute__((weak)) void post_process_record_modules(uint16_t keycode, keyrecord_t *record) {} + __attribute__((weak)) void post_process_record_kb(uint16_t keycode, keyrecord_t *record) { post_process_record_user(keycode, record); } __attribute__((weak)) void post_process_record_user(uint16_t keycode, keyrecord_t *record) {} +__attribute__((weak)) bool shutdown_modules(bool jump_to_bootloader) { + return true; +} + +__attribute__((weak)) void suspend_power_down_modules(void) {} + +__attribute__((weak)) void suspend_wakeup_init_modules(void) {} + void shutdown_quantum(bool jump_to_bootloader) { clear_keyboard(); #if defined(MIDI_ENABLE) && defined(MIDI_BASIC) @@ -199,11 +217,13 @@ void shutdown_quantum(bool jump_to_bootloader) { # endif uint16_t timer_start = timer_read(); PLAY_SONG(goodbye_song); + shutdown_modules(jump_to_bootloader); shutdown_kb(jump_to_bootloader); while (timer_elapsed(timer_start) < 250) wait_ms(1); stop_all_notes(); #else + shutdown_modules(jump_to_bootloader); shutdown_kb(jump_to_bootloader); wait_ms(250); #endif @@ -258,7 +278,7 @@ uint16_t get_event_keycode(keyevent_t event, bool update_layer_cache) { /* Get keycode, and then process pre tapping functionality */ bool pre_process_record_quantum(keyrecord_t *record) { - return pre_process_record_kb(get_record_keycode(record, true), record) && + return pre_process_record_modules(get_record_keycode(record, true), record) && pre_process_record_kb(get_record_keycode(record, true), record) && #ifdef COMBO_ENABLE process_combo(get_record_keycode(record, true), record) && #endif @@ -268,6 +288,7 @@ bool pre_process_record_quantum(keyrecord_t *record) { /* Get keycode, and then call keyboard function */ void post_process_record_quantum(keyrecord_t *record) { uint16_t keycode = get_record_keycode(record, false); + post_process_record_modules(keycode, record); post_process_record_kb(keycode, record); } @@ -332,6 +353,7 @@ bool process_record_quantum(keyrecord_t *record) { #if defined(POINTING_DEVICE_ENABLE) && defined(POINTING_DEVICE_AUTO_MOUSE_ENABLE) process_auto_mouse(keycode, record) && #endif + process_record_modules(keycode, record) && // modules must run before kb process_record_kb(keycode, record) && #if defined(VIA_ENABLE) process_record_via(keycode, record) && @@ -526,6 +548,7 @@ __attribute__((weak)) bool shutdown_kb(bool jump_to_bootloader) { } void suspend_power_down_quantum(void) { + suspend_power_down_modules(); suspend_power_down_kb(); #ifndef NO_SUSPEND_POWER_DOWN // Turn off backlight @@ -593,6 +616,7 @@ __attribute__((weak)) void suspend_wakeup_init_quantum(void) { #if defined(RGB_MATRIX_ENABLE) rgb_matrix_set_suspend_state(false); #endif + suspend_wakeup_init_modules(); suspend_wakeup_init_kb(); } diff --git a/quantum/quantum.h b/quantum/quantum.h index 9db88a54d4..59a415ead4 100644 --- a/quantum/quantum.h +++ b/quantum/quantum.h @@ -244,6 +244,10 @@ extern layer_state_t layer_state; # include "layer_lock.h" #endif +#ifdef COMMUNITY_MODULES_ENABLE +# include "community_modules.h" +#endif + void set_single_default_layer(uint8_t default_layer); void set_single_persistent_default_layer(uint8_t default_layer); -- cgit v1.2.3