霓虹语标题我都想好了。evdevの力を貸して、Linuxでホットキーの魔改造
Linux用户就像Minecraft玩家,虽然大家玩的都是Minecraft,但是,卧槽,我们一定是在玩不同的游戏(见到建筑师的MC作品时来自小白的惊叹)。要让自己的Linux别人不会用,别人的Linux自己不会用,最重要的当然是要把快捷键改得惊天地泣鬼神。
作为一个Vim瘾君子,我的需求就是手尽量不要离开主键盘区。对,方向键我都不想按。于是我想通过一些组合键去实现上下左右。有人认为CapsLock按起来方便,我自己比较喜欢按Alt,因为就在空格键旁边,触手可及。总的来说,我希望Alt+[H|J|K|L]
分别变成左下上右,Alt+0
变成Home, Alt+4
变成End。
我自己试用过很多修改键位或者添加热键的工具,包括著名的AutoKey。很可惜,它们大都不好用。比如说,我采取这样的操作序列Alt Down, K Down, K Up, Alt Up
,这些软件大多会采取在检测到K Down
的时候,同时发出Alt Up, ↑ Down
,以便撤销掉先前一个Alt Down
的作用,然后发出↑
键按下的事件。这里有一个问题,就是很多GUI在Alt Down, Alt Up
之后,会唤出菜单,从而失焦于输入框。
另外一方面就是,真的不是哪里都能用。至少,你不可能拿它去玩赛车游戏。你也不能在TTY中继续使用这些热键。此外还有许许多多的地方不能使用。热键是一种会上瘾的东西,在它失效的时候,你就会有戒断综合征。想摔键盘。
evdev和uinput
我试过从GNOME和X11入手,貌似没有什么好用的方案。XGrabKeyboard一定程度上可以做到接管的效果,但是要指定窗口,似乎还会让窗口失焦。总而言之稍微有点太绕。不过Linux最好的一点,就是它很裸露。如果从高于驱动低于X11的层入手,兴许会有比较好的效果。
evdev内核中通用的输入设备驱动,它为设备提供了/dev/input下字符设备接口。它非常底层,内核在进行中断处理后,第一时间就将输入数据交由它处理。但是它一点都不反直觉,甚至还提供了很好用的工具libevdev,可以直接用Python处理消息。不少人改游戏手柄都是通过evdev进行的。
uinput是一个特殊的虚拟设备,它允许你直接在用户态向内核插入输入事件 —— 一般而言就是直接向/dev/uinput
写数据,不过当然,要服从libevdev提供的接口/数据结构。这些事件之后会在另一个evdev字符设备里,假装是物理设备的输入,被X的libinput取出来或者由TTY转为stdin。
Quick Start
这种实验性的东西就不用C写了。evdev提供了十分好用的Python Bindings,我们可以直接在Python里写我们的快捷键配置。我们先用pip install evdev
安装它,然后在用它提供的示例脚本来测试一下evdev输入的究竟是什么东西:sudo python -m evdev.evtest /dev/input/by-path/platform-i8042-serio-0-event-kbd
(这里我键盘的路径是i8042键盘控制器上的,你需要根据你电脑上的配置来调整)
time 1504189579.19 type 4 (EV_MSC), code 4 (MSC_SCAN), value 21
time 1504189579.19 type 1 (EV_KEY), code 21 (KEY_Y), value 1
time 1504189579.19 --------- SYN_REPORT --------
time 1504189579.28 type 4 (EV_MSC), code 4 (MSC_SCAN), value 21
time 1504189579.28 type 1 (EV_KEY), code 21 (KEY_Y), value 0
time 1504189579.28 --------- SYN_REPORT --------
time 1504189579.29 type 4 (EV_MSC), code 4 (MSC_SCAN), value 18
time 1504189579.29 type 1 (EV_KEY), code 18 (KEY_E), value 1
time 1504189579.29 --------- SYN_REPORT --------
time 1504189579.4 type 4 (EV_MSC), code 4 (MSC_SCAN), value 18
time 1504189579.4 type 1 (EV_KEY), code 18 (KEY_E), value 0
time 1504189579.4 --------- SYN_REPORT --------
time 1504189579.48 type 4 (EV_MSC), code 4 (MSC_SCAN), value 31
time 1504189579.48 type 1 (EV_KEY), code 31 (KEY_S), value 1
time 1504189579.48 --------- SYN_REPORT --------
time 1504189579.64 type 4 (EV_MSC), code 4 (MSC_SCAN), value 31
time 1504189579.64 type 1 (EV_KEY), code 31 (KEY_S), value 0
time 1504189579.64 --------- SYN_REPORT --------
这里我按了yes三个键,可以看到,每一个动作(按下或释放,表现在EV_KEY的value的1或0上),都会产生三个事件,分别是EV_MSC
,EV_KEY
和EV_SYN
。根据 https://lp007819.wordpress.com/2013/02/12/再谈linux-input子系统/的说法,事实上是有四个消息的发出,但是第一个通常不被支持(隐身了),第二个MSC_SCAN通常会被应用程序忽略,第三个EV_KEY才是真正会被接收的,第四个是同步,可以看到就是用来产生萌萌的分界线的(笑)
有了这一层认识我们就知道,三个事件合起来,才是一次真正的输入。
开始动工
就我的需求而言,我脑子里第一个浮现的抽象机制就是状态机,不知为何。一个比较合适的设计是两层的自动机,第一层用来为这三个一组的事件分组,分好组之后成为第二层状态机的输入,一个二元组(key-scan-code, up/down/hold)
。第二层状态机我花了好些时间去构思,结果大概是这样的:
State | Input Pattern | Transition | Action |
---|---|---|---|
Normal | (Left Alt, Down) | Alt | - |
Normal | ELSE | Normal | inject |
Alt | (J/K/H/L/0/4, *) | Mapped | mapped |
Alt | (Left Alt, Up) | Normal | inject_alt_down, inject_alt_up |
Alt | ELSE | Inject | inject_alt_down, inject |
Inject | (J/K/H/L/0/4, *) | Mapped | inject_alt_up, mapped |
Inject | (Left Alt, Up) | Normal | inject_alt_up |
Inject | ELSE | Inject | inject |
Mapped | (J/K/H/L/0/4, *) | Mapped | mapped |
Mapped | (Left Alt, Up) | Normal | - |
Mapped | ELSE | Inject | inject_alt_down, inject |
是不是头都晕了。把它画出来或许会比较清楚,不过我也没这个闲心拿绘图软件再画一遍了。有四个状态,分别是:
正常状态 (Normal),除了Alt以外所有键都直接发射到uinput里
刚按了下Alt键 (Alt),现在还不能确定Alt键会否形成组合键
插入状态 (Inject),不是我们想要的热键,连刚才的Alt一起发射到uinput里
映射状态 (Mapped),是热键,把映射过之后的键发射出去,比如按下了K就发射Up
Inject和Mapped状态之间转换时,还要把一些Alt键的动作补充一下,以防误导其它应用程序。
关于evdev本身的使用上,evdev的文档已经说得非常详尽。在这个脚本里,仅仅用到了少量的功能,比如从/dev/input
里读事件,我是block read,但是你也可以用select或者epoll去异步完成这些操作。:
# 留意!需要Root权限!
dev = evdev.InputDevice('/dev/input/by-path/platform-i8042-serio-0-event-kbd')
for event in dev.read_loop():
kev = evdev.categorize(event)
ks.input(kev) # 第一层状态机
process(ks) # 第二层状态机,以第一层状态的结果为输入
Inject这样的操作就是往uinput里面一次过写三个事件(含同步):
ui = evdev.UInput()
def __inject(keycode, keystate):
global ui
ui.write(ecodes.EV_MSC, ecodes.MSC_SCAN, keycode)
ui.write(ecodes.EV_KEY, keycode, keystate)
ui.syn()
当然,最重要的一点是怎样做到独占读,也就是托管整个设备的事件处理不让它侧漏呢?我们为它设置GRAB。Grab了之后,整个系统只有当前进程读得到键盘的输入。如果这个设备已经被别人Grab了,这个操作就会失败。
dev.grab()
dev.ungrab()
整份代码已经上传到 https://github.com/Shihira/la...,欢迎斧正。
参考资料:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。