wmii

git clone git://oldgit.suckless.org/wmii/
Log | Files | Refs | README | LICENSE

event.py (10660B)


      1 import os
      2 import re
      3 import sys
      4 import traceback
      5 
      6 import pygmi
      7 from pygmi.util import prop
      8 from pygmi import monitor, client, curry, call, program_list, _
      9 
     10 __all__ = ('keys', 'events', 'Match')
     11 
     12 class Match(object):
     13     """
     14     A class used for matching events based on simple patterns.
     15     """
     16     def __init__(self, *args):
     17         """
     18         Creates a new Match object based on arbitrary arguments
     19         which constitute a match pattern. Each argument matches an
     20         element of the original event. Arguments are matched based
     21         on their type:
     22 
     23             _:      Matches anything
     24             set:    Matches any string equal to any of its elements
     25             list:   Matches any string equal to any of its elements
     26             tuple:  Matches any string equal to any of its elements
     27 
     28         Additionally, any type with a 'search' attribute matches if
     29         that callable attribute returns True given element in
     30         question as its first argument.
     31 
     32         Any other object matches if it compares equal to the
     33         element.
     34         """
     35         self.args = args
     36         self.matchers = []
     37         for a in args:
     38             if a is _:
     39                 a = lambda k: True
     40             elif isinstance(a, basestring):
     41                 a = a.__eq__
     42             elif isinstance(a, (list, tuple, set)):
     43                 a = (lambda ary: (lambda k: k in ary))(a)
     44             elif hasattr(a, 'search'):
     45                 a = a.search
     46             else:
     47                 a = str(a).__eq__
     48             self.matchers.append(a)
     49 
     50     def match(self, string):
     51         """
     52         Returns true if this object matches an arbitrary string when
     53         split on ascii spaces.
     54         """
     55         ary = string.split(' ', len(self.matchers))
     56         if all(m(a) for m, a in zip(self.matchers, ary)):
     57             return ary
     58 
     59 def flatten(items):
     60     """
     61     Given an iterator which returns (key, value) pairs, returns a
     62     new iterator of (k, value) pairs such that every list- or
     63     tuple-valued key in the original sequence yields an individual
     64     pair.
     65 
     66     Example: flatten({(1, 2, 3): 'foo', 4: 'bar'}.items()) ->
     67         (1, 'foo'), (2: 'foo'), (3: 'foo'), (4: 'bar')
     68     """
     69     for k, v in items:
     70         if isinstance(k, (list, tuple)):
     71             for key in k:
     72                 yield key, v
     73         else:
     74             yield k, v
     75 
     76 class Events():
     77     """
     78     A class to handle events read from wmii's '/event' file.
     79     """
     80     def __init__(self):
     81         """
     82         Initializes the event handler
     83         """
     84         self.events = {}
     85         self.eventmatchers = {}
     86         self.alive = True
     87 
     88     def dispatch(self, event, args=''):
     89         """
     90         Distatches an event to any matching event handlers.
     91 
     92         The handler which specifically matches the event name will
     93         be called first, followed by any handlers with a 'match'
     94         method which matches the event name concatenated to the args
     95         string.
     96 
     97         Param event: The name of the event to dispatch.
     98         Param args:  The single arguments string for the event.
     99         """
    100         try:
    101             if event in self.events:
    102                 self.events[event](args)
    103             for matcher, action in self.eventmatchers.iteritems():
    104                 ary = matcher.match(' '.join((event, args)))
    105                 if ary is not None:
    106                     action(*ary)
    107         except Exception, e:
    108             try:
    109                 traceback.print_exc(sys.stderr)
    110             except:
    111                 pass
    112 
    113     def loop(self):
    114         """
    115         Enters the event loop, reading lines from wmii's '/event'
    116         and dispatching them, via #dispatch, to event handlers.
    117         Continues so long as #alive is True.
    118         """
    119         keys.mode = 'main'
    120         for line in client.readlines('/event'):
    121             if not self.alive:
    122                 break
    123             self.dispatch(*line.split(' ', 1))
    124         self.alive = False
    125 
    126     def bind(self, items={}, **kwargs):
    127         """
    128         Binds a number of event handlers for wmii events. Keyword
    129         arguments other than 'items' are added to the 'items' dict.
    130         Handlers are called by #loop when a matching line is read
    131         from '/event'. Each handler is called with, as its sole
    132         argument, the string read from /event with its first token
    133         stripped.
    134 
    135         Param items: A dict of action-handler pairs to bind. Passed
    136             through pygmi.event.flatten. Keys with a 'match' method,
    137             such as pygmi.event.Match objects or regular expressions,
    138             are matched against the entire event string. Any other
    139             object matches if it compares equal to the first token of
    140             the event.
    141         """
    142         kwargs.update(items)
    143         for k, v in flatten(kwargs.iteritems()):
    144             if hasattr(k, 'match'):
    145                 self.eventmatchers[k] = v
    146             else:
    147                 self.events[k] = v
    148 
    149     def event(self, fn):
    150         """
    151         A decorator which binds its wrapped function, as via #bind,
    152         for the event which matches its name.
    153         """
    154         self.bind({fn.__name__: fn})
    155 events = Events()
    156 
    157 class Keys(object):
    158     """
    159     A class to manage wmii key bindings.
    160     """
    161     def __init__(self):
    162         """
    163         Initializes the class and binds an event handler for the Key
    164         event, as via pygmi.event.events.bind.
    165 
    166         Takes no arguments.
    167         """
    168         self.modes = {}
    169         self.modelist = []
    170         self.mode = 'main'
    171         self.defs = {}
    172         events.bind(Key=self.dispatch)
    173 
    174     def _add_mode(self, mode):
    175         if mode not in self.modes:
    176             self.modes[mode] = {
    177                 'name': mode,
    178                 'desc': {},
    179                 'groups': [],
    180                 'keys': {},
    181                 'import': {},
    182             }
    183             self.modelist.append(mode)
    184 
    185     mode = property(lambda self: self._mode,
    186                    doc="The current mode for which to dispatch keys")
    187     @mode.setter
    188     def mode(self, mode):
    189         self._add_mode(mode)
    190         self._mode = mode
    191         self._keys = dict((k % self.defs, v) for k, v in
    192                           self.modes[mode]['keys'].items() +
    193                           self.modes[mode]['import'].items());
    194         if hasattr(self, 'defs'):
    195             client.write('/keys', '\n'.join(self._keys.keys()) + '\n')
    196 
    197 
    198     @prop(doc="Returns a short help text describing the bound keys in all modes")
    199     def help(self):
    200         return '\n\n'.join(
    201             ('Mode %s\n' % mode['name']) +
    202             '\n\n'.join(('  %s\n' % str(group or '')) +
    203                         '\n'.join('    %- 20s %s' % (key % self.defs,
    204                                                      mode['keys'][key].__doc__)
    205                                   for key in mode['desc'][group])
    206                         for group in mode['groups'])
    207             for mode in (self.modes[name]
    208                          for name in self.modelist))
    209 
    210     def bind(self, mode='main', keys=(), import_={}):
    211         """
    212         Binds a series of keys for the given 'mode'. Keys may be
    213         specified as a dict or as a sequence of tuple values and
    214         strings.
    215         
    216         In the latter case, documentation may be interspersed with
    217         key bindings. Any value in the sequence which is not a tuple
    218         begins a new key group, with that value as a description.
    219         A tuple with two values is considered a key-value pair,
    220         where the value is the handler for the named key. A
    221         three valued tuple is considered a key-description-value
    222         tuple, with the same semantics as above.
    223 
    224         Each key binding is interpolated with the values of
    225         #defs, as if processed by (key % self.defs)
    226 
    227         Param mode: The name of the mode for which to bind the keys.
    228         Param keys: A sequence of keys to bind.
    229         Param import_: A dict specifying keys which should be
    230                        imported from other modes, of the form 
    231                          { 'mode': ['key1', 'key2', ...] }
    232         """
    233         self._add_mode(mode)
    234         mode = self.modes[mode]
    235         group = None
    236         def add_desc(key, desc):
    237             if group not in mode['desc']:
    238                 mode['desc'][group] = []
    239                 mode['groups'].append(group)
    240             if key not in mode['desc'][group]:
    241                 mode['desc'][group].append(key);
    242 
    243         if isinstance(keys, dict):
    244             keys = keys.iteritems()
    245         for obj in keys:
    246             if isinstance(obj, tuple) and len(obj) in (2, 3):
    247                 if len(obj) == 2:
    248                     key, val = obj
    249                     desc = ''
    250                 elif len(obj) == 3:
    251                     key, desc, val = obj
    252                 mode['keys'][key] = val
    253                 add_desc(key, desc)
    254                 val.__doc__ = str(desc)
    255             else:
    256                 group = obj
    257 
    258         def wrap_import(mode, key):
    259             return lambda k: self.modes[mode]['keys'][key](k)
    260         for k, v in flatten((v, k) for k, v in import_.iteritems()):
    261             mode['import'][k % self.defs] = wrap_import(v, k)
    262 
    263     def dispatch(self, key):
    264         """
    265         Dispatches a key event for the current mode.
    266 
    267         Param key: The key spec for which to dispatch.
    268         """
    269         mode = self.modes[self.mode]
    270         if key in self._keys:
    271             return self._keys[key](key)
    272 keys = Keys()
    273 
    274 class Actions(object):
    275     """
    276     A class to represent user-callable actions. All methods without
    277     leading underscores in their names are treated as callable actions.
    278     """
    279     def __getattr__(self, name):
    280         if name.startswith('_') or name.endswith('_'):
    281             raise AttributeError()
    282         if hasattr(self, name + '_'):
    283             return getattr(self, name + '_')
    284         cmd = pygmi.find_script(name)
    285         if not cmd:
    286             raise AttributeError()
    287         return lambda args='': call(pygmi.shell, '-c', '$* %s' % args, '--', cmd,
    288                                     background=True)
    289 
    290     def _call(self, args):
    291         """
    292         Calls a method named for the first token of 'args', with the
    293         rest of the string as its first argument. If the method
    294         doesn't exist, a trailing underscore is appended.
    295         """
    296         a = args.split(' ', 1)
    297         if a:
    298             getattr(self, a[0])(*a[1:])
    299 
    300     @prop(doc="Returns the names of the public methods callable as actions, with trailing underscores stripped.")
    301     def _choices(self):
    302         return sorted(
    303             program_list(pygmi.confpath) +
    304             [re.sub('_$', '', k) for k in dir(self)
    305              if not re.match('^_', k) and callable(getattr(self, k))])
    306 
    307 
    308 # vim:se sts=4 sw=4 et: