|
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 等。其实只要可以通过命令行控制的
话,用脚本来实现这个功能应该是很方便的,例如,这样一个脚本:
- #!/bin/sh
- wind=$1
- cmd=$2
- if ! wmctrl -a $wind
- then
- $cmd &
- fi
复制代码
便可以实现这个功能,只是还没有快捷键,KDE 的控制中心里可以设置快捷键,
但是还有一个独立的程序叫做 xbindkeys 可以在任何窗口管理器下设置全局快捷
键,用于运行特定的程序。这样,我写了一个通用的 Python 脚本:
- #!/usr/bin/python
- ######################################################################
- ## file: jump-or-exec.py
- ## author: pluskid <pluskid.zju@gmail.com>
- ## date: 2007-01-21
- ##
- ## description:
- ## Jump or Exec utilities -- jump to perticular window or start a
- ## new one if it doesn't exist.
- ##
- ## There's `wmctrl' that can control a interact with an X Window
- ## manager that is compatible with the EWMH/NetWM
- ## specification. (Some examples of EWMH/NetWM compatible window
- ## managers include recent versions of Enlightenment, Icewm, Kwin,
- ## Sawfish and Xfce.)
- ##
- ## There's another utility `xbindkeys' which can be used to customize
- ## your own keyboard shortcut.
- ##
- ## Combining the two tools, here comes the jump-or-exec utilities:
- ## save this file as `jump-or-exec.py' and then:
- ## $ chmod +x jump-or-exec.py
- ## $ ln -s joe-browser jump-or-exec.py
- ## then you can execute ./joe-browser to jump or exec the firefox web
- ## browser. If it works, you can customize the progs variable to set
- ## your own programs to execute and create those symlinks. Finally you
- ## can bind a shortcut to each program with `xbindkeys'. This method
- ## works under any window manager that is compatible with the EWMH/NetWM
- ## specification.
- ##
- ######################################################################
- import os
- import sys
- progs = {
- "joe-terminal" : ("urxvt.URxvt -x", "urxvt -e screen -xRRS terminal"),
- "joe-emacs" : ("emacs@", "fe -f server-start"),
- "joe-gnus" : ("gnus@", "fe --eval \'(setq frame-title-format "gnus@%b")\' -f gnus"),
- "joe-fm" : ("konqueror.Konqueror -x", "kfmclient openProfile filemanagement"),
- "joe-browser" : ("Gecko.Firefox-bin -x", "firefox"),
- "joe-bbs" : ("qterm.Qterm -x", "qterm")
- }
- if len(sys.argv) < 1:
- print "Don't know what program to start. Please execute with one of:\n%s\n" % \
- "\n".join(["\t"+prog_name for prog_name in progs.keys()])
- sys.exit(-1)
- prog = os.path.basename(sys.argv[0])
- wind,cmdline = progs[prog]
- if os.system("wmctrl -a %s" % wind) != 0:
- os.system("%s &" % cmdline) # execute in background
复制代码
并且做一系列的链接 ( joe-fm、joe-gnus 等 ) 到这个脚本文件,它根据自己
被调用的名字来启动对应的程序,于是我就在 xbindkeys 里面执行这些软链接:
- ###########################
- # xbindkeys configuration #
- ###########################
- # My jump-or-exec utilities
- "/home/kid/bin/joe-terminal"
- Mod4 + t
- "/home/kid/bin/joe-browser"
- Mod4 + f
- "/home/kid/bin/joe-emacs"
- Mod4 + e
- "/home/kid/bin/joe-gnus"
- Mod4 + g
- "/home/kid/bin/joe-bbs"
- Mod4 + q
- "/home/kid/bin/joe-fm"
- Mod4 + c
- ##################################
- # End of xbindkeys configuration #
- ##################################
复制代码
这样就能实现 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 的源代码,我发现这段代码可以列出这些窗口:
- tasks = root.get_full_property(disp.intern_atom("_NET_CLIENT_LIST"),Xatom.WINDOW).value
- for task in tasks:
- wind = disp.create_resource_object("window", task)
- name = wind.get_wm_name()
复制代码
=======================================================
将一个窗口切换到前台
=======================================================
如果找到对应的窗口,就把它切换到前台,这段代码可以完成这个工作:
- wind.configure(stack_mode=X.Above)
- wind.set_input_focus(X.RevertToNone, X.CurrentTime)
复制代码
但是当窗口是最小化的时候并不能正常工作,而是要特殊处理,先用 map 函数
让窗口显示出来。但是由于 X 是异步的, map 并不是立即有效果,而设置输入
焦点又必须是在窗口显示出来的时候才有用,于是通常在 map 后面的设置焦点
的调用都没有任何效果,导致虽然窗口切换到前面了,但是还是没有输入焦点。
我试了一下 pypanel 发现它确实也是有这个问题的。不过这也并不是不能解决,我
只要登记这个窗口的显示事件,在窗口显示出来的时候再设置焦点就可以了。
为了避免同时有多个窗口要设置焦点的情况,我设置了一个标识,用于标识下一
个需要设置焦点的窗口,我想一个窗口应该有唯一标识符,但是 Python 的
Xlib 文档好像东西实在太少了,于是我就用窗口的类名称来标识吧,反正这个
也不需要太精确。
如果窗口没有最小化,那么直接升到前台,并设置焦点,如果是最小化了,那么
就订阅这个窗口的显示事件,并设置下一个等待设置焦点的窗口为它。在事件循
环里面捕捉到窗口显示的事件并且标识对应的时候,再设置焦点,同时退订该窗
口的显示事件。具体可以参考下面给出的源代码。
=======================================================
构造快捷键值
=======================================================
修饰键不多,可以建立一个对应关系表,比如 "Control" 对应到
ControlMask ,多个修饰键的时候按位或一下就可以了。而键值可以使用 Xlib
里面的 string_to_keysym 转换为 keysym 然后由 keysym_to_keycode 转换为
可以用于截获快捷键的 keycode 。其中 CapsLock 、 NumLock 和
ScrollLock 这三个键是要自己计算一下的。
=======================================================
提示窗口
=======================================================
如果要达到 Sawfish 里面那种效果,在没有找到窗口,启动一个新程序的时候,显
示一个气泡提示窗口,说正在启动某某程序,而避免让用户以为是没有反应了,
那么需要手工画一个窗口,这好像又好涉及到字体等一系列的问题,而且要这个
窗口在几秒钟之后自动消失的话,还要用定时器吧,在 google 了一下没有发现
Xlib 的定时器相关的特别有用的资料之后,我决定放弃这个功能了。或者留到
以后来实现吧。
=======================================================
代码
=======================================================
最后写出来的代码就是这样子了。为了避免再解析配置文件而添加更多的代码,
配置就直接在代码里面了,但是配置仍然是很直观的:
- #!/usr/bin/python
- ######################################################################
- ## file: jump-or-exec.py
- ## author: pluskid <pluskid.zju@gmail.com>
- ## date: 2007-01-25
- ######################################################################
- import re
- import subprocess
- from Xlib import X, display, Xatom, XK, Xutil, protocol
- class JumpOrExec:
- modifiers = {
- 'Control':X.ControlMask,
- 'Shift':X.ShiftMask,
- 'Mod1':X.Mod1Mask,
- 'Alt':X.Mod1Mask,
- 'Mod2':X.Mod2Mask,
- 'Mod3':X.Mod3Mask,
- 'Mod4':X.Mod4Mask,
- 'Mod5':X.Mod5Mask
- }
- def __init__(self):
- self.display = display.Display()
- self.root = self.display.screen().root
- (self.capslock_mask, self.numlock_mask, self.scrolllock_mask) = \
- self.get_lock_masks()
- self.jobs = {}
- def get_lock_masks(self):
- """Get the value of capslock_mask, numlock_mask and scrolllock_mask."""
- mask_table = [X.ShiftMask, X.LockMask, X.ControlMask,
- X.Mod1Mask, X.Mod2Mask, X.Mod3Mask,
- X.Mod4Mask, X.Mod5Mask]
- nlock = self.display.keysym_to_keycode(XK.XK_Num_Lock)
- slock = self.display.keysym_to_keycode(XK.XK_Scroll_Lock)
- modmap = self.display.get_modifier_mapping()
- slock_mask = 0
- nlock_mask = 0
- for i in range(len(modmap)):
- for key in modmap[i]:
- if key != 0 and key == nlock:
- nlock_mask = mask_table[i]
- elif key != 0 and key == slock:
- slock_mask = mask_table[i]
- return (X.LockMask, nlock_mask, slock_mask)
- def install_job(self, key_string, job):
- """
- Install the job with key_string.
- key_string's format is just like that used by `xbindkeys'. But
- the syntax is more restrictly here. Keys should be seperated
- EXACTLY by ' + ', thus 'Control + +' is valid while 'Control++'
- is invalid.
- job is an instance of the Job class.
- If the key is detected, first we will ask the job object to
- find an existing window (See the document for the class Job
- for more information), if found, we will raise it, otherwise
- we will tell the job to start a new process.
- """
- (key_code, modifier) = self.decode_key(key_string)
- self.jobs[(key_code, modifier)] = job
- self.grab_key(key_code, modifier)
- def decode_key(self, key_string):
- """Decode key_string into (key_code, modifier)"""
- modifier = 0
- the_key = None
- for key in key_string.split(' + '):
- mod = self.modifiers.get(key)
- if mod:
- modifier |= mod
- else:
- the_key = self.display.keysym_to_keycode(XK.string_to_keysym(key))
- return (the_key, self.escape_lock_mask(modifier))
- def escape_lock_mask(self, modifier):
- """
- Mask off numlock_mask, capslock_mask and scrolllock_mask. Otherwise
- you may find your key doesn't work only because the capslock(or anything)
- is open. That's not what we needed, so we mask it off.
- """
- return modifier & ~(self.scrolllock_mask | \
- self.capslock_mask | \
- self.numlock_mask)
- def grab_key(self, key_code, modifier):
- """
- Grab key press event of key_code+modifier.
- This will enable the X to send us a KeyPress event when the
- key is pressed.
- """
- self.do_grab_key(key_code, modifier)
- if self.numlock_mask:
- self.do_grab_key(key_code, modifier|self.numlock_mask)
- if self.capslock_mask:
- self.do_grab_key(key_code, modifier|self.capslock_mask)
- if self.scrolllock_mask:
- self.do_grab_key(key_code, modifier|self.scrolllock_mask)
- if self.numlock_mask and self.capslock_mask:
- self.do_grab_key(key_code, modifier|self.capslock_mask \
- |self.numlock_mask)
- if self.numlock_mask and self.scrolllock_mask:
- self.do_grab_key(key_code, modifier|self.scrolllock_mask \
- |self.numlock_mask)
- if self.scrolllock_mask and self.capslock_mask:
- self.do_grab_key(key_code, modifier|self.capslock_mask \
- |self.scrolllock_mask)
- if self.numlock_mask and \
- self.capslock_mask and self.scrolllock_mask:
- self.do_grab_key(key_code, modifier|self.capslock_mask \
- | self.numlock_mask | self.scrolllock_mask)
- def do_grab_key(self, key_code, modifier):
- """Helper function of grab_key."""
- self.root.grab_key(key_code, modifier, False, X.GrabModeAsync, X.GrabModeAsync)
- def event_loop(self):
- """Start an infinity loop and handle the incoming events."""
- self.root.change_attributes(event_mask=X.KeyPressMask)
- while 1:
- event = self.display.next_event()
- if event.type == X.KeyPress:
- self.handle_key_press(event)
- elif event.type == X.VisibilityNotify:
- self.handle_visibility_notify(event)
- def handle_key_press(self, event):
- """Key pressed means we will Jump or Exec now."""
- key_code = event.detail
- modifier = self.escape_lock_mask(event.state)
- job = self.jobs.get((key_code, modifier))
- if job:
- wind = job.search_existing(self.display, self.root)
- if wind:
- self.raise_window(wind)
- else:
- job.start_new()
- def raise_window(self, wind):
- """
- Raise the window.
- If it is not in iconic state, we just raise it and
- give it input focus. But if it is in iconic state,
- we have to invoke map to show it first. And since
- map doesn't show the window immediately, we can not
- set the focus immediately (setting the focus to a
- window not viewable have no effect). What we can do
- is to record the window(class name) and start
- listening the visibility notify event of this window,
- when the event arrive and the window(class name)
- matches, we set focus to it and remove the listener.
- """
- if wind.get_wm_state()['state'] == Xutil.IconicState:
- wind.map()
- # install a event listener
- wind.change_attributes(event_mask=X.VisibilityChangeMask)
- self.to_be_focus = ("%s.%s" % wind.get_wm_class())
- else:
- self.raise_and_focus_window(wind)
- def raise_and_focus_window(self, wind):
- wind.configure(stack_mode=X.Above)
- wind.set_input_focus(X.RevertToNone, X.CurrentTime)
- def handle_visibility_notify(self, event):
- """
- Visibility notify normally can only received from the window
- we will give focus. But to avoid accidently received event
- from other window(e.g. the user switchs to one another window
- very very very quickly), we compare the window class name
- before giving it focus.
- """
- wind = event.window
- if ("%s.%s" % wind.get_wm_class()) == self.to_be_focus:
- self.raise_and_focus_window(wind)
- self.to_be_focus = None
- # uninstall the listener
- wind.change_attributes(event_mask=0)
- class Job:
- """
- A Job is consist of a Matcher and a Command. A Matcher is
- an object of the class Matcher, which is used to match against
- the window name and window class name to search for existing
- window. A Command is just a string, which is invoked to start
- a new process if no existing window is found.
- """
- def __init__(self, matcher, command):
- self.matcher = matcher
- self.command = command
- def search_existing(self, disp, root):
- tasks = root.get_full_property(
- disp.intern_atom("_NET_CLIENT_LIST"), Xatom.WINDOW).value
- for task in tasks:
- wind = disp.create_resource_object("window", task)
- if self.matcher.match(wind):
- return wind
- return None
- def start_new(self):
- subprocess.Popen(self.command, shell=True)
- class Matcher:
- """
- Matcher is used to match against window. The window name and window
- class name is to be matched use the regexp given.
- """
- def __init__(self, wm_name = None, wm_class = None):
- self.wm_name = re.compile(wm_name or ".*")
- self.wm_class = re.compile(wm_class or ".*")
- def match(self, window):
- return self.wm_name.search(window.get_wm_name()) and \
- self.wm_class.search("%s.%s" % window.get_wm_class())
- if __name__ == "__main__":
- jobs = [
- ("Mod4 + t", Job(Matcher(wm_class="urxvt.URxvt"), "urxvt -e screen -xRRS terminal")),
- ("Mod4 + f", Job(Matcher(wm_class="Gecko.Firefox-bin"), "firefox")),
- ("Mod4 + e", Job(Matcher(wm_name="emacs@.*"), "/home/kid/bin/emacs -f server-start")),
- ("Mod4 + g", Job(Matcher(wm_name="gnus@.*"),
- "/home/kid/bin/gnus --eval \'(setq frame-title-format "gnus@%b")\' -f gnus")),
- ("Mod4 + q", Job(Matcher(wm_class="qterm.Qterm"), "qterm")),
- ("Mod4 + c", Job(Matcher(wm_class="konqueror.Konqueror"), "kfmclient openProfile filemanagement"))
- ]
- joe = JumpOrExec()
- for job in jobs:
- joe.install_job(job[0], job[1])
- joe.event_loop()
复制代码 |
|