mirror of
https://github.com/0intro/wmii
synced 2024-11-22 22:02:30 +03:00
d4d8a6b891
Update issue #132 Status: Fixed This is a very strange issue. It turns out that subprocess won't work from non-main threads while a module is being imported. Since the wmiirc entered its event loop rather than returning, it was causing problems. I suspect it was also the cause of the stack traces being printed at interperater shutdown.
305 lines
10 KiB
Python
305 lines
10 KiB
Python
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:
|