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

分享一个 Linux 下的改键工具

  •  
  •   yadam ·
    jialeicui · 4 天前 · 849 次点击

    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 的方式

    7 条回复    2025-03-28 18:32:14 +08:00
    ho121
        1
    ho121  
       4 天前 via Android
    xiling000000
        2
    xiling000000  
       3 天前
    我一般直接改这里面,哪都能生效 /usr/share/X11/xkb/symbols/pc
    yadam
        3
    yadam  
    OP
       3 天前
    @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  
       3 天前   ❤️ 1
    我大致看了一下你的实现,讲道理你是个狠人啊……你不仅给 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  
       3 天前   ❤️ 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
       2 天前
    @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  
       2 天前   ❤️ 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 的常见需求。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1287 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 20ms · UTC 23:57 · PVG 07:57 · LAX 16:57 · JFK 19:57
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.