foot/key-binding.c
2024-02-06 12:36:45 +01:00

584 lines
17 KiB
C

#include "key-binding.h"
#include <stdlib.h>
#define LOG_MODULE "key-binding"
#define LOG_ENABLE_DBG 0
#include "log.h"
#include "config.h"
#include "debug.h"
#include "terminal.h"
#include "util.h"
#include "wayland.h"
#include "xmalloc.h"
struct key_set {
struct key_binding_set public;
const struct config *conf;
const struct seat *seat;
size_t conf_ref_count;
};
typedef tll(struct key_set) bind_set_list_t;
struct key_binding_manager {
struct key_set *last_used_set;
bind_set_list_t binding_sets;
};
static void load_keymap(struct key_set *set);
static void unload_keymap(struct key_set *set);
struct key_binding_manager *
key_binding_manager_new(void)
{
struct key_binding_manager *mgr = xcalloc(1, sizeof(*mgr));
return mgr;
}
void
key_binding_manager_destroy(struct key_binding_manager *mgr)
{
xassert(tll_length(mgr->binding_sets) == 0);
free(mgr);
}
void
key_binding_new_for_seat(struct key_binding_manager *mgr,
const struct seat *seat)
{
#if defined(_DEBUG)
tll_foreach(mgr->binding_sets, it)
xassert(it->item.seat != seat);
#endif
tll_foreach(seat->wayl->terms, it) {
struct key_set set = {
.public = {
.key = tll_init(),
.search = tll_init(),
.url = tll_init(),
.mouse = tll_init(),
},
.conf = it->item->conf,
.seat = seat,
.conf_ref_count = 1,
};
tll_push_back(mgr->binding_sets, set);
LOG_DBG("new (seat): set=%p, seat=%p, conf=%p, ref-count=1",
(void *)&tll_back(mgr->binding_sets),
(void *)set.seat, (void *)set.conf);
load_keymap(&tll_back(mgr->binding_sets));
}
LOG_DBG("new (seat): total number of sets: %zu",
tll_length(mgr->binding_sets));
}
void
key_binding_new_for_conf(struct key_binding_manager *mgr,
const struct wayland *wayl, const struct config *conf)
{
tll_foreach(wayl->seats, it) {
struct seat *seat = &it->item;
struct key_set *existing =
(struct key_set *)key_binding_for(mgr, conf, seat);
if (existing != NULL) {
existing->conf_ref_count++;
continue;
}
struct key_set set = {
.public = {
.key = tll_init(),
.search = tll_init(),
.url = tll_init(),
.mouse = tll_init(),
},
.conf = conf,
.seat = seat,
.conf_ref_count = 1,
};
tll_push_back(mgr->binding_sets, set);
load_keymap(&tll_back(mgr->binding_sets));
/* Chances are high this set will be requested next */
mgr->last_used_set = &tll_back(mgr->binding_sets);
LOG_DBG("new (conf): set=%p, seat=%p, conf=%p, ref-count=1",
(void *)&tll_back(mgr->binding_sets),
(void *)set.seat, (void *)set.conf);
}
LOG_DBG("new (conf): total number of sets: %zu",
tll_length(mgr->binding_sets));
}
struct key_binding_set * NOINLINE
key_binding_for(struct key_binding_manager *mgr, const struct config *conf,
const struct seat *seat)
{
struct key_set *last_used = mgr->last_used_set;
if (last_used != NULL &&
last_used->conf == conf &&
last_used->seat == seat)
{
// LOG_DBG("lookup: last used");
return &last_used->public;
}
tll_foreach(mgr->binding_sets, it) {
struct key_set *set = &it->item;
if (set->conf != conf)
continue;
if (set->seat != seat)
continue;
#if 0
LOG_DBG("lookup: set=%p, seat=%p, conf=%p, ref-count=%zu",
(void *)set, (void *)seat, (void *)conf, set->conf_ref_count);
#endif
mgr->last_used_set = set;
return &set->public;
}
return NULL;
}
static void
key_binding_set_destroy(struct key_binding_manager *mgr,
struct key_set *set)
{
unload_keymap(set);
if (mgr->last_used_set == set)
mgr->last_used_set = NULL;
/* Note: caller must remove from binding_sets */
}
void
key_binding_remove_seat(struct key_binding_manager *mgr,
const struct seat *seat)
{
tll_foreach(mgr->binding_sets, it) {
struct key_set *set = &it->item;
if (set->seat != seat)
continue;
key_binding_set_destroy(mgr, set);
tll_remove(mgr->binding_sets, it);
LOG_DBG("remove seat: set=%p, seat=%p, total number of sets: %zu",
(void *)set, (void *)seat, tll_length(mgr->binding_sets));
}
LOG_DBG("remove seat: total number of sets: %zu",
tll_length(mgr->binding_sets));
}
void
key_binding_unref(struct key_binding_manager *mgr, const struct config *conf)
{
tll_foreach(mgr->binding_sets, it) {
struct key_set *set = &it->item;
if (set->conf != conf)
continue;
xassert(set->conf_ref_count > 0);
if (--set->conf_ref_count == 0) {
LOG_DBG("unref conf: set=%p, seat=%p, conf=%p",
(void *)set, (void *)set->seat, (void *)conf);
key_binding_set_destroy(mgr, set);
tll_remove(mgr->binding_sets, it);
}
}
LOG_DBG("unref conf: total number of sets: %zu",
tll_length(mgr->binding_sets));
}
static xkb_keycode_list_t
key_codes_for_xkb_sym(struct xkb_keymap *keymap, xkb_keysym_t sym)
{
xkb_keycode_list_t key_codes = tll_init();
/*
* Find all key codes that map to this symbol.
*
* This allows us to match bindings in other layouts
* too.
*/
struct xkb_state *state = xkb_state_new(keymap);
for (xkb_keycode_t code = xkb_keymap_min_keycode(keymap);
code <= xkb_keymap_max_keycode(keymap);
code++)
{
if (xkb_state_key_get_one_sym(state, code) == sym)
tll_push_back(key_codes, code);
}
xkb_state_unref(state);
return key_codes;
}
static xkb_keysym_t
maybe_repair_key_combo(const struct seat *seat,
xkb_keysym_t sym, xkb_mod_mask_t mods)
{
/*
* Detect combos containing a shifted symbol and the corresponding
* modifier, and replace the shifted symbol with its unshifted
* variant.
*
* For example, the combo is "Control+Shift+U". In this case,
* Shift is the modifier used to "shift" 'u' to 'U', after which
* 'Shift' will have been "consumed". Since we filter out consumed
* modifiers when matching key combos, this key combo will never
* trigger (we will never be able to match the 'Shift' modifier).
*
* There are two correct variants of the above key combo:
* - "Control+U" (upper case 'U')
* - "Control+Shift+u" (lower case 'u')
*
* What we do here is, for each key *code*, check if there are any
* (shifted) levels where it produces 'sym'. If there are, check
* *which* sets of modifiers are needed to produce it, and compare
* with 'mods'.
*
* If there is at least one common modifier, it means 'sym' is a
* "shifted" symbol, with the corresponding shifting modifier
* explicitly included in the key combo. I.e. the key combo will
* never trigger.
*
* We then proceed and "repair" the key combo by replacing 'sym'
* with the corresponding unshifted symbol.
*
* To reduce the noise, we ignore all key codes where the shifted
* symbol is the same as the unshifted symbol.
*/
for (xkb_keycode_t code = xkb_keymap_min_keycode(seat->kbd.xkb_keymap);
code <= xkb_keymap_max_keycode(seat->kbd.xkb_keymap);
code++)
{
xkb_layout_index_t layout_idx =
xkb_state_key_get_layout(seat->kbd.xkb_state, code);
/* Get all unshifted symbols for this key */
const xkb_keysym_t *base_syms = NULL;
size_t base_count = xkb_keymap_key_get_syms_by_level(
seat->kbd.xkb_keymap, code, layout_idx, 0, &base_syms);
if (base_count == 0 || sym == base_syms[0]) {
/* No unshifted symbols, or unshifted symbol is same as 'sym' */
continue;
}
/* Name of the unshifted symbol, for logging */
char base_name[100];
xkb_keysym_get_name(base_syms[0], base_name, sizeof(base_name));
/* Iterate all shift levels */
for (xkb_level_index_t level_idx = 1;
level_idx < xkb_keymap_num_levels_for_key(
seat->kbd.xkb_keymap, code, layout_idx);
level_idx++) {
/* Get all symbols for current shift level */
const xkb_keysym_t *shifted_syms = NULL;
size_t shifted_count = xkb_keymap_key_get_syms_by_level(
seat->kbd.xkb_keymap, code,
layout_idx, level_idx, &shifted_syms);
for (size_t i = 0; i < shifted_count; i++) {
if (shifted_syms[i] != sym)
continue;
/* Get modifier sets that produces the current shift level */
xkb_mod_mask_t mod_masks[16];
size_t mod_mask_count = xkb_keymap_key_get_mods_for_level(
seat->kbd.xkb_keymap, code, layout_idx, level_idx,
mod_masks, ALEN(mod_masks));
/* Check if key combo's modifier set intersects */
for (size_t j = 0; j < mod_mask_count; j++) {
if ((mod_masks[j] & mods) != mod_masks[j])
continue;
char combo[64] = {0};
for (int k = 0; k < sizeof(xkb_mod_mask_t) * 8; k++) {
if (!(mods & (1u << k)))
continue;
const char *mod_name = xkb_keymap_mod_get_name(
seat->kbd.xkb_keymap, k);
strcat(combo, mod_name);
strcat(combo, "+");
}
size_t len = strlen(combo);
xkb_keysym_get_name(
sym, &combo[len], sizeof(combo) - len);
LOG_WARN(
"%s: combo with both explicit modifier and shifted symbol "
"(level=%d, mod-mask=0x%08x), "
"replacing with %s",
combo, level_idx, mod_masks[j], base_name);
/* Replace with unshifted symbol */
return base_syms[0];
}
}
}
}
return sym;
}
static int
key_cmp(struct key_binding a, struct key_binding b)
{
xassert(a.type == b.type);
/*
* Sort bindings such that bindings with the same symbol are
* sorted with the binding having the most modifiers comes first.
*
* This fixes an issue where the "wrong" key binding are triggered
* when used with "consumed" modifiers.
*
* For example: if Control+BackSpace is bound before
* Control+Shift+BackSpace, then the latter binding is never
* triggered.
*
* Why? Because Shift is a consumed modifier. This means
* Control+BackSpace is "the same" as Control+Shift+BackSpace.
*
* By sorting bindings with more modifiers first, we work around
* the problem. But note that it is *just* a workaround, and I'm
* not confident there aren't cases where it doesn't work.
*
* See https://codeberg.org/dnkl/foot/issues/1280
*/
const int a_mod_count = __builtin_popcount(a.mods);
const int b_mod_count = __builtin_popcount(b.mods);
switch (a.type) {
case KEY_BINDING:
if (a.k.sym != b.k.sym)
return b.k.sym - a.k.sym;
return b_mod_count - a_mod_count;
case MOUSE_BINDING: {
if (a.m.button != b.m.button)
return b.m.button - a.m.button;
if (a_mod_count != b_mod_count)
return b_mod_count - a_mod_count;
return b.m.count - a.m.count;
}
}
BUG("invalid key binding type");
return 0;
}
static void NOINLINE
sort_binding_list(key_binding_list_t *list)
{
tll_sort(*list, key_cmp);
}
static xkb_mod_mask_t
mods_to_mask(const struct seat *seat, const config_modifier_list_t *mods)
{
xkb_mod_mask_t mask = 0;
tll_foreach(*mods, it) {
xkb_mod_index_t idx = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, it->item);
if (idx == XKB_MOD_INVALID) {
LOG_ERR("%s: invalid modifier name", it->item);
continue;
}
mask |= 1 << idx;
}
return mask;
}
static void NOINLINE
convert_key_binding(struct key_set *set,
const struct config_key_binding *conf_binding,
key_binding_list_t *bindings)
{
const struct seat *seat = set->seat;
xkb_mod_mask_t mods = mods_to_mask(seat, &conf_binding->modifiers);
xkb_keysym_t sym = maybe_repair_key_combo(seat, conf_binding->k.sym, mods);
struct key_binding binding = {
.type = KEY_BINDING,
.action = conf_binding->action,
.aux = &conf_binding->aux,
.mods = mods,
.k = {
.sym = sym,
.key_codes = key_codes_for_xkb_sym(seat->kbd.xkb_keymap, sym),
},
};
tll_push_back(*bindings, binding);
sort_binding_list(bindings);
}
static void
convert_key_bindings(struct key_set *set)
{
const struct config *conf = set->conf;
for (size_t i = 0; i < conf->bindings.key.count; i++) {
const struct config_key_binding *binding = &conf->bindings.key.arr[i];
convert_key_binding(set, binding, &set->public.key);
}
}
static void
convert_search_bindings(struct key_set *set)
{
const struct config *conf = set->conf;
for (size_t i = 0; i < conf->bindings.search.count; i++) {
const struct config_key_binding *binding = &conf->bindings.search.arr[i];
convert_key_binding(set, binding, &set->public.search);
}
}
static void
convert_url_bindings(struct key_set *set)
{
const struct config *conf = set->conf;
for (size_t i = 0; i < conf->bindings.url.count; i++) {
const struct config_key_binding *binding = &conf->bindings.url.arr[i];
convert_key_binding(set, binding, &set->public.url);
}
}
static void
convert_mouse_binding(struct key_set *set,
const struct config_key_binding *conf_binding)
{
struct key_binding binding = {
.type = MOUSE_BINDING,
.action = conf_binding->action,
.aux = &conf_binding->aux,
.mods = mods_to_mask(set->seat, &conf_binding->modifiers),
.m = {
.button = conf_binding->m.button,
.count = conf_binding->m.count,
},
};
tll_push_back(set->public.mouse, binding);
sort_binding_list(&set->public.mouse);
}
static void
convert_mouse_bindings(struct key_set *set)
{
const struct config *conf = set->conf;
for (size_t i = 0; i < conf->bindings.mouse.count; i++) {
const struct config_key_binding *binding =
&conf->bindings.mouse.arr[i];
convert_mouse_binding(set, binding);
}
}
static void NOINLINE
load_keymap(struct key_set *set)
{
LOG_DBG("load keymap: set=%p, seat=%p, conf=%p",
(void *)set, (void *)set->seat, (void *)set->conf);
if (set->seat->kbd.xkb_state == NULL ||
set->seat->kbd.xkb_keymap == NULL)
{
LOG_DBG("no XKB keymap");
return;
}
convert_key_bindings(set);
convert_search_bindings(set);
convert_url_bindings(set);
convert_mouse_bindings(set);
set->public.selection_overrides = mods_to_mask(
set->seat, &set->conf->mouse.selection_override_modifiers);
}
void
key_binding_load_keymap(struct key_binding_manager *mgr,
const struct seat *seat)
{
tll_foreach(mgr->binding_sets, it) {
struct key_set *set = &it->item;
if (set->seat == seat)
load_keymap(set);
}
}
static void NOINLINE
key_bindings_destroy(key_binding_list_t *bindings)
{
tll_foreach(*bindings, it) {
struct key_binding *bind = &it->item;
switch (bind->type) {
case KEY_BINDING: tll_free(it->item.k.key_codes); break;
case MOUSE_BINDING: break;
}
tll_remove(*bindings, it);
}
}
static void NOINLINE
unload_keymap(struct key_set *set)
{
key_bindings_destroy(&set->public.key);
key_bindings_destroy(&set->public.search);
key_bindings_destroy(&set->public.url);
key_bindings_destroy(&set->public.mouse);
set->public.selection_overrides = 0;
}
void
key_binding_unload_keymap(struct key_binding_manager *mgr,
const struct seat *seat)
{
tll_foreach(mgr->binding_sets, it) {
struct key_set *set = &it->item;
if (set->seat != seat)
continue;
LOG_DBG("unload keymap: set=%p, seat=%p, conf=%p",
(void *)set, (void *)seat, (void *)set->conf);
unload_keymap(set);
}
}