Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/config_options.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,10 @@ If you define these options you will enable the associated feature, which may in
* Sets the key repeat interval for [key overrides](features/key_overrides).
* `#define LEGACY_MAGIC_HANDLING`
* Enables magic configuration handling for advanced keycodes (such as Mod Tap and Layer Tap)
* `#define PROGRESSIVE_KEYBOARD_REPORTS`
* Splits a single logical report change into ordered sub-reports so that modifier and key ordering is preserved on the host: keys that are no longer held are released first, then the new modifier byte is applied against the surviving keys, then the full report is sent. This guarantees that e.g. `Shift` is registered before the key it modifies (and that keys release before their modifiers), avoiding mis-shifted characters on fast or chorded input. When the define is absent, report emission is unchanged. Has no effect on `PROTOCOL_VUSB` boards, which always send a single report.
* `#define PROGRESSIVE_REPORT_DELAY 1`
* Optional. When `PROGRESSIVE_KEYBOARD_REPORTS` is enabled, sets the delay in milliseconds inserted between each sub-report. If not defined, no delay is added.


## RGB Light Configuration
Expand Down
71 changes: 71 additions & 0 deletions quantum/action_util.c
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "keycode_config.h"
#include <string.h>

#if defined(PROGRESSIVE_KEYBOARD_REPORTS) && defined(PROGRESSIVE_REPORT_DELAY)
# include "wait.h"
#endif // defined(PROGRESSIVE_KEYBOARD_REPORTS) && defined(PROGRESSIVE_REPORT_DELAY)

extern keymap_config_t keymap_config;

static uint8_t real_mods = 0;
Expand Down Expand Up @@ -306,6 +310,42 @@ void send_6kro_report(void) {
#else
static report_keyboard_t last_report;

# ifdef PROGRESSIVE_KEYBOARD_REPORTS
# ifdef KEYBOARD_SHARED_EP
last_report.report_id = keyboard_report->report_id;
# endif // KEYBOARD_SHARED_EP

/* Release any keys that are no longer held before applying the new mods. This only splits out
key releases (a slot cleared to 0); a slot that changes from one key to another in a single
scan is not split, as 6KRO reports keys positionally rather than as a bitmap. */
if (memcmp(keyboard_report->keys, last_report.keys, sizeof(keyboard_report->keys)) != 0) {
bool changed = false;
for (uint8_t i = 0; i < KEYBOARD_REPORT_KEYS; ++i) {
if (keyboard_report->keys[i] == 0) {
if (last_report.keys[i] != 0) {
last_report.keys[i] = 0;
changed = true;
}
}
}
if (changed) {
host_keyboard_send(&last_report);
# ifdef PROGRESSIVE_REPORT_DELAY
wait_ms(PROGRESSIVE_REPORT_DELAY);
# endif // PROGRESSIVE_REPORT_DELAY
}
}

/* Send the new mods alongside the keys that are still held. */
if (keyboard_report->mods != last_report.mods) {
last_report.mods = keyboard_report->mods;
host_keyboard_send(&last_report);
# ifdef PROGRESSIVE_REPORT_DELAY
wait_ms(PROGRESSIVE_REPORT_DELAY);
# endif // PROGRESSIVE_REPORT_DELAY
}
# endif // PROGRESSIVE_KEYBOARD_REPORTS

/* Only send the report if there are changes to propagate to the host. */
if (memcmp(keyboard_report, &last_report, sizeof(report_keyboard_t)) != 0) {
memcpy(&last_report, keyboard_report, sizeof(report_keyboard_t));
Expand All @@ -320,6 +360,37 @@ void send_nkro_report(void) {

static report_nkro_t last_report;

# ifdef PROGRESSIVE_KEYBOARD_REPORTS
last_report.report_id = nkro_report->report_id;

/* Remove existing keys that aren't in the intended report. */
if (memcmp(nkro_report->bits, last_report.bits, sizeof(nkro_report->bits)) != 0) {
bool changed = false;
for (uint8_t i = 0; i < NKRO_REPORT_BITS; ++i) {
uint8_t orig = last_report.bits[i];
last_report.bits[i] &= nkro_report->bits[i];
if (last_report.bits[i] != orig) {
changed = true;
}
}
if (changed) {
host_nkro_send(&last_report);
# ifdef PROGRESSIVE_REPORT_DELAY
wait_ms(PROGRESSIVE_REPORT_DELAY);
# endif // PROGRESSIVE_REPORT_DELAY
}
}

/* Send the new mods with the intersecting set of keys */
if (nkro_report->mods != last_report.mods) {
last_report.mods = nkro_report->mods;
host_nkro_send(&last_report);
# ifdef PROGRESSIVE_REPORT_DELAY
wait_ms(PROGRESSIVE_REPORT_DELAY);
# endif // PROGRESSIVE_REPORT_DELAY
}
# endif // PROGRESSIVE_KEYBOARD_REPORTS

/* Only send the report if there are changes to propagate to the host. */
if (memcmp(nkro_report, &last_report, sizeof(report_nkro_t)) != 0) {
memcpy(&last_report, nkro_report, sizeof(report_nkro_t));
Expand Down
21 changes: 21 additions & 0 deletions tests/progressive_keyboard_reports/config.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/* Copyright 2026 QMK
*
* 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 <http://www.gnu.org/licenses/>.
*/

#pragma once

#include "test_common.h"

#define PROGRESSIVE_KEYBOARD_REPORTS
18 changes: 18 additions & 0 deletions tests/progressive_keyboard_reports/test.mk
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Copyright 2026 QMK
#
# 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 <http://www.gnu.org/licenses/>.

# --------------------------------------------------------------------------------
# Keep this file, even if it is empty, as a marker that this folder contains tests
# --------------------------------------------------------------------------------
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/* Copyright 2026 QMK
*
* 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 <http://www.gnu.org/licenses/>.
*/

#include "action_util.h"
#include "keyboard_report_util.hpp"
#include "test_common.hpp"

using testing::_;
using testing::AnyNumber;
using testing::InSequence;

// These tests drive the QMK core report builders directly (add_mods/add_key/... then a single
// send_keyboard_report()) so that a modifier and a key change land in one report transition,
// which is what exercises the progressive reporting. The calls are qualified with `::` because the
// test body is a TestFixture member, and TestFixture::add_key(KeymapKey) would otherwise shadow
// the global core function ::add_key(uint8_t). The mod helpers have no such collision, but are
// qualified too for symmetry and to make clear these are core functions, not fixture helpers.

class ProgressiveKeyboardReports : public TestFixture {};

// On press, a single logical change that adds both a modifier and a key must be
// split so the modifier reaches the host first, guaranteeing the key is shifted.
TEST_F(ProgressiveKeyboardReports, ModifierIsReportedBeforeKeyOnPress) {
TestDriver driver;
InSequence s;

EXPECT_REPORT(driver, (KC_LEFT_SHIFT));
EXPECT_REPORT(driver, (KC_LEFT_SHIFT, KC_A));

::add_mods(MOD_BIT(KC_LEFT_SHIFT));
::add_key(KC_A);
send_keyboard_report();

VERIFY_AND_CLEAR(driver);
}

// On release, the same change in reverse must clear the key while the modifier
// is still held, then clear the modifier, so the key never registers unshifted.
TEST_F(ProgressiveKeyboardReports, KeyIsReleasedBeforeModifierOnRelease) {
TestDriver driver;

/* Establish the held state: Shift + A. */
EXPECT_ANY_REPORT(driver).Times(AnyNumber());
::add_mods(MOD_BIT(KC_LEFT_SHIFT));
::add_key(KC_A);
send_keyboard_report();
VERIFY_AND_CLEAR(driver);

InSequence s;

EXPECT_REPORT(driver, (KC_LEFT_SHIFT));
EXPECT_EMPTY_REPORT(driver);

::del_mods(MOD_BIT(KC_LEFT_SHIFT));
::del_key(KC_A);
send_keyboard_report();

VERIFY_AND_CLEAR(driver);
}

// With no modifier change, a plain key press still collapses to a single report.
TEST_F(ProgressiveKeyboardReports, PlainKeyPressSendsSingleReport) {
TestDriver driver;

EXPECT_REPORT(driver, (KC_A)).Times(1);

::add_key(KC_A);
send_keyboard_report();

VERIFY_AND_CLEAR(driver);
}