分享一个 Linux 下的改键工具 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Distributions
Ubuntu
Fedora
CentOS
中文资源站
网易开源镜像站
yadam
V2EX    Linux

分享一个 Linux 下的改键工具

  • &nsp;
  •   yadam
    jialeicui 197 天前 1914 次点击
    这是一个创建于 197 天前的主题,其中的信息可能已经有所发展或是发生改变。

    https://github.com/jialeicui/KeySwift

    特点:

    • 配置基于 Javascript, 比较灵活
    • 支持基于 window class 进行配置

    不足:

    • 目前只支持 gnome
    • 目前不支持鼠标

    示例配置

    const curWindowClass = KeySwift.getActiveWindowClass(); const Terminals = ["kitty", "Gnome-terminal", "org.gnome.Terminal"]; const inTerminal = Terminals.includes(curWindowClass); KeySwift.onKeyPress(["cmd", "v"], () => { if (curWindowClass === "com.mitchellh.ghostty") { KeySwift.sendKeys(["shift", "ctrl", "v"]); return } if (inTerminal) { KeySwift.sendKeys(["cmd", "shift", "v"]); } }); 

    背景:

    我个人比较喜欢 macOS 下的快捷键方式, 一部分是 Cmd+xxx 对 terminal 和 vim-mode 的编辑器比较友好, 一部分是绝大部分编辑框都支持 emacs 方式的编辑

    用了 xremap 两年多, 很好用, 但是偶尔会出些问题, 某些 modifier 键的处理可能有 corner case, 会卡在 pressed 状态, 又不会 rust, 用 go 写了一个, 用来学习+自己方便按照自己的思路搞搞
    获取当前窗口的功能评估了很多方案, 最后还是采取了 xremap 的 gnome extension 的方式

    9 条回复    2025-04-28 16:23:15 +08:00
    ho121
        1
    ho121  
       197 天前 via Android
    xiling000000
        2
    xiling000000  
       197 天前
    我一般直接改这里面,哪都能生效 /usr/share/X11/xkb/symbols/pc
    yadam
        3
    yadam  
    OP
       197 天前
    @xiling000000 #2
    学习了, 原来还可以这样, 确实方便

    我现在比较依赖于不同的 app 不同的快捷键
    比如 emacs 形式的快捷键和 terminal 以及开了 vim mode 的程序都会打架 (比如 ctrl+b 和 tmux 打架)

    以前用基于 X 的桌面的时候用过一阵子 autokey, 也挺好用的

    我现在的配置是: https://github.com/jialeicui/KeySwift/blob/main/examples/config.js
    kuanat
        4
    kuanat  
       196 天前   3
    我大致看了一下你的实现,讲道理你是个狠人啊……你不仅给 libevdev 做了 binding ,还内嵌了 quickjs 用来做配置自定义。因为我经常给自己的设备做固件写驱动,所以改键这部分还算比较熟悉,随便说一点想法。

    1.
    通过 uinput 虚拟输入设备,拦截物理设备事件(独占),映射完成后通过虚拟设备输出事件,这是唯一正确的做法。另一个我认为哲学上不那么正确的做法是,原物理设备不管,只重新映射事件。更准确的说法是这是处理两种不同需求的做法,前者的重点在于映射,后者的应用场景是脚本化、自动化,比如 AHK 这种,两者有重合的地方,但实际上是两种不同的设计方向。

    2.
    在这个“正确”的设计下,需要处理很多 corner cases ,我随便举一个类似方案很容易踩的坑:很多 gui 应用响应 alt 呼出菜单,假如有个改键逻辑是 alt+h 映射为 left ,当用户按下 alt 的时候,需要原样输出 alt 按下,同时设置 modifier ,之后按下 h ,此时以完全拦截,然后重新映射的视角来看,应该先释放 alt ,然后插入 left 。这时就会触发 alt 按下又释放的事件,就会被 gui 应用响应到。同时用户也没有办法再输入 alt+j 这个组合,这就给如何设计配置文件语法带来了挑战。

    3.
    配置文件用 js 这样的动态语言是一个可选的方向,但我不确定是不是个好的做法,比较直观的缺陷就是可读性和 debug 。以我对现有的各个实现的观察来看,应用提供 sane defaults 是非常重要,它可以极大简化配置文件的编写。特别是当你想实现类似 qmk layer 之类的功能的时候,全拦截重映射实际上要求每个按键都是重映射过的,配置文件如果要把所有逻辑都写一遍体验会不太好。

    4.
    获得当前活动窗口的名字是个看起来简单实际上复杂的事情,首先底层是 xorg 还是 wayland 要区分一次,xorg 稍微好解决一点,wayland 要做全兼容,大概主流的 mutter/kwin/wlroots 都要兼顾。各个实现对于 foreign-toplevel-manager 这个协议的态度差别很大。要么对 gnome 用 dbus ,对 kde 用 script api 一个一个适配,要么等各家支持相关协议。

    5.
    ctrl+c/v/x 映射到 ctrl+insert/shift+insert/shift+delete 是一个思路,但不是唯一的思路,如你可以直接 super+c/v/x 映射到 XF86 的 copy/paste/cut ,更接近 mac 的逻辑。这方面 sway 这样的 wm 比起 gnome 这样的 de 要容易。另外 linux 的 clipboard 目前也是 wayland 协议的一部分,除此之外还有 primary selection 能用,可以发挥想象力。

    6.
    在这种系统调用层面上,go/rust 或者其他语言都差不多,只有不是 c 都会有 ffi 的问题。我只是单纯觉得,binding 都写了,不如直接写 c 了。另外不用 c 的话 IPC 带来的延迟会比较明显。




    PS

    下面写给对改键这个事情感兴趣的人看的。

    首先明确几个概念 scancode/keycode/keysym ,scancode 是设备产生的硬件信号,经过内核 udev/evdev 之后转化为 keycode ,keycode 由内核空间传递到用户空间后,由桌面/窗口管理根据键盘 layout 转换为 keysym ,也就是应用程序获得的输入。

    现在能够接触到的键盘类设备,只要不想专门做驱动,都会按照 linux headers 头文件( input-event-codes.h )定义的 keycode 去做,不然就是给自己找麻烦。当然像笔记本、游戏设备就需要自定义使用头文件里没有的 scancode/keycode 映射了。

    以当前的 linux 生态来说,整个按键输入流程是这样的,硬件产生 scancode ,在内核空间经过 udev/evdev 处理映射为 keycode ,通过 /dev/input/eventX 暴露给用户空间。用户空间有 xorg/wayland 两个大的框架,对应的实现分别是 xorg-input-evdev 和 libinput ,注意前者虽然名字有 evdev 但实际上是用户空间的,意思是它利用 evdev 机制,此二者都是操作内核暴露的 /dev/input/eventX 接口,然后根据环境 locale/layout ,将 keycode 转换为 keysym 。由于目前 xorg 的实现也都过渡到 xf86-input-libinput 上面了,所以可以认为,用户空间就是 libinput 。此阶段之后,上层的桌面/窗口管理器可以获得特定的 keysym ,进一步传递给活动窗口应用。



    综合上面的背景信息,可以说理论上有三个,但实践中只有两个可以做改键的位置。第一个是在内核空间通过 udev 规则改写 scancode 和 keycode 的映射,说它没有实践意义是因为这是设备厂家要做的事情。剩下两个有实践意义的,一个是 keycode 转 keysym 的阶段,另一个是 keysym 传递给应用窗口的阶段。

    如果继续观察市面上已有的各种改键工具,会发现在 keycode 转 keysym 的阶段完成映射的方式,传统基于 xorg-input-evdev 的实现都只能支持 xorg ,而基于 libinput 的可以同时支持 xorg/wayland 。

    至于为什么 keysym 传递给应用窗口这个实现也会存在,原因很简单,用户有针对特定应用改键的需求,而这个需求是前一种方式不能独立完成的。

    所以现在改键就剩一种方式了,就是虚拟 uinput ,然后拦截物理设备事件做映射。区别就是要不要识别当前应用,以及如何识别。至于在 keysym 传递过程做映射,只有纯 wm 能实现,一般 de 是不开放这个自定义的。
    jqtmviyu
        5
    jqtmviyu  
       196 天前   1
    我是用 keyd , 支持 arch+wayland, 大部分都是支持 x11 的.

    配置也很简单

    sudo vim /etc/keyd/default.conf
    ===
    [ids]

    *

    [main]
    capslock = overload(capslock_layer, esc)

    [capslock_layer]
    esc = capslock

    h = left
    j = down
    k = up
    l = right

    u = pageup
    p = pagedown
    i = home
    o = end

    m = backspace
    ===
    yadam
        6
    yadam  
    OP
       196 天前
    @kuanat #4


    感谢回复, 老哥太专业了, 句句直戳要害
    我说一下在做这个工具的时候自己的一点儿思考, 希望老哥能够百忙之中再指导指导


    关于 2:

    这个真是大坑而且不太好在 remapping 的逻辑里处理完
    我目前的做法是针对 ctrl 和 alt 做了特殊处理 (其实如你所说, alt 更加不好处理,我自己疏忽了没有考虑到 alt 在很多场景是不是完全的 modifier)

    我对 ctrl 处理的需求是想解决按住 ctrl 打开浏览器链接的时候会强制在新 tab 打开
    当前的处理逻辑是(以 ctrl 为例):
    - 独立按下的时候透传一个 down 的 event
    - 在 down 的状态如果还有其他键比如 C 按下并且匹配到改键逻辑, 则在发送改键 event 之前发送这个 ctrl 的 up event
    - 发送修改之后的组合到 uinput
    - 丢掉 src dev 后面 ctrl 和 C 的 up event

    所有的 remap 的配置我都是使用当前 down 的所有 key 做匹配


    关于 3:

    我的初版实现是用 yaml 做配置文件的, 好处就是所有用户的逻辑可以提前知道, 性能可以做到比较好
    但是只要想表达复杂逻辑 if else or and 之类的, 在 yaml 上做就很反人类

    那自然想到脚本语言, 我评估了三种:

    1. lua, 最开始就想到它, vim, 以及我个人比较喜欢的一个叫做 Hammerspoon 工具都用它
    但是我自己不太会写 lua, 以及看到一些大佬评价 lua 的某些缺点, 就不做最优先考虑了
    2. 像 AHK 一样, 有自己的脚本引擎, 这个是我最想要做的, 好处有:
    - 可以做到配置特别简洁, 比如 remap 只需要两个 a::b 就行
    - 脚本运行之前就能掌控所有的用户配置, 好做优化
    - 支持复杂的 if else 等逻辑

    缺点: 对我这样的初级开发者来讲, 实现起来太难了. 对用户来讲需要学习一个新的脚本语法
    3. js, 算是综合前面所有考虑之后的妥协

    另外提到全拦截, 我看到的最优雅的是 kmonad, 最开始我也实现了一个 layer 的方案, 但是后来发现对于我这样只想映射少量键的请, 要配置层多了之后每个层都要把所有键配置一下也挺烦的
    最后想开了: 不要想大而全, 不要想让很多人都用, 就只做好一小部分功能就不错了
    从这个角度出发, 我甚至都考虑过不开放配置, 就叫做: 让你的 linux 快捷键用起来像 macOS
    我自己用的话可能就用 go 写死一个这个 [Engine]( https://github.com/jialeicui/KeySwift/blob/d21ee1e683cab0ee16862d08612ea0ccadb50327/pkg/engine/interfaces.go#L15) 的实现了


    关于 4:

    是我自己的强需求, 目前只实现了 gnome 相关的, 要做好确实要有好多路要走

    老哥其他提到的很多知识点/名词都让我学到很多, 再次感谢!
    kuanat
        7
    kuanat  
       196 天前   1
    @yadam #6

    关于 2 中你的处理原则是合理的,ctrl down 一定要在用户按下的时候就响应,而不是等到再按下其他键的时候才发送按下事件。这里主要的问题是,很多软件会希望获得 modifier 的状态,或者 modifier key 的按下释放事件。比较常见的除了 alt 触发菜单,还有把 ctrl 或者空格当作 push-to-talk ,把 ctrl+alt 作为虚拟机的跳出按键,你在设计的时候可以思考一下,因为历史遗留原因和软件开发的多样性,软件到底是检测 modifier 状态,还是检测按键事件,或者是检测 keysym 可能需要不同的处理方式。

    我在 AHK 那边看到过一个处理逻辑,就是交叉释放,比如你在 alt+h 这个场景,正常逻辑是先发送 alt down ,再拦截 h ,发送 alt up ,发送 left 这样,在发送 alt up 之前插入 ctrl down ,然后 alt up ,ctrl up 这样,就不会被认为是按下了 alt 键又松开。可能目前只有这样的 hack 方式才能解决。



    配置文件方面,我更倾向于把改键应用分为两类,当前语境下的映射类建议用静态配置文件,自动化意义上的 AHK 建议用 DSL 自定义语法。后者的话工作量比较大,因为有 quickjs 的存在,我觉得反倒很适合直接拿来用。如果主程序是 c 的话,lua 是个不错的选项。

    映射类的配置文件,我个人比较喜欢 keyd 的 ini 方式。重点不在格式上,而是 keyd 对于 layer 功能实际上是有默认的,就是配置里没有写的,直接 fallback 到原始按键上。本质上这些配置还是存在的,只是不需要使用者去写了。你看 #5 那个写法,符合直觉而且好写好懂。如果你要设计的话,可以提前想象一下,类似长短按,按下生效一次,按下自动重复,按下切换 layer 需要什么样的语法。我之前尝试过,无一例外都把自己绕晕了……



    另外有些功能在 de/wm 阶段处理 keysym 可能更容易,gnome 的话有 keyboard shortcuts 可以自定义,只是可以绑定的功能比较有限。如果你用过类似 sway 这样的纯窗口管理器,会发现 keysym 触发特定功能能满足大部分 AHK 的常见需求。
    weixiangzhe
        8
    weixiangzhe  
       190 天前 via Android
    现在用 xremap 了,这个真不错 对于习惯 macos cmd 加 ctrl 两套键位 可以轻松移植到 linux
    XiaoSean
        9
    XiaoSean  
       165 天前
    @jqtmviyu 看着好简单的,大佬请务必,写个简单的教程.
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2955 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 21ms UTC 13:40 PVG 21:40 LAX 06:40 JFK 09:40
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86