diff --git a/keymaps/custom/keymap.c b/keymaps/custom/keymap.c index c96e023..72f1a49 100644 --- a/keymaps/custom/keymap.c +++ b/keymaps/custom/keymap.c @@ -14,44 +14,104 @@ enum custom_keycodes { CK_NOREMAP, // hold to bypass all remaps }; +// --- Remap rules engine ------------------------------------------------- +// +// Each rule defines a single key remap: when the trigger key is pressed with +// exactly the specified modifiers, on a matching OS and focused app, the +// modifiers are swapped and (optionally) the keycode is changed. +// +// Rules are evaluated in order; the first match wins. + +// OS match bitmasks +#define OS_ANY 0xFF +#define OS_LINUX (1 << OS_MODE_LINUX) +#define OS_MAC (1 << OS_MODE_MAC) +#define OS_WIN (1 << OS_MODE_WINDOWS) + +// App match bitmasks +#define APP_ANY 0xFF +#define APP(a) (1 << (a)) +#define APP_NOT(a) (APP_ANY & ~APP(a)) + typedef struct { - uint8_t from; - uint8_t to; -} mod_rewrite_t; + uint8_t os_mask; // bitmask of OS modes that match + uint8_t app_mask; // bitmask of focused apps that match + uint8_t mods_from; // required modifiers (exact match after normalization) + uint16_t key_from; // trigger keycode + uint8_t mods_to; // replacement modifiers + uint16_t key_to; // output keycode (0 = same as trigger key) +} remap_rule_t; -static const mod_rewrite_t CTRL_TO_SUPER = { MOD_MASK_CTRL, MOD_MASK_GUI }; -static const mod_rewrite_t CTRL_SHIFT_TO_SUPER = { MOD_MASK_CTRL | MOD_MASK_SHIFT, MOD_MASK_GUI }; -static const mod_rewrite_t CTRL_SHIFT_TO_CMD_SHIFT = { MOD_MASK_CTRL | MOD_MASK_SHIFT, MOD_MASK_GUI | MOD_MASK_SHIFT }; -static const mod_rewrite_t CTRL_ALT_TO_CTRL = { MOD_MASK_CTRL | MOD_MASK_ALT, MOD_MASK_CTRL }; -static const mod_rewrite_t CTRL_TO_ALT = { MOD_MASK_CTRL, MOD_MASK_ALT }; -static const mod_rewrite_t ALT_TO_SUPER = { MOD_MASK_ALT, MOD_MASK_GUI }; -static const mod_rewrite_t SUPER_TO_SUPER = { MOD_MASK_GUI, MOD_MASK_GUI }; -static const mod_rewrite_t SUPER_TO_CTRL = { MOD_MASK_GUI, MOD_MASK_CTRL }; -static const mod_rewrite_t NONE_TO_SUPER = { 0, MOD_MASK_GUI }; -static const mod_rewrite_t NONE_TO_CTRL = { 0, MOD_MASK_CTRL }; +static const remap_rule_t remap_rules[] = { + // --- Copy/paste (Mac) --- + // Ctrl+Shift+C/V -> Cmd+C/V (terminal copy/paste — Ctrl+Shift is the trigger in terminals) + { OS_MAC, APP_ANY, MOD_MASK_CTRL | MOD_MASK_SHIFT, KC_C, MOD_MASK_GUI, 0 }, + { OS_MAC, APP_ANY, MOD_MASK_CTRL | MOD_MASK_SHIFT, KC_V, MOD_MASK_GUI, 0 }, + // Ctrl+C/V -> Cmd+C/V (not in terminal — let Ctrl+C = SIGINT through) + { OS_MAC, APP_NOT(APP_TERMINAL), MOD_MASK_CTRL, KC_C, MOD_MASK_GUI, 0 }, + { OS_MAC, APP_NOT(APP_TERMINAL), MOD_MASK_CTRL, KC_V, MOD_MASK_GUI, 0 }, + // --- Common Ctrl -> Cmd shortcuts (Mac) --- + { OS_MAC, APP_ANY, MOD_MASK_CTRL, KC_X, MOD_MASK_GUI, 0 }, // cut + { OS_MAC, APP_ANY, MOD_MASK_CTRL, KC_A, MOD_MASK_GUI, 0 }, // select all + { OS_MAC, APP_ANY, MOD_MASK_CTRL, KC_S, MOD_MASK_GUI, 0 }, // save + { OS_MAC, APP_ANY, MOD_MASK_CTRL, KC_F, MOD_MASK_GUI, 0 }, // find + { OS_MAC, APP_ANY, MOD_MASK_CTRL | MOD_MASK_SHIFT, KC_F, MOD_MASK_GUI | MOD_MASK_SHIFT, 0 }, // find in files + { OS_MAC, APP_ANY, MOD_MASK_CTRL, KC_W, MOD_MASK_GUI, 0 }, // close tab + { OS_MAC, APP_ANY, MOD_MASK_CTRL, KC_L, MOD_MASK_GUI, 0 }, // address bar + { OS_MAC, APP_ANY, MOD_MASK_CTRL, KC_R, MOD_MASK_GUI, 0 }, // reload + + // --- Undo/redo family: Ctrl -> Cmd, Ctrl+Shift -> Cmd+Shift (Mac) --- + { OS_MAC, APP_ANY, MOD_MASK_CTRL, KC_Z, MOD_MASK_GUI, 0 }, // undo + { OS_MAC, APP_ANY, MOD_MASK_CTRL | MOD_MASK_SHIFT, KC_Z, MOD_MASK_GUI | MOD_MASK_SHIFT, 0 }, // redo + { OS_MAC, APP_ANY, MOD_MASK_CTRL, KC_T, MOD_MASK_GUI, 0 }, // new tab + { OS_MAC, APP_ANY, MOD_MASK_CTRL | MOD_MASK_SHIFT, KC_T, MOD_MASK_GUI | MOD_MASK_SHIFT, 0 }, // reopen tab + { OS_MAC, APP_ANY, MOD_MASK_CTRL, KC_N, MOD_MASK_GUI, 0 }, // new window + { OS_MAC, APP_ANY, MOD_MASK_CTRL | MOD_MASK_SHIFT, KC_N, MOD_MASK_GUI | MOD_MASK_SHIFT, 0 }, // incognito + + // --- Arrow key modifier rewrites (Mac) --- + { OS_MAC, APP_ANY, MOD_MASK_CTRL | MOD_MASK_ALT, KC_LEFT, MOD_MASK_CTRL, 0 }, // switch workspace + { OS_MAC, APP_ANY, MOD_MASK_CTRL, KC_LEFT, MOD_MASK_ALT, 0 }, // word left + { OS_MAC, APP_ANY, MOD_MASK_CTRL | MOD_MASK_ALT, KC_RGHT, MOD_MASK_CTRL, 0 }, // switch workspace + { OS_MAC, APP_ANY, MOD_MASK_CTRL, KC_RGHT, MOD_MASK_ALT, 0 }, // word right + + // --- Delete word (Mac) --- + { OS_MAC, APP_ANY, MOD_MASK_CTRL, KC_BSPC, MOD_MASK_ALT, 0 }, + + // --- Home/End (Mac) --- + // Terminal: Home -> Ctrl+A, End -> Ctrl+E (must be listed before the general rules) + { OS_MAC, APP(APP_TERMINAL), 0, KC_HOME, MOD_MASK_CTRL, KC_A }, + { OS_MAC, APP(APP_TERMINAL), 0, KC_END, MOD_MASK_CTRL, KC_E }, + // General: Home -> Cmd+Left, End -> Cmd+Right + { OS_MAC, APP_ANY, 0, KC_HOME, MOD_MASK_GUI, KC_LEFT }, + { OS_MAC, APP_ANY, 0, KC_END, MOD_MASK_GUI, KC_RGHT }, + + // --- Alt+F4 -> Cmd+Q (Mac) --- + { OS_MAC, APP_ANY, MOD_MASK_ALT, KC_F4, MOD_MASK_GUI, KC_Q }, + + // --- Alt+Tab -> Cmd+Tab (Mac) --- + { OS_MAC, APP_ANY, MOD_MASK_ALT, KC_TAB, MOD_MASK_GUI, 0 }, + + // --- Workspace overview (all OS) --- + { OS_MAC, APP_ANY, MOD_MASK_GUI, KC_UP, MOD_MASK_CTRL, 0 }, // Mission Control + { OS_LINUX, APP_ANY, MOD_MASK_GUI, KC_UP, MOD_MASK_GUI, KC_W }, // KDE overview + { OS_WIN, APP_ANY, MOD_MASK_GUI, KC_UP, MOD_MASK_GUI, KC_TAB }, // Task View +}; + +// --- Modifier normalization ---------------------------------------------- // Collapse left/right mods into modifier types for comparison. // e.g. either LCTL (0x01) or RCTL (0x10) both become 0x01. static uint8_t normalize_mods(uint8_t mods) { return (mods | (mods >> 4)) & 0x0F; } -// Check if current mods exactly match a rule's `from` mods. -static bool mods_match(uint8_t mods, mod_rewrite_t rule) { - return normalize_mods(mods) == normalize_mods(rule.from); -} - -// Active rewrite tracking for hold-to-repeat support. +// --- Active rewrite tracking (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). +// register_code() keeps the key held for auto-repeat. 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 + 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 @@ -62,10 +122,6 @@ typedef struct { 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); @@ -73,14 +129,9 @@ static void teardown_active_rewrite(void) { 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. -// +// Intercept release events for active rewrites. +// 1. Rewritten key released -> clean up via teardown. +// 2. Original modifier released while key held -> remove from saved_mods. // 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; @@ -98,58 +149,37 @@ static bool process_rewrite_release(uint16_t keycode, keyrecord_t *record) { 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. -// -// 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) { +// Apply a rewrite unconditionally (matching already done by caller). +static void apply_rewrite(uint16_t trigger_keycode, uint8_t mods_from, uint8_t mods_to, uint16_t key_out) { uint8_t mods = get_mods(); - if (mods_match(mods, rule)) { - 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, + if (active_rw.trigger_keycode) teardown_active_rewrite(); + active_rw = (active_rewrite_t){ + .trigger_keycode = trigger_keycode, + .output_keycode = key_out, + .from_mods = normalize_mods(mods_from), + .to_mods = normalize_mods(mods_to), + .saved_mods = mods, + }; + set_mods(active_rw.to_mods); + register_code(key_out); +} - }; - set_mods(active_rw.to_mods); - register_code(keycode); +// Walk the rule table; first matching rule wins. +static bool apply_remap_rules(uint16_t keycode) { + uint8_t norm = normalize_mods(get_mods()); + for (uint8_t i = 0; i < sizeof(remap_rules) / sizeof(remap_rules[0]); i++) { + const remap_rule_t *r = &remap_rules[i]; + if (r->key_from != keycode) continue; + if (!((1 << os_mode) & r->os_mask)) continue; + if (!((1 << focused_app) & r->app_mask)) continue; + if (norm != normalize_mods(r->mods_from)) continue; + apply_rewrite(keycode, r->mods_from, r->mods_to, r->key_to ? r->key_to : keycode); return true; } return false; } -// 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)) { - 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; -} +// --- Keymaps ------------------------------------------------------------- const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = { @@ -259,8 +289,6 @@ bool process_record_user(uint16_t keycode, keyrecord_t *record) { } // Cycle RGB animation (FN+Backspace) - // Must update layer_rgb for all layers because system76_ec_rgb_layer() - // resets rgb_matrix_config from layer_rgb on every layer change. if (keycode == CK_RGBMOD) { if (record->event.pressed) { uint8_t mode = layer_rgb[0].mode + 1; @@ -290,130 +318,26 @@ bool process_record_user(uint16_t keycode, keyrecord_t *record) { super_tapped = false; } - switch (keycode) { - // Mac mode: tap Super -> Cmd+Space (Spotlight) - // Linux/Windows: tap Super passes through normally (KDE app menu) - case KC_LGUI: - if (os_mode == OS_MODE_MAC) { - if (record->event.pressed) { - super_tapped = true; - register_mods(MOD_BIT(KC_LGUI)); - } else { - if (super_tapped) { - tap_code(KC_SPC); // GUI still held, sends Super+Space - super_tapped = false; - } - unregister_mods(MOD_BIT(KC_LGUI)); - } - return false; + // Mac: tap Super -> Cmd+Space (Spotlight) + if (keycode == KC_LGUI && os_mode == OS_MODE_MAC) { + if (record->event.pressed) { + super_tapped = true; + register_mods(MOD_BIT(KC_LGUI)); + } else { + if (super_tapped) { + tap_code(KC_SPC); + super_tapped = false; } - break; - // Mac mode rewrites: - // Ctrl+C/V -> Super+C/V (copy/paste) - // Ctrl+Shift+C/V -> Super+C/V (terminal copy/paste) - case KC_C: - case KC_V: - if (os_mode == OS_MODE_MAC && record->event.pressed) { - if (rewrite_mods(keycode, CTRL_SHIFT_TO_SUPER)) return false; - // Terminal: plain Ctrl+C/V passes through (SIGINT, etc.) - if (focused_app != APP_TERMINAL) { - if (rewrite_mods(keycode, CTRL_TO_SUPER)) return false; - } - } - break; - // Mac mode rewrites: - // Ctrl+X/A/S/F -> Super+X/A/S/F (cut, select all, save, find) - // Ctrl+W/L/R -> Super+W/L/R (close tab, address bar, reload) - case KC_X: - case KC_A: - case KC_S: - case KC_F: - case KC_W: - case KC_L: - case KC_R: - if (os_mode == OS_MODE_MAC && record->event.pressed) { - if (rewrite_mods(keycode, CTRL_TO_SUPER)) return false; - } - break; - // Mac mode rewrites: - // Ctrl+Z/T/N -> Super+Z/T/N (undo, new tab, new window) - // Ctrl+Shift+Z/T/N -> Super+Shift+Z/T/N (redo, reopen tab, incognito) - case KC_Z: - case KC_T: - case KC_N: - if (os_mode == OS_MODE_MAC && record->event.pressed) { - if (rewrite_mods(keycode, CTRL_TO_SUPER)) return false; - if (rewrite_mods(keycode, CTRL_SHIFT_TO_CMD_SHIFT)) return false; - } - break; - // Mac mode rewrites: - // Ctrl+Alt+Left/Right -> Ctrl+Left/Right (switch workspace) - // Ctrl+Left/Right -> Alt+Left/Right (word navigation) - case KC_LEFT: - case KC_RGHT: - if (os_mode == OS_MODE_MAC && record->event.pressed) { - if (rewrite_mods(keycode, CTRL_ALT_TO_CTRL)) return false; - if (rewrite_mods(keycode, CTRL_TO_ALT)) return false; - } - break; - // Mac mode rewrites: - // Ctrl+Backspace -> Alt+Backspace (delete word backward) - case KC_BSPC: - if (os_mode == OS_MODE_MAC && record->event.pressed) { - if (rewrite_mods(keycode, CTRL_TO_ALT)) return false; - } - break; - // Mac mode rewrites: - // Home -> Super+Left (line start) - // End -> Super+Right (line end) - case KC_HOME: - 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(keycode, NONE_TO_CTRL, KC_A)) 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(keycode, NONE_TO_CTRL, KC_E)) 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(keycode, ALT_TO_SUPER, KC_Q)) return false; - } - break; - // Mac mode rewrites: - // Alt+Tab -> Super+Tab (app switcher) - case KC_TAB: - if (os_mode == OS_MODE_MAC && record->event.pressed) { - if (rewrite_mods(keycode, ALT_TO_SUPER)) return false; - } - break; - // Workspace overview: - // Super+Up -> Super+W (KDE overview, linux) - // Super+Up -> Ctrl+Up (Mission Control, mac) - // Super+Up -> Super+Tab (Task View, windows) - case KC_UP: - if (record->event.pressed) { - 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(keycode, SUPER_TO_SUPER, KC_W)) return false; - } else if (os_mode == OS_MODE_WINDOWS) { - if (rewrite_mods_and_key(keycode, SUPER_TO_SUPER, KC_TAB)) return false; - } - } - break; + unregister_mods(MOD_BIT(KC_LGUI)); + } + return false; } + + // Apply remap rules on key press + if (record->event.pressed) { + if (apply_remap_rules(keycode)) return false; + } + return true; } @@ -421,4 +345,3 @@ void matrix_init_user(void) { // Force dynamic keymap reset so PROGMEM keymaps always take effect dynamic_keymap_reset(); } -