<?xml version="1.0" encoding="UTF-8"?>
<commit>
  <added type="array">
    <added>
      <filename>errors</filename>
    </added>
  </added>
  <modified type="array">
    <modified>
      <diff>@@ -2,53 +2,75 @@
 
 from whimsy.base_config import *
 
+import socket
+
+class mpd(object):
+    'control music player daemon (http://musicpd.org)'
+    def __init__(self, cmd):
+        self.cmd = cmd
+    def __call__(self, **kw):
+        try:
+            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+            s.connect(('localhost', 6600))
+            s.send(&quot;%s\n&quot; % self.cmd)
+            s.close()
+        except socket.error:
+            pass
+
 wm.vwidth = W * 3
 wm.vheight = H * 3
 
-actions = [
-    # these take you to the nine viewports, laid out in a 3x3 grid.
-    # Ctrl+Alt+...
-    #     u i o
-    #     j k l
-    #     m , .
-    # W is the screen width and H is the screen height.
-    (viewport_absolute_move(  0,   0), if_key_press(&quot;u&quot;,      C+A)),
-    (viewport_absolute_move(  W,   0), if_key_press(&quot;i&quot;,      C+A)),
-    (viewport_absolute_move(W*2,   0), if_key_press(&quot;o&quot;,      C+A)),
-    (viewport_absolute_move(  0,   H), if_key_press(&quot;j&quot;,      C+A)),
-    (viewport_absolute_move(  W,   H), if_key_press(&quot;k&quot;,      C+A)),
-    (viewport_absolute_move(W*2,   H), if_key_press(&quot;l&quot;,      C+A)),
-    (viewport_absolute_move(  0, H*2), if_key_press(&quot;m&quot;,      C+A)),
-    (viewport_absolute_move(  W, H*2), if_key_press(&quot;comma&quot;,  C+A)),
-    (viewport_absolute_move(W*2, H*2), if_key_press(&quot;period&quot;, C+A)),
-
-    # relative movements of the viewport with Ctrl+up, Ctrl+left, etc
-    (viewport_relative_move(-W,  0), if_key_press(&quot;Left&quot;,  C)),
-    (viewport_relative_move(+W,  0), if_key_press(&quot;Right&quot;, C)),
-    (viewport_relative_move( 0, -H), if_key_press(&quot;Up&quot;,    C)),
-    (viewport_relative_move( 0, +H), if_key_press(&quot;Down&quot;,  C)),
-
-    # Ctrl+Alt+x or double click desktop (root window) to open xterm
-    (execute(&quot;xterm&quot;), if_key_press(&quot;x&quot;, C+A)),
-    (execute(&quot;xterm&quot;), if_root, if_button_press(1, Any), if_doubleclick),
-
-    # click on client to focus
-    (client_method('focus'), if_client, if_button_press(1, Any, passthrough=True)),
-
-    # Ctrl+Alt+w to close window
-    (delete_client(), if_client, if_key_press('w', C+A)),
-
-    # Alt+left/right mouse buttons to move/resize interactively
-    (start_move(),                  if_button_press(1, A), if_client),
-    (start_resize(),                if_button_press(3, A), if_client),
-
-    # Alt+wheelup to lower window, Alt+wheeldown to raise
-    (client_method('stack_bottom'), if_button_press(4, A), if_client),
-    (client_method('stack_top'),    if_button_press(5, A), if_client),
+chains = [
+    (if_key_press(&quot;u&quot;,      C+A), viewport_absolute_move(  0,   0)),
+    (if_key_press(&quot;i&quot;,      C+A), viewport_absolute_move(  W,   0)),
+    (if_key_press(&quot;o&quot;,      C+A), viewport_absolute_move(W*2,   0)),
+    (if_key_press(&quot;j&quot;,      C+A), viewport_absolute_move(  0,   H)),
+    (if_key_press(&quot;k&quot;,      C+A), viewport_absolute_move(  W,   H)),
+    (if_key_press(&quot;l&quot;,      C+A), viewport_absolute_move(W*2,   H)),
+    (if_key_press(&quot;m&quot;,      C+A), viewport_absolute_move(  0, H*2)),
+    (if_key_press(&quot;comma&quot;,  C+A), viewport_absolute_move(  W, H*2)),
+    (if_key_press(&quot;period&quot;, C+A), viewport_absolute_move(W*2, H*2)),
+
+    (if_key_press(&quot;Left&quot;,  C), viewport_relative_move(-W,  0)),
+    (if_key_press(&quot;Right&quot;, C), viewport_relative_move(+W,  0)),
+    (if_key_press(&quot;Up&quot;,    C), viewport_relative_move( 0, -H)),
+    (if_key_press(&quot;Down&quot;,  C), viewport_relative_move( 0, +H)),
+
+    (if_key_press(&quot;x&quot;, C+A), execute(&quot;aterm&quot;)),
+
+    (if_key_press(&quot;s&quot;, C+A), execute(&quot;sleep 1; xset s activate&quot;)),
+
+    (if_key_press(&quot;z&quot;, M4), mpd(&quot;previous&quot;)),
+    (if_key_press(&quot;x&quot;, M4), mpd(&quot;stop&quot;)),
+    (if_key_press(&quot;c&quot;, M4), mpd(&quot;play&quot;)),
+    (if_key_press(&quot;v&quot;, M4), mpd(&quot;pause&quot;)),
+    (if_key_press(&quot;b&quot;, M4), mpd(&quot;next&quot;)),
+
+    #(client_method('focus'), if_client, if_button_press(1, Any, passthrough=True)),
+    (if_client, if_key_press('w', C+A), delete_client()),
+    (if_client, if_button_press(1, A), start_move()),
+    (if_client, if_button_press(3, A), start_resize()),
+    (if_client, if_button_press(4, A), client_method('stack_bottom')),
+    (if_client, if_button_press(5, A), client_method('stack_top')),
+
+    # maximizations: full screen, left half, right half
+
+    (if_key_press(&quot;f&quot;, M4), if_client, client_method('moveresize', x=0,   y=0, width=W,   height=H)),
+    (if_key_press(&quot;h&quot;, M4), if_client, client_method('moveresize', x=0,   y=0, width=W/2, height=H)),
+    (if_key_press(&quot;l&quot;, M4), if_client, client_method('moveresize', x=W/2, y=0, width=W/2, height=H)),
 ]
 
-for action in actions:
-    app.hub.register(&quot;event&quot;, action[0], *action[1:])
+# the recursiveness of grabbing on root is also causing global keybindings to
+# temporarily shift focus to root when using them.
+
+for chain in chains:
+    app.hub.attach(&quot;event&quot;, *chain)
+    for func in chain:
+        if isinstance(func, binding_base):
+            if if_client not in chain:
+                app.hub.attach('wm_manage_after', func.grab)
+            if if_root not in chain:
+                app.hub.attach('client_init_after', func.grab)
 
 app.run()
 </diff>
      <filename>config.py</filename>
    </modified>
    <modified>
      <diff>@@ -4,22 +4,19 @@ import os, subprocess, logging
 
 from whimsy.x11 import props
 
-def _unmanage(hub, wm, win, delete=False, **kw):
-    c = wm.window_to_client(win)
-    if c:
-        wm.clients.remove(c)
-        c.shutdown()
-        if delete:
-            c.delete()
-    hub.signal('after_unmanage_window', win=win)
+class delete_client(object):
+    &quot;&quot;&quot;this is when we tell the client to go away&quot;&quot;&quot;
+    def __call__(self, wm, win, **kw):
+        wm.find_client(win).delete()
 
 class unmanage_window(object):
-    def __call__(self, **kw):
-        _unmanage(**kw)
-
-class delete_client(object):
-    def __call__(self, **kw):
-        _unmanage(delete=True, **kw)
+    &quot;&quot;&quot;
+    this is when a client has gone away (potentially after we told it to do so,
+    or maybe it decided to do so)
+    &quot;&quot;&quot;
+    def __call__(self, hub, wm, win, **kw):
+        wm.clients.remove(wm.find_client(win))
+        hub.emit('after_unmanage_window', win=win)
 
 
 # _WHIMSY_CLIENT_LIST_FOCUS: lists managed windows that have been
@@ -39,7 +36,7 @@ class focus_last_focused(object):
     def __call__(self, wm, win, **kw):
         wins = props.get_prop(wm.dpy, wm.root, '_WHIMSY_CLIENT_LIST_FOCUS')
         while wins:
-            c = wm.window_id_to_client(wins[0])
+            c = wm.find_client(wins[0])
             if c:
                 c.focus()
                 break
@@ -56,7 +53,7 @@ class client_method(object):
         if 'client' in kw:
             c = kw['client']
         else:
-            c = wm.window_to_client(kw['win'])
+            c = wm.find_client(kw['win'])
         getattr(c, self.methodname)(*self.a, **self.kw)
 
 class execute(object):
@@ -94,7 +91,7 @@ class viewport_absolute_move(object):
             #c.dpy.sync() # necessary?  maybe not
             #wish list: discard enternotifies
 
-        hub.signal('after_viewport_move', x=to_x, y=to_y)
+        hub.emit('after_viewport_move', x=to_x, y=to_y)
 
 class viewport_relative_move(object):
     def __init__(self, x, y):
@@ -110,5 +107,5 @@ class viewport_relative_move(object):
 class discover_existing_windows(object):
     def __call__(self, hub, wm, **kw):
         for win in wm.root.query_tree().children:
-            hub.signal('existing_window_discovered', win=win)
+            hub.emit('existing_window_discovered', win=win)
 </diff>
      <filename>whimsy/actions/builtins.py</filename>
    </modified>
    <modified>
      <diff>@@ -7,14 +7,16 @@ class update_client_property(object):
     def __call__(self, wm, win, ev, **kw):
         propname = wm.dpy.get_atom_name(ev.atom)
         if propname in props.supported_props():
-            wm.window_to_client(win).update_prop(propname)
+            c = wm.find_client(win)
+            if propname in c.props:
+                c.update_prop(propname)
 
 # todo: click focus handler &amp; sloppy focus handler
 
 #todo: circulate request
 class configure_request_handler(object):
     def __call__(self, wm, win, ev, **kw):
-        client_or_win = wm.window_to_client(win) or win
+        client_or_win = wm.find_client(win) or win
         client_or_win.configure(**util.configure_request_changes(ev))
 
 class install_colormap(object):</diff>
      <filename>whimsy/actions/event_handling.py</filename>
    </modified>
    <modified>
      <diff>@@ -5,7 +5,12 @@ from Xlib import X
 from whimsy import util
 from whimsy.x11 import props
 
-class net_supported(object):
+class startup_and_shutdown_with_wm(object):
+    def __init__(self, hub):
+        hub.attach('wm_manage_after', self.startup)
+        hub.attach('wm_shutdown_before', self.shutdown)
+
+class net_supported(startup_and_shutdown_with_wm):
     def startup(self, wm, **kw):
         props.change_prop(wm.dpy, wm.root, '_NET_SUPPORTED', [
             wm.dpy.get_atom(propname)
@@ -16,19 +21,19 @@ class net_supported(object):
     def shutdown(self, wm, **kw):
         props.delete_prop(wm.dpy, wm.root, '_NET_SUPPORTED')
 
-class net_number_of_desktops(object):
+class net_number_of_desktops(startup_and_shutdown_with_wm):
     def startup(self, wm, **kw):
         props.change_prop(wm.dpy, wm.root, '_NET_NUMBER_OF_DESKTOPS', 1)
     def shutdown(self, wm, **kw):
         props.delete_prop(wm.dpy, wm.root, '_NET_NUMBER_OF_DESKTOPS')
 
-class net_current_desktop(object):
+class net_current_desktop(startup_and_shutdown_with_wm):
     def startup(self, wm, **kw):
         props.change_prop(wm.dpy, wm.root, '_NET_CURRENT_DESKTOP', 0)
     def shutdown(self, wm, **kw):
         props.delete_prop(wm.dpy, wm.root, '_NET_CURRENT_DESKTOP')
 
-class net_supporting_wm_check(object):
+class net_supporting_wm_check(startup_and_shutdown_with_wm):
     def startup(self, wm, **kw):
         self.win = wm.root.create_window(-5000, -5000, 1, 1, 0, X.CopyFromParent)
         props.change_prop(wm.dpy, self.win, '_NET_WM_NAME', 'Whimsy')
@@ -41,7 +46,7 @@ class net_supporting_wm_check(object):
         props.delete_prop(wm.dpy, self.win, '_NET_WM_NAME')
         self.win.destroy()
 
-class net_desktop_geometry(object):
+class net_desktop_geometry(startup_and_shutdown_with_wm):
     def startup(self, wm, **kw):
         props.change_prop(
             wm.dpy, wm.root, '_NET_DESKTOP_GEOMETRY',
@@ -52,6 +57,11 @@ class net_desktop_geometry(object):
         props.delete_prop(wm.dpy, wm.root, '_NET_DESKTOP_GEOMETRY')
 
 class net_client_list(object):
+    def __init__(self, hub):
+        hub.attach('after_manage_window', self.refresh)
+        hub.attach('after_unmanage_window', self.refresh)
+        hub.attach('wm_shutdown_before', self.shutdown)
+
     def refresh(self, wm, **kw):
         props.change_prop(
             wm.dpy, wm.root, '_NET_CLIENT_LIST',
@@ -62,6 +72,10 @@ class net_client_list(object):
         props.delete_prop(wm.dpy, wm.root, '_NET_CLIENT_LIST')
 
 class net_desktop_viewport(object):
+    def __init__(self, hub):
+        hub.attach('wm_manage_after', self.startup)
+        hub.attach('after_viewport_move', self.refresh)
+
     def startup(self, hub, wm, **kw):
         viewport = props.get_prop(wm.dpy, wm.root,
             '_NET_DESKTOP_VIEWPORT')
@@ -71,7 +85,7 @@ class net_desktop_viewport(object):
             props.change_prop(wm.dpy, wm.root,
                 '_NET_DESKTOP_VIEWPORT', viewport)
 
-        hub.signal('viewport_discovered', x=viewport[0], y=viewport[1])
+        hub.emit('viewport_discovered', x=viewport[0], y=viewport[1])
 
     def refresh(self, wm, x, y, **kw):
         props.change_prop(</diff>
      <filename>whimsy/actions/ewmh.py</filename>
    </modified>
    <modified>
      <diff>@@ -18,11 +18,11 @@ class interactive_pointer_transformer(object):
     def __call__(self, hub, **kw):
         self.grab(hub=hub, **kw)
         #hub.push_context()
-        hub.register('motion_notify', self.motion)
-        hub.register('button_release', self.ungrab)
+        hub.attach('motion_notify', self.motion)
+        hub.attach('button_release', self.ungrab)
 
     def grab(self, wm, win, ev, **kw):
-        client = wm.window_to_client(win)
+        client = wm.find_client(win)
         self.state = transformation(client, ev.root_x, ev.root_y)
         client.win.grab_pointer(True,
             X.PointerMotionMask | X.ButtonReleaseMask,
@@ -39,8 +39,8 @@ class interactive_pointer_transformer(object):
 
     def ungrab(self, hub, wm, **kw):
         wm.dpy.ungrab_pointer(X.CurrentTime)
-        hub.unregister(self.motion)
-        hub.unregister(self.ungrab)
+        hub.detach(self.motion)
+        hub.detach(self.ungrab)
         self.state = None
 
     def transform(self, xdelta, ydelta):</diff>
      <filename>whimsy/actions/transformers.py</filename>
    </modified>
    <modified>
      <diff>@@ -7,7 +7,6 @@ from whimsy.actions import ewmh
 from whimsy.actions.builtins import *
 from whimsy.actions.transformers import *
 from whimsy.actions.event_handling import *
-from whimsy.actions.replay import *
 from whimsy.filters.bindings import *
 from whimsy.filters import *
 from whimsy.x11.modifiers import modifier_mask, modifier_core
@@ -47,74 +46,52 @@ root_geometry = app.wm.root.get_geometry()
 W = root_geometry.width
 H = root_geometry.height
 
-startup_shutdown_signal_methods = {
-    'wm_manage_after': 'startup',
-    'wm_shutdown_before': 'shutdown',
-}
-
-client_list_tracking_signal_methods = {
-    'after_manage_window': 'refresh',
-    'after_unmanage_window': 'refresh',
-    'wm_shutdown_before': 'shutdown',
-}
-
-viewport_tracking_signal_methods = {
-    'wm_manage_after': 'startup',
-    'after_viewport_move': 'refresh',
-}
-
-clicks = click_counter()
-
-def if_doubleclick(**kw):
-    return clicks.if_multi(2)(**kw)
-
-actions = [
-    (startup_shutdown_signal_methods,     ewmh.net_supported()),
-    (startup_shutdown_signal_methods,     ewmh.net_supporting_wm_check()),
-    (startup_shutdown_signal_methods,     ewmh.net_number_of_desktops()),
-    (startup_shutdown_signal_methods,     ewmh.net_current_desktop()),
-    (startup_shutdown_signal_methods,     ewmh.net_desktop_geometry()),
-    (viewport_tracking_signal_methods,    ewmh.net_desktop_viewport()),
-    (client_list_tracking_signal_methods, ewmh.net_client_list()),
+ewmh.net_supported(hub)
+ewmh.net_supporting_wm_check(hub)
+ewmh.net_number_of_desktops(hub)
+ewmh.net_current_desktop(hub)
+ewmh.net_desktop_geometry(hub)
+ewmh.net_client_list(hub)
+ewmh.net_desktop_viewport(hub)
 
+# XXX use event names as signal names
+chains = [
     ('wm_manage_after', discover_existing_windows()),
 
-    ('existing_window_discovered', lambda win, **kw: wm.manage_window(win),
-     if_should_manage_existing_window),
-
-    ('event', lambda win, **kw: wm.manage_window(win),
-     if_(X.MapRequest, wintype=&quot;unmanaged&quot;), if_should_manage_new_window),
+    ('existing_window_discovered', if_should_manage_existing_window,
+                                   lambda wm, win, **kw: wm.manage_window(win)),
 
-    ('event', client_method('focus'), if_(X.MapRequest, wintype='client')),
+    ('map_request', if_unmanaged,
+                    if_should_manage_new_window,
+                    lambda wm, win, **kw: wm.manage_window(win)),
 
-    ('event', client_method('focus'),
-     if_(X.EnterNotify, wintype='client'), if_state(~ButtonMask)),
+    ('map_request', if_client,
+                    client_method('focus')),
 
-    ('event', unmanage_window(), if_(X.DestroyNotify, wintype='client')),
+    ('enter_notify', if_client,
+                     if_state(~ButtonMask),
+                     client_method('focus')),
 
-    ('event', unmanage_window(), if_(X.UnmapNotify, wintype='client')),
+    ('enter_notify', if_root,
+                     if_state(~ButtonMask),
+                     lambda wm, **kw: wm.dpy.set_input_focus(wm.root,
+                                      X.RevertToPointerRoot, X.CurrentTime)),
 
-    ('event', update_client_list_focus(), if_(X.FocusIn, wintype='client')),
+    ('destroy_notify',  if_client, unmanage_window()),
+    ('unmap_notify',    if_client, unmanage_window()),
+    ('focus_in',        if_client, update_client_list_focus()),
+    ('property_notify', if_client, update_client_property()),
 
-    ('event', update_client_property(),
-     if_(X.PropertyNotify, wintype='client')),
-
-    ('event', focus_last_focused(), if_(X.DestroyNotify)),
-
-    ('event', install_colormap(), if_(X.ColormapNotify)),
-
-    ('event', configure_request_handler(), if_(X.ConfigureRequest)),
-
-    ('event', clicks, if_(X.ButtonPress)),
+    ('destroy_notify',    focus_last_focused()),
+    ('colormap_notify',   install_colormap()),
+    ('configure_request', configure_request_handler()), # rename this function
 
     ('client_init_after', client_method('configure', border_width=0)),
-
     ('client_init_after', client_method('map_normal')),
-
-    ('event_done', smart_replay(),
-     if_event_type(X.KeyPress, X.KeyRelease, X.ButtonPress, X.ButtonRelease)),
 ]
 
-for action in actions:
-    app.hub.register(action[0], action[1], *action[2:])
+for chaininfo in chains:
+    name = chaininfo[0]
+    chain = chaininfo[1:]
+    hub.attach(name, *chain)
 </diff>
      <filename>whimsy/base_config.py</filename>
    </modified>
    <modified>
      <diff>@@ -8,7 +8,7 @@ class tick_controller(object):
     def tick_forever(self):
         self.alive = True
         while self.alive:
-            self.hub.signal('tick')
+            self.hub.emit('tick')
 
     def stop(self):
         self.alive = False</diff>
      <filename>whimsy/controllers/tick_controller.py</filename>
    </modified>
    <modified>
      <diff>@@ -23,8 +23,8 @@ class x_event_controller(object):
         ev = self.dpy.next_event()
         # the specific event name, like button_press (converted from ButtonPress)
         lowered = capital_letter_re.sub('_\\1', ev.__class__.__name__).lower()
-        self.hub.signal('event_begin', ev=ev, win=ev.window)
-        self.hub.signal(lowered,       ev=ev, win=ev.window)
-        self.hub.signal('event',       ev=ev, win=ev.window)
-        self.hub.signal('event_done',  ev=ev, win=ev.window)
+        self.hub.emit('event_begin', ev=ev, win=ev.window)
+        self.hub.emit(lowered,       ev=ev, win=ev.window)
+        self.hub.emit('event',       ev=ev, win=ev.window)
+        self.hub.emit('event_done',  ev=ev, win=ev.window)
     </diff>
      <filename>whimsy/controllers/x_event_controller.py</filename>
    </modified>
    <modified>
      <diff>@@ -26,6 +26,12 @@ def if_root(wm, win, ev, **kw):
         util.window_type(wm, win) == 'root'
     )
 
+def if_unmanaged(wm, win, ev, **kw):
+    return (
+        hasattr(ev, 'window') and
+        util.window_type(wm, win) == 'unmanaged'
+    )
+
 class if_state(object):
     'true if modifier (shift/control/etc) keys currently match mods'
     def __init__(self, mods):
@@ -45,47 +51,6 @@ class if_(object):
             return True
         return util.window_type(wm, win) == self.wintype
 
-class click_counter(object):
-    &quot;&quot;&quot;built like an action but yields a filter which is its main purpose -- to
-    filter for double clicks, triple clicks, etc&quot;&quot;&quot;
-
-    def __init__(self, pixel_distance=15, timeout_ms=400):
-        self.pixel_distance = pixel_distance
-        self.timeout_ms = timeout_ms
-        self.count = 0
-
-    def if_multi(self, desired_count):
-        &quot;&quot;&quot;returns a filter that will tell you whether the current click is a
-        double click or triple click or whatever you specify&quot;&quot;&quot;
-        def filt(**kw):
-            return self.count == desired_count
-        return filt
-
-    def __call__(self, ev, **kw):
-        &quot;&quot;&quot;should be called on every button press event, to keep track of fast
-        successive clicks&quot;&quot;&quot;
-
-        try:
-            prev = self.prev_click
-        except:
-            is_repeat = False
-        else:
-            is_repeat = (
-                ev.window.id == prev.window.id and
-                ev.detail == prev.detail and
-                ev.state == prev.state and
-                (ev.time - prev.time) &lt;= self.timeout_ms and
-                abs(ev.root_x - prev.root_x) &lt;= self.pixel_distance and
-                abs(ev.root_y - prev.root_y) &lt;= self.pixel_distance
-            )
-
-        if is_repeat:
-            self.count += 1
-        else:
-            self.count = 1
-
-        self.prev_click = ev
-
 # TODO: move these into window_manager, minus the if_; keep if_ versions here
 # as wrapper filters
 def if_should_manage_existing_window(win, **kw):</diff>
      <filename>whimsy/filters/__init__.py</filename>
    </modified>
    <modified>
      <diff>@@ -3,52 +3,41 @@
 from Xlib import X, XK
 
 class binding_base(object):
-    def __init__(self, detail, mods, passthrough=False):
+    def __init__(self, detail, mods):
         self.detail = detail
         self.mods = mods
-        self.passthrough = passthrough
 
-    def __call__(self, ev, **kw):
-        if self._should_at_least_be_swallowed(ev):
-            if not self.passthrough:
-                ev.swallow = True
-            return self._should_be_executed(ev)
-
-    def _should_at_least_be_swallowed(self, ev):
+    def __call__(self, hub, ev, **kw):
         return (
-            ev.type in self.swallow_event_types and
+            # type must be first because the attributes of ev depend on it
+            ev.type == self.event_type and
             self.detail == ev.detail and
             self.mods.matches(ev.state)
         )
-                                                                                                           
-    def _should_be_executed(self, ev):
-        return ev.type in self.execute_event_types
 
-class if_key_press(binding_base):
-    execute_event_types = [X.KeyPress]
-    swallow_event_types = [X.KeyPress, X.KeyRelease]
+    def grab(self, wm, **kw):
+        win = kw['client'].win if 'client' in kw else wm.root
+        for mask in self.mods.every_lock_combination():
+            self._grab(win, mask)
 
-    def __init__(self, keyname, mods, **kw):
-        self.keyname = keyname
-        binding_base.__init__(self, None, mods, **kw)
+    def _grab(self, win, detail, mask):
+        raise NotImplementedError
 
-    def __call__(self, wm, **kw):
-        if self.detail is None:
-            # maybe we should just do this in __init__
-            self.detail = wm.dpy.keysym_to_keycode(
-                XK.string_to_keysym(self.keyname)
-            )
-        return binding_base.__call__(self, wm=wm, **kw)
+class if_key_press(binding_base):
+    event_type = X.KeyPress
 
-class if_key_release(if_key_press):
-    execute_event_types = [X.KeyRelease]
-    swallow_event_types = [X.KeyRelease]
+    def __connected__(self, wm, **kw):
+        # for convenience, what we initially get passed is a string of a key
+        # name; convert that to a real keycode before anyone tries to call us
+        self.detail = wm.dpy.keysym_to_keycode(
+            XK.string_to_keysym(self.detail))
 
-class if_button_press(binding_base):
-    execute_event_types = [X.ButtonPress]
-    swallow_event_types = [X.ButtonPress, X.ButtonRelease]
+    def _grab(self, win, mask):
+        win.grab_key(self.detail, mask, 1, X.GrabModeAsync, X.GrabModeAsync)
 
-class if_button_release(if_button_press):
-    execute_event_types = [X.ButtonRelease]
-    swallow_event_types = [X.ButtonRelease]
+class if_button_press(binding_base):
+    event_type = X.ButtonPress
 
+    def _grab(self, win, mask):
+        win.grab_button(self.detail, mask, 1, X.NoEventMask, X.GrabModeAsync,
+                X.GrabModeAsync, X.NONE, X.NONE)</diff>
      <filename>whimsy/filters/bindings.py</filename>
    </modified>
    <modified>
      <diff>@@ -1,6 +1,6 @@
 # Written by Nick Welch in the years 2005-2008.  Author disclaims copyright.
 
-import os, signal, logging, logging.handlers
+import os, signal
 
 from Xlib import display
 from Xlib.support.connect import get_display
@@ -20,41 +20,18 @@ class main(object):
     def __init__(self):
         os.environ['DISPLAY'] = get_display(None)[0]
 
-        self.dpy    = dpy    = display.Display()
-        self.hub    = hub    = publisher()
-        self.wm     = wm     = window_manager(hub, dpy)
-        self.xec    = xec    = x_event_controller(hub, dpy)
-        self.ticker = ticker = tick_controller(hub)
+        self.dpy    = display.Display()
+        self.hub    = publisher()
+        self.wm     = window_manager(self.hub, self.dpy)
+        self.xec    = x_event_controller(self.hub, self.dpy)
+        self.ticker = tick_controller(self.hub)
 
-        self.hub.defaults['wm'] = wm
-        self.hub.defaults['hub'] = hub
+        self.hub.defaults['wm'] = self.wm
+        self.hub.defaults['hub'] = self.hub
 
-        # if a tick hasn't happened for 10 seconds we've definitely gotten stuck
-        self.hub.register('tick', lambda **kw: signal.alarm(10))
-        self.hub.register('tick', xec.select_and_emit_all)
-
-        self.log_filename = None
-
-    def setup_logging(self):
-        root_logger = logging.getLogger('')
-        for handler in root_logger.handlers:
-            root_logger.removeHandler(handler)
-
-        if not self.log_filename:
-            return
-
-        root_logger.setLevel(logging.DEBUG)
-        root_logger.addHandler(
-            logging.handlers.RotatingFileHandler(self.log_filename, backupCount=5)
-        )
-        if os.path.exists(self.log_filename):
-            # rollover upon every startup, not based on file size.  most recent
-            # is log_filename, previous is log_filename+&quot;.1&quot;, and so on.
-            root_logger.handlers[0].doRollover()
+        self.hub.attach('tick', self.xec.select_and_emit_all)
 
     def run(self):
-        self.setup_logging()
-
         signal.signal(signal.SIGCHLD, wait_signal_handler)
         signal.signal(signal.SIGTERM, lambda signum, frame: self.ticker.stop())
         signal.signal(signal.SIGINT,  lambda signum, frame: self.ticker.stop())</diff>
      <filename>whimsy/main.py</filename>
    </modified>
    <modified>
      <diff>@@ -9,7 +9,7 @@ from whimsy.x11 import props
 class managed_client(object):
 
     mask = (
-        X.KeyReleaseMask | X.ButtonReleaseMask |
+        #X.KeyReleaseMask | X.ButtonReleaseMask |
         X.EnterWindowMask | X.FocusChangeMask
     )
 
@@ -18,7 +18,7 @@ class managed_client(object):
         self.dpy = dpy
         self.win = win
 
-        self.hub.signal(&quot;client_init_before&quot;, client=self)
+        self.hub.emit(&quot;client_init_before&quot;, client=self)
 
         self.win.change_attributes(event_mask=self.mask)
 
@@ -34,7 +34,7 @@ class managed_client(object):
 
         self.props = {}
 
-        self.grab_all()
+        #self.grab_all()
 
         self.update_prop('WM_NAME')
         self.update_prop('WM_ICON_NAME')
@@ -42,13 +42,7 @@ class managed_client(object):
         self.update_prop('WM_STATE')
         self.update_prop('WM_PROTOCOLS')
 
-        self.hub.signal(&quot;client_init_after&quot;, client=self)
-
-    def shutdown(self):
-        catch = Xerror.CatchError(Xerror.BadWindow, Xerror.BadValue) # not working...
-        self.win.change_attributes(event_mask=X.NoEventMask)
-        self.ungrab_all()
-        self.dpy.sync()
+        self.hub.emit(&quot;client_init_after&quot;, client=self)
 
     def update_prop(self, propname):
         # some properties are specified to only change at certain times (such
@@ -58,15 +52,11 @@ class managed_client(object):
     def fetch_prop(self, propname):
         return props.get_prop(self.dpy, self.win, propname)
 
-    def ungrab_all(self):
-        self.win.ungrab_button(X.AnyButton, X.AnyModifier)
-        self.win.ungrab_key(X.AnyKey, X.AnyModifier)
-
-    def grab_all(self):
-        self.win.grab_button(X.AnyButton, X.AnyModifier, X.AnyButton,
-                X.NoEventMask, X.GrabModeSync, X.GrabModeSync, X.NONE, X.NONE)
-        self.win.grab_key(X.AnyKey, X.AnyModifier, 1, X.GrabModeSync,
-                X.GrabModeSync)
+    #def grab_all(self):
+    #    self.win.grab_button(X.AnyButton, X.AnyModifier, 1,
+    #            X.NoEventMask, X.GrabModeSync, X.GrabModeSync, X.NONE, X.NONE)
+    #    self.win.grab_key(X.AnyKey, X.AnyModifier, 1, X.GrabModeSync,
+    #            X.GrabModeSync)
 
     def map_normal(self):
         self.win.map()</diff>
      <filename>whimsy/models/client.py</filename>
    </modified>
    <modified>
      <diff>@@ -20,8 +20,8 @@ class window_manager(object):
     &quot;&quot;&quot;
 
     mask = (
-        X.ButtonPressMask | X.ButtonReleaseMask |
-        X.KeyPressMask | X.KeyReleaseMask |
+        #X.ButtonPressMask | X.ButtonReleaseMask |
+        #X.KeyPressMask | X.KeyReleaseMask |
         X.EnterWindowMask | X.LeaveWindowMask |
         X.PropertyChangeMask | X.FocusChangeMask |
         X.SubstructureRedirectMask | X.SubstructureNotifyMask
@@ -30,7 +30,7 @@ class window_manager(object):
     running = False
 
     def __init__(self, hub, dpy):
-        hub.signal(&quot;wm_init_before&quot;)
+        hub.emit(&quot;wm_init_before&quot;)
         self.hub = hub
         self.dpy = dpy
         self.root = dpy.screen().root
@@ -40,9 +40,9 @@ class window_manager(object):
         self.vx = 0
         self.vy = 0
         self.clients = []
-        hub.register('viewport_discovered', self.update_viewport)
-        hub.register('after_viewport_move', self.update_viewport)
-        hub.signal(&quot;wm_init_after&quot;)
+        hub.attach('viewport_discovered', self.update_viewport)
+        hub.attach('after_viewport_move', self.update_viewport)
+        hub.emit(&quot;wm_init_after&quot;)
 
     # MOVE TO SCREEN CLASS?
 
@@ -51,18 +51,18 @@ class window_manager(object):
         self.vy = y
 
     def shutdown(self):
-        self.hub.signal(&quot;wm_shutdown_before&quot;)
+        self.hub.emit(&quot;wm_shutdown_before&quot;)
         self.root.change_attributes(event_mask=X.NoEventMask)
         self.dpy.set_input_focus(X.PointerRoot, X.RevertToPointerRoot, X.CurrentTime)
         self.shutdown_all()
         self.running = False
-        self.hub.signal(&quot;wm_shutdown_after&quot;)
+        self.hub.emit(&quot;wm_shutdown_after&quot;)
 
     def manage(self):
-        self.hub.signal(&quot;wm_manage_before&quot;)
+        self.hub.emit(&quot;wm_manage_before&quot;)
         self.get_wm_selection()
         self.running = True
-        self.hub.signal(&quot;wm_manage_after&quot;)
+        self.hub.emit(&quot;wm_manage_after&quot;)
 
     def get_wm_selection(self):
         catch = Xerror.CatchError(Xerror.BadAccess)
@@ -75,7 +75,7 @@ class window_manager(object):
 
     def manage_window(self, win):
         self.clients.append(client.managed_client(self.hub, self.dpy, win))
-        self.hub.signal('after_manage_window', win=win)
+        self.hub.emit('after_manage_window', win=win)
 
     # move to action
     def shutdown_all(self):
@@ -83,14 +83,14 @@ class window_manager(object):
             # make wm method for removing client
             self.clients.pop().shutdown()
 
-    # move to util?
-    def window_to_client(self, win):
-        return self.window_id_to_client(win.id)
+    def find_client(self, win_or_xid):
+        if hasattr(win_or_xid, 'id'):
+            xid = win_or_xid.id
+        else:
+            xid = win_or_xid
 
-    # move to util?
-    def window_id_to_client(self, wid):
         for client in self.clients:
-            if wid == client.win.id:
+            if client.win.id == xid:
                 return client
 
     def can_move_viewport_to(self, x, y):</diff>
      <filename>whimsy/models/window_manager.py</filename>
    </modified>
    <modified>
      <diff>@@ -1,34 +1,29 @@
 # Written by Nick Welch in the years 2005-2008.  Author disclaims copyright.
 
-import types
-
 class publisher(object):
     def __init__(self, **defaults):
         self.signals = {}
         self.defaults = defaults
 
-    def signal(self, name, **kw):
+    def emit(self, name, **kw):
+        import time, sys
+        if name != 'tick':
+            print &gt;&gt;sys.stderr, time.time(), name, sorted(kw.keys())
         kw_dict = dict(self.defaults, **kw)
-        for func, filters in self.signals.get(name, [])[:]:
-            for filt in filters:
-                if not filt(**kw_dict):
+        for chain in self.signals.get(name, [])[:]:
+            for func in chain:
+                if not func(**kw_dict):
                     break
-            else:
-                ret = func(**kw_dict)
-
-    def register(self, mapping, callobj, *filters):
-        if isinstance(mapping, types.DictType):
-            for name, methodname in mapping.items():
-                self.register_func(name, getattr(callobj, methodname), *filters)
-        else:
-            self.register_func(mapping, callobj, *filters)
 
-    def register_func(self, name, func, *filters):
-        self.signals.setdefault(name, []).append([func, filters])
+    def attach(self, name, *chain):
+        self.signals.setdefault(name, []).append(chain)
+        for f in chain: # HACK
+            if hasattr(f, '__connected__'):
+                f.__connected__(**self.defaults)
 
-    def unregister(self, func):
-        for sigset in self.signals.values():
-            for actionset in sigset:
-                if actionset[0] == func:
-                    sigset.remove(actionset)
+    def detach(self, func):
+        for chains in self.signals.values():
+            for i, chain in enumerate(chains):
+                if func in chain:
+                    chains.pop(i)
 </diff>
      <filename>whimsy/signals.py</filename>
    </modified>
    <modified>
      <diff>@@ -123,7 +123,7 @@ def configure_request_changes(ev):
 def window_type(wm, window):
     if window == wm.root:
         return &quot;root&quot;
-    elif wm.window_to_client(window):
+    elif wm.find_client(window):
         return &quot;client&quot;
     return &quot;unmanaged&quot;
 </diff>
      <filename>whimsy/util.py</filename>
    </modified>
    <modified>
      <diff>@@ -25,6 +25,19 @@ class modifier_core(object):
             if slock_key and slock_key in mapping[index]:
                 self.slock = mask
 
+    def every_lock_combination(self, mask):
+        if mask &amp; X.AnyModifier:
+            return (X.AnyModifier,)
+        clean = mask &amp; ~(X.LockMask | self.nlock | self.slock)
+        return (
+            clean | X.LockMask,
+            clean | X.LockMask | self.nlock,
+            clean | X.LockMask | self.nlock | self.slock,
+            clean | self.nlock,
+            clean | self.nlock | self.slock,
+            clean | self.slock,
+        )
+
     def modmask_eq(self, lhs, rhs):
         if lhs &amp; X.AnyModifier or rhs &amp; X.AnyModifier:
             return True
@@ -62,3 +75,5 @@ class modifier_mask(object):
         return (self.modcore.modmask_and(modmask, self.match) == self.match and
                  self.modcore.modmask_and(modmask, self.negate) == 0)
 
+    def every_lock_combination(self):
+        return self.modcore.every_lock_combination(self.match)</diff>
      <filename>whimsy/x11/modifiers.py</filename>
    </modified>
  </modified>
  <removed type="array">
    <removed>
      <filename>example_config.py</filename>
    </removed>
    <removed>
      <filename>whimsy/actions/replay.py</filename>
    </removed>
    <removed>
      <filename>whimsy/x11/replay.py</filename>
    </removed>
  </removed>
  <parents type="array">
    <parent>
      <id>146bed9d893e9f842b1fa50a9ff3faf269a13990</id>
    </parent>
    <parent>
      <id>8bbdf5b836d2d74c08b727b9c2f4c8685357075e</id>
    </parent>
  </parents>
  <author>
    <name>Nick Welch</name>
    <email>mack@incise.org</email>
  </author>
  <url>http://github.com/mackstann/whimsy/commit/dd7578b99592dbc2189c1c1ccd8b212cd78b65bc</url>
  <id>dd7578b99592dbc2189c1c1ccd8b212cd78b65bc</id>
  <committed-date>2008-09-15T19:35:56-07:00</committed-date>
  <authored-date>2008-09-15T19:35:56-07:00</authored-date>
  <message>merging async-grabs branch</message>
  <tree>dbfa17517fa030717fbc632ae4bc1829e926d6e9</tree>
  <committer>
    <name>Nick Welch</name>
    <email>mack@incise.org</email>
  </committer>
</commit>
