Contents
jump or exec 的概念是我在接触 Sawfish 的时候接知道的。通常对于一个任 务,你并不需要关心它是否已经在运行了,你需要它的时候只需要一个指令,如 果它已经在运行了,那么就把他带到前台来,否则就启动一个。而把这个指令绑 定到一个全局快捷键上,也就成了 jump or exec 了。
在 Sawfish 里面有好几个扩展可以方便地实现这个功能,我不知道在其它窗口 管理器里面怎么做,自己也没有用过 FVWM 之类的,而像 KDE 的 kwin 之类的 窗口管理器好像也没有相关的东西。
由于对 Konqueror 文件管理器特别喜爱,同时 Konqueror 又要在 KDE 下面使 用才舒服,在使用 Sawfish 代替 KDE 的 kwin 一段时间以后,我还是决定制作 在其它窗口管理器下可以使用的 jump or exec 功能,因为 Sawfish 搭配 KDE 使用还是有一些小问题的。
EWMH/NetWM compatible X Window Manager
无意中发现 wmctrl
这个程序,可以通过命令行的方式控制 EWMH/NetWM 兼容的
窗口管理器,其中就包括 kwin 和 Icewm 等。其实只要可以通过命令行控制的
话,用脚本来实现这个功能应该是很方便的,例如,这样一个脚本:
便可以实现这个功能,只是还没有快捷键,KDE 的控制中心里可以设置快捷键,
但是还有一个独立的程序叫做 xbindkeys
可以在任何窗口管理器下设置全局快捷
键,用于运行特定的程序。这样,我写了一个通用的 Python 脚本:
并且做一系列的链接 ( joe-fm、joe-gnus 等 ) 到这个脚本文件,它根据自己
被调用的名字来启动对应的程序,于是我就在 xbindkeys
里面执行这些软链接:
这样就能实现 jump-or-exec 的功能了。但是效果不如 Sawfish 里面内置的
jump-or-exec 好,因为每次都是启动一个脚本,有一些微微的延迟,而且有时
候切换过去之后没有获得焦点。于是我决定做一个像 xbindkeys
一样的后台进
程,自己处理快捷键捕获、窗口切换已经运行程序,而不是用几个命令组合起来。
独立的 Python 版 jump-or-exec
正好 Python 有 Xlib 的库,可是后来发现它的文档其实非常不全,很多东西都
没有提到,上它的网站也看到提到这个问题,并且还在征集维护者。不过我对
Xlib 和 Python 都还不熟悉,只好看一些源代码来摸索如何做。其中主要查看
了 xbindkeys
和 pypanel
两个程序的源代码,大概搞清楚了几个重要的地方如
何实现。
截获全局快捷键
X 被设计于可以在网络上运行,为了不被大量的事件传输阻塞,通常 X 在你明
确要求某些事件之后才会将事件发送给你。我通过注册 root
窗口的
KeyPress
事件来收取全局快捷键的信息:
root.change_attributes(event_mask=(X.KeyPressMask|X.KeyReleaseMask))
但是并不是所有的按键都会被截获,还必须告诉 X 你对哪些快捷键感兴趣:
root.grab_key(keycode, modifier, False, X.GrabModeAsync, X.GrabModeAsync)
这样就可以截获带 modifier
修饰的 keycode
按键了。通常在按下快捷键的时
候不会关心 CapsLock
、 NumLock
等键的状态,但是在 X 里面这是有区别的,
于是需要把他们分别按下、同时按下以及没有按下的情况都进行捕获。而这几个
Lock
键的值又是可以经过映射的,所以需要动态获取,具体可以参见下面所附
的源代码。
列出所有任务
因为要查找是否有已经存在的窗口,所以需要列出所有的正在运行的任务的窗
口,通过查看 pypanel
的源代码,我发现这段代码可以列出这些窗口:
将一个窗口切换到前台
如果找到对应的窗口,就把它切换到前台,这段代码可以完成这个工作:
但是当窗口是最小化的时候并不能正常工作,而是要特殊处理,先用 map
函数
让窗口显示出来。但是由于 X 是异步的, map
并不是立即有效果,而设置输入
焦点又必须是在窗口显示出来的时候才有用,于是通常在 map
后面的设置焦点
的调用都没有任何效果,导致虽然窗口切换到前面了,但是还是没有输入焦点。
我试了一下 pypanel
发现它确实也是有这个问题的。不过这也并不是不能解决,我
只要登记这个窗口的显示事件,在窗口显示出来的时候再设置焦点就可以了。
为了避免同时有多个窗口要设置焦点的情况,我设置了一个标识,用于标识下一 个需要设置焦点的窗口,我想一个窗口应该有唯一标识符,但是 Python 的 Xlib 文档好像东西实在太少了,于是我就用窗口的类名称来标识吧,反正这个 也不需要太精确。
如果窗口没有最小化,那么直接升到前台,并设置焦点,如果是最小化了,那么 就订阅这个窗口的显示事件,并设置下一个等待设置焦点的窗口为它。在事件循 环里面捕捉到窗口显示的事件并且标识对应的时候,再设置焦点,同时退订该窗 口的显示事件。具体可以参考下面给出的源代码。
构造快捷键值
修饰键不多,可以建立一个对应关系表,比如 "Control" 对应到
ControlMask
,多个修饰键的时候按位或一下就可以了。而键值可以使用 Xlib
里面的 string_to_keysym
转换为 keysym
然后由 keysym_to_keycode
转换为
可以用于截获快捷键的 keycode
。其中 CapsLock
、 NumLock
和
ScrollLock
这三个键是要自己计算一下的。
提示窗口
如果要达到 Sawfish 里面那种效果,在没有找到窗口,启动一个新程序的时候,显 示一个气泡提示窗口,说正在启动某某程序,而避免让用户以为是没有反应了, 那么需要手工画一个窗口,这好像又好涉及到字体等一系列的问题,而且要这个 窗口在几秒钟之后自动消失的话,还要用定时器吧,在 google 了一下没有发现 Xlib 的定时器相关的特别有用的资料之后,我决定放弃这个功能了。或者留到 以后来实现吧。
代码
最后写出来的代码就是这样子了。为了避免再解析配置文件而添加更多的代码, 配置就直接在代码里面了,但是配置仍然是很直观的:
在 FVWM 中实现
众所周知 FVWM 是可扩展性非常强的窗口管理器。不过我并没有仔细研究这个窗 口管理器,我觉得它那个配置实在是太难读了,哈哈。但是 danran 最近告诉我 在 FVWM 里面也可以很方便地实现 JOE 的功能,类似于这样:
DestroyFunc RaiseAndFocus AddToFunc RaiseAndFocus + I Focus + I Raise DestroyFunc JOE AddToFunc JOE + I All ($0) RaiseAndFocus + I TestRc (NoMatch) Exec exec $1 &
JOE 函数需要两个参数,一个是 window class
,另一个是要执行的命令。
其中 window class
一定要有引号,命令有没有都可以。
例如:
Key V A 4 JOE "Gvim" gvim