XKB Key & Wayland
看到 Treeland 在修改这部分的逻辑,想起来自己这里也没弄明白,于是开始看。
- https://github.com/linuxdeepin/treeland/pull/1406
- https://github.com/xkbcommon/libxkbcommon
- https://xkbcommon.org/
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)时,顺着上节的四层查询走一遍:
XkbKey()查得<AD01>,state->components.group解析出 layout 0(us);- q 的 type 是
ALPHABETIC,active_mods = Shift & (Shift|Lock) = Shift,在map表里命中 Shift 项; - 该项指向 Level 2(index 1),取出
levels[1]; - 读出 keysym
Q。
US layout — 两级 level
q 是字母键(ALPHABETIC),2 是数字键(TWO_LEVEL)。
TWO_LEVEL 的 mask 不含 Lock,所以 CapsLock 只对 q 生效:
| 修饰键 | q | 2 |
|---|---|---|
| 无 | 1 → q | 1 → 2 |
| Shift | 2 → Q | 2 → @ |
| CapsLock | 2 → Q | 1 → 2 |
| Shift + CapsLock | 1 → q | 2 → @ |
DE layout — 四级 level
多了 AltGr(LevelThree),q 变成 FOUR_LEVEL_SEMIALPHABETIC,2 变成 FOUR_LEVEL:
| 修饰键 | q | 2 |
|---|---|---|
| 无 | 1 → q | 1 → 2 |
| Shift | 2 → Q | 2 → “ |
| AltGr | 3 → @ | 3 → ² |
| AltGr + Shift | 4 → Ω | 4 → ⅛ |