import collections
from datetime import datetime, timedelta
import re
from pyxp import *
from pyxp.client import *
from pygmi import *
from pygmi.util import prop
__all__ = ('wmii', 'Tags', 'Tag', 'Area', 'Frame', 'Client',
'Button', 'Colors', 'Color', 'Toggle', 'Always', 'Never')
spacere = re.compile(r'\s')
sentinel = {}
def tounicode(obj):
if isinstance(obj, str):
return obj.decode('UTF-8')
return unicode(obj)
class utf8(object):
def __str__(self):
return unicode(self).encode('utf-8')
@apply
class Toggle(utf8):
def __unicode__(self):
return unicode(self.__class__.__name__)
@apply
class Always(Toggle.__class__):
pass
@apply
class Never(Toggle.__class__):
pass
def constrain(min, max, val):
return min if val < min else max if val > max else val
class Map(collections.Mapping):
def __init__(self, cls, *args):
self.cls = cls
self.args = args
def __repr__(self):
return 'Map(%s%s)' % (self.cls.__name__, (', %s' % ', '.join(map(repr, self.args)) if self.args else ''))
def __getitem__(self, item):
ret = self.cls(*(self.args + (item,)))
if not ret.exists:
raise KeyError('no such %s %s' % (self.cls.__name__.lower(), repr(item)))
return ret
def __len__(self):
return len(iter(self))
def __keys__(self):
return [v for v in self.cls.all(*self.args)]
def __iter__(self):
return (v for v in self.cls.all(*self.args))
def iteritems(self):
return ((v, self.cls(*(self.args + (v,)))) for v in self.cls.all(*self.args))
def itervalues(self):
return (self.cls(*(self.args + (v,))) for v in self.cls.all(*self.args))
class Ctl(object):
"""
An abstract class to represent the 'ctl' files of the wmii filesystem.
Instances act as live, writable dictionaries of the settings represented
in the file.
Abstract roperty ctl_path: The path to the file represented by this
control.
Property ctl_hasid: When true, the first line of the represented
file is treated as an id, rather than a key-value pair. In this
case, the value is available via the 'id' property.
Property ctl_types: A dict mapping named dictionary keys to two valued
tuples, each containing a decoder and encoder function for the
property's plain text value.
"""
ctl_types = {}
ctl_hasid = False
ctl_open = 'aopen'
ctl_file = None
def __eq__(self, other):
if self.ctl_hasid and isinstance(other, Ctl) and other.ctl_hasid:
return self.id == other.id
return False
def __init__(self):
self.cache = {}
def ctl(self, *args):
"""
Arguments are joined by ascii spaces and written to the ctl file.
"""
def next(file):
if file:
self.ctl_file = file
file.awrite(u' '.join(map(tounicode, args)))
if self.ctl_file:
return next(self.ctl_file)
getattr(client, self.ctl_open)(self.ctl_path, callback=next, mode=OWRITE)
def __getitem__(self, key):
for line in self.ctl_lines():
key_, rest = line.split(' ', 1)
if key_ == key:
if key in self.ctl_types:
return self.ctl_types[key][0](rest)
return rest
raise KeyError()
def __hasitem__(self, key):
return key in self.keys()
def __setitem__(self, key, val):
assert '\n' not in key
self.cache[key] = val
if key in self.ctl_types:
if self.ctl_types[key][1] is None:
raise NotImplementedError('%s: %s is not writable' % (self.ctl_path, key))
val = self.ctl_types[key][1](val)
self.ctl(key, val)
def get(self, key, default=sentinel):
"""
Gets the instance's dictionary value for 'key'. If the key doesn't
exist, 'default' is returned. If 'default' isn't provided and the key
doesn't exist, a KeyError is raised.
"""
try:
return self[key]
except KeyError, e:
if default is not self.sentinel:
return default
raise e
def set(self, key, val):
"""
Sets the dictionary value for 'key' to 'val', as self[key] = val
"""
self[key] = val
def keys(self):
return [line.split(' ', 1)[0]
for line in self.ctl_lines()]
def iteritems(self):
return (tuple(line.split(' ', 1))
for line in self.ctl_lines())
def items(self):
return [tuple(line.split(' ', 1))
for line in self.ctl_lines()]
def ctl_lines(self):
"""
Returns the lines of the ctl file as a tuple, with the first line
stripped if #ctl_hasid is set.
"""
lines = tuple(client.readlines(self.ctl_path))
if self.ctl_hasid:
lines = lines[1:]
return lines
_id = None
@prop(doc="If #ctl_hasid is set, returns the id of this ctl file.")
def id(self):
if self._id is None and self.ctl_hasid:
return self.name_read(client.read(self.ctl_path).split('\n', 1)[0])
return self._id
class Dir(Ctl):
"""
An abstract class representing a directory in the wmii filesystem with a
ctl file and sub-objects.
Abstract property base_path: The path directly under which all objects
represented by this class reside. e.g., /client, /tag
"""
ctl_hasid = True
name_read = unicode
name_write = unicode
def __init__(self, id):
"""
Initializes the directory object.
Param id: The id of the object in question. If 'sel', the object
dynamically represents the selected object, even as it
changes. In this case, #id will return the actual ID of the
object.
"""
super(Dir, self).__init__()
if isinstance(id, Dir):
id = id.id
if id != 'sel':
self._id = self.name_read(id)
def __eq__(self, other):
return (self.__class__ == other.__class__ and
self.id == other.id)
class ctl_property(object):
"""
A class which maps instance properties to ctl file properties.
"""
def __init__(self, key):
self.key = key
def __get__(self, dir, cls):
return dir.get(self.key, None)
def __set__(self, dir, val):
dir[self.key] = val
class toggle_property(ctl_property):
"""
A class which maps instance properties to ctl file properties. The
values True and False map to the strings "on" and "off" in the
filesystem.
"""
props = {
'on': True,
'off': False,
'toggle': Toggle,
'always': Always,
'never': Never
}
def __get__(self, dir, cls):
val = dir[self.key]
if val in self.props:
return self.props[val]
return val
def __set__(self, dir, val):
for k, v in self.props.iteritems():
if v == val:
val = k
break
dir[self.key] = val
class file_property(object):
"""
A class which maps instance properties to files in the directory
represented by this object.
"""
def __init__(self, name, writable=False):
self.name = name
self.writable = writable
def __get__(self, dir, cls):
return client.read('%s/%s' % (dir.path, self.name))
def __set__(self, dir, val):
if not self.writable:
raise NotImplementedError('File %s is not writable' % self.name)
return client.awrite('%s/%s' % (dir.path, self.name),
str(val))
@prop(doc="The path to this directory's ctl file")
def ctl_path(self):
return '%s/ctl' % self.path
@prop(doc="The path to this directory")
def path(self):
return '%s/%s' % (self.base_path, self.name_write(self._id or 'sel'))
@prop(doc="True if the given object exists in the wmii filesystem")
def exists(self):
return bool(client.stat(self.path))
@classmethod
def all(cls):
"""
Returns all of the objects that exist for this type of directory.
"""
return (cls.name_read(s.name)
for s in client.readdir(cls.base_path)
if s.name != 'sel')
@classmethod
def map(cls, *args):
return Map(cls, *args)
def __repr__(self):
return '%s(%s)' % (self.__class__.__name__,
repr(self._id or 'sel'))
class Client(Dir):
"""
A class which represents wmii clients. Maps to the directories directly
below /client.
"""
base_path = '/client'
ctl_types = {
'group': (lambda s: int(s, 16), str),
'pid': (int, None),
}
@staticmethod
def name_read(name):
if isinstance(name, int):
return name
try:
return int(name, 16)
except:
return unicode(name)
name_write = lambda self, name: name if isinstance(name, basestring) else '%#x' % name
allow = Dir.ctl_property('allow')
fullscreen = Dir.toggle_property('fullscreen')
group = Dir.ctl_property('group')
pid = Dir.ctl_property('pid')
tags = Dir.ctl_property('tags')
urgent = Dir.toggle_property('urgent')
label = Dir.file_property('label', writable=True)
props = Dir.file_property('props')
def kill(self):
"""Politely asks a client to quit."""
self.ctl('kill')
def slay(self):
"""Forcibly severs a client's connection to the X server."""
self.ctl('slay')
class liveprop(object):
def __init__(self, get):
self.get = get
self.attr = str(self)
def __get__(self, area, cls):
if getattr(area, self.attr, sentinel) is not sentinel:
return getattr(area, self.attr)
return self.get(area)
def __set__(self, area, val):
setattr(area, self.attr, val)
class Area(object):
def __init__(self, tag, ord, screen='sel', offset=sentinel, width=sentinel, height=sentinel, frames=sentinel):
self.tag = tag
if ':' in str(ord):
screen, ord = ord.split(':', 2)
self.ord = str(ord)
self.screen = str(screen)
self.offset = offset
self.width = width
self.height = height
self.frames = frames
def prop(key):
@liveprop
def prop(self):
for area in self.tag.index:
if str(area.ord) == str(self.ord):
return getattr(area, key)
return prop
offset = prop('offset')
width = prop('width')
height = prop('height')
frames = prop('frames')
@property
def spec(self):
if self.screen is not None:
return '%s:%s' % (self.screen, self.ord)
return self.ord
@property
def mode(self):
for k, v in self.tag.iteritems():
if k == 'colmode':
v = v.split(' ')
if v[0] == self.ord:
return v[1]
@mode.setter
def mode(self, val):
self.tag['colmode %s' % self.spec] = val
def grow(self, dir, amount=None):
self.tag.grow(self, dir, amount)
def nudge(self, dir, amount=None):
self.tag.nudge(self, dir, amount)
class Frame(object):
live = False
def __init__(self, client, area=sentinel, ord=sentinel, offset=sentinel, height=sentinel):
self.client = client
self.ord = ord
self.offset = offset
self.height = height
@property
def width(self):
return self.area.width
def prop(key):
@liveprop
def prop(self):
for area in self.tag.index:
for frame in area.frames:
if frame.client == self.client:
return getattr(frame, key)
return prop
offset = prop('area')
offset = prop('ord')
offset = prop('offset')
height = prop('height')
def grow(self, dir, amount=None):
self.area.tag.grow(self, dir, amount)
def nudge(self, dir, amount=None):
self.area.tag.nudge(self, dir, amount)
class Tag(Dir):
base_path = '/tag'
@classmethod
def framespec(cls, frame):
if isinstance(frame, Frame):
frame = frame.client
if isinstance(frame, Area):
frame = (frame.ord, 'sel')
if isinstance(frame, Client):
if frame._id is None:
return 'sel sel'
return 'client %s' % frame.id
elif isinstance(frame, basestring):
return frame
else:
return '%s %s' % tuple(map(str, frame))
def dirspec(cls, dir):
if isinstance(dir, tuple):
dir = ' '.join(dir)
return dir
@property
def selected(self):
return tuple(self['select'].split(' '))
@selected.setter
def selected(self, frame):
if not isinstance(frame, basestring) or ' ' not in frame:
frame = self.framespec(frame)
self['select'] = frame
@property
def selclient(self):
for k, v in self.iteritems():
if k == 'select' and 'client' in v:
return Client(v.split(' ')[1])
return None
@selclient.setter
def selclient(self, val):
self['select'] = self.framespec(val)
@property
def selcol(self):
return Area(self, self.selected[0])
@property
def index(self):
areas = []
for l in (l.split(' ')
for l in client.readlines('%s/index' % self.path)
if l):
if l[0] == '#':
m = re.match(r'(?:(\d+):)?(\d+|~)', l[1])
if m.group(2) == '~':
area = Area(tag=self, screen=m.group(1), ord=l[1], width=l[2],
height=l[3], frames=[])
else:
area = Area(tag=self, screen=m.group(1) or 0,
height=None, ord=m.group(2), offset=l[2], width=l[3],
frames=[])
areas.append(area)
i = 0
else:
area.frames.append(
Frame(client=Client(l[1]), area=area, ord=i,
offset=l[2], height=l[3]))
i += 1
return areas
def delete(self):
id = self.id
for a in self.index:
for f in a.frames:
if f.client.tags == id:
f.client.kill()
else:
f.client.tags = '-%s' % id
if self == Tag('sel'):
Tags.instance.select(Tags.instance.next())
def select(self, frame, stack=False):
self['select'] = '%s %s' % (
self.framespec(frame),
stack and 'stack' or '')
def send(self, src, dest, stack=False, cmd='send'):
if isinstance(src, tuple):
src = ' '.join(src)
if isinstance(src, Frame):
src = src.client
if isinstance(src, Client):
src = src._id or 'sel'
if isinstance(dest, tuple):
dest = ' '.join(dest)
self[cmd] = '%s %s' % (src, dest)
def swap(self, src, dest):
self.send(src, dest, cmd='swap')
def nudge(self, frame, dir, amount=None):
frame = self.framespec(frame)
self['nudge'] = '%s %s %s' % (frame, dir, str(amount or ''))
def grow(self, frame, dir, amount=None):
frame = self.framespec(frame)
self['grow'] = '%s %s %s' % (frame, dir, str(amount or ''))
class Color(utf8):
def __init__(self, colors):
if isinstance(colors, Color):
colors = colors.rgb
elif isinstance(colors, basestring):
match = (re.match(r'^#(..)(..)(..)((?:..)?)$', colors) or
re.match(r'^rgba:(..)/(..)/(..)/(..)$', colors))
colors = tuple(int(match.group(group), 16) for group in range(1, 4))
if match.group(4):
colors += int(match.group(4), 16),
def toint(val):
if isinstance(val, float):
val = int(255 * val)
assert 0 <= val <= 255
return val
self.rgb = tuple(map(toint, colors))
def __getitem__(self, key):
if isinstance(key, basestring):
key = {'red': 0, 'green': 1, 'blue': 2}[key]
return self.rgb[key]
@property
def hex(self):
if len(self.rgb) > 3:
return 'rgba:%02x/%02x/%02x/%02x' % self.rgb
return '#%02x%02x%02x' % self.rgb
def __unicode__(self):
if len(self.rgb) > 3:
return 'rgba(%d, %d, %d, %d)' % self.rgb
return 'rgb(%d, %d, %d)' % self.rgb
def __repr__(self):
return 'Color(%s)' % repr(self.rgb)
class Colors(utf8):
def __init__(self, foreground=None, background=None, border=None):
vals = foreground, background, border
self.vals = tuple(map(Color, vals))
def __iter__(self):
return iter(self.vals)
def __list__(self):
return list(self.vals)
def __tuple__(self):
return self.vals
@classmethod
def from_string(cls, val):
return cls(*val.split(' '))
def __getitem__(self, key):
if isinstance(key, basestring):
key = {'foreground': 0, 'background': 1, 'border': 2}[key]
return self.vals[key]
def __unicode__(self):
return ' '.join(c.hex for c in self.vals)
def __repr__(self):
return 'Colors(%s, %s, %s)' % tuple(repr(c.rgb) for c in self.vals)
class Button(Ctl):
sides = {
'left': 'lbar',
'right': 'rbar',
}
ctl_types = {
'colors': (Colors.from_string, lambda c: str(Colors(*c))),
}
ctl_open = 'acreate'
colors = Dir.ctl_property('colors')
label = Dir.ctl_property('label')
def __init__(self, side, name, colors=None, label=None):
super(Button, self).__init__()
self.side = side
self.name = name
self.base_path = self.sides[side]
self.ctl_path = '%s/%s' % (self.base_path, self.name)
self.ctl_file = None
if colors or label:
self.create(colors, label)
def create(self, colors=None, label=None):
if not self.ctl_file:
self.ctl_file = client.create(self.ctl_path, ORDWR)
if colors:
self.colors = colors
if label:
self.label = label
def remove(self):
if self.ctl_file:
self.ctl_file.aremove()
self.ctl_file = None
@property
def exists(self):
return bool(self.file.stat() if self.file else client.stat(self.ctl_path))
@classmethod
def all(cls, side):
return (s.name
for s in client.readdir(cls.sides[side])
if s.name != 'sel')
@classmethod
def map(cls, *args):
return Map(cls, *args)
class Rules(collections.MutableMapping, utf8):
_items = ()
def __init__(self, path, rules=None):
self.path = path
if rules:
self.setitems(rules)
_quotere = re.compile(ur'(\\(.)|/)')
@classmethod
def quoteslash(cls, str):
return cls._quotere.sub(lambda m: m.group(0) if m.group(2) else r'\/', str)
__get__ = lambda self, obj, cls: self
def __set__(self, obj, val):
self.setitems(val)
def __getitem__(self, key):
for k, v in self.iteritems():
if k == key:
return v
raise KeyError()
def __setitem__(self, key, val):
items = [(k, v) for k, v in self.iteritems() if k != key]
items.append((key, val))
self.setitems(items)
def __delitem__(self, key):
self.setitems((k, v) for k, v in self.iteritems() if k != key)
def __len__(self):
return len(tuple(self.iteritems()))
def __iter__(self):
return (k for k, v in self.iteritems())
def __list__(self):
return list(iter(self))
def __tuple__(self):
return tuple(iter(self))
def append(self, item):
self.setitems(self + (item,))
def __add__(self, items):
return tuple(self.iteritems()) + tuple(items)
def rewrite(self):
client.awrite(self.path, unicode(self))
def setitems(self, items):
self._items = [(k, v if isinstance(v, Rule) else Rule(self, k, v))
for (k, v) in items]
self.rewrite()
def __unicode__(self):
return u''.join(unicode(value) for (key, value) in self.iteritems()) or u'\n'
def iteritems(self):
return iter(self._items)
def items(self):
return list(self._items())
class Rule(collections.MutableMapping, utf8):
_items = ()
parent = None
@classmethod
def quotekey(cls, key):
if key.endswith('_'):
key = key[:-1]
return key.replace('_', '-')
@classmethod
def quotevalue(cls, val):
if val is True: return "on"
if val is False: return "off"
if val in (Toggle, Always, Never):
return unicode(val).lower()
return tounicode(val)
def __get__(self, obj, cls):
return self
def __set__(self, obj, val):
self.setitems(val)
def __init__(self, parent, key, items={}):
self.key = key
self._items = []
self.setitems(items.iteritems() if isinstance(items, dict) else items)
self.parent = parent
def __getitem__(self, key):
for k, v in reversed(self._items):
if k == key:
return v
raise KeyError()
def __setitem__(self, key, val):
items = [(k, v) for k, v in self.iteritems() if k != key]
items.append((key, val))
self.setitems(items)
def __delitem__(self, key):
self.setitems([(k, v) for k, v in self.iteritems() if k != key])
def __len__(self):
return len(self._items)
def __iter__(self):
return iter(self._items)
def __list__(self):
return list(iter(self))
def __tuple__(self):
return tuple(iter(self))
def append(self, item):
self.setitems(self + (item,))
def __add__(self, items):
return tuple(self.iteritems()) + tuple(items)
def setitems(self, items):
items = list(items)
assert not any('=' in key or
spacere.search(self.quotekey(key)) or
spacere.search(self.quotevalue(val)) for (key, val) in items)
self._items = items
if self.parent:
self.parent.rewrite()
def __unicode__(self):
return u'/%s/ %s\n' % (
Rules.quoteslash(self.key),
u' '.join(u'%s=%s' % (self.quotekey(k), self.quotevalue(v))
for (k, v) in self.iteritems()))
def iteritems(self):
return iter(self._items)
def items(self):
return list(self._items)
@apply
class wmii(Ctl):
ctl_path = '/ctl'
ctl_types = {
'normcolors': (Colors.from_string, lambda c: str(Colors(*c))),
'focuscolors': (Colors.from_string, lambda c: str(Colors(*c))),
'border': (int, str),
}
clients = Client.map()
tags = Tag.map()
lbuttons = Button.map('left')
rbuttons = Button.map('right')
rules = Rules('/rules')
class Tags(object):
PREV = []
NEXT = []
def __init__(self, normcol=None, focuscol=None):
self.ignore = set()
self.tags = {}
self.sel = None
self.normcol = normcol
self.focuscol = focuscol
self.lastselect = datetime.now()
for t in wmii.tags:
self.add(t)
for b in wmii.lbuttons.itervalues():
if b.name not in self.tags:
b.remove()
self.focus(Tag('sel').id)
self.mru = [self.sel.id]
self.idx = -1
Tags.instance = self
def add(self, tag):
self.tags[tag] = Tag(tag)
self.tags[tag].button = Button('left', tag, self.normcol or wmii.cache['normcolors'], tag)
def delete(self, tag):
self.tags.pop(tag).button.remove()
def focus(self, tag):
self.sel = self.tags[tag]
self.sel.button.colors = self.focuscol or wmii.cache['focuscolors']
def unfocus(self, tag):
self.tags[tag].button.colors = self.normcol or wmii.cache['normcolors']
def set_urgent(self, tag, urgent=True):
self.tags[tag].button.label = urgent and '*' + tag or tag
def next(self, reverse=False):
tags = [t for t in wmii.tags if t not in self.ignore]
tags.append(tags[0])
if reverse:
tags.reverse()
for i in range(0, len(tags)):
if tags[i] == self.sel.id:
return tags[i+1]
return self.sel
def select(self, tag, take_client=None):
def goto(tag):
if take_client:
# Make a new instance in case this is Client('sel'),
# which would cause problems given 'sel' changes in the
# process.
client = Client(take_client.id)
sel = Tag('sel').id
client.tags = '+%s' % tag
wmii['view'] = tag
if tag != sel:
client.tags = '-%s' % sel
else:
wmii['view'] = tag
if tag is self.PREV:
if self.sel.id not in self.ignore:
self.idx -= 1
elif tag is self.NEXT:
self.idx += 1
else:
if isinstance(tag, Tag):
tag = tag.id
goto(tag)
if tag not in self.ignore:
if self.idx < -1:
self.mru = self.mru[:self.idx + 1]
self.idx = -1
if self.mru and datetime.now() - self.lastselect < timedelta(seconds=.5):
self.mru[self.idx] = tag
elif tag != self.mru[-1]:
self.mru.append(tag)
self.mru = self.mru[-10:]
self.lastselect = datetime.now()
return
self.idx = constrain(-len(self.mru), -1, self.idx)
goto(self.mru[self.idx])
# vim:se sts=4 sw=4 et: