linux 键盘映射xremap

十二月 25, 2025 更新: 八月 09, 2026 [Linux 与系统] #xremap

xremap

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

   <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 会创建一个名为 xremap 的虚拟键盘。如果程序崩溃了但设备还在,GNOME 会继续尝试从这个“僵尸”设备读取输入

lsinput
# 或者使用更通用的命令
grep -E 'Name|Handlers' /proc/bus/input/devices

使用 libinput 监控实时事件

sudo libinput debug-events

排查设备“锁死”(EVIOCGRAB)

ps aux | grep xremap
sudo killall -9 xremap

通常进程结束后,内核会自动释放设备抓取。

快速恢复手段

sudo modprobe -r uinput && sudo modprobe uinput

终极手段:重置 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)

2. 逻辑转换层 (Logic)

3. 重发层 (uinput)

类似工具

loadkeys(console keymap)

# 编辑 keymap.map
sudo loadkeys keymap.map

kbd / console-setup

用于

sudo dpkg-reconfigure keyboard-configuration

其他类似项目

项目架构Wayland特点
xremapevdev + uinput⚠️YAML,灵活
keydevdev + uinput✅稳定、简单
kmonadevdev + uinput⚠️Lisp DSL
interception-toolsevdev⚠️管道式
setxkbmapXKB✅最稳
xmodmapX11❌已过时
Karabiner (macOS)HID✅架构标杆