键位映射问题

vim 一直是我的主力编辑器,但其实我之前每天使用编辑器的时间并不是很多,并且每次的使用时间也不是很长,在最近编辑文件和写脚本的时间比较多,发现之前键盘映射的一些问题

由于 esc 键在最左上,如果使用 vim 的话需要频繁按 esc 键盘,这样的话左小拇指就会很累,手动的幅度也比较大,众所周知,vim 用户都很 lazy,为了手的动作幅度能够尽可能小,想出了各种各样的键位和快捷键的映射,我之前就是在编辑模式下把 jj 映射成了 esc,但是最近发现一个问题,由于 fcitx5 默认配置的剪切板 的快捷键是 ctrl+; 在 vim 中偶尔会误触按到 fcitx5 的剪切板,这个时候 jj 是没法当作 esc 来使用关闭掉剪切板的,而且使用 jj 来映射的时候,如果你只是想输入 j 字母的时候,vim 会等待一段时间来看你是想输入 j 字符还是要使用映射的键位,虽然这个时间是可以来调整的,但是还是会有视觉上的卡顿,所以,我还是打算放弃掉这个映射,来使用其他键来映射

我的想法是把 esc 映射 capslock, capslock 映射 ctrl, ctrl 映射 esc

通过 Google 搜索到的结果,很多人给出了使用 setxkbmap/xmodmap 来实现,但是这个方案也有各种各样的问题,并且这个只能在 xorg 中使用,如果在 console 中使用,还需要再为 console 单独配置

虽然这个方案被我放弃了,但是我再搜索这个方案的时候看到了依云老师关于键盘映射的解决方案,使用 hwdb 和可编程的键盘,我的 keychron k6 不支持编程,所以当然是使用 hwdb 来实现啦

关于 hwdb

hwdbudev 提供的一个内置函数,用于维护 /etc/udev/hwdb.bin 中的硬件数据库索引,我们可以通过自定义 hwdb 配置文件来实现更底层的键位映射,从而可以同时在 xorg / console 中生效,而且没有那么多的问题

hwdb 可以通过修改 scancodeskeycodes 的映射来实现键位映射

关于 scancodes 和 keycodes

扫描码(scancode)是一个键的最小识别 ID。如果一个键没有扫描码值,我们无法做任何事,因为内核看不到它。

键位码(keycode)是一个键的第二级识别 ID,对应到一个函数。

符号(symbol)是一个键的第三级识别 ID,Xorg 通过该 ID 引用按键。

我们在键盘输入时,键盘发送键位的 scancode 给 内核的键盘驱动,除了可编程键盘外,键位的 scancode 一般来说都是固定的,然后 linux 内核将 scancode 映射到 keycode,然后根据你的键盘布局的键码映射到一个符号或键符,当然,这只是简要描述,只是为了让你了解之后我们要做的事情

改变 scancode 映射的 keycode

  1. 安装 evtest 包

    我们使用 evtest 来获取当前要修改的键位的 scancode,然后修改该键位映射的 keycode

    evtest 这个包在大部分发行版上可能都没有安装,我使用的是 Arch Linux,首先安装这个包

    1
    
    sudo pacman -S evtest
    

    如果你使用的是 Debian 和其衍生发行版

    1
    
    sudo apt install evtest
    
  2. 找到你的键盘设备

    通过 cat /proc/bus/input/devices 列出当前的输入设备,然后找到你的键盘设备

    一把都可以通过 Name 字段来找到你的设备型号,以下是截取部分我的输出结果

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    
    I: Bus=0003 Vendor=046d Product=4074 Version=0111
    N: Name="Logitech G304"
    P: Phys=usb-0000:03:00.3-3/input2:1
    S: Sysfs=/devices/pci0000:00/0000:00:08.1/0000:03:00.3/usb1/1-3/1-3:1.2/0003:046D:C53F.0004/0003:046D:4074.0005/input/input26
    U: Uniq=4074-32-9b-8e-5f
    H: Handlers=sysrq kbd leds event11 mouse2 
    B: PROP=0
    B: EV=120017
    B: KEY=ffff0000 1000000000007 ff9f207ac14057ff febeffdfffefffff fffffffffffffffe
    B: REL=1943
    B: MSC=10
    B: LED=1f
    
    I: Bus=0005 Vendor=05ac Product=024f Version=011b
    N: Name="Keychron K6"
    P: Phys=80:30:49:4e:60:e8
    S: Sysfs=/devices/pci0000:00/0000:00:08.1/0000:03:00.4/usb3/3-2/3-2:1.0/bluetooth/hci0/hci0:2/0005:05AC:024F.0007/input/input30
    U: Uniq=dc:2c:26:05:c3:f8
    H: Handlers=sysrq kbd leds event12 
    B: PROP=0
    B: EV=12001f
    B: KEY=3f000303ff 0 10000 483ffff17aff32d bfd4444600000000 1 130ffb8b17d007 ffff7bfad9415fff ffbeffdfffefffff fffffffffffffffe
    B: REL=1040
    B: ABS=10100000000
    B: MSC=10
    B: LED=1f
    

    可以看到第一个 Name 的值是 Logitech G304,是我的无线鼠标 罗技g304,第二个就是我的键盘设备,keychron k6

    我们只需要关注该键盘的 bus id, vendor id,product id , version id 和 event id ,也就是第二段的第一行 Bus=0005 Vendor=05ac Product=024f Version=011bevent id 在 H: 开头的那一行,可以看到我的键盘 event id12

  3. 获取到你要修改的键位的 scancode

    执行 sudo evtest /dev/input/event<id> #使用上一步的 event id 替换命令里的 id

    然后分别按下你要修改的键,比如我按的 esccapslockctrl,输出结果

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    
    ...
    ...
    ...
    Event: time 1636561197.770309, -------------- SYN_REPORT ------------
    Event: time 1636561207.561382, type 4 (EV_MSC), code 4 (MSC_SCAN), value 70029
    Event: time 1636561207.561382, type 1 (EV_KEY), code 58 (KEY_CAPSLOCK), value 1
    Event: time 1636561207.561382, -------------- SYN_REPORT ------------
    Event: time 1636561207.561771, type 17 (EV_LED), code 1 (LED_CAPSL), value 1
    Event: time 1636561207.561771, -------------- SYN_REPORT ------------
    Event: time 1636561207.594132, type 4 (EV_MSC), code 4 (MSC_SCAN), value 70029
    Event: time 1636561207.594132, type 1 (EV_KEY), code 58 (KEY_CAPSLOCK), value 0
    Event: time 1636561207.594132, -------------- SYN_REPORT ------------
    Event: time 1636561208.907878, type 4 (EV_MSC), code 4 (MSC_SCAN), value 70039
    Event: time 1636561208.907878, type 1 (EV_KEY), code 29 (KEY_LEFTCTRL), value 1
    Event: time 1636561208.907878, -------------- SYN_REPORT ------------
    Event: time 1636561208.952822, type 4 (EV_MSC), code 4 (MSC_SCAN), value 70039
    Event: time 1636561208.952822, type 1 (EV_KEY), code 29 (KEY_LEFTCTRL), value 0
    Event: time 1636561208.952822, -------------- SYN_REPORT ------------
    Event: time 1636561209.819355, type 4 (EV_MSC), code 4 (MSC_SCAN), value 700e0
    Event: time 1636561209.819355, type 1 (EV_KEY), code 1 (KEY_ESC), value 1
    Event: time 1636561209.819355, -------------- SYN_REPORT ------------
    Event: time 1636561209.875377, type 4 (EV_MSC), code 4 (MSC_SCAN), value 700e0
    Event: time 1636561209.875377, type 1 (EV_KEY), code 1 (KEY_ESC), value 0
    Event: time 1636561209.875377, -------------- SYN_REPORT ------------
    
    
    

    前两个 SYN_REPORT 分别代表着按下 esc 键的按下和释放的信息

    第三,四个是 capslock 的按下和释放的信息

    最后两个是 ctrl 的按下和释放的信息

    从中可以获取到我的键盘的 esc 键的 scancode70029capslock70039ctrl700e0

  4. 创建一个 hwdb 配置文件

    数据库是从位于/usr/lib/udev/hwdb.d//run/udev/hwdb.d//etc/udev/hwdb.d/ 目录中的扩展名为.hwdb的文件编译而成的。 默认的 “扫描码到键码” 映射文件是 /usr/lib/udev/hwdb.d/60-keyboard.hwdb

    我们不要去修改系统默认的键盘映射文件,我们在 /etc/udev/hwdb.d/ 新建一个格式为数字开头的后缀是.hwdb 的文件,数字代表了执行顺序,我们这里随便给一个就可以,比如 99_k6_keyboard.hwdb

    格式为

    1
    2
    3
    4
    
    evdev:input:b<bus_id>v<vendor_id>p<product_id>e<version_id>-<modalias> 
     KEYBOARD_KEY_<scancode>=<keycode>
     KEYBOARD_KEY_<scancode>=<keycode>
    
    

    其中 <bus_id>,<vendor_id>, <product_id><version_id> 是我们之前第二步里面通过 cat /proc/bus/input/devices 获取到的,<bus_id> 是4位数的十六进制总线ID,对于USB设备,应为0003,我的是蓝牙设备(k6 是蓝牙键盘),是 0005 ,不确定是否都一样,以你的实际输出结果为准,其他的都是 4 位十六进制大写(不够4位在前面补0)<modalias> 是描述设备功能的任意长度的输入模态 (input-modalias)

    注意一定要是大写,我之前就因为没有大写所以一直没有生效

    <scan_code> 的值是上一步通过 sudo evtest /dev/input/event<id> 获取的

    <keycode> 的值是小写的键名名称字符串,就像在/usr/include/linux/input-event-codes.h中列的一样(参阅KEY_*<KEYCODE>* 变量)

    我的配置文件为

    1
    2
    3
    4
    
    evdev:input:b0005v05ACp024Fe011B*	# 这里可以不用写这么多信息,可以使用通配符 * , 比如只写一个 evdev:input:b0005* 也是可以的,因为busid 是唯一的,不会重复的值
     KEYBOARD_KEY_70029=capslock	#70029 是 esc 的 scancode,这里让它映射为 capslock
     KEYBOARD_KEY_70039=leftctrl	#70039 是 capslock 的 scancode,这里让它映射为 leftctrl
     KEYBOARD_KEY_700e0=esc			#700e0 是 leftctrl 的 scancode,这里让它映射为 esc
    
  5. 更新硬件数据库索引 && 重新加载硬件数据库索引

    写好配置文件并保存后,更新硬件数据库索引

    sudo systemd-hwdb update

    然后重新加载硬件数据库索引,映射会立即生效

    sudo udevadm trigger

  6. 检查按键映射

    最好我们检查映射是否生效

    udevadm info /dev/input/event<id> | grep KEYBOARD_KEY

    这个命令的输出结果会显示你的键盘映射结果,有这些输出就代表已经生效了,比如我的

    1
    2
    3
    
    E: KEYBOARD_KEY_70029=capslock
    E: KEYBOARD_KEY_70039=leftctrl
    E: KEYBOARD_KEY_700e0=esc
    

参考文档

ArchWiki: Extra_keyboard_keys

ArchWiki: Map_scancodes_to_keycodes

ArchWiki: Keyboard_input