This commit is contained in:
Filipe Azevedo 2026-01-29 11:02:27 +11:00 committed by GitHub
commit 8565f1832a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 247 additions and 22 deletions

View file

@ -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-bracketright 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

View file

@ -98,6 +98,16 @@ void container_resize_tiled(struct sway_container *parent, uint32_t axis,
struct sway_container *container_find_resize_parent(struct sway_container *con,
uint32_t edge);
// Keysym to keycode translation (used by bind.c and keyboard.c)
struct keycode_matches {
xkb_keysym_t keysym;
xkb_keycode_t keycode;
int count;
};
void find_keycode(struct xkb_keymap *keymap, xkb_keycode_t keycode, void *data);
struct keycode_matches get_keycode_for_keysym(xkb_keysym_t keysym);
/**
* Handlers shared by exec and exec_always.
*/
@ -168,6 +178,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;

View file

@ -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.
*/
@ -504,6 +515,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;

View file

@ -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 },

View file

@ -647,21 +647,11 @@ void seat_execute_command(struct sway_seat *seat, struct sway_binding *binding)
transaction_commit_dirty();
}
/**
* The last found keycode associated with the keysym
* and the total count of matches.
*/
struct keycode_matches {
xkb_keysym_t keysym;
xkb_keycode_t keycode;
int count;
};
/**
* Iterate through keycodes in the keymap to find ones matching
* the specified keysym.
*/
static void find_keycode(struct xkb_keymap *keymap,
void find_keycode(struct xkb_keymap *keymap,
xkb_keycode_t keycode, void *data) {
xkb_keysym_t keysym = xkb_state_key_get_one_sym(
config->keysym_translation_state, keycode);
@ -680,7 +670,7 @@ static void find_keycode(struct xkb_keymap *keymap,
/**
* Return the keycode for the specified keysym.
*/
static struct keycode_matches get_keycode_for_keysym(xkb_keysym_t keysym) {
struct keycode_matches get_keycode_for_keysym(xkb_keysym_t keysym) {
struct keycode_matches matches = {
.keysym = keysym,
.keycode = XKB_KEYCODE_INVALID,

101
sway/commands/remap.c Normal file
View file

@ -0,0 +1,101 @@
#include <string.h>
#include <strings.h>
#include <xkbcommon/xkbcommon.h>
#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 <from-combo> <to-combo> [for <app_id>]
// 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);
}

View file

@ -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;

View file

@ -9,12 +9,15 @@
#include <wlr/types/wlr_keyboard_group.h>
#include <xkbcommon/xkbcommon-names.h>
#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,75 @@ 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 : "");
// Build remapped modifiers
struct wlr_keyboard_modifiers new_mods = keyboard->wlr->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;
// Find the keycode for the target keysym using existing bind.c logic
struct keycode_matches matches = get_keycode_for_keysym(remap->to_keysym);
if (matches.count != 1) {
sway_log(SWAY_ERROR, "Failed to find unique keycode for remapped keysym 0x%x (found %d matches)",
remap->to_keysym, matches.count);
return;
}
// XKB keycodes are offset by 8 from evdev keycodes
uint32_t target_keycode = matches.keycode - 8;
// Send with remapped modifiers AND keycode
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, target_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 +617,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.raw_keysyms,
keyinfo.raw_keysyms_len, keyinfo.raw_modifiers);
handled = true;
}
}
@ -577,9 +646,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.raw_keysyms,
keyinfo.raw_keysyms_len, keyinfo.raw_modifiers);
}
free(device_identifier);

View file

@ -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',