linux 键盘映射xremap
十二月 25, 2025 更新: 八月 09, 2026 [Linux 与系统] #xremapxremap
- xremap is a key remapper for Linux. Unlike xmodmap, it supports app-specific remapping and Wayland.
- xremap 和 evdev 都是 Linux 下的键盘重映射工具,主要区别在于 xremap 专注于应用层,提供更灵活的组合键和上下文感知(如窗口特定、虚拟修饰键)功能,而 evdev(底层驱动)则更侧重硬件/驱动层面的低级事件捕获与重映射,xremap 可以基于 evdev 实现高级功能,两者常结合使用,xremap 更易用,evdev 性能更低延迟。
cargo install xremap --features gnome # GNOME Wayland
查看当前 uinput 设备(需要 root)
xremap 使用 /dev/uinput 创建虚拟输入设备。异常退出可能导致设备未释放。
sudo cat /sys/class/misc/uinput/dev
# 查看 uinput 使用者(粗略判断是否被占用)
lsof /dev/uinput
# 或
fuser /dev/uinput
Running xremap without sudo
To do so, your normal user should be able to use evdev and uinput without sudo. In Ubuntu, this can be configured by running the following commands and rebooting your machine.
sudo gpasswd -a YOUR_USER input
echo 'KERNEL=="uinput", GROUP="input", TAG+="uaccess"' | sudo tee /etc/udev/rules.d/input.rules
lsmod | grep uinput
# If it shows up empty:
echo uinput | sudo tee /etc/modules-load.d/uinput.conf
Reboot the machine afterwards or try:
sudo modprobe uinput
sudo udevadm control --reload-rules && sudo udevadm trigger
安装为 systemd 用户服务(不用系统服务)
xremap(gnome 特性)走 DBus 与 GNOME Shell 扩展通信,随图形会话启动即可,用户级服务更合适(无需 root、能拿到会话环境)。
mkdir -p ~/.config/systemd/user
cat > ~/.config/systemd/user/my_keymap.service <<EOF
[Unit]
Description=Config my keymap
[Service]
Type=simple
ExecStart=/home/$USER/.cargo/bin/xremap /path/to/keymap.yml
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target
EOF
systemctl --user enable --now my_keymap
注意:用户服务由登录时启动的用户 systemd 管理器 fork,继承的是管理器的组。所以先 usermod -aG input $USER 然后必须重新登录,新组才会被用户管理器继承,服务才能读 /dev/input。可执行 awk '/Groups/' /proc/$(pgrep -u $USER -f 'systemd --user' | head -1)/status 确认。
从系统服务迁移到用户服务的完整操作
实际迁移时的操作清单(含坑):
# 1. 停用并删除旧系统服务(同时杀掉占用键盘的 root 进程)
sudo systemctl disable --now my_keymap
sudo rm /usr/lib/systemd/system/my_keymap.service
# 2. 加入 input 组(/dev/uinput 由 udev 规则归 input 组,一并解决)
sudo usermod -aG input $USER
# 3. 安装用户服务并启动
bash ~/.my_local/bin/my_keymap.sh # 生成服务文件 + enable + start
# 4. 临时方案:当前会话用户管理器未继承新组时,用 ACL 让服务立即工作
sudo setfacl -m u:$USER:rw /dev/input/event4 /dev/input/event7
systemctl --user start my_keymap
# 5. 持久方案:注销重登一次,用户管理器继承 input 组,之后无需 ACL
注意先后顺序:先清系统服务再加组。如果旧 root 进程还占着键盘,新服务会报 Device or resource busy;如果没加组就启动,会报 No input devices(重启循环)。两个错误按上面顺序可一次规避。
坑:loginctl enable-linger 会让"注销重登"失效
如果开了 linger(loginctl enable-linger),用户 systemd 管理器会常驻不随注销重启——重登只是新建会话,管理器还是老进程,新组永远不生效(awk '/Groups/' /proc/$(pgrep -u $USER -f 'systemd --user' | head -1)/status 里一直没有 input 组)。
# 排查:确认 linger 状态
loginctl show-user $USER | grep -i linger # Linger=yes 则中招
# 解决一:关掉 linger 后注销重登
sudo loginctl disable-linger $USER
# 解决二:保留 linger,直接重启用户管理器(会杀掉所有该用户的进程,包括当前会话)
sudo systemctl stop user@1000.service # 重登后管理器自动带新组
判断是否需要 linger:systemctl --user list-units 看有没有真正需要注销后常驻的服务(如备份 timer)。会话级服务(gnome-keyring、pipewire、lan-mouse)不需要 linger。
误区:linger 保不住从图形终端派生的 tmux
想靠 linger 让 tmux 在注销后存活?不行。tmux 若从 GNOME Terminal 启动,进程挂在 user@1000.service/app.slice/app-org.gnome.Terminal.slice/ 的 cgroup 里,注销时 GNOME Terminal 进程树被杀,tmux 随之退出,linger 拦不住。
# 看 tmux 在哪个 cgroup:结果在 app.slice/app-org.gnome.Terminal.slice 下就是图形终端派生的
cat /proc/$(pgrep -x tmux | tail -1)/cgroup
要让 tmux 注销后存活,需要独立于图形会话的 tmux server(如从 SSH 启动、或 server 脱离终端 daemon 化),而不是从 GNOME Terminal 里开。linger 对这类进程无能为力。
不需要 --watch
--watch 用于常驻场景:--watch device 热插拔自动抓新设备,--watch config 改配置自动 reload。用户服务(WantedBy=default.target、无 linger)注销时随会话停止、重登后全新进程重新枚举 /dev/input,不存在"残留旧 input fd"的问题,无需 --watch。改配置用 my_keymap.sh 重启即可(等价于 --watch config)。
免注销让新组立即生效(ACL 方案)
不想注销时,可直接给键盘设备节点加 ACL(服务进程按 uid 匹配 ACL,不依赖组)。只对当前设备节点有效,重启/重插键盘后失效,仅作临时手段。
sudo setfacl -m u:$USER:rw /dev/input/event4 /dev/input/event7
systemctl --user start my_keymap
坑:SupplementaryGroups= 对用户服务无效
在用户服务里写 SupplementaryGroups=input 会报 status=216/GROUP(Changing group credentials failed: Operation not permitted)。原因是用户管理器非 root,无权 setgroups() 给自己加它没有的组(只能丢弃组)。这个指令只对系统服务有效。
残留 root 进程占键盘 → Device or resource busy
曾用 root 系统服务跑 xremap,改用户服务后忘记清理,旧 root 进程一直 EVIOCGRAB 占着物理键盘,新用户服务启动时报:
Error: Device or resource busy (os error 16). Another program might have grabbed the device: 'SEMICO USB Gaming Keyboard'
解决:sudo systemctl disable --now my_keymap 停掉旧服务、删 /usr/lib/systemd/system/my_keymap.service 后,新服务即可正常抓键盘。
GNOME Wayland
- Install xremap's GNOME Shell extension from this link, switching OFF to ON.
- If you use sudo to run xremap, also click here.
- Update /usr/share/dbus-1/session.conf as follows, and reboot your machine.
<policy context="default">
+ <allow user="root"/>
<!-- Allow everything to be sent -->
<allow send_destination="*" eavesdrop="true"/>
<!-- Allow everything to be received -->
example
This allows a key to be held indefinitely without triggering its held state, which is ideal for keys that also serve as modifiers. For example, you can make the Space key act as Shift when held and combined with another key, but still type a regular Space when tapped.
modmap:
- name: Space as Shift
remap:
Space:
held: Shift_L
alone: Space
free_hold: true # Optional, defaults to false.
问题排查
# 普通用户执行要求
sudo usermod -aG input $USER
# 创建 udev 规则: 新建文件 /etc/udev/rules.d/99-input.rules,内容如下:
KERNEL=="uinput", GROUP="input", MODE="0660"
sudo udevadm control --reload-rules && sudo udevadm trigger
# 查找你的键盘设备 寻找 Name="...Keyboard..." 的条目,记下它的 Handlers(例如 event3)
cat /proc/bus/input/devices
/home/peter/.cargo/bin/xremap /home/peter/project/tools/dotfiles/keymap.yml --device /dev/input/event3
错误举例
- 如果 xremap 以错误权限启动并崩溃,它可能已经打开了虚拟设备但没有正常关闭,导致 GNOME 认为有一个“死键”一直被按下。
xremap 会创建一个名为 xremap 的虚拟键盘。如果程序崩溃了但设备还在,GNOME 会继续尝试从这个“僵尸”设备读取输入
lsinput
# 或者使用更通用的命令
grep -E 'Name|Handlers' /proc/bus/input/devices
- 寻找关键字: 在输出中找名称包含 "xremap" 的设备。
- 判断异常: 如果 xremap 进程已经杀掉了,但 xremap 虚拟设备依然出现在列表中,说明设备未正常卸载,这通常就是导致 GNOME 异常的元凶。
使用 libinput 监控实时事件
- 你可以实时观察到底是哪个设备在“发疯”(比如不停发送 Ctrl 信号)。
sudo libinput debug-events
- 观察输出: * 如果你没有按键,但屏幕滚动显示 KEY_LEFTCTRL 或 KEY_RIGHTALT 的 pressed 事件,说明存在“死键”。
- 看输出条目左侧的设备名称。如果是来自 xremap 设备,说明是驱动层模拟出的问题;如果是来自你的物理键盘,说明物理设备可能被锁死在了按下状态
排查设备“锁死”(EVIOCGRAB)
- xremap 运行时会“抓取”物理键盘。如果它崩溃时没有释放抓取权限,物理键盘的信号就无法到达 GNOME,导致你感觉键盘“失灵”。
ps aux | grep xremap
sudo killall -9 xremap
通常进程结束后,内核会自动释放设备抓取。
快速恢复手段
- 如果你正处于“环境异常”(键盘乱跳或没反应)中,尝试以下组合拳:
- 拔插键盘: 如果是外接键盘,拔掉重插可以强制重置内核对该设备的输入状态。
- 强制销毁虚拟设备: 如果 xremap 已经退出但设备还在,尝试移除 uinput 模块(慎用,仅在没反应时尝试):
sudo modprobe -r uinput && sudo modprobe uinput
- 重启 GNOME Shell:
- X11 环境: 按 Alt + F2,输入 r 然后回车。
- Wayland 环境: 只能通过登出(Log out)并重新登录来重置输入状态。
终极手段:重置 uinput 子系统
# 卸载并重载 uinput 模块(需 root,会断开所有虚拟输入设备)
sudo rmmod uinput
sudo modprobe uinput
# 此操作会使所有依赖 uinput 的程序(如 xremap、keyd、interception-tools)失效,需重启它们
强制释放所有“卡住”的修饰键(最常用、最有效)
在 GNOME/X11 下,模拟释放所有修饰键即可快速恢复:
# 方法 1:使用 xdotool(推荐)
xdotool keyup Shift_L Shift_R Control_L Control_R Alt_L Alt_R Super_L Super_R
# 方法 2:使用 xte(来自 xautomation 包)
xte 'keyup Shift_L' 'keyup Control_L' 'keyup Alt_L' 'keyup Super_L'
# 方法 3:重启输入法/IM 框架(有时 IBus/Rime 会缓存按键状态)
ibus restart
在 GNOME Wayland 会话中,xdotool 可能失效(因非 X11 后端),此时:
Ctrl+Alt+F3 → 登录 → Ctrl+Alt+F1(或 F2)回到 GNOME
# 这会强制重置输入栈。
# 重启 GNOME Shell(不注销):
# 在 GNOME 中按 Alt+F2,输入:
r
# 或终端执行
busctl --user call org.gnome.Shell /org/gnome/Shell org.gnome.Shell Eval s 'global.reexec_self()'
事件监听
sudo apt install evtest
# 列出所有 X 输入设备
xinput list
# 查找类似 “xremap virtual keyboard” 的设备
xinput list --short | grep -i remap
# 若找到,检查其按键状态(需 evtest)
sudo evtest
# → 选择对应设备编号,观察是否有持续的 KEY_XXX (code xxx) DOWN 事件
# xremap 默认创建一个 uinput virtual device,名字可能为 xremap virtual keyboard 或类似。
xremap 的实现原理
xremap 的工作流程可以分为:拦截 (Grab) -> 转换 (Remap) -> 重发 (Emit) 。
1. 拦截层 (evdev + EVIOCGRAB)
- 读取: Linux 内核通过
/dev/input/event*暴露出原始的输入事件。xremap启动后,会打开你的物理键盘设备文件。 - 独占锁: 这是关键。它会对物理键盘调用
ioctl(fd, EVIOCGRAB, 1)。这会告诉内核:“除了我,谁也不要把这个键盘的事件发给别人”。 - 结果: 此时你按下键盘,GNOME 或 X11 是接收不到任何信号的,信号全被
xremap截获了。
2. 逻辑转换层 (Logic)
xremap内部维护一个状态机。当你按下 $Alt\_R$ 时,它不会立即动作,而是根据你的配置文件(如application过滤)检查当前活跃窗口。- 在 Wayland 下,它通过特定的合成器协议或 GNOME 扩展获取窗口信息;在 X11 下,它通过 Xlib 获取
_NET_ACTIVE_WINDOW。
3. 重发层 (uinput)
- 虚拟设备:
xremap调用/dev/uinput模块,在系统中注册一个全新的虚拟键盘(通常名字就叫xremap)。 - 转换输出: 当它拦截到 $Alt\_R$ 并判断需要映射时,它会向
uinput设备写入 $Control\_L$ 的事件序列。 - 接收: GNOME 看到的是来自这个“虚拟键盘”的 $Control\_L$ 信号,从而执行相应的操作
类似工具
- https://github.com/jtroo/kanata
- https://github.com/kmonad/kmonad
loadkeys(console keymap)
- 这是 Linux 控制台专用 的键盘映射。
- dumpkeys > keymap.map
# 编辑 keymap.map
sudo loadkeys keymap.map
- 直接作用在 VT
- 内核级
- 稳定
- 不支持复杂逻辑
kbd / console-setup
用于
- 服务器
- rescue
- initramfs
sudo dpkg-reconfigure keyboard-configuration
其他类似项目
| 项目 | 架构 | Wayland | 特点 |
|---|---|---|---|
| xremap | evdev + uinput | ⚠️ | YAML,灵活 |
| keyd | evdev + uinput | ✅ | 稳定、简单 |
| kmonad | evdev + uinput | ⚠️ | Lisp DSL |
| interception-tools | evdev | ⚠️ | 管道式 |
| setxkbmap | XKB | ✅ | 最稳 |
| xmodmap | X11 | ❌ | 已过时 |
| Karabiner (macOS) | HID | ✅ | 架构标杆 |
- 路线 A:evdev + uinput(xremap 路线)
- 做系统级 remap
- 不依赖 Wayland 应用识别
- 能接受 input 权限
- 路线 B:XKB 层(最稳)
- 不抓设备
- 改的是 键位语义
- compositor 原生支持
- 路线 C:Wayland Compositor 插件(最干净)
- 在 compositor 内部改键