diff --git a/include/sway/config.h b/include/sway/config.h index eecdde3aa..405092e3f 100644 --- a/include/sway/config.h +++ b/include/sway/config.h @@ -46,7 +46,8 @@ struct sway_mouse_binding { */ struct sway_mode { char *name; - list_t *bindings; + list_t *keysym_bindings; + list_t *keycode_bindings; }; /** diff --git a/include/sway/input/keyboard.h b/include/sway/input/keyboard.h index d9251f4c3..8ec3eb356 100644 --- a/include/sway/input/keyboard.h +++ b/include/sway/input/keyboard.h @@ -3,6 +3,8 @@ #include "sway/input/seat.h" +#define SWAY_KEYBOARD_PRESSED_KEYSYMS_CAP 32 + struct sway_keyboard { struct sway_seat_device *seat_device; @@ -10,6 +12,12 @@ struct sway_keyboard { struct wl_listener keyboard_key; struct wl_listener keyboard_modifiers; + + xkb_keysym_t pressed_keysyms_translated[SWAY_KEYBOARD_PRESSED_KEYSYMS_CAP]; + uint32_t modifiers_translated; + + xkb_keysym_t pressed_keysyms_raw[SWAY_KEYBOARD_PRESSED_KEYSYMS_CAP]; + uint32_t modifiers_raw; }; struct sway_keyboard *sway_keyboard_create(struct sway_seat *seat, diff --git a/sway/commands.c b/sway/commands.c index 34afb6a0f..b0078a468 100644 --- a/sway/commands.c +++ b/sway/commands.c @@ -127,6 +127,8 @@ struct cmd_results *add_color(const char *name, char *buffer, const char *color) /* Keep alphabetized */ static struct cmd_handler handlers[] = { + { "bindcode", cmd_bindcode }, + { "bindsym", cmd_bindsym }, { "exec", cmd_exec }, { "exec_always", cmd_exec_always }, { "exit", cmd_exit }, diff --git a/sway/commands/bind.c b/sway/commands/bind.c new file mode 100644 index 000000000..791214041 --- /dev/null +++ b/sway/commands/bind.c @@ -0,0 +1,245 @@ +#ifdef __linux__ +#include +#elif __FreeBSD__ +#include +#endif +#include +#include +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "list.h" +#include "log.h" +#include "stringop.h" +#include "util.h" + +int binding_order = 0; + +void free_sway_binding(struct sway_binding *binding) { + if (!binding) { + return; + } + + if (binding->keys) { + free_flat_list(binding->keys); + } + free(binding->command); + free(binding); +} + +/** + * Returns true if the bindings have the same key and modifier combinations. + * Note that keyboard layout is not considered, so the bindings might actually + * not be equivalent on some layouts. + */ +bool binding_key_compare(struct sway_binding *binding_a, + struct sway_binding *binding_b) { + if (binding_a->release != binding_b->release) { + return false; + } + + if (binding_a->bindcode != binding_b->bindcode) { + return false; + } + + if (binding_a->modifiers ^ binding_b->modifiers) { + return false; + } + + if (binding_a->keys->length != binding_b->keys->length) { + return false; + } + + int keys_len = binding_a->keys->length; + for (int i = 0; i < keys_len; ++i) { + uint32_t key_a = *(uint32_t*)binding_a->keys->items[i]; + bool found = false; + for (int j = 0; j < keys_len; ++j) { + uint32_t key_b = *(uint32_t*)binding_b->keys->items[j]; + if (key_b == key_a) { + found = true; + break; + } + } + if (!found) { + return false; + } + } + + return true; +} + +struct cmd_results *cmd_bindsym(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "bindsym", EXPECTED_MORE_THAN, 1))) { + return error; + } + + struct sway_binding *binding = calloc(1, sizeof(struct sway_binding)); + if (!binding) { + return cmd_results_new(CMD_FAILURE, "bindsym", + "Unable to allocate binding"); + } + binding->keys = create_list(); + binding->modifiers = 0; + binding->release = false; + binding->bindcode = false; + + // Handle --release + if (strcmp("--release", argv[0]) == 0) { + if (argc >= 3) { + binding->release = true; + argv++; + argc--; + } else { + free_sway_binding(binding); + return cmd_results_new(CMD_FAILURE, "bindsym", + "Invalid bindsym command " + "(expected more than 2 arguments, got %d)", argc); + } + } + + binding->command = join_args(argv + 1, argc - 1); + + list_t *split = split_string(argv[0], "+"); + for (int i = 0; i < split->length; ++i) { + // Check for a modifier key + uint32_t mod; + if ((mod = get_modifier_mask_by_name(split->items[i])) > 0) { + binding->modifiers |= mod; + continue; + } + // Check for xkb key + xkb_keysym_t sym = xkb_keysym_from_name(split->items[i], + XKB_KEYSYM_CASE_INSENSITIVE); + + // Check for mouse binding + if (strncasecmp(split->items[i], "button", strlen("button")) == 0 && + strlen(split->items[i]) == strlen("button0")) { + sym = ((char *)split->items[i])[strlen("button")] - '1' + BTN_LEFT; + } + if (!sym) { + struct cmd_results *ret = cmd_results_new(CMD_INVALID, "bindsym", + "Unknown key '%s'", (char *)split->items[i]); + free_sway_binding(binding); + free_flat_list(split); + return ret; + } + xkb_keysym_t *key = calloc(1, sizeof(xkb_keysym_t)); + if (!key) { + free_sway_binding(binding); + free_flat_list(split); + return cmd_results_new(CMD_FAILURE, "bindsym", + "Unable to allocate binding"); + } + *key = sym; + list_add(binding->keys, key); + } + free_flat_list(split); + binding->order = binding_order++; + + list_t *mode_bindings = config->current_mode->keysym_bindings; + + // overwrite the binding if it already exists + bool overwritten = false; + for (int i = 0; i < mode_bindings->length; ++i) { + struct sway_binding *config_binding = mode_bindings->items[i]; + if (binding_key_compare(binding, config_binding)) { + sway_log(L_DEBUG, "overwriting old binding with command '%s'", + config_binding->command); + free_sway_binding(config_binding); + mode_bindings->items[i] = binding; + overwritten = true; + } + } + + if (!overwritten) { + list_add(mode_bindings, binding); + } + + sway_log(L_DEBUG, "bindsym - Bound %s to command %s", + argv[0], binding->command); + return cmd_results_new(CMD_SUCCESS, NULL, NULL); +} + +struct cmd_results *cmd_bindcode(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "bindcode", EXPECTED_MORE_THAN, 1))) { + return error; + } + + struct sway_binding *binding = calloc(1, sizeof(struct sway_binding)); + if (!binding) { + return cmd_results_new(CMD_FAILURE, "bindsym", + "Unable to allocate binding"); + } + binding->keys = create_list(); + binding->modifiers = 0; + binding->release = false; + binding->bindcode = true; + + // Handle --release + if (strcmp("--release", argv[0]) == 0) { + if (argc >= 3) { + binding->release = true; + argv++; + argc--; + } else { + free_sway_binding(binding); + return cmd_results_new(CMD_FAILURE, "bindcode", + "Invalid bindcode command " + "(expected more than 2 arguments, got %d)", argc); + } + } + + binding->command = join_args(argv + 1, argc - 1); + + list_t *split = split_string(argv[0], "+"); + for (int i = 0; i < split->length; ++i) { + // Check for a modifier key + uint32_t mod; + if ((mod = get_modifier_mask_by_name(split->items[i])) > 0) { + binding->modifiers |= mod; + continue; + } + // parse keycode + xkb_keycode_t keycode = (int)strtol(split->items[i], NULL, 10); + if (!xkb_keycode_is_legal_ext(keycode)) { + error = + cmd_results_new(CMD_INVALID, "bindcode", + "Invalid keycode '%s'", (char *)split->items[i]); + free_sway_binding(binding); + list_free(split); + return error; + } + xkb_keycode_t *key = calloc(1, sizeof(xkb_keycode_t)); + *key = keycode - 8; + list_add(binding->keys, key); + } + free_flat_list(split); + + binding->order = binding_order++; + + list_t *mode_bindings = config->current_mode->keycode_bindings; + + // overwrite the binding if it already exists + bool overwritten = false; + for (int i = 0; i < mode_bindings->length; ++i) { + struct sway_binding *config_binding = mode_bindings->items[i]; + if (binding_key_compare(binding, config_binding)) { + sway_log(L_DEBUG, "overwriting old binding with command '%s'", + config_binding->command); + free_sway_binding(config_binding); + mode_bindings->items[i] = binding; + overwritten = true; + } + } + + if (!overwritten) { + list_add(mode_bindings, binding); + } + + sway_log(L_DEBUG, "bindcode - Bound %s to command %s", + argv[0], binding->command); + return cmd_results_new(CMD_SUCCESS, NULL, NULL); +} diff --git a/sway/config.c b/sway/config.c index b591ae9e4..312e0779c 100644 --- a/sway/config.c +++ b/sway/config.c @@ -53,7 +53,8 @@ static void config_defaults(struct sway_config *config) { goto cleanup; if (!(config->current_mode->name = malloc(sizeof("default")))) goto cleanup; strcpy(config->current_mode->name, "default"); - if (!(config->current_mode->bindings = create_list())) goto cleanup; + if (!(config->current_mode->keysym_bindings = create_list())) goto cleanup; + if (!(config->current_mode->keycode_bindings = create_list())) goto cleanup; list_add(config->modes, config->current_mode); config->floating_mod = 0; diff --git a/sway/input/keyboard.c b/sway/input/keyboard.c index 724941d85..c2bb25780 100644 --- a/sway/input/keyboard.c +++ b/sway/input/keyboard.c @@ -1,8 +1,325 @@ +#include +#include +#include #include "sway/input/seat.h" #include "sway/input/keyboard.h" #include "sway/input/input-manager.h" +#include "sway/commands.h" #include "log.h" +static bool keysym_is_modifier(xkb_keysym_t keysym) { + switch (keysym) { + case XKB_KEY_Shift_L: case XKB_KEY_Shift_R: + case XKB_KEY_Control_L: case XKB_KEY_Control_R: + case XKB_KEY_Caps_Lock: + case XKB_KEY_Shift_Lock: + case XKB_KEY_Meta_L: case XKB_KEY_Meta_R: + case XKB_KEY_Alt_L: case XKB_KEY_Alt_R: + case XKB_KEY_Super_L: case XKB_KEY_Super_R: + case XKB_KEY_Hyper_L: case XKB_KEY_Hyper_R: + return true; + default: + return false; + } +} + +static size_t pressed_keysyms_length(xkb_keysym_t *pressed_keysyms) { + size_t n = 0; + for (size_t i = 0; i < SWAY_KEYBOARD_PRESSED_KEYSYMS_CAP; ++i) { + if (pressed_keysyms[i] != XKB_KEY_NoSymbol) { + ++n; + } + } + return n; +} + +static ssize_t pressed_keysyms_index(xkb_keysym_t *pressed_keysyms, + xkb_keysym_t keysym) { + for (size_t i = 0; i < SWAY_KEYBOARD_PRESSED_KEYSYMS_CAP; ++i) { + if (pressed_keysyms[i] == keysym) { + return i; + } + } + return -1; +} + +static void pressed_keysyms_add(xkb_keysym_t *pressed_keysyms, + xkb_keysym_t keysym) { + ssize_t i = pressed_keysyms_index(pressed_keysyms, keysym); + if (i < 0) { + i = pressed_keysyms_index(pressed_keysyms, XKB_KEY_NoSymbol); + if (i >= 0) { + pressed_keysyms[i] = keysym; + } + } +} + +static void pressed_keysyms_remove(xkb_keysym_t *pressed_keysyms, + xkb_keysym_t keysym) { + ssize_t i = pressed_keysyms_index(pressed_keysyms, keysym); + if (i >= 0) { + pressed_keysyms[i] = XKB_KEY_NoSymbol; + } +} + +static void pressed_keysyms_update(xkb_keysym_t *pressed_keysyms, + const xkb_keysym_t *keysyms, size_t keysyms_len, + enum wlr_key_state state) { + for (size_t i = 0; i < keysyms_len; ++i) { + if (keysym_is_modifier(keysyms[i])) { + continue; + } + if (state == WLR_KEY_PRESSED) { + pressed_keysyms_add(pressed_keysyms, keysyms[i]); + } else { // WLR_KEY_RELEASED + pressed_keysyms_remove(pressed_keysyms, keysyms[i]); + } + } +} + +static bool binding_matches_key_state(struct sway_binding *binding, + enum wlr_key_state key_state) { + if (key_state == WLR_KEY_PRESSED && !binding->release) { + return true; + } + if (key_state == WLR_KEY_RELEASED && binding->release) { + return true; + } + + return false; +} + +static void binding_execute_command(struct sway_binding *binding) { + sway_log(L_DEBUG, "running command for binding: %s", + binding->command); + struct cmd_results *results = handle_command(binding->command); + if (results->status != CMD_SUCCESS) { + sway_log(L_DEBUG, "could not run command for binding: %s", + binding->command); + } + free_cmd_results(results); +} + +/** + * Execute a built-in, hardcoded compositor binding. These are triggered from a + * single keysym. + * + * Returns true if the keysym was handled by a binding and false if the event + * should be propagated to clients. + */ +static bool keyboard_execute_compositor_binding(struct sway_keyboard *keyboard, + xkb_keysym_t *pressed_keysyms, uint32_t modifiers, size_t keysyms_len) { + for (size_t i = 0; i < keysyms_len; ++i) { + xkb_keysym_t keysym = pressed_keysyms[i]; + if (keysym >= XKB_KEY_XF86Switch_VT_1 && + keysym <= XKB_KEY_XF86Switch_VT_12) { + if (wlr_backend_is_multi(server.backend)) { + struct wlr_session *session = + wlr_multi_get_session(server.backend); + if (session) { + unsigned vt = keysym - XKB_KEY_XF86Switch_VT_1 + 1; + wlr_session_change_vt(session, vt); + } + } + return true; + } + } + + return false; +} + +/** + * Execute keyboard bindings bound with `bindysm` for the given keyboard state. + * + * Returns true if the keysym was handled by a binding and false if the event + * should be propagated to clients. + */ +static bool keyboard_execute_bindsym(struct sway_keyboard *keyboard, + xkb_keysym_t *pressed_keysyms, uint32_t modifiers, + enum wlr_key_state key_state) { + // configured bindings + int n = pressed_keysyms_length(pressed_keysyms); + list_t *keysym_bindings = config->current_mode->keysym_bindings; + for (int i = 0; i < keysym_bindings->length; ++i) { + struct sway_binding *binding = keysym_bindings->items[i]; + if (!binding_matches_key_state(binding, key_state) || + modifiers ^ binding->modifiers || + n != binding->keys->length) { + continue; + } + + bool match = true; + for (int j = 0; j < binding->keys->length; ++j) { + match = + pressed_keysyms_index(pressed_keysyms, + *(int*)binding->keys->items[j]) >= 0; + + if (!match) { + break; + } + } + + if (match) { + binding_execute_command(binding); + return true; + } + } + + return false; +} + +static bool binding_matches_keycodes(struct wlr_keyboard *keyboard, + struct sway_binding *binding, struct wlr_event_keyboard_key *event) { + assert(binding->bindcode); + + uint32_t keycode = event->keycode + 8; + + if (!binding_matches_key_state(binding, event->state)) { + return false; + } + + uint32_t modifiers = wlr_keyboard_get_modifiers(keyboard); + if (modifiers ^ binding->modifiers) { + return false; + } + + // on release, the released key must be in the binding + if (event->state == WLR_KEY_RELEASED) { + bool found = false; + for (int i = 0; i < binding->keys->length; ++i) { + uint32_t binding_keycode = *(uint32_t*)binding->keys->items[i] + 8; + if (binding_keycode == keycode) { + found = true; + break; + } + } + if (!found) { + return false; + } + } + + // every keycode in the binding must be present in the pressed keys on the + // keyboard + for (int i = 0; i < binding->keys->length; ++i) { + uint32_t binding_keycode = *(uint32_t*)binding->keys->items[i] + 8; + if (event->state == WLR_KEY_RELEASED && keycode == binding_keycode) { + continue; + } + + bool found = false; + for (size_t j = 0; j < keyboard->num_keycodes; ++j) { + xkb_keycode_t keycode = keyboard->keycodes[j] + 8; + if (keycode == binding_keycode) { + found = true; + break; + } + } + + if (!found) { + return false; + } + } + + // every keycode pressed on the keyboard must be present within the binding + // keys (unless it is a modifier) + for (size_t i = 0; i < keyboard->num_keycodes; ++i) { + xkb_keycode_t keycode = keyboard->keycodes[i] + 8; + bool found = false; + for (int j = 0; j < binding->keys->length; ++j) { + uint32_t binding_keycode = *(uint32_t*)binding->keys->items[j] + 8; + if (binding_keycode == keycode) { + found = true; + break; + } + } + + if (!found) { + if (!binding->modifiers) { + return false; + } + + // check if it is a modifier, which we know matched from the check + // above + const xkb_keysym_t *keysyms; + int num_keysyms = + xkb_state_key_get_syms(keyboard->xkb_state, + keycode, &keysyms); + if (num_keysyms != 1 || !keysym_is_modifier(keysyms[0])) { + return false; + } + } + } + + return true; +} + +/** + * Execute keyboard bindings bound with `bindcode` for the given keyboard state. + * + * Returns true if the keysym was handled by a binding and false if the event + * should be propagated to clients. + */ +static bool keyboard_execute_bindcode(struct sway_keyboard *keyboard, + struct wlr_event_keyboard_key *event) { + struct wlr_keyboard *wlr_keyboard = + keyboard->seat_device->input_device->wlr_device->keyboard; + list_t *keycode_bindings = config->current_mode->keycode_bindings; + for (int i = 0; i < keycode_bindings->length; ++i) { + struct sway_binding *binding = keycode_bindings->items[i]; + if (binding_matches_keycodes(wlr_keyboard, binding, event)) { + binding_execute_command(binding); + return true; + } + } + + return false; +} + +/** + * Get keysyms and modifiers from the keyboard as xkb sees them. + * + * This uses the xkb keysyms translation based on pressed modifiers and clears + * the consumed modifiers from the list of modifiers passed to keybind + * detection. + * + * On US layout, pressing Alt+Shift+2 will trigger Alt+@. + */ +static size_t keyboard_keysyms_translated(struct sway_keyboard *keyboard, + xkb_keycode_t keycode, const xkb_keysym_t **keysyms, + uint32_t *modifiers) { + struct wlr_input_device *device = + keyboard->seat_device->input_device->wlr_device; + *modifiers = wlr_keyboard_get_modifiers(device->keyboard); + xkb_mod_mask_t consumed = xkb_state_key_get_consumed_mods2( + device->keyboard->xkb_state, keycode, XKB_CONSUMED_MODE_XKB); + *modifiers = *modifiers & ~consumed; + + return xkb_state_key_get_syms(device->keyboard->xkb_state, + keycode, keysyms); +} + +/** + * Get keysyms and modifiers from the keyboard as if modifiers didn't change + * keysyms. + * + * This avoids the xkb keysym translation based on modifiers considered pressed + * in the state. + * + * This will trigger keybinds such as Alt+Shift+2. + */ +static size_t keyboard_keysyms_raw(struct sway_keyboard *keyboard, + xkb_keycode_t keycode, const xkb_keysym_t **keysyms, + uint32_t *modifiers) { + struct wlr_input_device *device = + keyboard->seat_device->input_device->wlr_device; + *modifiers = wlr_keyboard_get_modifiers(device->keyboard); + + xkb_layout_index_t layout_index = xkb_state_key_get_layout( + device->keyboard->xkb_state, keycode); + return xkb_keymap_key_get_syms_by_level(device->keyboard->keymap, + keycode, layout_index, 0, keysyms); +} + static void handle_keyboard_key(struct wl_listener *listener, void *data) { struct sway_keyboard *keyboard = wl_container_of(listener, keyboard, keyboard_key); @@ -10,9 +327,70 @@ static void handle_keyboard_key(struct wl_listener *listener, void *data) { struct wlr_input_device *wlr_device = keyboard->seat_device->input_device->wlr_device; struct wlr_event_keyboard_key *event = data; - wlr_seat_set_keyboard(wlr_seat, wlr_device); - wlr_seat_keyboard_notify_key(wlr_seat, event->time_msec, - event->keycode, event->state); + + xkb_keycode_t keycode = event->keycode + 8; + bool handled = false; + + // handle keycodes + handled = keyboard_execute_bindcode(keyboard, event); + + // handle translated keysyms + if (!handled && event->state == WLR_KEY_RELEASED) { + handled = keyboard_execute_bindsym(keyboard, + keyboard->pressed_keysyms_translated, + keyboard->modifiers_translated, + event->state); + } + const xkb_keysym_t *translated_keysyms; + size_t translated_keysyms_len = + keyboard_keysyms_translated(keyboard, keycode, &translated_keysyms, + &keyboard->modifiers_translated); + pressed_keysyms_update(keyboard->pressed_keysyms_translated, + translated_keysyms, translated_keysyms_len, event->state); + if (!handled && event->state == WLR_KEY_PRESSED) { + handled = keyboard_execute_bindsym(keyboard, + keyboard->pressed_keysyms_translated, + keyboard->modifiers_translated, + event->state); + } + + // Handle raw keysyms + if (!handled && event->state == WLR_KEY_RELEASED) { + handled = keyboard_execute_bindsym(keyboard, + keyboard->pressed_keysyms_raw, keyboard->modifiers_raw, + event->state); + } + const xkb_keysym_t *raw_keysyms; + size_t raw_keysyms_len = + keyboard_keysyms_raw(keyboard, keycode, &raw_keysyms, &keyboard->modifiers_raw); + pressed_keysyms_update(keyboard->pressed_keysyms_raw, raw_keysyms, + raw_keysyms_len, event->state); + if (!handled && event->state == WLR_KEY_PRESSED) { + handled = keyboard_execute_bindsym(keyboard, + keyboard->pressed_keysyms_raw, keyboard->modifiers_raw, + event->state); + } + + // Compositor bindings + if (!handled && event->state == WLR_KEY_PRESSED) { + handled = + keyboard_execute_compositor_binding(keyboard, + keyboard->pressed_keysyms_translated, + keyboard->modifiers_translated, + translated_keysyms_len); + } + if (!handled && event->state == WLR_KEY_PRESSED) { + handled = + keyboard_execute_compositor_binding(keyboard, + keyboard->pressed_keysyms_raw, keyboard->modifiers_raw, + raw_keysyms_len); + } + + if (!handled || event->state == WLR_KEY_RELEASED) { + wlr_seat_set_keyboard(wlr_seat, wlr_device); + wlr_seat_keyboard_notify_key(wlr_seat, event->time_msec, + event->keycode, event->state); + } } static void handle_keyboard_modifiers(struct wl_listener *listener, diff --git a/sway/meson.build b/sway/meson.build index fee2ddd21..01d5ef36d 100644 --- a/sway/meson.build +++ b/sway/meson.build @@ -6,6 +6,7 @@ sway_sources = files( 'input/seat.c', 'input/cursor.c', 'input/keyboard.c', + 'commands/bind.c', 'commands/exit.c', 'commands/exec.c', 'commands/exec_always.c',