From e3fa6244a779f06fa14a4c4015de2eff647a8301 Mon Sep 17 00:00:00 2001 From: Zuckerberg Date: Tue, 24 Mar 2026 07:19:51 -0700 Subject: [PATCH] Support hold-to-repeat for rewritten modifier combos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- keymaps/custom/keymap.c | 131 ++++++++++++++++++++++++++++++++++------ 1 file changed, 111 insertions(+), 20 deletions(-) diff --git a/keymaps/custom/keymap.c b/keymaps/custom/keymap.c index 4fdcc2f..c96e023 100644 --- a/keymaps/custom/keymap.c +++ b/keymaps/custom/keymap.c @@ -41,28 +41,111 @@ static bool mods_match(uint8_t mods, mod_rewrite_t rule) { 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. -// 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) { uint8_t mods = get_mods(); if (mods_match(mods, rule)) { - set_mods(normalize_mods(rule.to)); - tap_code(keycode); - set_mods(mods); + if (active_rw.trigger_keycode) teardown_active_rewrite(); + active_rw = (active_rewrite_t){ + .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 false; } -// Rewrite modifier combo AND change the keycode sent. -// Returns true if rewrite occurred. -static bool rewrite_mods_and_key(mod_rewrite_t rule, uint16_t keycode_out) { +// Like rewrite_mods(), but also changes the keycode sent to the host. +// trigger_keycode is the physical key pressed (used to match the release +// 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(); if (mods_match(mods, rule)) { - set_mods(normalize_mods(rule.to)); - tap_code(keycode_out); - set_mods(mods); + if (active_rw.trigger_keycode) teardown_active_rewrite(); + active_rw = (active_rewrite_t){ + .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 false; @@ -155,9 +238,13 @@ static bool super_tapped = false; static bool no_remap = false; 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) if (keycode == CK_OSMODE) { if (record->event.pressed) { + teardown_active_rewrite(); os_mode = (os_mode + 1) % 4; os_mode_manual = true; 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 if (keycode == CK_NOREMAP) { no_remap = record->event.pressed; + if (no_remap) teardown_active_rewrite(); return false; } - // No rewrites in none mode or when noremap held - if (os_mode == OS_MODE_NONE || no_remap) return true; + // No rewrites when noremap held + 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 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) { // Terminal: Home -> Ctrl+A (line start) 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; case KC_END: if (os_mode == OS_MODE_MAC && record->event.pressed) { // Terminal: End -> Ctrl+E (line end) 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; // Mac mode rewrites: // Alt+F4 -> Super+Q (quit app) case KC_F4: 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; // Mac mode rewrites: @@ -316,9 +407,9 @@ bool process_record_user(uint16_t keycode, keyrecord_t *record) { if (os_mode == OS_MODE_MAC) { if (rewrite_mods(keycode, SUPER_TO_CTRL)) return false; } 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) { - 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;