wmii

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

fs.py (26597B)


      1 import collections
      2 from datetime import datetime, timedelta
      3 import re
      4 
      5 from pyxp import *
      6 from pyxp.client import *
      7 from pygmi import *
      8 from pygmi.util import prop
      9 
     10 __all__ = ('wmii', 'Tags', 'Tag', 'Area', 'Frame', 'Client',
     11            'Button', 'Colors', 'Color', 'Toggle', 'Always', 'Never')
     12 
     13 spacere = re.compile(r'\s')
     14 sentinel = {}
     15 
     16 def tounicode(obj):
     17     if isinstance(obj, str):
     18         return obj.decode('UTF-8')
     19     return unicode(obj)
     20 
     21 class utf8(object):
     22     def __str__(self):
     23         return unicode(self).encode('utf-8')
     24 
     25 @apply
     26 class Toggle(utf8):
     27     def __unicode__(self):
     28         return unicode(self.__class__.__name__)
     29 @apply
     30 class Always(Toggle.__class__):
     31     pass
     32 @apply
     33 class Never(Toggle.__class__):
     34     pass
     35 
     36 def constrain(min, max, val):
     37     return min if val < min else max if val > max else val
     38 
     39 class Map(collections.Mapping):
     40     def __init__(self, cls, *args):
     41         self.cls = cls
     42         self.args = args
     43     def __repr__(self):
     44         return 'Map(%s%s)' % (self.cls.__name__, (', %s' % ', '.join(map(repr, self.args)) if self.args else ''))
     45     def __getitem__(self, item):
     46         ret = self.cls(*(self.args + (item,)))
     47         if not ret.exists:
     48             raise KeyError('no such %s %s' % (self.cls.__name__.lower(), repr(item)))
     49         return ret
     50     def __len__(self):
     51         return len(iter(self))
     52     def __keys__(self):
     53         return [v for v in self.cls.all(*self.args)]
     54     def __iter__(self):
     55         return (v for v in self.cls.all(*self.args))
     56     def iteritems(self):
     57         return ((v, self.cls(*(self.args + (v,)))) for v in self.cls.all(*self.args))
     58     def itervalues(self):
     59         return (self.cls(*(self.args + (v,))) for v in self.cls.all(*self.args))
     60 
     61 class Ctl(object):
     62     """
     63     An abstract class to represent the 'ctl' files of the wmii filesystem.
     64     Instances act as live, writable dictionaries of the settings represented
     65     in the file.
     66 
     67     Abstract roperty ctl_path: The path to the file represented by this
     68             control.
     69     Property ctl_hasid: When true, the first line of the represented
     70             file is treated as an id, rather than a key-value pair. In this
     71             case, the value is available via the 'id' property.
     72     Property ctl_types: A dict mapping named dictionary keys to two valued
     73             tuples, each containing a decoder and encoder function for the
     74             property's plain text value.
     75     """
     76     ctl_types = {}
     77     ctl_hasid = False
     78     ctl_open = 'aopen'
     79     ctl_file = None
     80 
     81     def __eq__(self, other):
     82         if self.ctl_hasid and isinstance(other, Ctl) and other.ctl_hasid:
     83             return self.id == other.id
     84         return False
     85 
     86     def __init__(self):
     87         self.cache = {}
     88 
     89     def ctl(self, *args):
     90         """
     91         Arguments are joined by ascii spaces and written to the ctl file.
     92         """
     93         def next(file):
     94             if file:
     95                 self.ctl_file = file
     96                 file.awrite(u' '.join(map(tounicode, args)))
     97         if self.ctl_file:
     98             return next(self.ctl_file)
     99         getattr(client, self.ctl_open)(self.ctl_path, callback=next, mode=OWRITE)
    100 
    101     def __getitem__(self, key):
    102         for line in self.ctl_lines():
    103             key_, rest = line.split(' ', 1)
    104             if key_ == key:
    105                 if key in self.ctl_types:
    106                     return self.ctl_types[key][0](rest)
    107                 return rest
    108         raise KeyError()
    109     def __hasitem__(self, key):
    110         return key in self.keys()
    111     def __setitem__(self, key, val):
    112         assert '\n' not in key
    113         self.cache[key] = val
    114         if key in self.ctl_types:
    115             if self.ctl_types[key][1] is None:
    116                 raise NotImplementedError('%s: %s is not writable' % (self.ctl_path, key))
    117             val = self.ctl_types[key][1](val)
    118         self.ctl(key, val)
    119 
    120     def get(self, key, default=sentinel):
    121         """
    122         Gets the instance's dictionary value for 'key'. If the key doesn't
    123         exist, 'default' is returned. If 'default' isn't provided and the key
    124         doesn't exist, a KeyError is raised.
    125         """
    126         try:
    127             return self[key]
    128         except KeyError, e:
    129             if default is not self.sentinel:
    130                 return default
    131             raise e
    132     def set(self, key, val):
    133         """
    134         Sets the dictionary value for 'key' to 'val', as self[key] = val
    135         """
    136         self[key] = val
    137 
    138     def keys(self):
    139         return [line.split(' ', 1)[0]
    140                 for line in self.ctl_lines()]
    141     def iteritems(self):
    142         return (tuple(line.split(' ', 1))
    143                 for line in self.ctl_lines())
    144     def items(self):
    145         return [tuple(line.split(' ', 1))
    146                 for line in self.ctl_lines()]
    147 
    148     def ctl_lines(self):
    149         """
    150         Returns the lines of the ctl file as a tuple, with the first line
    151         stripped if #ctl_hasid is set.
    152         """
    153         lines = tuple(client.readlines(self.ctl_path))
    154         if self.ctl_hasid:
    155             lines = lines[1:]
    156         return lines
    157 
    158     _id = None
    159     @prop(doc="If #ctl_hasid is set, returns the id of this ctl file.")
    160     def id(self):
    161         if self._id is None and self.ctl_hasid:
    162             return self.name_read(client.read(self.ctl_path).split('\n', 1)[0])
    163         return self._id
    164 
    165 class Dir(Ctl):
    166     """
    167     An abstract class representing a directory in the wmii filesystem with a
    168     ctl file and sub-objects.
    169 
    170     Abstract property base_path: The path directly under which all objects
    171             represented by this class reside. e.g., /client, /tag
    172     """
    173     ctl_hasid = True
    174     name_read = unicode
    175     name_write = unicode
    176 
    177     def __init__(self, id):
    178         """
    179         Initializes the directory object.
    180 
    181         Param id: The id of the object in question. If 'sel', the object
    182                 dynamically represents the selected object, even as it
    183                 changes. In this case, #id will return the actual ID of the
    184                 object.
    185         """
    186         super(Dir, self).__init__()
    187         if isinstance(id, Dir):
    188             id = id.id
    189         if id != 'sel':
    190             self._id = self.name_read(id)
    191 
    192     def __eq__(self, other):
    193         return (self.__class__ == other.__class__ and
    194                 self.id == other.id)
    195 
    196     class ctl_property(object):
    197         """
    198         A class which maps instance properties to ctl file properties.
    199         """
    200         def __init__(self, key):
    201             self.key = key
    202         def __get__(self, dir, cls):
    203             return dir.get(self.key, None)
    204         def __set__(self, dir, val):
    205             dir[self.key] = val
    206 
    207     class toggle_property(ctl_property):
    208         """
    209         A class which maps instance properties to ctl file properties. The
    210         values True and False map to the strings "on" and "off" in the
    211         filesystem.
    212         """
    213         props = {
    214             'on': True,
    215             'off': False,
    216             'toggle': Toggle,
    217             'always': Always,
    218             'never': Never
    219         }
    220         def __get__(self, dir, cls):
    221             val = dir[self.key]
    222             if val in self.props:
    223                 return self.props[val]
    224             return val
    225         def __set__(self, dir, val):
    226             for k, v in self.props.iteritems():
    227                 if v == val:
    228                     val = k
    229                     break
    230             dir[self.key] = val
    231 
    232     class file_property(object):
    233         """
    234         A class which maps instance properties to files in the directory
    235         represented by this object.
    236         """
    237         def __init__(self, name, writable=False):
    238             self.name = name
    239             self.writable = writable
    240         def __get__(self, dir, cls):
    241             return client.read('%s/%s' % (dir.path, self.name))
    242         def __set__(self, dir, val):
    243             if not self.writable:
    244                 raise NotImplementedError('File %s is not writable' % self.name)
    245             return client.awrite('%s/%s' % (dir.path, self.name),
    246                                  str(val))
    247 
    248     @prop(doc="The path to this directory's ctl file")
    249     def ctl_path(self):
    250         return '%s/ctl' % self.path
    251 
    252     @prop(doc="The path to this directory")
    253     def path(self):
    254         return '%s/%s' % (self.base_path, self.name_write(self._id or 'sel'))
    255     @prop(doc="True if the given object exists in the wmii filesystem")
    256     def exists(self):
    257         return bool(client.stat(self.path))
    258 
    259     @classmethod
    260     def all(cls):
    261         """
    262         Returns all of the objects that exist for this type of directory.
    263         """
    264         return (cls.name_read(s.name)
    265                 for s in client.readdir(cls.base_path)
    266                 if s.name != 'sel')
    267     @classmethod
    268     def map(cls, *args):
    269         return Map(cls, *args)
    270 
    271     def __repr__(self):
    272         return '%s(%s)' % (self.__class__.__name__,
    273                            repr(self._id or 'sel'))
    274 
    275 class Client(Dir):
    276     """
    277     A class which represents wmii clients. Maps to the directories directly
    278     below /client.
    279     """
    280     base_path = '/client'
    281     ctl_types = {
    282         'group': (lambda s: int(s, 16), str),
    283         'pid': (int, None),
    284     }
    285     @staticmethod
    286     def name_read(name):
    287         if isinstance(name, int):
    288             return name
    289         try:
    290             return int(name, 16)
    291         except:
    292             return unicode(name)
    293     name_write = lambda self, name: name if isinstance(name, basestring) else '%#x' % name
    294 
    295     allow  = Dir.ctl_property('allow')
    296     fullscreen = Dir.toggle_property('fullscreen')
    297     group  = Dir.ctl_property('group')
    298     pid    = Dir.ctl_property('pid')
    299     tags   = Dir.ctl_property('tags')
    300     urgent = Dir.toggle_property('urgent')
    301 
    302     label = Dir.file_property('label', writable=True)
    303     props = Dir.file_property('props')
    304 
    305     def kill(self):
    306         """Politely asks a client to quit."""
    307         self.ctl('kill')
    308 
    309     def slay(self):
    310         """Forcibly severs a client's connection to the X server."""
    311         self.ctl('slay')
    312 
    313 class liveprop(object):
    314     def __init__(self, get):
    315         self.get = get
    316         self.attr = str(self)
    317     def __get__(self, area, cls):
    318         if getattr(area, self.attr, sentinel) is not sentinel:
    319             return getattr(area, self.attr)
    320         return self.get(area)
    321     def __set__(self, area, val):
    322         setattr(area, self.attr, val)
    323 
    324 class Area(object):
    325     def __init__(self, tag, ord, screen='sel', offset=sentinel, width=sentinel, height=sentinel, frames=sentinel):
    326         self.tag = tag
    327         if ':' in str(ord):
    328             screen, ord = ord.split(':', 2)
    329         self.ord = str(ord)
    330         self.screen = str(screen)
    331         self.offset = offset
    332         self.width = width
    333         self.height = height
    334         self.frames = frames
    335 
    336     def prop(key):
    337         @liveprop
    338         def prop(self):
    339             for area in self.tag.index:
    340                 if str(area.ord) == str(self.ord):
    341                     return getattr(area, key)
    342         return prop
    343     offset = prop('offset')
    344     width = prop('width')
    345     height = prop('height')
    346     frames = prop('frames')
    347 
    348     @property
    349     def spec(self):
    350         if self.screen is not None:
    351             return '%s:%s' % (self.screen, self.ord)
    352         return self.ord
    353 
    354     @property
    355     def mode(self):
    356         for k, v in self.tag.iteritems():
    357             if k == 'colmode':
    358                 v = v.split(' ')
    359                 if v[0] == self.ord:
    360                     return v[1]
    361     @mode.setter
    362     def mode(self, val):
    363         self.tag['colmode %s' % self.spec] = val
    364 
    365     def grow(self, dir, amount=None):
    366         self.tag.grow(self, dir, amount)
    367     def nudge(self, dir, amount=None):
    368         self.tag.nudge(self, dir, amount)
    369 
    370 class Frame(object):
    371     live = False
    372 
    373     def __init__(self, client, area=sentinel, ord=sentinel, offset=sentinel, height=sentinel):
    374         self.client = client
    375         self.ord = ord
    376         self.offset = offset
    377         self.height = height
    378 
    379     @property
    380     def width(self):
    381         return self.area.width
    382 
    383     def prop(key):
    384         @liveprop
    385         def prop(self):
    386             for area in self.tag.index:
    387                 for frame in area.frames:
    388                     if frame.client == self.client:
    389                         return getattr(frame, key)
    390         return prop
    391     offset = prop('area')
    392     offset = prop('ord')
    393     offset = prop('offset')
    394     height = prop('height')
    395 
    396     def grow(self, dir, amount=None):
    397         self.area.tag.grow(self, dir, amount)
    398     def nudge(self, dir, amount=None):
    399         self.area.tag.nudge(self, dir, amount)
    400 
    401 class Tag(Dir):
    402     base_path = '/tag'
    403 
    404     @classmethod
    405     def framespec(cls, frame):
    406         if isinstance(frame, Frame):
    407             frame = frame.client
    408         if isinstance(frame, Area):
    409             frame = (frame.ord, 'sel')
    410         if isinstance(frame, Client):
    411             if frame._id is None:
    412                 return 'sel sel'
    413             return 'client %s' % frame.id
    414         elif isinstance(frame, basestring):
    415             return frame
    416         else:
    417             return '%s %s' % tuple(map(str, frame))
    418     def dirspec(cls, dir):
    419         if isinstance(dir, tuple):
    420             dir = ' '.join(dir)
    421         return dir
    422 
    423     @property
    424     def selected(self):
    425         return tuple(self['select'].split(' '))
    426     @selected.setter
    427     def selected(self, frame):
    428         if not isinstance(frame, basestring) or ' ' not in frame:
    429             frame = self.framespec(frame)
    430         self['select'] = frame
    431 
    432     @property
    433     def selclient(self):
    434         for k, v in self.iteritems():
    435             if k == 'select' and 'client' in v:
    436                 return Client(v.split(' ')[1])
    437         return None
    438     @selclient.setter
    439     def selclient(self, val):
    440         self['select'] = self.framespec(val)
    441 
    442     @property
    443     def selcol(self):
    444         return Area(self, self.selected[0])
    445 
    446     @property
    447     def index(self):
    448         areas = []
    449         for l in (l.split(' ')
    450                   for l in client.readlines('%s/index' % self.path)
    451                   if l):
    452             if l[0] == '#':
    453                 m = re.match(r'(?:(\d+):)?(\d+|~)', l[1])
    454                 if m.group(2) == '~':
    455                     area = Area(tag=self, screen=m.group(1), ord=l[1], width=l[2],
    456                                 height=l[3], frames=[])
    457                 else:
    458                     area = Area(tag=self, screen=m.group(1) or 0,
    459                                 height=None, ord=m.group(2), offset=l[2], width=l[3],
    460                                 frames=[])
    461                 areas.append(area)
    462                 i = 0
    463             else:
    464                 area.frames.append(
    465                     Frame(client=Client(l[1]), area=area, ord=i,
    466                           offset=l[2], height=l[3]))
    467                 i += 1
    468         return areas
    469 
    470     def delete(self):
    471         id = self.id
    472         for a in self.index:
    473             for f in a.frames:
    474                 if f.client.tags == id:
    475                     f.client.kill()
    476                 else:
    477                     f.client.tags = '-%s' % id
    478         if self == Tag('sel'):
    479             Tags.instance.select(Tags.instance.next())
    480 
    481     def select(self, frame, stack=False):
    482         self['select'] = '%s %s' % (
    483             self.framespec(frame),
    484             stack and 'stack' or '')
    485 
    486     def send(self, src, dest, stack=False, cmd='send'):
    487         if isinstance(src, tuple):
    488             src = ' '.join(src)
    489         if isinstance(src, Frame):
    490             src = src.client
    491         if isinstance(src, Client):
    492             src = src._id or 'sel'
    493 
    494         if isinstance(dest, tuple):
    495             dest = ' '.join(dest)
    496 
    497         self[cmd] = '%s %s' % (src, dest)
    498 
    499     def swap(self, src, dest):
    500         self.send(src, dest, cmd='swap')
    501     
    502     def nudge(self, frame, dir, amount=None):
    503         frame = self.framespec(frame)
    504         self['nudge'] = '%s %s %s' % (frame, dir, str(amount or ''))
    505     def grow(self, frame, dir, amount=None):
    506         frame = self.framespec(frame)
    507         self['grow'] = '%s %s %s' % (frame, dir, str(amount or ''))
    508 
    509 class Color(utf8):
    510     def __init__(self, colors):
    511         if isinstance(colors, Color):
    512             colors = colors.rgb
    513         elif isinstance(colors, basestring):
    514             match = (re.match(r'^#(..)(..)(..)((?:..)?)$', colors) or
    515                      re.match(r'^rgba:(..)/(..)/(..)/(..)$', colors))
    516             colors = tuple(int(match.group(group), 16) for group in range(1, 4))
    517             if match.group(4):
    518                 colors += int(match.group(4), 16),
    519         def toint(val):
    520             if isinstance(val, float):
    521                 val = int(255 * val)
    522             assert 0 <= val <= 255
    523             return val
    524         self.rgb = tuple(map(toint, colors))
    525 
    526     def __getitem__(self, key):
    527         if isinstance(key, basestring):
    528             key = {'red': 0, 'green': 1, 'blue': 2}[key]
    529         return self.rgb[key]
    530 
    531     @property
    532     def hex(self):
    533         if len(self.rgb) > 3:
    534             return 'rgba:%02x/%02x/%02x/%02x' % self.rgb
    535         return '#%02x%02x%02x' % self.rgb
    536 
    537     def __unicode__(self):
    538         if len(self.rgb) > 3:
    539             return 'rgba(%d, %d, %d, %d)' % self.rgb
    540         return 'rgb(%d, %d, %d)' % self.rgb
    541     def __repr__(self):
    542         return 'Color(%s)' % repr(self.rgb)
    543 
    544 class Colors(utf8):
    545     def __init__(self, foreground=None, background=None, border=None):
    546         vals = foreground, background, border
    547         self.vals = tuple(map(Color, vals))
    548 
    549     def __iter__(self):
    550         return iter(self.vals)
    551     def __list__(self):
    552         return list(self.vals)
    553     def __tuple__(self):
    554         return self.vals
    555 
    556     @classmethod
    557     def from_string(cls, val):
    558         return cls(*val.split(' '))
    559 
    560     def __getitem__(self, key):
    561         if isinstance(key, basestring):
    562             key = {'foreground': 0, 'background': 1, 'border': 2}[key]
    563         return self.vals[key]
    564 
    565     def __unicode__(self):
    566         return ' '.join(c.hex for c in self.vals)
    567     def __repr__(self):
    568         return 'Colors(%s, %s, %s)' % tuple(repr(c.rgb) for c in self.vals)
    569 
    570 class Button(Ctl):
    571     sides = {
    572         'left': 'lbar',
    573         'right': 'rbar',
    574     }
    575     ctl_types = {
    576         'colors': (Colors.from_string, lambda c: str(Colors(*c))),
    577     }
    578     ctl_open = 'acreate'
    579     colors = Dir.ctl_property('colors')
    580     label  = Dir.ctl_property('label')
    581 
    582     def __init__(self, side, name, colors=None, label=None):
    583         super(Button, self).__init__()
    584         self.side = side
    585         self.name = name
    586         self.base_path = self.sides[side]
    587         self.ctl_path = '%s/%s' % (self.base_path, self.name)
    588         self.ctl_file = None
    589         if colors or label:
    590             self.create(colors, label)
    591 
    592     def create(self, colors=None, label=None):
    593         if not self.ctl_file:
    594             self.ctl_file = client.create(self.ctl_path, ORDWR)
    595         if colors:
    596             self.colors = colors
    597         if label:
    598             self.label = label
    599 
    600     def remove(self):
    601         if self.ctl_file:
    602             self.ctl_file.aremove()
    603             self.ctl_file = None
    604 
    605     @property
    606     def exists(self):
    607         return bool(self.file.stat() if self.file else client.stat(self.ctl_path))
    608 
    609     @classmethod
    610     def all(cls, side):
    611         return (s.name
    612                 for s in client.readdir(cls.sides[side])
    613                 if s.name != 'sel')
    614     @classmethod
    615     def map(cls, *args):
    616         return Map(cls, *args)
    617 
    618 class Rules(collections.MutableMapping, utf8):
    619 
    620     _items = ()
    621     def __init__(self, path, rules=None):
    622         self.path = path
    623         if rules:
    624             self.setitems(rules)
    625 
    626     _quotere = re.compile(ur'(\\(.)|/)')
    627     @classmethod
    628     def quoteslash(cls, str):
    629         return cls._quotere.sub(lambda m: m.group(0) if m.group(2) else r'\/', str)
    630 
    631     __get__ = lambda self, obj, cls: self
    632     def __set__(self, obj, val):
    633         self.setitems(val)
    634 
    635     def __getitem__(self, key):
    636         for k, v in self.iteritems():
    637             if k == key:
    638                 return v
    639         raise KeyError()
    640     def __setitem__(self, key, val):
    641         items = [(k, v) for k, v in self.iteritems() if k != key]
    642         items.append((key, val))
    643         self.setitems(items)
    644     def __delitem__(self, key):
    645         self.setitems((k, v) for k, v in self.iteritems() if k != key)
    646 
    647     def __len__(self):
    648         return len(tuple(self.iteritems()))
    649     def __iter__(self):
    650         return (k for k, v in self.iteritems())
    651     def __list__(self):
    652         return list(iter(self))
    653     def __tuple__(self):
    654         return tuple(iter(self))
    655 
    656     def append(self, item):
    657         self.setitems(self + (item,))
    658     def __add__(self, items):
    659         return tuple(self.iteritems()) + tuple(items)
    660 
    661     def rewrite(self):
    662         client.awrite(self.path, unicode(self))
    663     def setitems(self, items):
    664         self._items = [(k, v if isinstance(v, Rule) else Rule(self, k, v))
    665                        for (k, v) in items]
    666         self.rewrite()
    667 
    668     def __unicode__(self):
    669         return u''.join(unicode(value) for (key, value) in self.iteritems()) or u'\n'
    670 
    671     def iteritems(self):
    672         return iter(self._items)
    673     def items(self):
    674         return list(self._items())
    675 
    676 class Rule(collections.MutableMapping, utf8):
    677     _items = ()
    678     parent = None
    679 
    680     @classmethod
    681     def quotekey(cls, key):
    682         if key.endswith('_'):
    683             key = key[:-1]
    684         return key.replace('_', '-')
    685     @classmethod
    686     def quotevalue(cls, val):
    687         if val is True:   return "on"
    688         if val is False:  return "off"
    689         if val in (Toggle, Always, Never):
    690             return unicode(val).lower()
    691         return tounicode(val)
    692 
    693     def __get__(self, obj, cls):
    694         return self
    695     def __set__(self, obj, val):
    696         self.setitems(val)
    697 
    698     def __init__(self, parent, key, items={}):
    699         self.key = key
    700         self._items = []
    701         self.setitems(items.iteritems() if isinstance(items, dict) else items)
    702         self.parent = parent
    703 
    704     def __getitem__(self, key):
    705         for k, v in reversed(self._items):
    706             if k == key:
    707                 return v
    708         raise KeyError()
    709 
    710     def __setitem__(self, key, val):
    711         items = [(k, v) for k, v in self.iteritems() if k != key]
    712         items.append((key, val))
    713         self.setitems(items)
    714 
    715     def __delitem__(self, key):
    716         self.setitems([(k, v) for k, v in self.iteritems() if k != key])
    717 
    718     def __len__(self):
    719         return len(self._items)
    720     def __iter__(self):
    721         return iter(self._items)
    722     def __list__(self):
    723         return list(iter(self))
    724     def __tuple__(self):
    725         return tuple(iter(self))
    726 
    727     def append(self, item):
    728         self.setitems(self + (item,))
    729     def __add__(self, items):
    730         return tuple(self.iteritems()) + tuple(items)
    731 
    732     def setitems(self, items):
    733         items = list(items)
    734         assert not any('=' in key or
    735                        spacere.search(self.quotekey(key)) or
    736                        spacere.search(self.quotevalue(val)) for (key, val) in items)
    737         self._items = items
    738         if self.parent:
    739             self.parent.rewrite()
    740 
    741     def __unicode__(self):
    742         return u'/%s/ %s\n' % (
    743             Rules.quoteslash(self.key),
    744             u' '.join(u'%s=%s' % (self.quotekey(k), self.quotevalue(v))
    745                       for (k, v) in self.iteritems()))
    746 
    747     def iteritems(self):
    748         return iter(self._items)
    749     def items(self):
    750         return list(self._items)
    751 
    752 
    753 @apply
    754 class wmii(Ctl):
    755     ctl_path = '/ctl'
    756     ctl_types = {
    757         'normcolors': (Colors.from_string, lambda c: str(Colors(*c))),
    758         'focuscolors': (Colors.from_string, lambda c: str(Colors(*c))),
    759         'border': (int, str),
    760     }
    761 
    762     clients = Client.map()
    763     tags = Tag.map()
    764     lbuttons = Button.map('left')
    765     rbuttons = Button.map('right')
    766 
    767     rules    = Rules('/rules')
    768 
    769 class Tags(object):
    770     PREV = []
    771     NEXT = []
    772 
    773     def __init__(self, normcol=None, focuscol=None):
    774         self.ignore = set()
    775         self.tags = {}
    776         self.sel = None
    777         self.normcol = normcol
    778         self.focuscol = focuscol
    779         self.lastselect = datetime.now()
    780         for t in wmii.tags:
    781             self.add(t)
    782         for b in wmii.lbuttons.itervalues():
    783             if b.name not in self.tags:
    784                 b.remove()
    785         self.focus(Tag('sel').id)
    786 
    787         self.mru = [self.sel.id]
    788         self.idx = -1
    789         Tags.instance = self
    790 
    791     def add(self, tag):
    792         self.tags[tag] = Tag(tag)
    793         self.tags[tag].button = Button('left', tag, self.normcol or wmii.cache['normcolors'], tag)
    794     def delete(self, tag):
    795         self.tags.pop(tag).button.remove()
    796 
    797     def focus(self, tag):
    798         self.sel = self.tags[tag]
    799         self.sel.button.colors = self.focuscol or wmii.cache['focuscolors']
    800     def unfocus(self, tag):
    801         self.tags[tag].button.colors = self.normcol or wmii.cache['normcolors']
    802 
    803     def set_urgent(self, tag, urgent=True):
    804         self.tags[tag].button.label = urgent and '*' + tag or tag
    805 
    806     def next(self, reverse=False):
    807         tags = [t for t in wmii.tags if t not in self.ignore]
    808         tags.append(tags[0])
    809         if reverse:
    810             tags.reverse()
    811         for i in range(0, len(tags)):
    812             if tags[i] == self.sel.id:
    813                 return tags[i+1]
    814         return self.sel
    815 
    816     def select(self, tag, take_client=None):
    817         def goto(tag):
    818             if take_client:
    819                 # Make a new instance in case this is Client('sel'),
    820                 # which would cause problems given 'sel' changes in the
    821                 # process.
    822                 client = Client(take_client.id)
    823 
    824                 sel = Tag('sel').id
    825                 client.tags = '+%s' % tag
    826                 wmii['view'] = tag
    827                 if tag != sel:
    828                     client.tags = '-%s' % sel
    829             else:
    830                 wmii['view'] = tag
    831 
    832         if tag is self.PREV:
    833             if self.sel.id not in self.ignore:
    834                 self.idx -= 1
    835         elif tag is self.NEXT:
    836             self.idx += 1
    837         else:
    838             if isinstance(tag, Tag):
    839                 tag = tag.id
    840             goto(tag)
    841 
    842             if tag not in self.ignore:
    843                 if self.idx < -1:
    844                     self.mru = self.mru[:self.idx + 1]
    845                     self.idx = -1
    846                 if self.mru and datetime.now() - self.lastselect < timedelta(seconds=.5):
    847                     self.mru[self.idx] = tag
    848                 elif tag != self.mru[-1]:
    849                     self.mru.append(tag)
    850                     self.mru = self.mru[-10:]
    851                 self.lastselect = datetime.now()
    852             return
    853 
    854         self.idx = constrain(-len(self.mru), -1, self.idx)
    855         goto(self.mru[self.idx])
    856 
    857 # vim:se sts=4 sw=4 et: