jump or exec

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 都还不熟悉,只好看一些源代码来摸索如何做。其中主要查看 了 xbindkeyspypanel 两个程序的源代码,大概搞清楚了几个重要的地方如 何实现。

截获全局快捷键

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 按键了。通常在按下快捷键的时 候不会关心 CapsLockNumLock 等键的状态,但是在 X 里面这是有区别的, 于是需要把他们分别按下、同时按下以及没有按下的情况都进行捕获。而这几个 Lock 键的值又是可以经过映射的,所以需要动态获取,具体可以参见下面所附 的源代码。

列出所有任务

因为要查找是否有已经存在的窗口,所以需要列出所有的正在运行的任务的窗 口,通过查看 pypanel 的源代码,我发现这段代码可以列出这些窗口:

将一个窗口切换到前台

如果找到对应的窗口,就把它切换到前台,这段代码可以完成这个工作:

但是当窗口是最小化的时候并不能正常工作,而是要特殊处理,先用 map 函数 让窗口显示出来。但是由于 X 是异步的, map 并不是立即有效果,而设置输入 焦点又必须是在窗口显示出来的时候才有用,于是通常在 map 后面的设置焦点 的调用都没有任何效果,导致虽然窗口切换到前面了,但是还是没有输入焦点。 我试了一下 pypanel 发现它确实也是有这个问题的。不过这也并不是不能解决,我 只要登记这个窗口的显示事件,在窗口显示出来的时候再设置焦点就可以了。

为了避免同时有多个窗口要设置焦点的情况,我设置了一个标识,用于标识下一 个需要设置焦点的窗口,我想一个窗口应该有唯一标识符,但是 Python 的 Xlib 文档好像东西实在太少了,于是我就用窗口的类名称来标识吧,反正这个 也不需要太精确。

如果窗口没有最小化,那么直接升到前台,并设置焦点,如果是最小化了,那么 就订阅这个窗口的显示事件,并设置下一个等待设置焦点的窗口为它。在事件循 环里面捕捉到窗口显示的事件并且标识对应的时候,再设置焦点,同时退订该窗 口的显示事件。具体可以参考下面给出的源代码。

构造快捷键值

修饰键不多,可以建立一个对应关系表,比如 "Control" 对应到 ControlMask ,多个修饰键的时候按位或一下就可以了。而键值可以使用 Xlib 里面的 string_to_keysym 转换为 keysym 然后由 keysym_to_keycode 转换为 可以用于截获快捷键的 keycode 。其中 CapsLockNumLockScrollLock 这三个键是要自己计算一下的。

提示窗口

如果要达到 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