Support hold-to-repeat for rewritten modifier combos
Replace tap_code() with register_code()/unregister_code() in the modifier rewrite system so that holding a rewritten key combo (e.g. Ctrl+Left → Alt+Left on Mac) produces auto-repeat instead of a single tap. Track active rewrites to correctly restore modifiers on release, handling edge cases like the modifier being released before the trigger key. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -41,28 +41,111 @@ static bool mods_match(uint8_t mods, mod_rewrite_t rule) {
|
|||||||
return normalize_mods(mods) == normalize_mods(rule.from);
|
return normalize_mods(mods) == normalize_mods(rule.from);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rewrite modifier combos: swap exact `from` mod types to `to` mods, tap keycode, restore.
|
// Active rewrite tracking for hold-to-repeat support.
|
||||||
|
//
|
||||||
|
// The old tap_code() approach sent an immediate press+release, so holding
|
||||||
|
// e.g. Ctrl+Left (remapped to Alt+Left on Mac) produced a single word-jump
|
||||||
|
// instead of repeating. Now rewrite_mods()/rewrite_mods_and_key() use
|
||||||
|
// register_code() to keep the key held, and this struct tracks the state
|
||||||
|
// needed to clean up correctly on release.
|
||||||
|
//
|
||||||
|
// Only one rewrite can be active at a time (only one key auto-repeats).
|
||||||
|
typedef struct {
|
||||||
|
uint16_t trigger_keycode; // physical key (e.g. KC_LEFT) — 0 when inactive
|
||||||
|
uint8_t output_keycode; // key registered with host (may differ from trigger)
|
||||||
|
uint8_t from_mods; // normalized mods that were removed
|
||||||
|
uint8_t to_mods; // normalized mods that were added
|
||||||
|
uint8_t saved_mods; // exact get_mods() at press time, for restoration
|
||||||
|
// updated as from-mods are released during hold
|
||||||
|
} active_rewrite_t;
|
||||||
|
|
||||||
|
static active_rewrite_t active_rw = {0};
|
||||||
|
|
||||||
|
// Undo an active rewrite: restore saved modifiers and unregister the held key.
|
||||||
|
// saved_mods starts as the exact get_mods() at press time, but is updated as
|
||||||
|
// individual from-mods are released during the hold, so it always reflects
|
||||||
|
// what should actually be active after teardown. No phantom mods.
|
||||||
|
// Mods are restored before unregister_code so only one USB report is sent.
|
||||||
|
static void teardown_active_rewrite(void) {
|
||||||
|
if (!active_rw.trigger_keycode) return;
|
||||||
|
set_mods(active_rw.saved_mods);
|
||||||
|
unregister_code(active_rw.output_keycode);
|
||||||
|
active_rw.trigger_keycode = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intercept release events for active rewrites. Called at the top of
|
||||||
|
// process_record_user (after OS_MODE_NONE/no_remap bail-outs).
|
||||||
|
//
|
||||||
|
// Handles two cases:
|
||||||
|
// 1. The rewritten key itself is released — clean up via teardown_active_rewrite().
|
||||||
|
// 2. The original modifier is released while the key is still held —
|
||||||
|
// remove it from saved_mods so teardown won't restore a phantom modifier.
|
||||||
|
//
|
||||||
|
// Returns false to consume the event, true to continue normal processing.
|
||||||
|
static bool process_rewrite_release(uint16_t keycode, keyrecord_t *record) {
|
||||||
|
if (!active_rw.trigger_keycode) return true;
|
||||||
|
if (!record->event.pressed) {
|
||||||
|
if (keycode == active_rw.trigger_keycode) {
|
||||||
|
teardown_active_rewrite();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (IS_MODIFIER_KEYCODE(keycode) &&
|
||||||
|
(normalize_mods(MOD_BIT(keycode)) & active_rw.from_mods)) {
|
||||||
|
active_rw.saved_mods &= ~MOD_BIT(keycode);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rewrite modifier combos: swap exact `from` mods to `to` mods and hold keycode.
|
||||||
// Left/right variants of the same modifier are treated as equivalent.
|
// Left/right variants of the same modifier are treated as equivalent.
|
||||||
// Returns true if rewrite occurred.
|
//
|
||||||
|
// Uses register_code() instead of tap_code() so the key stays held for
|
||||||
|
// auto-repeat. The key is unregistered later by process_rewrite_release()
|
||||||
|
// when the physical key is released. If another rewrite is already active,
|
||||||
|
// it is cleaned up first (only one key can auto-repeat at a time).
|
||||||
|
//
|
||||||
|
// Returns true if a rewrite was applied (caller should return false to
|
||||||
|
// suppress the original keycode).
|
||||||
static bool rewrite_mods(uint16_t keycode, mod_rewrite_t rule) {
|
static bool rewrite_mods(uint16_t keycode, mod_rewrite_t rule) {
|
||||||
uint8_t mods = get_mods();
|
uint8_t mods = get_mods();
|
||||||
if (mods_match(mods, rule)) {
|
if (mods_match(mods, rule)) {
|
||||||
set_mods(normalize_mods(rule.to));
|
if (active_rw.trigger_keycode) teardown_active_rewrite();
|
||||||
tap_code(keycode);
|
active_rw = (active_rewrite_t){
|
||||||
set_mods(mods);
|
.trigger_keycode = keycode,
|
||||||
|
.output_keycode = keycode,
|
||||||
|
.from_mods = normalize_mods(rule.from),
|
||||||
|
.to_mods = normalize_mods(rule.to),
|
||||||
|
.saved_mods = mods,
|
||||||
|
|
||||||
|
};
|
||||||
|
set_mods(active_rw.to_mods);
|
||||||
|
register_code(keycode);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rewrite modifier combo AND change the keycode sent.
|
// Like rewrite_mods(), but also changes the keycode sent to the host.
|
||||||
// Returns true if rewrite occurred.
|
// trigger_keycode is the physical key pressed (used to match the release
|
||||||
static bool rewrite_mods_and_key(mod_rewrite_t rule, uint16_t keycode_out) {
|
// event later), while keycode_out is what the host actually receives.
|
||||||
|
// e.g. Home (trigger) -> Cmd+Left (output): trigger_keycode=KC_HOME,
|
||||||
|
// keycode_out=KC_LEFT, rule=NONE_TO_SUPER.
|
||||||
|
static bool rewrite_mods_and_key(uint16_t trigger_keycode, mod_rewrite_t rule, uint16_t keycode_out) {
|
||||||
uint8_t mods = get_mods();
|
uint8_t mods = get_mods();
|
||||||
if (mods_match(mods, rule)) {
|
if (mods_match(mods, rule)) {
|
||||||
set_mods(normalize_mods(rule.to));
|
if (active_rw.trigger_keycode) teardown_active_rewrite();
|
||||||
tap_code(keycode_out);
|
active_rw = (active_rewrite_t){
|
||||||
set_mods(mods);
|
.trigger_keycode = trigger_keycode,
|
||||||
|
.output_keycode = keycode_out,
|
||||||
|
.from_mods = normalize_mods(rule.from),
|
||||||
|
.to_mods = normalize_mods(rule.to),
|
||||||
|
.saved_mods = mods,
|
||||||
|
|
||||||
|
};
|
||||||
|
set_mods(active_rw.to_mods);
|
||||||
|
register_code(keycode_out);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -155,9 +238,13 @@ static bool super_tapped = false;
|
|||||||
static bool no_remap = false;
|
static bool no_remap = false;
|
||||||
|
|
||||||
bool process_record_user(uint16_t keycode, keyrecord_t *record) {
|
bool process_record_user(uint16_t keycode, keyrecord_t *record) {
|
||||||
|
// Safety: no rewrites in none mode
|
||||||
|
if (os_mode == OS_MODE_NONE) return true;
|
||||||
|
|
||||||
// Cycle OS mode (FN+Del)
|
// Cycle OS mode (FN+Del)
|
||||||
if (keycode == CK_OSMODE) {
|
if (keycode == CK_OSMODE) {
|
||||||
if (record->event.pressed) {
|
if (record->event.pressed) {
|
||||||
|
teardown_active_rewrite();
|
||||||
os_mode = (os_mode + 1) % 4;
|
os_mode = (os_mode + 1) % 4;
|
||||||
os_mode_manual = true;
|
os_mode_manual = true;
|
||||||
rgb_matrix_mode_noeeprom(RGB_MATRIX_SOLID_COLOR);
|
rgb_matrix_mode_noeeprom(RGB_MATRIX_SOLID_COLOR);
|
||||||
@@ -188,11 +275,15 @@ bool process_record_user(uint16_t keycode, keyrecord_t *record) {
|
|||||||
// Hold CK_NOREMAP to bypass all remaps
|
// Hold CK_NOREMAP to bypass all remaps
|
||||||
if (keycode == CK_NOREMAP) {
|
if (keycode == CK_NOREMAP) {
|
||||||
no_remap = record->event.pressed;
|
no_remap = record->event.pressed;
|
||||||
|
if (no_remap) teardown_active_rewrite();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// No rewrites in none mode or when noremap held
|
// No rewrites when noremap held
|
||||||
if (os_mode == OS_MODE_NONE || no_remap) return true;
|
if (no_remap) return true;
|
||||||
|
|
||||||
|
// Handle release events for active modifier rewrites (key repeat support)
|
||||||
|
if (!process_rewrite_release(keycode, record)) return false;
|
||||||
|
|
||||||
// Any other key pressed while GUI held means it's being used as a modifier
|
// Any other key pressed while GUI held means it's being used as a modifier
|
||||||
if (record->event.pressed && keycode != KC_LGUI) {
|
if (record->event.pressed && keycode != KC_LGUI) {
|
||||||
@@ -279,25 +370,25 @@ bool process_record_user(uint16_t keycode, keyrecord_t *record) {
|
|||||||
if (os_mode == OS_MODE_MAC && record->event.pressed) {
|
if (os_mode == OS_MODE_MAC && record->event.pressed) {
|
||||||
// Terminal: Home -> Ctrl+A (line start)
|
// Terminal: Home -> Ctrl+A (line start)
|
||||||
if (focused_app == APP_TERMINAL) {
|
if (focused_app == APP_TERMINAL) {
|
||||||
if (rewrite_mods_and_key(NONE_TO_CTRL, KC_A)) return false;
|
if (rewrite_mods_and_key(keycode, NONE_TO_CTRL, KC_A)) return false;
|
||||||
}
|
}
|
||||||
if (rewrite_mods_and_key(NONE_TO_SUPER, KC_LEFT)) return false;
|
if (rewrite_mods_and_key(keycode, NONE_TO_SUPER, KC_LEFT)) return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case KC_END:
|
case KC_END:
|
||||||
if (os_mode == OS_MODE_MAC && record->event.pressed) {
|
if (os_mode == OS_MODE_MAC && record->event.pressed) {
|
||||||
// Terminal: End -> Ctrl+E (line end)
|
// Terminal: End -> Ctrl+E (line end)
|
||||||
if (focused_app == APP_TERMINAL) {
|
if (focused_app == APP_TERMINAL) {
|
||||||
if (rewrite_mods_and_key(NONE_TO_CTRL, KC_E)) return false;
|
if (rewrite_mods_and_key(keycode, NONE_TO_CTRL, KC_E)) return false;
|
||||||
}
|
}
|
||||||
if (rewrite_mods_and_key(NONE_TO_SUPER, KC_RGHT)) return false;
|
if (rewrite_mods_and_key(keycode, NONE_TO_SUPER, KC_RGHT)) return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
// Mac mode rewrites:
|
// Mac mode rewrites:
|
||||||
// Alt+F4 -> Super+Q (quit app)
|
// Alt+F4 -> Super+Q (quit app)
|
||||||
case KC_F4:
|
case KC_F4:
|
||||||
if (os_mode == OS_MODE_MAC && record->event.pressed) {
|
if (os_mode == OS_MODE_MAC && record->event.pressed) {
|
||||||
if (rewrite_mods_and_key(ALT_TO_SUPER, KC_Q)) return false;
|
if (rewrite_mods_and_key(keycode, ALT_TO_SUPER, KC_Q)) return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
// Mac mode rewrites:
|
// Mac mode rewrites:
|
||||||
@@ -316,9 +407,9 @@ bool process_record_user(uint16_t keycode, keyrecord_t *record) {
|
|||||||
if (os_mode == OS_MODE_MAC) {
|
if (os_mode == OS_MODE_MAC) {
|
||||||
if (rewrite_mods(keycode, SUPER_TO_CTRL)) return false;
|
if (rewrite_mods(keycode, SUPER_TO_CTRL)) return false;
|
||||||
} else if (os_mode == OS_MODE_LINUX) {
|
} else if (os_mode == OS_MODE_LINUX) {
|
||||||
if (rewrite_mods_and_key(SUPER_TO_SUPER, KC_W)) return false;
|
if (rewrite_mods_and_key(keycode, SUPER_TO_SUPER, KC_W)) return false;
|
||||||
} else if (os_mode == OS_MODE_WINDOWS) {
|
} else if (os_mode == OS_MODE_WINDOWS) {
|
||||||
if (rewrite_mods_and_key(SUPER_TO_SUPER, KC_TAB)) return false;
|
if (rewrite_mods_and_key(keycode, SUPER_TO_SUPER, KC_TAB)) return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
Reference in New Issue
Block a user