修改桌面键绑定的那些坑
我在使用 sawfish 作为窗口管理器,同时会参考 Emacs 的键绑定,定制很多全局的窗口快捷键,比如在 gedit 里,也要求按下 Ctrl-b 之后,可以起到向后移动一个字符(即方向键 Left)的效果。
这个操作本身就已经有比较多的坑了,然而,由于我调换了 Ctrl 键和 Alt 键的位置,所有的坑也变得更深了。
1 Gtk 的窗口
Sawfish 自带的定制键盘消息,是通过这个命令:synthesize-event
,它是通过 X11 的接口 XSendEvent 来实现的,然而,在 gtk 的窗口上,这个接口被认为有安全隐患,因此把它给禁用了,不会处理你发给它的消息。
但是,X11 还有一个扩展,叫 XTest,也可以用于发送伪造的键盘消息,不知道为什么,gtk 窗口没有认为这个一样的机制也是一种安全隐患,还是支持它的。但是,sawfish 自己不支持 XTest 接口。
但是,Linux 下有一个叫 xdotool 的工具,它支持 XTest 的接口。
2 Xdotool 的坑
Xdotool 在使用的过程中,比如我想把 Crtl-b 转换成 Left,必须做如下操作:
keyup b # 1
key --clearmodifiers Left # 2
- # 1 先把当前接着的 b 给松开,不松开的话,窗口系统不会处理接下来发送的 Left 键。
- # 2 –clearmodifiers 开关会把 Ctrl 先松开,然后按下、松开 Left,再把 Ctrl 键按回去
我花了很多时间,才总结出这个规律来。
- 之前一直以为直接发送 Left 就可以(因为 sawfish 就是这样操作的!),
- 然后,发现基本不 work,想发出去的 Left 根本没被接收到,
- 但是又不知道怎么解决,多次尝试之后发现加个 sleep 等待一会儿就可以接收
怀疑到可能是要等键盘上的键全部松开才可以,于是写了个脚本,
xdotool-when-keyboard-clear
这个脚本能用是能用,但是体验很不好,我每按一次 Ctrl-b,都需要自己把两个键都松开,然后脚本检测到之后才会发送 Left 键。
由于使用了轮循检测所有按键是否已松开,脚本的性能也有问题,时序也无法保证(比如连续快速按下两个快捷键的话,发送出来的顺序跟按下的顺序不一定是一致的)。
- 用了很多年
xdotool-when-keyboard-clear
之后,重新折腾 xdotool 的源代码,才发现应该可以用上面的方法,用 xdotool 自带的机制直接搞定。
2.1 剩下的一个小坑
由于 --clearmodifiers
的关系,xdotool 会把上例中的 Ctrl 键先松开,最后再『按』回去。这一按不要紧,有时候它按回去的时候,实际上你的手指自己已经松开了 Ctrl 键。
你猜这种情况下会发生什么现象呢?
在这里,我只告诉你这个情况我是怎么解决的,就是把键盘上的所有 ctrl、alt、super、shift 等辅助按键全部都按下、再松开。
3 切换在 ctrl 和 alt 引入的问题
前面已经说过,我自己切换过 ctrl 和 alt 的位置,因此导致了一个非常奇怪的问题。
我的 sawfish 配置是这样的(其实是从 Emacs 中拷贝出来的):
- 按下 Ctrl-b 的时候,实际给窗口发送 Left 键消息(向后移动一个字符)
- 按下 Alt-b 的时候,实际给窗口发送 Ctrl-Left 键消息(向后移动一个单词)
但是,我在实际使用的时候,发现一个奇怪的现象,即刚启动的时候,我可以连续按多次 Ctrl-b(更确切地说,是按住 Ctrl 之后,按多次 b),系统会发送多个 Left 消息。这个跟系统自带的按住 Left 键不松开,就会发送多个 Left 比起来,不算最理想的体验,但对我来说暂时够用了。
然而,用了一段时间之后,我就发现按住 Ctrl 不放开,按多次 b 键的话,只会在第一次按 b 时发送一个 Left,后面的 Left 就不发送了。
触发这个问题的方法,就是我自己再运行一次 swap alt/ctrl 键的脚本。这个脚本的工作原理是这样的:
- 调用 setxkbmap 设置我的键盘布局为 dvp
- 调用 xmodmap 交换我的 alt/ctrl、caps/escape 键的位置。
经过一番艰苦的调试之后,我发现这时候 xdotool 又进入了键盘状态不正确的工作方式。在处理完第一个 Ctrl-b 之后,我发现 sawfish 调用 xdotools 的方式变了,不再是发送 Left,而是发送了 Ctrl-Left,也就是说,XWindow 系统把 Ctrl-b 认成了 Alt-b。
这一点,也可以用 xev 程序来验证:
KeyPress event, serial 43, synthetic NO, window 0x3c00001,
root 0x217, subw 0x0, time 4496810, (831,443), root872,565),
state 0x8, keycode 113 (keysym 0xff51, Left), same_screen YES,
XLookupString gives 0 bytes:
XmbLookupString gives 0 bytes:
XFilterEvent returns: False
那个 state 0x8,记录的就是当前的辅助按键的状态,0x8 代表 Alt 键被按下了。如果是 Ctrl 键的话,应该是 0x4(第一个 Ctrl-b 时能看到 0x4)。
3.1 解决方案
我在写这篇博客的时候,还没有想到最终的解决方案,还在想一个 workaround(这也是为什么这篇博客的归类是 workaround)。
但是,现在我已经知道问题的根本原因,以及解决方案了。
问题的根本原因,是因为 X 的键盘程序,和 xmodmap 有点儿不兼容。虽然我用 xmodmap 交换了 ctrl/alt,但是被 xdotool 的 clearmodifiers 一发送,就会被 X 给认成原来没有交换的辅助按键,即 ctrl 被认成 alt,alt 被认成 ctrl。然后因为键盘状态一错,xdotool 就不再工作了(否则 Ctrl-b 被认成 Alt-b,sawfish 调用 xdotool 发送了 Ctrl-Left,光标应该向后移动一个单词才对,事实上光标一动也不动)。
明白了这一点,解决方案也就很简单了,因为我用的是 dvp 布局,我马上想到把 qwepty 布局能改成 dvp 布局,那这个改布局的程序(setxkbmap)肯定也拥有修改辅助按键的能力,并且通过它改的话,肯定不会有问题了吧?
果然,试了一下,确实没有问题了。
可怜我十多年就这么凑和过来了,今天才知道事情的真相。
使用的命令:
setxkbmap -layout us -variant dvp -option ctrl:swap_lalt_lctl -option caps:swapescape -option ctrl:swap_ralt_rctl
其中,前两个 option 是系统自带本身就支持的,最后一个 option 需要按照这篇文档进行修改:
https://askubuntu.com/questions/885045/how-to-swap-ctrl-and-alt-keys-in-ubuntu-16-04
4 总结
- 以后不要用 xmodmap 了(反正在 wayland 上已经是不能用了的,setxkbmap 不知道能不能用)
- 通过 setxkbmap 修改 modifier keys。