LinuxSir.cn,穿越时空的Linuxsir!

 找回密码
 注册
搜索
热搜: shell linux mysql
查看: 1220|回复: 2

Jump Or Exec

[复制链接]
发表于 2007-1-26 10:20:42 | 显示全部楼层 |阅读模式
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 等。其实只要可以通过命令行控制的
话,用脚本来实现这个功能应该是很方便的,例如,这样一个脚本:


  1. #!/bin/sh

  2. wind=$1
  3. cmd=$2

  4. if ! wmctrl -a $wind
  5. then
  6.     $cmd &
  7. fi
复制代码


便可以实现这个功能,只是还没有快捷键,KDE 的控制中心里可以设置快捷键,
但是还有一个独立的程序叫做 xbindkeys 可以在任何窗口管理器下设置全局快捷
键,用于运行特定的程序。这样,我写了一个通用的  Python  脚本:


  1. #!/usr/bin/python

  2. ######################################################################
  3. ## file:   jump-or-exec.py
  4. ## author: pluskid <pluskid.zju@gmail.com>
  5. ## date:   2007-01-21
  6. ##
  7. ## description:
  8. ##   Jump or Exec utilities -- jump to perticular window or start a
  9. ##   new one if it doesn't exist.
  10. ##
  11. ##   There's `wmctrl' that can control a interact with an X Window
  12. ##   manager that is compatible with the EWMH/NetWM
  13. ##   specification. (Some examples of EWMH/NetWM compatible window
  14. ##   managers include recent versions of Enlightenment, Icewm, Kwin,
  15. ##   Sawfish and Xfce.)
  16. ##
  17. ##   There's another utility `xbindkeys' which can be used to customize
  18. ##   your own keyboard shortcut.
  19. ##
  20. ##   Combining the two tools, here comes the jump-or-exec utilities:
  21. ##   save this file as `jump-or-exec.py' and then:
  22. ##    $ chmod +x jump-or-exec.py
  23. ##    $ ln -s joe-browser jump-or-exec.py
  24. ##   then you can execute ./joe-browser to jump or exec the firefox web
  25. ##   browser. If it works, you can customize the progs variable to set
  26. ##   your own programs to execute and create those symlinks. Finally you
  27. ##   can bind a shortcut to each program with `xbindkeys'. This method
  28. ##   works under any window manager that is compatible with the EWMH/NetWM
  29. ##   specification.
  30. ##
  31. ######################################################################

  32. import os
  33. import sys

  34. progs = {
  35.     "joe-terminal" : ("urxvt.URxvt -x", "urxvt -e screen -xRRS terminal"),
  36.     "joe-emacs" : ("emacs@", "fe -f server-start"),
  37.     "joe-gnus" : ("gnus@", "fe --eval \'(setq frame-title-format "gnus@%b")\' -f gnus"),
  38.     "joe-fm" : ("konqueror.Konqueror -x", "kfmclient openProfile filemanagement"),
  39.     "joe-browser" : ("Gecko.Firefox-bin -x", "firefox"),
  40.     "joe-bbs" : ("qterm.Qterm -x", "qterm")
  41.     }

  42. if len(sys.argv) < 1:
  43.     print "Don't know what program to start. Please execute with one of:\n%s\n" % \
  44.           "\n".join(["\t"+prog_name for prog_name in progs.keys()])
  45.     sys.exit(-1)

  46. prog = os.path.basename(sys.argv[0])
  47. wind,cmdline = progs[prog]
  48. if os.system("wmctrl -a %s" % wind) != 0:
  49.     os.system("%s &" % cmdline)         # execute in background
复制代码


并且做一系列的链接 ( joe-fm、joe-gnus 等 ) 到这个脚本文件,它根据自己
被调用的名字来启动对应的程序,于是我就在 xbindkeys 里面执行这些软链接:


  1. ###########################
  2. # xbindkeys configuration #
  3. ###########################
  4. # My jump-or-exec utilities
  5. "/home/kid/bin/joe-terminal"
  6. Mod4 + t

  7. "/home/kid/bin/joe-browser"
  8. Mod4 + f

  9. "/home/kid/bin/joe-emacs"
  10. Mod4 + e

  11. "/home/kid/bin/joe-gnus"
  12. Mod4 + g

  13. "/home/kid/bin/joe-bbs"
  14. Mod4 + q

  15. "/home/kid/bin/joe-fm"
  16. Mod4 + c

  17. ##################################
  18. # End of xbindkeys configuration #
  19. ##################################
复制代码


这样就能实现 jump-or-exec 的功能了。但是效果不如  Sawfish  里面内置的
jump-or-exec 好,因为每次都是启动一个脚本,有一些微微的延迟,而且有时
候切换过去之后没有获得焦点。于是我决定做一个像 xbindkeys 一样的后台进
程,自己处理快捷键捕获、窗口切换已经运行程序,而不是用几个命令组合起来。


=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
  独立的 Python 版 jump-or-exec
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-


正好  Python  有 Xlib 的库,可是后来发现它的文档其实非常不全,很多东西都
没有提到,上它的网站也看到提到这个问题,并且还在征集维护者。不过我对
Xlib 和  Python  都还不熟悉,只好看一些源代码来摸索如何做。其中主要查看
xbindkeyspypanel 两个程序的源代码,大概搞清楚了几个重要的地方如
何实现。

=======================================================
    截获全局快捷键
=======================================================


X 被设计于可以在网络上运行,为了不被大量的事件传输阻塞,通常 X 在你明
确要求某些事件之后才会将事件发送给你。我通过注册 root 窗口的
KeyPress 事件来收取全局快捷键的信息:


  1. root.change_attributes(event_mask=(X.KeyPressMask|X.KeyReleaseMask))
复制代码


但是并不是所有的按键都会被截获,还必须告诉 X 你对哪些快捷键感兴趣:


  1. root.grab_key(keycode, modifier, False, X.GrabModeAsync, X.GrabModeAsync)
复制代码


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


=======================================================
    列出所有任务
=======================================================


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


  1. tasks = root.get_full_property(disp.intern_atom("_NET_CLIENT_LIST"),Xatom.WINDOW).value
  2. for task in tasks:
  3.     wind = disp.create_resource_object("window", task)
  4.     name = wind.get_wm_name()
复制代码



=======================================================
    将一个窗口切换到前台
=======================================================


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


  1. wind.configure(stack_mode=X.Above)
  2. 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 。其中 CapsLockNumLock
ScrollLock 这三个键是要自己计算一下的。


=======================================================
    提示窗口
=======================================================


如果要达到  Sawfish  里面那种效果,在没有找到窗口,启动一个新程序的时候,显
示一个气泡提示窗口,说正在启动某某程序,而避免让用户以为是没有反应了,
那么需要手工画一个窗口,这好像又好涉及到字体等一系列的问题,而且要这个
窗口在几秒钟之后自动消失的话,还要用定时器吧,在 google 了一下没有发现
Xlib 的定时器相关的特别有用的资料之后,我决定放弃这个功能了。或者留到
以后来实现吧。


=======================================================
    代码
=======================================================


最后写出来的代码就是这样子了。为了避免再解析配置文件而添加更多的代码,
配置就直接在代码里面了,但是配置仍然是很直观的:


  1. #!/usr/bin/python

  2. ######################################################################
  3. ## file:   jump-or-exec.py
  4. ## author: pluskid <pluskid.zju@gmail.com>
  5. ## date:   2007-01-25
  6. ######################################################################

  7. import re
  8. import subprocess
  9. from Xlib import X, display, Xatom, XK, Xutil, protocol

  10. class JumpOrExec:
  11.     modifiers =  {
  12.         'Control':X.ControlMask,
  13.         'Shift':X.ShiftMask,
  14.         'Mod1':X.Mod1Mask,
  15.         'Alt':X.Mod1Mask,
  16.         'Mod2':X.Mod2Mask,
  17.         'Mod3':X.Mod3Mask,
  18.         'Mod4':X.Mod4Mask,
  19.         'Mod5':X.Mod5Mask
  20.         }
  21.     def __init__(self):
  22.         self.display = display.Display()
  23.         self.root = self.display.screen().root
  24.         (self.capslock_mask, self.numlock_mask, self.scrolllock_mask) = \
  25.             self.get_lock_masks()
  26.         self.jobs = {}
  27.     def get_lock_masks(self):
  28.         """Get the value of capslock_mask, numlock_mask and scrolllock_mask."""
  29.         mask_table = [X.ShiftMask, X.LockMask, X.ControlMask,
  30.                       X.Mod1Mask, X.Mod2Mask, X.Mod3Mask,
  31.                       X.Mod4Mask, X.Mod5Mask]
  32.         nlock = self.display.keysym_to_keycode(XK.XK_Num_Lock)
  33.         slock = self.display.keysym_to_keycode(XK.XK_Scroll_Lock)
  34.         modmap = self.display.get_modifier_mapping()
  35.         slock_mask = 0
  36.         nlock_mask = 0
  37.         for i in range(len(modmap)):
  38.             for key in modmap[i]:
  39.                 if key != 0 and key == nlock:
  40.                     nlock_mask = mask_table[i]
  41.                 elif key != 0 and key == slock:
  42.                     slock_mask = mask_table[i]
  43.         return (X.LockMask, nlock_mask, slock_mask)
  44.     def install_job(self, key_string, job):
  45.         """
  46.         Install the job with key_string.

  47.         key_string's format is just like that used by `xbindkeys'. But
  48.         the syntax is more restrictly here. Keys should be seperated
  49.         EXACTLY by ' + ', thus 'Control + +' is valid while 'Control++'
  50.         is invalid.

  51.         job is an instance of the Job class.

  52.         If the key is detected, first we will ask the job object to
  53.         find an existing window (See the document for the class Job
  54.         for more information), if found, we will raise it, otherwise
  55.         we will tell the job to start a new process.
  56.         """
  57.         (key_code, modifier) = self.decode_key(key_string)
  58.         self.jobs[(key_code, modifier)] = job
  59.         self.grab_key(key_code, modifier)
  60.     def decode_key(self, key_string):
  61.         """Decode key_string into (key_code, modifier)"""
  62.         modifier = 0
  63.         the_key = None
  64.         for key in key_string.split(' + '):
  65.             mod = self.modifiers.get(key)
  66.             if mod:
  67.                 modifier |= mod
  68.             else:
  69.                 the_key = self.display.keysym_to_keycode(XK.string_to_keysym(key))
  70.         return (the_key, self.escape_lock_mask(modifier))
  71.     def escape_lock_mask(self, modifier):
  72.         """
  73.         Mask off numlock_mask, capslock_mask and scrolllock_mask. Otherwise
  74.         you may find your key doesn't work only because the capslock(or anything)
  75.         is open. That's not what we needed, so we mask it off.
  76.         """
  77.         return modifier & ~(self.scrolllock_mask | \
  78.                                self.capslock_mask | \
  79.                                self.numlock_mask)
  80.     def grab_key(self, key_code, modifier):
  81.         """
  82.         Grab key press event of key_code+modifier.
  83.         This will enable the X to send us a KeyPress event when the
  84.         key is pressed.
  85.         """
  86.         self.do_grab_key(key_code, modifier)
  87.         if self.numlock_mask:
  88.             self.do_grab_key(key_code, modifier|self.numlock_mask)
  89.         if self.capslock_mask:
  90.             self.do_grab_key(key_code, modifier|self.capslock_mask)
  91.         if self.scrolllock_mask:
  92.             self.do_grab_key(key_code, modifier|self.scrolllock_mask)
  93.         if self.numlock_mask and self.capslock_mask:
  94.             self.do_grab_key(key_code, modifier|self.capslock_mask \
  95.                                  |self.numlock_mask)
  96.         if self.numlock_mask and self.scrolllock_mask:
  97.             self.do_grab_key(key_code, modifier|self.scrolllock_mask \
  98.                                  |self.numlock_mask)
  99.         if self.scrolllock_mask and self.capslock_mask:
  100.             self.do_grab_key(key_code, modifier|self.capslock_mask \
  101.                                  |self.scrolllock_mask)
  102.         if self.numlock_mask and \
  103.                 self.capslock_mask and self.scrolllock_mask:
  104.             self.do_grab_key(key_code, modifier|self.capslock_mask \
  105.                                  | self.numlock_mask | self.scrolllock_mask)
  106.     def do_grab_key(self, key_code, modifier):
  107.         """Helper function of grab_key."""
  108.         self.root.grab_key(key_code, modifier, False, X.GrabModeAsync, X.GrabModeAsync)
  109.     def event_loop(self):
  110.         """Start an infinity loop and handle the incoming events."""
  111.         self.root.change_attributes(event_mask=X.KeyPressMask)
  112.         while 1:
  113.             event = self.display.next_event()
  114.             if event.type == X.KeyPress:
  115.                 self.handle_key_press(event)
  116.             elif event.type == X.VisibilityNotify:
  117.                 self.handle_visibility_notify(event)
  118.     def handle_key_press(self, event):
  119.         """Key pressed means we will Jump or Exec now."""
  120.         key_code = event.detail
  121.         modifier = self.escape_lock_mask(event.state)
  122.         job = self.jobs.get((key_code, modifier))
  123.         if job:
  124.             wind = job.search_existing(self.display, self.root)
  125.             if wind:
  126.                 self.raise_window(wind)
  127.             else:
  128.                 job.start_new()
  129.     def raise_window(self, wind):
  130.         """
  131.         Raise the window.

  132.         If it is not in iconic state, we just raise it and
  133.         give it input focus. But if it is in iconic state,
  134.         we have to invoke map to show it first. And since
  135.         map doesn't show the window immediately, we can not
  136.         set the focus immediately (setting the focus to a
  137.         window not viewable have no effect). What we can do
  138.         is to record the window(class name) and start
  139.         listening the visibility notify event of this window,
  140.         when the event arrive and the window(class name)
  141.         matches, we set focus to it and remove the listener.
  142.         """
  143.         if wind.get_wm_state()['state'] == Xutil.IconicState:
  144.             wind.map()
  145.             # install a event listener
  146.             wind.change_attributes(event_mask=X.VisibilityChangeMask)
  147.             self.to_be_focus = ("%s.%s" % wind.get_wm_class())
  148.         else:
  149.             self.raise_and_focus_window(wind)
  150.     def raise_and_focus_window(self, wind):
  151.         wind.configure(stack_mode=X.Above)
  152.         wind.set_input_focus(X.RevertToNone, X.CurrentTime)

  153.     def handle_visibility_notify(self, event):
  154.         """
  155.         Visibility notify normally can only received from the window
  156.         we will give focus. But to avoid accidently received event
  157.         from other window(e.g. the user switchs to one another window
  158.         very very very quickly), we compare the window class name
  159.         before giving it focus.
  160.         """
  161.         wind = event.window
  162.         if ("%s.%s" % wind.get_wm_class()) == self.to_be_focus:
  163.             self.raise_and_focus_window(wind)
  164.             self.to_be_focus = None
  165.             # uninstall the listener
  166.             wind.change_attributes(event_mask=0)

  167. class Job:
  168.     """
  169.     A Job is consist of a Matcher and a Command. A Matcher is
  170.     an object of the class Matcher, which is used to match against
  171.     the window name and window class name to search for existing
  172.     window. A Command is just a string, which is invoked to start
  173.     a new process if no existing window is found.
  174.     """
  175.     def __init__(self, matcher, command):
  176.         self.matcher = matcher
  177.         self.command = command
  178.     def search_existing(self, disp, root):
  179.         tasks = root.get_full_property(
  180.             disp.intern_atom("_NET_CLIENT_LIST"), Xatom.WINDOW).value
  181.         for task in tasks:
  182.             wind = disp.create_resource_object("window", task)
  183.             if self.matcher.match(wind):
  184.                 return wind
  185.         return None
  186.     def start_new(self):
  187.         subprocess.Popen(self.command, shell=True)

  188. class Matcher:
  189.     """
  190.     Matcher is used to match against window. The window name and window
  191.     class name is to be matched use the regexp given.
  192.     """
  193.     def __init__(self, wm_name = None, wm_class = None):
  194.         self.wm_name = re.compile(wm_name or ".*")
  195.         self.wm_class = re.compile(wm_class or ".*")
  196.     def match(self, window):
  197.         return self.wm_name.search(window.get_wm_name()) and \
  198.             self.wm_class.search("%s.%s" % window.get_wm_class())

  199. if __name__ == "__main__":
  200.     jobs = [
  201.     ("Mod4 + t", Job(Matcher(wm_class="urxvt.URxvt"), "urxvt -e screen -xRRS terminal")),
  202.     ("Mod4 + f", Job(Matcher(wm_class="Gecko.Firefox-bin"), "firefox")),
  203.     ("Mod4 + e", Job(Matcher(wm_name="emacs@.*"), "/home/kid/bin/emacs -f server-start")),
  204.     ("Mod4 + g", Job(Matcher(wm_name="gnus@.*"),
  205.                      "/home/kid/bin/gnus --eval \'(setq frame-title-format "gnus@%b")\' -f gnus")),
  206.     ("Mod4 + q", Job(Matcher(wm_class="qterm.Qterm"), "qterm")),
  207.     ("Mod4 + c", Job(Matcher(wm_class="konqueror.Konqueror"), "kfmclient openProfile filemanagement"))
  208.     ]
  209.     joe = JumpOrExec()
  210.     for job in jobs:
  211.         joe.install_job(job[0], job[1])
  212.     joe.event_loop()
复制代码
发表于 2007-1-26 11:03:49 | 显示全部楼层
好文,收获颇丰,感谢楼主的辛勤劳动!
回复 支持 反对

使用道具 举报

发表于 2007-2-3 15:36:35 | 显示全部楼层
不错,不错。

试试去。。。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

快速回复 返回顶部 返回列表