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: