☰
Wayland下用wev定位keycode,完成按键重映射的完整指南
2026/9/28 5:45:37 网站建设 项目流程

先说个实际场景:我的主力键盘右Alt键在TKL配列上位置很别扭,Ctrl+Alt组合操作得把手拧成麻花,索性想把右Alt改成Compose键。在X11时代这事儿很简单,xmodmap加xev两件套就能搞定。但换到Hyprland之后,我很长时间没绕过来——直到搞清楚wev这个Wayland事件查看器的玩法。

如果你也在Hyprland上折腾按键重映射,或者某个键坏了想换键顶、想给特殊按键重新定义功能,这篇内容能帮你把"检测按键事件—拿到扫描码—修改映射—验证结果"整条链路一次性跑通。整个过程不依赖X11,纯Wayland生态内的方案,这也是现在很多从X11迁过来的人最懵的地方。

1. 先搞清楚你拿到的是"扫描码"还是"键码":wev输出里的三层编码体系

用wev之前,建议先把Linux输入链路上的几个概念理清楚。很多人在这一步就栽了,拿着xev里的老经验套Wayland,结果配置怎么都不生效。

1.1 从物理按键到应用层:evdev扫描码到内核键码再到Keysym

物理按键被按下的瞬间,键盘控制器会生成一个硬件扫描码(Hardware Scan Code),这个值跟键盘的具体电路设计有关,同一把键盘上不同位置的键对应不同数值。Linux内核拿到硬件扫描码之后,会查表把它翻译成标准键码(Keycode),我们常说的KEY_A、KEY_LEFTALT就是这个层面的东西。再往上,XKB层会把键码继续翻译成Keysym(符号),也就是应用层最终看到的字符或功能名。

需要强调一个关键点:wev显示出来的并不是硬件扫描码,而是内核翻译后的键码。不过在Linux生态里,对于标准键盘,内核的翻译表基本就是一一对应,所以大家习惯性把"扫描码"和"键码"混着叫。真正改的时候,你改的也是键码层面的映射。

理解这三层有什么好处?当你说"我要修改物理按键的扫描码"时,实际上有几个不同层级的修改位置:

层级修改工具生效范围修改对象
内核键码层udev hwdb全局,所有应用硬件扫描码->内核键码
用户态键码层keyd全局,用户态键码->键码
合成器/配置层Hyprland bind仅Hyprland内键码->功能/按键
XKB层xkb_options等按布局/应用键码->Keysym

大部分情况下,你只需要改最外层的逻辑映射就够了。"物理扫描码"这种说法更多是个口误,真正需求是让某个物理位置上的键触发另外一个键的行为。

1.2 wev输出逐行拆解:读懂Keycode与Keysym的对应关系

wev的输出长这样:

[14:36:28:06] wl_keyboard.keymap: [14:36:28:06] wl_keyboard.enter: [14:36:28:06] wl_keyboard.enter.surface: 1 [14:36:28:06] wl_keyboard.enter.serial: 123456 [14:36:28:06] event: wl_keyboard.key [14:36:28:06] wl_keyboard.key.surface: 1 [14:36:28:06] wl_keyboard.key.serial: 123458 [14:36:28:06] wl_keyboard.key.time: 11234 [14:36:28:06] wl_keyboard.key.keycode: 56 [14:36:28:06] wl_keyboard.key.state: 1 (press) [14:36:28:06] wl_keyboard.key.state: 1 (updated state) [14:36:28:06] wl_keyboard.modifiers: [14:36:28:06] wl_keyboard.modifiers.mods: 0 [14:36:28:06] wl_keyboard.modifiers.locked: 0 [14:36:28:06] wl_keyboard.modifiers.group: 0 [14:36:28:06] wl_keyboard.modifiers.mods: 0 (Depressed) [14:36:28:06] wl_keyboard.modifiers.locked: 0 (Latched) [14:36:28:06] wl_keyboard.modifiers.locked: 0 (Locked) [14:36:28:06] wl_keyboard.modifiers.group: 0 (Layout) [14:36:28:06] libinput.device.name: "AT Translated Set 2 keyboard" [14:36:28:06] libinput.device.capabilities: keyboard

这里最核心的两行是wl_keyboard.key.keycode和wl_keyboard.key.state。keycode就是内核键码,state=1表示按下,state=0表示抬起。上面示例里keycode是56,对应KEY_LEFTALT。你要改哪个键,就用wev按一下那个键,看它在哪个keycode上。

注意wev还会打印libinput.device.name,这个字段特别有用——它可以帮你确认事件到底来自哪个输入设备。我见过有人在笔记本上插着外接键盘,结果一直看内置键盘的事件,配置写到外接键盘上当然不生效。

1.3 常见误解:为什么同一个键在不同布局下显示不同字符

很多人会问:"我按了一下字母A,wev输出里的keysym怎么是a而不是A?"这是因为wev默认打印的是按下瞬间状态下的符号输出,没有Shift组合的时候自然就是小写。还有更迷惑的情况:如果键盘布局是Dvorak或者Colemak,按物理位置上的A键,keysym可能显示的是其他字母。

这里给你一个建议:判断物理按键应该看keycode,而不是看keysym。keycode是和物理位置强相关的,keysym是和布局相关的。你改扫描码/键码映射,本质上是改物理位置对应的行为,所以拿keycode当锚点最可靠。

2. 用wev当"放大镜":安装、运行与踩坑记录

wev本质是一个极简的Wayland协议客户端。它的工作方式很直接:连接Wayland socket,订阅wl_keyboard事件,然后把收到的每一个键盘事件格式化打印到终端。因为Wayland走的是进程间通信的socket协议,它没有X11那套全局输入转发机制,所以wev只能显示"自己窗口获得键盘焦点时"的事件。这既是限制也是优势——它不会像xev那样把整个桌面的按键全部打出来,输出非常干净。

2.1 安装方式:Arch/Ubuntu/NixOS一句话搞定

主流发行版仓库里都有这个包,安装很顺手:

# Arch / Manjaro sudo pacman -S wev # Ubuntu / Debian sudo apt install wev # Fedora sudo dnf install wev # NixOS / home-manager nix profile install nixpkgs#wev

如果你用的发行版仓库里没有,也可以直接从源码编译,依赖只有wayland和wayland-protocols两个。编译不过十秒钟,不需要额外折腾。

源码编译的路径:

git clone https://gitlab.freedesktop.org/wayland/tools/wev cd wev meson setup build ninja -C build sudo ninja -C build install

2.2 运行姿势:从Hyprland终端里启动和从TTY启动结果不一样

wev比较调皮的一点是,它的输出取决于它自己是否持有键盘焦点。你在Hyprland的终端里运行,按下某键时,Hyprland自己先接收到键,然后把事件转发给焦点窗口——也就是你的终端,终端的进程拿到后交给wev,wev再打印出来。

所以只要wev所在的窗口有焦点,你按下的键就会出现在输出里。但有两个情况需要单独处理:

  • 如果你把wev跑在一个没有焦点的终端里(比如窗口被切走了),按键盘它没反应。
  • 如果你在Hyprland里绑定了某个键作为全局快捷键(比如SUPER+Return启动终端),wev能看到这个键事件吗?能看到一部分。因为Hyprland是合成器,它先拿到原生事件,然后才决定是发给客户端还是自己保留。wev作为客户端能收到的是"合成器决定发给它"的事件。如果Hyprland内部把某个快捷键消费掉了,没有转发给焦点客户端,wev其实也能收到原始事件的一个"影子"——从输出格式上你能看到相同的keycode,但可能没有后续的keysym处理。

我自己用得比较多的一种姿势是:在Hyprland的浮动窗口里开一个特小号的终端跑wev,然后设一个临时快捷键切换focus到这个窗口,避免焦点跑掉。你也可以在TUI终端比如foot、kitty里启动,它们对Wayland事件的处理比较标准。

2.3 版本差异:老协议版本和新版本输出字段不一样

如果你在Debian稳定版仓库装到的wev版本比较老,输出格式会和上游最新版有差别。早期版本只打wl_keyboard.key相关字段,新版才加了libinput.device.name、libinput.device.capabilities这些设备信息字段。

建议尽量用新版。判断依据很简单:跑一下wev --version。如果版本号低于2.0,说明仓库里的比较老,要么换源要么自己编译新版。因为判断"事件来自哪个设备"这个能力在重映射键盘时太关键了,老版本做不到。

另外一个实用技巧:wev默认会持续运行直到你按Ctrl+C退出。如果只想看单个键按下和抬起两个事件,按完键之后立即用Ctrl+C终止,输出会留在终端里方便你慢慢看。没必要让它一直挂着,浪费终端空间。

3. 修改扫描码的三个层次:从临时remap到内核级hwdb

拿到wev输出的keycode之后,接下来就是选择修改方案。我先说说为什么不在Hyprland的binds里直接改,然后再给真正能修改物理按键映射的路线。

3.1 为什么Hyprland的bind只能算"表面功夫"

很多人第一反应是:既然我用Hyprland,直接bind = , code:56, exec, ...不就行了?这个思路没错,但它改的是"Hyprland收到这个按键后干什么",不是"系统收到这个按键后它是什么"。

举个例子,你用Hyprland把右Alt(keycode 108)绑成了Compose功能,那么当你有另一个应用开着、焦点不在Hyprland管理的窗口上时(比如某个全屏的SDL游戏),Wayland合成器可能把输入事件直通给那个应用,Hyprland的bind根本不会生效。再有,如果你在终端里用vim,vim接收到的是"右Alt被按下"这个原始事件,它不会知道Hyprland把它改成了Compose。

所以Hyprland的bind适合做快捷键绑定,不适合做"按键重映射"。重映射要做到系统层面,往前看一下层。

3.2 keyd:用户态全局重映射,开箱即用

keyd是一个非常流行的用户态按键重映射守护进程。它监听/dev/input/设备上的原始事件,在用户空间完成键码到键码的翻译,然后通过uinput创建虚拟设备把改后的事件注入系统。

使用体验上,keyd的配置文件非常舒服:

# /etc/keyd/default.conf [ids] * [main] # 把右Alt(keycode 108)映射成左Ctrl(keycode 29) rightalt = leftctrl # 把大写锁定映射成Esc,长按是Ctrl(经典HHKB方案) capslock = overload(control, esc) # 交换左右Ctrl键 leftctrl = rightctrl rightctrl = leftctrl

配置完执行:

sudo systemctl enable --now keyd sudo keyd reload

keyd识别按键用的是键名字,比如rightalt、capslock、kpenter。你在配置文件里写的是逻辑键名,如果记不住,可以看/usr/include/linux/input-event-codes.h头文件里的宏定义,KEY_前缀去掉转小写就是键名。

关键优势是:keyd工作在用户态,对所有Wayland客户端、XWayland窗口、甚至命令行程序都生效,因为它在更底层的事件源上做了翻译。缺点是它需要常驻一个守护进程,如果你的环境极简、连systemd都没有,那就不太方便。

3.3 udev hwdb:内核级修改,真正改"扫描码"

如果你希望修改在驱动层面就完成,连用户态守护进程都不要,那就用udev hwdb。

hwdb的全称是Hardware Database,内核通过它来维护"硬件特性->属性"的映射关系。对于键盘来说,你可以通过hwdb告诉内核:某个厂商、某个型号的键盘,它的某个物理扫描码应该翻译成哪个标准键码。

配置文件样例:

# /etc/udev/hwdb.d/99-keyboard-remap.hwdb # 匹配键盘设备 evdev:input:b0003v1234p5678* KEYBOARD_KEY_7000e=leftctrl

这里b0003是USB总线(0003=USB),v1234是厂商ID,p5678是产品ID,7000e是物理按键的扫描码,leftctrl是想要映射成的键码。配置完需要更新数据库:

sudo systemd-hwdb update sudo udevadm trigger

hwdb是内核级方案,理论上最"硬核"。它有很强的过滤能力,可以精准匹配到某一把键盘,不会影响别的设备。但是它的配置语法更底层,查扫描码也比较麻烦,一般键盘厂商不公开扫描码表,得自己用evtest或者evemu-record去抓。

对于普通玩家,我的排序建议是:keyd优先,hwdb备选,Hyprland bind最后。理由后文实战部分详细说。

4. 实战:用一个"坏了的键"走完检测到修改再验证的全流程

下面用真实案例把整条链路串起来。假设我现在要做的需求是:把笔记本自带键盘左边的Win键(keycode 125)改成Alt键(keycode 56的兄弟左Alt)。原因是我不小心把左Alt拆坏了,临时顶一下,或者纯粹是觉得这个位置放Win键没用。

4.1 第一步:用wev锁定物理按键的keycode

在Hyprland里启动一个终端,运行:

wev

然后按一下左边那个Win键。输出里找到:

wl_keyboard.key.keycode: 125 libinput.device.name: "AT Translated Set 2 keyboard"

这就拿到了两个关键信息:keycode是125,设备是内置键盘。如果只想临时实验,wev确认后按Ctrl+C退出。

注意一点:这里125这个值,在不同键盘上不一定一样。很多USB键盘的左Win键也是125,但个别厂商自定义键盘可能不同。永远以wev实际输出为准,不要拿网上教程里的数字硬套。

4.2 第二步:选keyd实现全局重映射

编辑/etc/keyd/default.conf:

[ids] * [main] # 左Win键映射成左Alt leftmeta = leftalt

然后重载:

sudo keyd reload

马上验证:在终端里按左Win+Tab,如果alt键失效之前这个组合能切的程序切不了,或者改了之后左Win的打开开始菜单行为消失了,说明映射生效。

如果你不想全局生效,只在一部分应用里生效,keyd还支持按应用映射:

[main] leftmeta = layer(meta) [meta] # 在这个层里,Win+空格变成Ctrl+空格 space = leftctrl-space

4.3 第三步:用hwdb做内核级修改(替代方案)

如果你想连keyd都不装,直接用hwdb改写:

先evtest看清楚设备的物理扫描码。evtest会直接告诉你哪个扫描码被按下时触发哪个事件码。假设你查到设备上左Win键触发的是KEY_LEFTMETA的扫描码为0x700e3。

在/etc/udev/hwdb.d/99-keyboard-remap.hwdb里写:

evdev:input:b0003v1234p5678* KEYBOARD_KEY_700e3=leftalt

注意这里的700e3是十六进制扫描码,leftalt是要映射成的目标键码。不同设备b/v/p值不一样,你需要根据自己设备改。查看可以用:

udevadm info -a -n /dev/input/event5 | grep -E "ATTRS{name}|ATTRS{idVendor}|ATTRS{idProduct}"

然后更新并触发:

sudo systemd-hwdb update sudo udevadm trigger

之后需要重新插拔键盘或者重启才完全生效。相比keyd的秒级生效,hwdb最大缺点就是验证链路长。

4.4 第四步:wev验证改码结果

修改完成后,重新跑wev,再按同一个物理按键。此时输出应该变成:

wl_keyboard.key.keycode: 56 libinput.device.name: "AT Translated Set 2 keyboard"

如果keycode从125变成了56,说明映射已经生效。这一步是最直观的验收方式,比"看起来好像能用"可靠得多。

4.5 出错了怎么回滚

keyd方案出错很简单:直接把配置文件里那行删掉,sudo keyd reload就回来了。hwdb方案稍微麻烦一点,删掉hwdb文件后,需要:

sudo systemd-hwdb update sudo udevadm trigger

然后重启或者重新插拔设备。如果配置错误导致某些键完全没反应,别慌,拔掉键盘重插一下,绝大部分情况都能恢复。

5. 实际使用中的几个细节坑(经验补充)

5.1 蓝牙键盘和外接键盘的keycode差异

同一款蓝牙键盘和USB键盘,即使物理布局一样,keycode也可能不一样。原因在于蓝牙HID协议和USB HID协议上报的扫描码不同,内核翻译表也不同。特别是笔记本内置键盘,它走的是AT/PS2协议,跟USB HID的扫描码完全不是一套表。

所以改键的时候一定要针对"某个设备"来改,而不是笼统地"把125改成56"。keyd默认是全局改,如果你想精细化控制,[ids]段要写对应的设备型号:

[ids] 0003:1234:5678 * # 这是兜底规则,别的设备走这个

hwdb天然支持evdev:input:b...格式的设备过滤,这就是它的强项。

5.2 keyd和Hyprland快捷键的冲突处理

keyd改完按键之后,Hyprland那边如果已经绑定了某个按键的快捷键,而且这个按键正是你改过的,可能两边都要触发。比如你把CapsLock改成了Esc,Hyprland里又绑定了bind = , CAPS, workspace, 1,这种配置就会很混乱。

经验是:重映射之后,Hyprland配置里尽量别重复绑定原键。可以在keyd配置里把原键"改掉",Hyprland只处理改后的键。或者反过来:Hyprland配置里用宏定义,把重映射后的键绑定好,不改的键保持原样。

5.3 更新内核后hwdb失效

hwdb的数据库和内核的输入子系统版本是绑定的。升级内核之后,偶尔会因为数据库格式变化需要重新执行systemd-hwdb update。我在一次Kernel 6.2升6.5的过程中就碰到过:hwdb配置突然失效,后来发现是systemd版本也同时升级了,重新更新数据库后恢复。这里建议每次升级内核后都主动跑一次更新命令,免得突然某个键失灵摸不着头脑。

5.4 双系统注意:Windows和Linux的扫描码方案不能互通

如果你在Linux下用hwdb改了扫描码,然后重启进Windows,你会发现按键行为回到原始状态。Windows下改扫播码需要改注册表或用第三方工具(类似SharpKeys),两套体系互不影响。这也是为什么有些"改键党"干脆买可编程键盘,把改键逻辑写进键盘固件里。硬件方案才是真正全平台统一的方案。

5.5 wev看不到按键事件的排查办法

如果你运行wev之后按键盘完全没输出,先检查两个东西:

  1. wev所在窗口是否持有焦点,在Hyprland的多窗口场景下焦点容易跑掉。
  2. 终端本身是否还在正常工作,先敲几个字母看有没有回显。

如果确认焦点没问题、终端正常但还是没反应,检查一下是否被Hyprland的按键绑定拦截了。有个小技巧:按一次某个键,观察Hyprland侧边栏有没有响应,如果响应了说明事件被合成器消费了,wev只能收到后续的释放事件或者干脆收不到完整序列。这种情况不是wev坏了,是输入事件分发机制决定的。你可以临时切到一个不绑定快捷键的TTY里跑wev,或者用evtest验证原始设备事件。

回到开头那个问题:在Wayland时代,按键重映射没有那么玄学,核心思路是"先精准定位keycode,再选一个层级的工具去改"。我个人实际用下来,keyd的性价比最高,因为它在用户态工作,配置直观、立即生效、回滚容易,也方便随时用wev快速验证。hwdb适合那些追求干净、不想常驻额外进程的人,优点是彻底,缺点是调试链路长。Hyprland自己的bind适合做快捷键,不适合做真正的按键重映射。

最后再分享一个日常小习惯:每次改完键盘映射,我都会专门用wev把改过的键重新按一遍,并把输出截图存下来。一方面方便日后排查,另一方面也方便对比验证——毕竟键盘映射这种东西,出问题时你根本想不起来之前到底改过什么。这个习惯帮我省了不少事,建议你也试试。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询