Linux下的修改键盘映射

最近买了一个chromebook,破解后安装了Linux。所有功能都很好,唯独键盘有些问题。了解过chromebook的都知道,它的键盘跟普通的笔记本键盘是不一样的。缺少Win键和F1~F12。其中Win键还好说,因为它有个Search键,可以当Win键使用,只是位置不一样而已。但是,F1~F12位置是各种功能键。平常还好说,如果写个程序啥的就非常不方便了。想按F5调试都不行。

于是,想着把F1~F12给重新映射回来。主要是两种方式,一个是通过xmodmap,另一个是通过Linux内核中input驱动的ioctl。

scancode和keycode

scancode是硬件提供的扫描码。不同的键盘,其扫描码可能会不一样。所以,软件并不会使用键盘的扫描码。而是使用keycode。由键盘驱动程序将scancode翻译成keycode。所以,想要改键盘映射,只需要改一下两者的映射关系就可以了。

首先,我们使用evtest这个工具查看键盘的扫描码和键位码。在linux中,每个输入设备,都会有一个设备文件。物理是键盘、鼠标、触摸屏、触摸板等等。运行evtest后,首先选择键盘的设备文件,通常会有keyboard字样。实在找不到还可以逐个尝试。哪个敲击键盘后有数据打印出来,哪个就是了。

image-20240325162752649

在我这个笔记本上是/dev/input/event2,于是,我选择2

然后,在按键盘时就会看到下面的输出:

Event: time 1711356056.489203, -------------- SYN_REPORT ------------
Event: time 1711356057.937835, type 4 (EV_MSC), code 4 (MSC_SCAN), value b0
Event: time 1711356057.937835, type 1 (EV_KEY), code 115 (KEY_VOLUMEUP), value 1
Event: time 1711356057.937835, -------------- SYN_REPORT ------------
Event: time 1711356058.053976, type 4 (EV_MSC), code 4 (MSC_SCAN), value b0
Event: time 1711356058.053976, type 1 (EV_KEY), code 115 (KEY_VOLUMEUP), value 0
Event: time 1711356058.053976, -------------- SYN_REPORT ------------

其中,

  • EV_MSC这一行最后的就是扫描码
  • EV_KEY这一行中包含的键盘码。

例如,我这儿按下了一下“音量上调”按键,其中,扫描码是:b0,键盘码是:115

通过这种方法我们就可以把所有想要映射掉的按键信息查到。一定要注意,每款硬件的扫描码是不一样的

拿到这个信息后,接下来就是实现键盘映射了。

使用xmodmap修改键盘映射

xmodmap 是一个在 X 图形环境下用于修改键盘和鼠标按钮映射的工具。通过写一个配置文件即可实现键盘映射。具体的用法可以搜索一下。注意,这个支持x11,如果使用的wayland,就不用尝试了。

配置文件

首先,使用上面查询到的keycode,编写一个xmodmap的配置文件chrome_keyboard.map

例如:

keycode 166 = XF86Back F1 F1 F1 F1 F1 XF86Switch_VT_1
keycode 181 = XF86Reload F2 F2 F2 F2 F2 XF86Switch_VT_2
keycode 380 = F3 F3 F3 F3
keycode 128 = XF86LaunchA F3 F3 F3
keycode 107 = Print F4 F4 F4
keycode 232 = XF86MonBrightnessDown F5 F5 F5
keycode 233 = XF86MonBrightnessUp F6 F6 F6
keycode 237 = XF86KbdBrightnessDown F7 F7 F7
keycode 238 = XF86KbdBrightnessUp F8 F8 F8
keycode 172 = XF86AudioPlay F9 F9 F9
keycode 121 = XF86AudioMute F10 F10 F10
keycode 122 = XF86AudioLowerVolume F11 F11 F11
keycode 123 = XF86AudioRaiseVolume F12 F12 F12
keycode 150 = XF86Sleep Delete Delete Delete
keycode 133 = Super_L NoSymbol Super_L


keycode 111 = Up NoSymbol Prior
keycode 116 = Down NoSymbol Next
keycode 113 = Left NoSymbol Home
keycode 114 = Right NoSymbol End

keycode  22 = BackSpace BackSpace Delete Delete

clear control
keycode  37 = Control_L NoSymbol Control_L
! keycode 105 = Control_R NoSymbol Control_R
keycode  105 = Mode_switch NoSymbol NoSymbol
add control = Control_L

配置生效

然后,使用xmodmap chrome_keyboard.map即可生效了。

规则是:

  1. 把右边的Ctrl键改成了Mode_switch

  2. Mode_switch+功能键改成了F1~F12、Delete,把Mode_switch+上下左右改成了PageUp、PageDown、Home和End。

开机自动生效

接下来,就是怎么才能开机自动生效了。因为这个是X11的键盘映射方法,我们可以把它放在shell的启动文件中实现自启动。比如,把命令写入$HOME/.bashrc或者$HOME/.zshrc中。

实际使用时就会发现,可能经常会不生效。这是因为系统启动的时候也会改键盘映射。我们改过以后又被改回去了。于是,写了一个脚本,监控这个改动。脚本.chrome_keyboard.sh内容如下:

#!/bin/bash

function change_key()
{
  if [ -f $HOME/.chrome_keyboard.map ]; then
      date >> $HOME/.chrome_keyboard.log
      echo "xxxx $1" >> $HOME/.chrome_keyboard.log
      xmodmap $HOME/.chrome_keyboard.map >> $HOME/.chrome_keyboard.log 
      echo "xxxx ret=$?" >> $HOME/.chrome_keyboard.log
  fi
}
rm $HOME/.mykey.log
count=0
while [ $count -lt 120 ]; do
    xmodmap -pke|grep 166|grep F1 >/dev/null 2>&1
    if [ $? -ne 0 ]; then
        change_key $count
    fi
    sleep 3
    count=$(($count+1))
done

然后,在.bashrc或者.zshrc里面后台启动一下这个脚本即可。

.....
bash $HOME/.chrome_keyboard.sh
.....

按说,到这一步基本就完美了。然而事与愿违。在实际使用中发现,键盘偶尔会乱掉。比如本来是Left_Ctrl+UpPageUp,结果变成了UpPageUp,Left_Ctrl+UpUp。这就很难受了。查了好久也没有查到原因,只好放弃。

使用Input更改键盘映射

我们前面提到硬件提供扫描码,软件使用键盘码。扫描码和键盘码之间的映射关系是有内核驱动完成的。而在Linux内核中,有一个input子系统,通过它,我们可以改变键盘映射的规则。在input子系统的ioctl中就提供了相关的功能。

image-20240325173936996

其中,EVIOCSKEYCODEEVIOCSKEYCODE_V2就是用来实现这个功能的。

程序源码

这个小程序的源码如下:

#include <errno.h>
#include <fcntl.h>
#include <linux/input.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

static const char *kbd_event_file = "/dev/input/event2";

static struct
{
    unsigned int scancode;
    unsigned int keycode;
} keys[] = {
    {0xea, KEY_F1},     // 后退          ==> F1
    {0xe7, KEY_F2},     // 刷新          ==> F2
    {0x91, KEY_F3},     // 全屏          ==> F3
    {0x92, KEY_F4},     // 缩小          ==> F4
    {0x93, KEY_F5},     // 截屏          ==> F5
    {0x94, KEY_F6},     // 屏幕亮度降低   ==> F6
    {0x95, KEY_F7},     // 屏幕亮度提升   ==> F7
    {0x97, KEY_F8},     // 键盘亮度降低   ==> F8
    {0x98, KEY_F9},     // 键盘亮度提升   ==> F9
    {0x9a, KEY_F10},    // 播放          ==> F10
    {0xa0, KEY_F11},    // 静音          ==> F11
    {0xae, KEY_F12},    // 音量降低       ==> F12
    {0xb0, KEY_DELETE}, // 音量提升       ==> Delete
};

int main()
{
    int fd, nkeys, i;
    unsigned int buf[2];

    fd = open(kbd_event_file, O_RDWR);
    if (fd < 0)
    {
        fprintf(stderr, "open: %s (%s)\n", kbd_event_file, strerror(errno));
        return 1;
    }
    // 通过ioctl更改键盘映射
    nkeys = sizeof(keys) / sizeof(keys[0]);
    for (i = 0; i < nkeys; i++)
    {
        buf[0] = keys[i].scancode;
        buf[1] = keys[i].keycode;
        if (ioctl(fd, EVIOCSKEYCODE, buf) < 0)
        {
            fprintf(stderr, "ioctl: %x -> %u (%s)\n", buf[0], buf[1],
                    strerror(errno));
        }
    }
    // 使用udevadm使键盘映射生效
    {
        char cmd[128];
        sprintf(cmd, "udevadm trigger %s", kbd_event_file);
        system(cmd);
    }
    return 0;
}

使用命令gcc chrome_keyboard.c -o chrome_keyboard即可编译。

需要留意的是:

1、设备文件要根据自己机器上的情况修改。

2、scancode也需要根据自己设备的情况修改。

开机自启动

因为这个是基于内核实现的键盘映射,所以,可以通过系统服务来实现。

首先,将编译好的文件复制到系统目录。

cp chrome_keyboard /sbin

然后,编写一个service文件,chrome_keyboard.service,内容如下:

[Unit]
Description=change chrome keyboard layout to pc, such as F1~F12 
After=syslog.target network.target remote-fs.target nss-lookup.target

[Service]
Type=simple
ExecStart=/sbin/chrome_keyboard
# ExecReload=target_dir/restart.sh 
# ExecStop=target_dir/shutdown.sh
SuccessExitStatus=0

[Install]
WantedBy=multi-user.target

最后,让服务生效。

cp chrome_keyboard.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable chrome_keyboard
systemctl start chrome_keyboard