Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

XKB Key & Wayland

看到 Treeland 在修改这部分的逻辑,想起来自己这里也没弄明白,于是开始看。

TinyWL

TinyWL 中有两个部分与 xkb 有关:键盘初始化和快捷键响应。

// tinywl.c @server_new_keyboard
struct xkb_context *context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
struct xkb_keymap *keymap = xkb_keymap_new_from_names(context, NULL, XKB_KEYMAP_COMPILE_NO_FLAGS);
wlr_keyboard_set_keymap(wlr_keyboard, keymap);
xkb_keymap_unref(keymap);
xkb_context_unref(context);

初始化是全 default 状态,libxkbcommon 会自动加载系统和用户自定义配置(具体配置查看 xkbcli info 和 xkbcli list):

xkb_keymap_new_from_names()
    -> xkb_components_from_rules_names()            // src/xkbcomp/rules.c
    -> FindFileInXkbPath()                          // src/xkbcomp/include.c
    -> xkb_context_num_include_paths()              // src/context.c
    -> xkb_context_init_includes()                  // src/context-priv.c
    -> xkb_context_include_path_append_default()    // src/context.c

wlr_keyboard_set_keymap() 中有创建 xkb_state 并持有在 wlr_keyboard::xkb_state 里。 后续的键盘响应需要把 keycode 做转换,之后就是拿到 xkb_keysym_t 做 switch 匹配

// tinywl.c @keyboard_handle_key
uint32_t keycode = event->keycode + 8;
int nsyms = xkb_state_key_get_syms(keyboard->wlr_keyboard->xkb_state, keycode, &syms);
for (int i = 0; i < nsyms; i++) {
    handle_keybinding(server, syms[i]);     // XKB_KEY_Escape, XKB_KEY_F1
}

至此就是 TinyWL 中的全部(感觉远远不够)。

SDL

Wayland 协议中有 wl_keyboard:event:keymap 事件 provide a keyboard mapping description, 这个 keymap fd 由 wlr_keyboard_set_keymap() 创建并保存,后续分发给客户端。

// types/seat/wlr_seat_keyboard.c @seat_client_send_keymap
wl_keyboard_send_keymap(resource, format, fd, size);

SDL 拿到 keymap 后做了一系列的刷新

// src/video/wayland/SDL_waylandevents.c @keyboard_handle_keymap
char *map_str = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
seat->keyboard.xkb.keymap = WAYLAND_xkb_keymap_new_from_string(
    seat->display->xkb_context, map_str, XKB_KEYMAP_FORMAT_TEXT_V1, 0
);
// update mod mask
seat->keyboard.xkb.shift_mask = WAYLAND_xkb_keymap_mod_get_mask(seat->keyboard.xkb.keymap, XKB_MOD_NAME_SHIFT);
// new state
seat->keyboard.xkb.state = WAYLAND_xkb_state_new(seat->keyboard.xkb.keymap);
// populate the new layout maps
seat->keyboard.xkb.num_layouts = WAYLAND_xkb_keymap_num_layouts(seat->keyboard.xkb.keymap);
WAYLAND_xkb_keymap_key_for_each(seat->keyboard.xkb.keymap, Wayland_KeymapIterator, seat);

迭代器里把 xkb 的 keymap 一次性翻译成 SDL 接口的 keymap 表。

Key

到目前为止已经接触到了很多类型,我们需要先明确这两个 Key(潮流一点叫两种接口的 Token)

Key Code

xkb_keycode_t 标识一个物理键,与布局和修饰键无关。 数值来自内核的 input_report_key(),经过 libinput 从 evdev 读取,给 xkb 解析之前需要换算一次:

keycode_A = KEY_A + 8

Key Symbol

xkb_keysym_t 是键的符号,表示各种字符和功能键,如 XKB_KEY_Tab 和 XKB_KEY_ISO_Left_Tab 就不是一个符号

$ xkbcli how-to-type --keysym Tab
keysym: Tab (0xff09)
...
KEYCODE  KEY NAME  LAYOUT   LAYOUT NAME          LEVEL#  MODIFIERS
23       TAB       1        English (US)         1       [ ]

$ xkbcli how-to-type --keysym ISO_Left_Tab
keysym: ISO_Left_Tab (0xfe20)
...
KEYCODE  KEY NAME  LAYOUT   LAYOUT NAME          LEVEL#  MODIFIERS
23       TAB       1        English (US)         2       [ Shift ]

XKB KeyMap

xkb_keymap 是 libxkbcommon 的核心对象,表示一份静态只读的键盘数据,创建之后不可变。

struct xkb_keymap {
    struct xkb_key *keys;

    struct xkb_key_type *types;
    struct xkb_sym_interpret *sym_interprets;
    struct xkb_mod_set mods;
}

它的本质是一张查询表,从 keycode + layout + mods 查到对应的 keysym,顺着调用, xkb_state_key_get_syms()依次翻完这张表的四层:

xkb_state_key_get_syms(state, kc, &syms_out)
├── xkb_state_key_get_layout(state, kc)          // keycode -> key -> layout
├── xkb_state_key_get_level(state, kc, layout)   // mods + type -> level
├── xkb_keymap_key_get_level(keymap, ...)        // level -> struct xkb_level
└── 读 level->num_syms / s.sym / s.syms          // level -> keysym

1. 决定用哪个 layout

先用 XkbKey() 把 keycode 换成 xkb_key(低 keycode 直接下标,高 keycode 二分查找), 再用 state->components.group 解析出该键实际使用的 layout:

// src/state.c @xkb_state_key_get_layout
const struct xkb_key *key = XkbKey(state->keymap, kc);
return xkb_keymap_key_effective_layout(key, state->components.group);

layout 定义的是 group 与 keysym 的关系,属于逻辑布局,并非键盘的实际物理布局,但也相关。

2. 决定 level 的索引

取该 layout 的 group,用它的 type 把 mods 过滤后匹配出 level:

// src/state.c @state_key_get_level @get_entry_for_key_state @get_entry_for_mods
const struct xkb_key_type *type = key->groups[layout].type;
xkb_mod_mask_t active_mods = state->components.mods & type->mods.mask;
const struct xkb_key_type_entry *entry = get_entry_for_mods(type, active_mods);
return (entry) ? entry->level : 0;

层级选择是精确匹配:当前按下的修饰键先和 key type 的 mods 做掩码过滤, 再到 type 的 map 表里找完全一致的一项;找不到就回退到第一个 level(index 0):

// src/state.c @get_entry_for_mods
for (i = 0; i < type->num_entries; i++)
    if (entry_is_active(&type->entries[i]) && type->entries[i].mods.mask == mods)
        return &type->entries[i];
return NULL;

修饰键影响到 level 的选择,我们一般不考虑那么多:

/**
 * Index of a shift level.
 *
 * Any key, in any layout, can have several <em>shift levels</em>.  Each
 * shift level can assign different keysyms to the key.  The shift level
 * to use is chosen according to the current keyboard state; for example,
 * if no keys are pressed, the first level may be used; if the Left Shift
 * key is pressed, the second; if Num Lock is pressed, the third; and
 * many such combinations are possible (see xkb_mod_index_t).
 *
 * Level indices are consecutive.  The first level has index 0.
 */
typedef uint32_t xkb_level_index_t;

3. 取到给定的 level

从 group 里取出该 level 的数据,只返回指针:

// src/keymap.c @xkb_keymap_key_get_level
return &key->groups[layout].levels[level];

keymap 文件的 Level 1..4,对应 xkb_level_index_t 的 0..3。

4. 读 keysym

回到 xkb_state_key_get_syms(),从 leveli 读出结果并返回个数:

const xkb_keysym_count_t num_syms = leveli->num_syms;
*syms_out = (num_syms > 1) ? leveli->s.syms : &leveli->s.sym;
return (int) num_syms;

Caps 生效时指针改指向预算好的大写形式(leveli->upper 或 s.syms + num_syms),数量不变。

Example

以 q 键(<AD01>)和 2 键(<AE02>)为例,枚举不同修饰键组合下选中的 level 与 keysym

按下 Shift+q(keycode 24)时,顺着上节的四层查询走一遍:

  1. XkbKey() 查得 <AD01>,state->components.group 解析出 layout 0(us);
  2. q 的 type 是 ALPHABETIC,active_mods = Shift & (Shift|Lock) = Shift,在 map 表里命中 Shift 项;
  3. 该项指向 Level 2(index 1),取出 levels[1];
  4. 读出 keysym Q。

US layout — 两级 level

q 是字母键(ALPHABETIC),2 是数字键(TWO_LEVEL)。 TWO_LEVEL 的 mask 不含 Lock,所以 CapsLock 只对 q 生效:

修饰键q2
无1 → q1 → 2
Shift2 → Q2 → @
CapsLock2 → Q1 → 2
Shift + CapsLock1 → q2 → @

DE layout — 四级 level

多了 AltGr(LevelThree),q 变成 FOUR_LEVEL_SEMIALPHABETIC,2 变成 FOUR_LEVEL:

修饰键q2
无1 → q1 → 2
Shift2 → Q2 → “
AltGr3 → @3 → ²
AltGr + Shift4 → Ω4 → ⅛