summaryrefslogtreecommitdiff
path: root/alternative_wmiircs/python/pygmi/event.py
blob: c56460af3118d94e2b002721afb089da64dd0936 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
import os
import re
import sys
import traceback

import pygmi
from pygmi.util import prop
from pygmi import monitor, client, curry, call, program_list, _

__all__ = ('keys', 'events', 'Match')

class Match(object):
    """
    A class used for matching events based on simple patterns.
    """
    def __init__(self, *args):
        """
        Creates a new Match object based on arbitrary arguments
        which constitute a match pattern. Each argument matches an
        element of the original event. Arguments are matched based
        on their type:

            _:      Matches anything
            set:    Matches any string equal to any of its elements
            list:   Matches any string equal to any of its elements
            tuple:  Matches any string equal to any of its elements

        Additionally, any type with a 'search' attribute matches if
        that callable attribute returns True given element in
        question as its first argument.

        Any other object matches if it compares equal to the
        element.
        """
        self.args = args
        self.matchers = []
        for a in args:
            if a is _:
                a = lambda k: True
            elif isinstance(a, basestring):
                a = a.__eq__
            elif isinstance(a, (list, tuple, set)):
                a = curry(lambda ary, k: k in ary, a)
            elif hasattr(a, 'search'):
                a = a.search
            else:
                a = str(a).__eq__
            self.matchers.append(a)

    def match(self, string):
        """
        Returns true if this object matches an arbitrary string when
        split on ascii spaces.
        """
        ary = string.split(' ', len(self.matchers))
        if all(m(a) for m, a in zip(self.matchers, ary)):
            return ary

def flatten(items):
    """
    Given an iterator which returns (key, value) pairs, returns a
    new iterator of (k, value) pairs such that every list- or
    tuple-valued key in the original sequence yields an individual
    pair.

    Example: flatten({(1, 2, 3): 'foo', 4: 'bar'}.items()) ->
        (1, 'foo'), (2: 'foo'), (3: 'foo'), (4: 'bar')
    """
    for k, v in items:
        if not isinstance(k, (list, tuple)):
            k = k,
        for key in k:
            yield key, v

class Events():
    """
    A class to handle events read from wmii's '/event' file.
    """
    def __init__(self):
        """
        Initializes the event handler
        """
        self.events = {}
        self.eventmatchers = {}
        self.alive = True

    def dispatch(self, event, args=''):
        """
        Distatches an event to any matching event handlers.

        The handler which specifically matches the event name will
        be called first, followed by any handlers with a 'match'
        method which matches the event name concatenated to the args
        string.

        Param event: The name of the event to dispatch.
        Param args:  The single arguments string for the event.
        """
        try:
            if event in self.events:
                self.events[event](args)
            for matcher, action in self.eventmatchers.iteritems():
                ary = matcher.match(' '.join((event, args)))
                if ary is not None:
                    action(*ary)
        except Exception, e:
            traceback.print_exc(sys.stderr)

    def loop(self):
        """
        Enters the event loop, reading lines from wmii's '/event'
        and dispatching them, via #dispatch, to event handlers.
        Continues so long as #alive is True.
        """
        keys.mode = 'main'
        for line in client.readlines('/event'):
            if not self.alive:
                break
            self.dispatch(*line.split(' ', 1))
        self.alive = False

    def bind(self, items={}, **kwargs):
        """
        Binds a number of event handlers for wmii events. Keyword
        arguments other than 'items' are added to the 'items' dict.
        Handlers are called by #loop when a matching line is read
        from '/event'. Each handler is called with, as its sole
        argument, the string read from /event with its first token
        stripped.

        Param items: A dict of action-handler pairs to bind. Passed
            through pygmi.event.flatten. Keys with a 'match' method,
            such as pygmi.event.Match objects or regular expressions,
            are matched against the entire event string. Any other
            object matches if it compares equal to the first token of
            the event.
        """
        kwargs.update(items)
        for k, v in flatten(kwargs.iteritems()):
            if hasattr(k, 'match'):
                self.eventmatchers[k] = v
            else:
                self.events[k] = v

    def event(self, fn):
        """
        A decorator which binds its wrapped function, as via #bind,
        for the event which matches its name.
        """
        self.bind({fn.__name__: fn})
events = Events()

class Keys(object):
    """
    A class to manage wmii key bindings.
    """
    def __init__(self):
        """
        Initializes the class and binds an event handler for the Key
        event, as via pygmi.event.events.bind.

        Takes no arguments.
        """
        self.modes = {}
        self.modelist = []
        self._set_mode('main', False)
        self.defs = {}
        events.bind(Key=self.dispatch)

    def _add_mode(self, mode):
        if mode not in self.modes:
            self.modes[mode] = {
                'name': mode,
                'desc': {},
                'groups': [],
                'keys': {},
                'import': {},
            }
            self.modelist.append(mode)

    def _set_mode(self, mode, execute=True):
        self._add_mode(mode)
        self._mode = mode
        self._keys = dict((k % self.defs, v) for k, v in
                          self.modes[mode]['keys'].items() +
                          self.modes[mode]['import'].items());
        if execute:
            client.write('/keys', '\n'.join(self._keys.keys()) + '\n')

    mode = property(lambda self: self._mode, _set_mode,
                   doc="The current mode for which to dispatch keys")

    @prop(doc="Returns a short help text describing the bound keys in all modes")
    def help(self):
        return '\n\n'.join(
            ('Mode %s\n' % mode['name']) +
            '\n\n'.join(('  %s\n' % str(group or '')) +
                        '\n'.join('    %- 20s %s' % (key % self.defs,
                                                     mode['keys'][key].__doc__)
                                  for key in mode['desc'][group])
                        for group in mode['groups'])
            for mode in (self.modes[name]
                         for name in self.modelist))

    def bind(self, mode='main', keys=(), import_={}):
        """
        Binds a series of keys for the given 'mode'. Keys may be
        specified as a dict or as a sequence of tuple values and
        strings.
        
        In the latter case, documentation may be interspersed with
        key bindings. Any value in the sequence which is not a tuple
        begins a new key group, with that value as a description.
        A tuple with two values is considered a key-value pair,
        where the value is the handler for the named key. A
        three valued tuple is considered a key-description-value
        tuple, with the same semantics as above.

        Each key binding is interpolated with the values of
        #defs, as if processed by (key % self.defs)

        Param mode: The name of the mode for which to bind the keys.
        Param keys: A sequence of keys to bind.
        Param import_: A dict specifying keys which should be
                       imported from other modes, of the form 
                         { 'mode': ['key1', 'key2', ...] }
        """
        self._add_mode(mode)
        mode = self.modes[mode]
        group = None
        def add_desc(key, desc):
            if group not in mode['desc']:
                mode['desc'][group] = []
                mode['groups'].append(group)
            if key not in mode['desc'][group]:
                mode['desc'][group].append(key);

        if isinstance(keys, dict):
            keys = keys.iteritems()
        for obj in keys:
            if isinstance(obj, tuple) and len(obj) in (2, 3):
                if len(obj) == 2:
                    key, val = obj
                    desc = ''
                elif len(obj) == 3:
                    key, desc, val = obj
                mode['keys'][key] = val
                add_desc(key, desc)
                val.__doc__ = str(desc)
            else:
                group = obj

        def wrap_import(mode, key):
            return lambda k: self.modes[mode]['keys'][key](k)
        for k, v in flatten((v, k) for k, v in import_.iteritems()):
            mode['import'][k % self.defs] = wrap_import(v, k)

    def dispatch(self, key):
        """
        Dispatches a key event for the current mode.

        Param key: The key spec for which to dispatch.
        """
        mode = self.modes[self.mode]
        if key in self._keys:
            return self._keys[key](key)
keys = Keys()

class Actions(object):
    """
    A class to represent user-callable actions. All methods without
    leading underscores in their names are treated as callable actions.
    """
    def __getattr__(self, name):
        if name.startswith('_') or name.endswith('_'):
            raise AttributeError()
        if hasattr(self, name + '_'):
            return getattr(self, name + '_')
        def action(args=''):
            cmd = pygmi.find_script(name)
            if cmd:
                call(pygmi.shell, '-c', '$* %s' % args, '--', cmd,
                     background=True)
        return action

    def _call(self, args):
        """
        Calls a method named for the first token of 'args', with the
        rest of the string as its first argument. If the method
        doesn't exist, a trailing underscore is appended.
        """
        a = args.split(' ', 1)
        if a:
            getattr(self, a[0])(*a[1:])

    @prop(doc="Returns the names of the public methods callable as actions, with trailing underscores stripped.")
    def _choices(self):
        return sorted(
            program_list(pygmi.confpath) +
            [re.sub('_$', '', k) for k in dir(self)
             if not re.match('^_', k) and callable(getattr(self, k))])


# vim:se sts=4 sw=4 et: