From 6e983f694300bc1ee526fb45b5b7b4ab04f23b4c Mon Sep 17 00:00:00 2001 From: Filipe Azevedo Date: Sun, 9 Nov 2025 23:10:38 +0000 Subject: [PATCH] add keybind remap --- config.in | 24 ++++++++++ include/sway/commands.h | 1 + include/sway/config.h | 12 +++++ sway/commands.c | 1 + sway/commands/remap.c | 101 ++++++++++++++++++++++++++++++++++++++++ sway/config.c | 15 ++++++ sway/input/keyboard.c | 78 +++++++++++++++++++++++++++---- sway/meson.build | 1 + 8 files changed, 223 insertions(+), 10 deletions(-) create mode 100644 sway/commands/remap.c diff --git a/config.in b/config.in index d71bc628b..8ab636d7f 100644 --- a/config.in +++ b/config.in @@ -239,3 +239,27 @@ bar { } include @sysconfdir@/sway/config.d/* + +# Basic clipboard operations (non-conflicting) +remap Super-c C-c +remap Super-x C-x +remap Super-z C-z +remap Super-Shift-z C-Shift-z + +remap Super-t C-t # New tab +remap Super-n C-n # New window +remap Super-o C-o # Open file +remap Super-q C-q # Quit + +# Tab navigation +remap Super-Shift-bracketleft C-Page_Up +remap Super-Shift-bracketleft C-Page_Down + + +# Terminal-specific +remap Super-c C-Shift-c for foot +remap Super-x C-Shift-x for foot + +remap Super-t C-Shift-t for com.mitchellh.ghostty +remap Super-n C-Shift-n for com.mitchellh.ghostty + diff --git a/include/sway/commands.h b/include/sway/commands.h index 389c382eb..d668b8f3c 100644 --- a/include/sway/commands.h +++ b/include/sway/commands.h @@ -168,6 +168,7 @@ sway_cmd cmd_popup_during_fullscreen; sway_cmd cmd_primary_selection; sway_cmd cmd_reject; sway_cmd cmd_reload; +sway_cmd cmd_remap; sway_cmd cmd_rename; sway_cmd cmd_resize; sway_cmd cmd_scratchpad; diff --git a/include/sway/config.h b/include/sway/config.h index 3c380933e..b501ff138 100644 --- a/include/sway/config.h +++ b/include/sway/config.h @@ -94,6 +94,17 @@ struct sway_gesture_binding { char *command; }; +/** + * A key remap rule for transforming key combinations + */ +struct sway_key_remap { + uint32_t from_modifiers; + xkb_keysym_t from_keysym; + uint32_t to_modifiers; + xkb_keysym_t to_keysym; + char *app_id; +}; + /** * Focus on window activation. */ @@ -498,6 +509,7 @@ struct sway_config { list_t *criteria; list_t *no_focus; list_t *active_bar_modifiers; + list_t *key_remaps; struct sway_mode *current_mode; struct bar_config *current_bar; uint32_t floating_mod; diff --git a/sway/commands.c b/sway/commands.c index c2c12ee65..33d78f469 100644 --- a/sway/commands.c +++ b/sway/commands.c @@ -81,6 +81,7 @@ static const struct cmd_handler handlers[] = { { "no_focus", cmd_no_focus }, { "output", cmd_output }, { "popup_during_fullscreen", cmd_popup_during_fullscreen }, + { "remap", cmd_remap }, { "seat", cmd_seat }, { "set", cmd_set }, { "show_marks", cmd_show_marks }, diff --git a/sway/commands/remap.c b/sway/commands/remap.c new file mode 100644 index 000000000..c79590515 --- /dev/null +++ b/sway/commands/remap.c @@ -0,0 +1,101 @@ +#include +#include +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/input/keyboard.h" +#include "list.h" +#include "log.h" +#include "stringop.h" + +// Parse a key combination string like "Super-Shift-c" into modifiers and keysym +static bool parse_key_combo(const char *combo_str, uint32_t *modifiers, xkb_keysym_t *keysym) { + *modifiers = 0; + *keysym = XKB_KEY_NoSymbol; + + list_t *split = split_string(combo_str, "-"); + if (!split || split->length == 0) { + list_free_items_and_destroy(split); + return false; + } + + // Parse all but last component as modifiers + for (int i = 0; i < split->length - 1; i++) { + char *mod_name = split->items[i]; + uint32_t mod = get_modifier_mask_by_name(mod_name); + if (mod == 0) { + // Special case for "C" and "Ctrl" shorthand + if (strcasecmp(mod_name, "C") == 0 || strcasecmp(mod_name, "Ctrl") == 0) { + mod = WLR_MODIFIER_CTRL; + } else { + list_free_items_and_destroy(split); + return false; + } + } + *modifiers |= mod; + } + + // Last component is the key + char *key_name = split->items[split->length - 1]; + *keysym = xkb_keysym_from_name(key_name, XKB_KEYSYM_CASE_INSENSITIVE); + + list_free_items_and_destroy(split); + + if (*keysym == XKB_KEY_NoSymbol) { + return false; + } + + return true; +} + +// Implements the 'remap' command +// Syntax: remap [for ] +// Example: remap Super-c C-c for firefox +struct cmd_results *cmd_remap(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "remap", EXPECTED_AT_LEAST, 2))) { + return error; + } + + const char *from_str = argv[0]; + const char *to_str = argv[1]; + const char *app_id = NULL; + + if (argc >= 4 && strcasecmp(argv[2], "for") == 0) { + app_id = argv[3]; + } + + struct sway_key_remap *remap = calloc(1, sizeof(struct sway_key_remap)); + if (!remap) { + return cmd_results_new(CMD_FAILURE, "Unable to allocate key remap"); + } + + if (!parse_key_combo(from_str, &remap->from_modifiers, &remap->from_keysym)) { + free(remap); + return cmd_results_new(CMD_FAILURE, "Invalid 'from' key combination: %s", from_str); + } + + if (!parse_key_combo(to_str, &remap->to_modifiers, &remap->to_keysym)) { + free(remap); + return cmd_results_new(CMD_FAILURE, "Invalid 'to' key combination: %s", to_str); + } + + if (app_id) { + remap->app_id = strdup(app_id); + // App-specific remaps go at the front (higher priority) + list_insert(config->key_remaps, 0, remap); + } else { + remap->app_id = NULL; + // Global remaps go at the back (lower priority) + list_add(config->key_remaps, remap); + } + + sway_log(SWAY_DEBUG, "Added key remap: 0x%x+0x%x -> 0x%x+0x%x%s%s", + remap->from_modifiers, remap->from_keysym, + remap->to_modifiers, remap->to_keysym, + app_id ? " for " : "", app_id ? app_id : ""); + + return cmd_results_new(CMD_SUCCESS, NULL); +} + + diff --git a/sway/config.c b/sway/config.c index d579022d3..ec288d258 100644 --- a/sway/config.c +++ b/sway/config.c @@ -100,6 +100,14 @@ static void free_mode(struct sway_mode *mode) { free(mode); } +static void free_key_remap(struct sway_key_remap *remap) { + if (!remap) { + return; + } + free(remap->app_id); + free(remap); +} + void free_config(struct sway_config *config) { if (!config) { return; @@ -166,6 +174,12 @@ void free_config(struct sway_config *config) { } list_free(config->criteria); } + if (config->key_remaps) { + for (int i = 0; i < config->key_remaps->length; i++) { + free_key_remap(config->key_remaps->items[i]); + } + list_free(config->key_remaps); + } list_free(config->no_focus); list_free(config->active_bar_modifiers); list_free_items_and_destroy(config->config_chain); @@ -228,6 +242,7 @@ static void config_defaults(struct sway_config *config) { if (!(config->input_configs = create_list())) goto cleanup; if (!(config->cmd_queue = create_list())) goto cleanup; + if (!(config->key_remaps = create_list())) goto cleanup; if (!(config->current_mode = malloc(sizeof(struct sway_mode)))) goto cleanup; diff --git a/sway/input/keyboard.c b/sway/input/keyboard.c index d18237375..8179af276 100644 --- a/sway/input/keyboard.c +++ b/sway/input/keyboard.c @@ -9,12 +9,15 @@ #include #include #include "sway/commands.h" +#include "sway/config.h" #include "sway/input/input-manager.h" #include "sway/input/keyboard.h" #include "sway/input/seat.h" #include "sway/input/cursor.h" #include "sway/ipc-server.h" #include "sway/server.h" +#include "sway/tree/container.h" +#include "sway/tree/view.h" #include "log.h" #if WLR_HAS_SESSION @@ -408,6 +411,63 @@ static void update_keyboard_state(struct sway_keyboard *keyboard, } } +static void send_key_with_remap_check(struct sway_keyboard *keyboard, struct sway_seat *seat, + struct wlr_seat *wlr_seat, uint32_t time_msec, uint32_t keycode, uint32_t state, + const xkb_keysym_t *keysyms, size_t keysyms_len, uint32_t modifiers) { + + // Check for remap on key press + if (state == WL_KEYBOARD_KEY_STATE_PRESSED && config->key_remaps && keysyms_len > 0) { + + // Get focused app for app-specific remaps + struct sway_container *focused = seat_get_focused_container(seat); + const char *focused_app_id = NULL; + if (focused && focused->view) { + focused_app_id = view_get_app_id(focused->view); + if (!focused_app_id) { + focused_app_id = view_get_class(focused->view); + } + } + + // Check each remap rule (app-specific are at front, so checked first) + for (int i = 0; i < config->key_remaps->length; i++) { + struct sway_key_remap *remap = config->key_remaps->items[i]; + + // Skip if app-specific and doesn't match focused app + if (remap->app_id && (!focused_app_id || !strstr(focused_app_id, remap->app_id))) { + continue; + } + + // Check if keysym and modifiers match + for (size_t j = 0; j < keysyms_len; j++) { + if (keysyms[j] == remap->from_keysym && modifiers == remap->from_modifiers) { + sway_log(SWAY_DEBUG, "Remap: 0x%x+0x%x -> 0x%x+0x%x%s%s", + remap->from_modifiers, remap->from_keysym, + remap->to_modifiers, remap->to_keysym, + remap->app_id ? " (app:" : "", remap->app_id ? remap->app_id : ""); + + struct wlr_keyboard_modifiers new_mods = keyboard->wlr->modifiers; + + // Remove "from" modifiers and add "to" modifiers + uint32_t new_mod_mask = modifiers; + new_mod_mask &= ~remap->from_modifiers; // Remove source mods + new_mod_mask |= remap->to_modifiers; // Add target mods + new_mods.depressed = new_mod_mask; + + // Send with remapped modifiers + wlr_seat_set_keyboard(wlr_seat, keyboard->wlr); + wlr_seat_keyboard_notify_modifiers(wlr_seat, &new_mods); + wlr_seat_keyboard_notify_key(wlr_seat, time_msec, keycode, state); + return; + } + } + } + } + + // No remap - send normally + wlr_seat_set_keyboard(wlr_seat, keyboard->wlr); + wlr_seat_keyboard_notify_key(wlr_seat, time_msec, keycode, state); +} + /** * Get keyboard grab of the seat from sway_keyboard if we should forward events * to it. @@ -545,17 +605,14 @@ static void handle_key_event(struct sway_keyboard *keyboard, event->state); } - if (event->state == WL_KEYBOARD_KEY_STATE_RELEASED) { - // If the pressed event was sent to a client and we have a focused - // surface immediately before this event, also send the released - // event. In particular, don't send the released event to the IM grab. + if (event->state == WL_KEYBOARD_KEY_STATE_RELEASED && !handled) { bool pressed_sent = update_shortcut_state( &keyboard->state_pressed_sent, event->keycode, event->state, keyinfo.keycode, 0); if (pressed_sent && seat->wlr_seat->keyboard_state.focused_surface) { - wlr_seat_set_keyboard(wlr_seat, keyboard->wlr); - wlr_seat_keyboard_notify_key(wlr_seat, event->time_msec, - event->keycode, event->state); + send_key_with_remap_check(keyboard, seat, wlr_seat, event->time_msec, + event->keycode, event->state, keyinfo.translated_keysyms, + keyinfo.translated_keysyms_len, keyinfo.translated_modifiers); handled = true; } } @@ -577,9 +634,10 @@ static void handle_key_event(struct sway_keyboard *keyboard, update_shortcut_state( &keyboard->state_pressed_sent, event->keycode, event->state, keyinfo.keycode, 0); - wlr_seat_set_keyboard(wlr_seat, keyboard->wlr); - wlr_seat_keyboard_notify_key(wlr_seat, event->time_msec, - event->keycode, event->state); + + send_key_with_remap_check(keyboard, seat, wlr_seat, event->time_msec, + event->keycode, event->state, keyinfo.translated_keysyms, + keyinfo.translated_keysyms_len, keyinfo.translated_modifiers); } free(device_identifier); diff --git a/sway/meson.build b/sway/meson.build index cb03a4d28..a0181a703 100644 --- a/sway/meson.build +++ b/sway/meson.build @@ -89,6 +89,7 @@ sway_sources = files( 'commands/popup_during_fullscreen.c', 'commands/primary_selection.c', 'commands/reload.c', + 'commands/remap.c', 'commands/rename.c', 'commands/resize.c', 'commands/scratchpad.c',