diff options
Diffstat (limited to 'alternative_wmiircs')
33 files changed, 4683 insertions, 0 deletions
diff --git a/alternative_wmiircs/Makefile b/alternative_wmiircs/Makefile new file mode 100644 index 0000000..066739c --- /dev/null +++ b/alternative_wmiircs/Makefile @@ -0,0 +1,13 @@ +ROOT=.. +include $(ROOT)/mk/hdr.mk +include $(ROOT)/mk/wmii.mk + +BIN = $(ETC)/wmii$(CONFVERSION) +DIRS = python \ + plan9port \ + ruby + +DOCS = README +DOCDIR = $(DOC)/alternative_wmiircs + +include $(ROOT)/mk/dir.mk diff --git a/alternative_wmiircs/README b/alternative_wmiircs/README new file mode 100644 index 0000000..a98d4fb --- /dev/null +++ b/alternative_wmiircs/README @@ -0,0 +1,23 @@ +Alternative wmiirc scripts +========================== + +This folder contains alternative implementations of wmii's rc +scripts. Each folder contains a different implementation, +described below, including its own README, wmiirc script, and +possibly other suppporting files and libraries. These scripts +are installed along with wmii to $(ETC) as defined in config.mk. + +It usually suffices to start the included `wmiirc` script at +wmii startup. Invoking wmii with the flag '-r python/wmiirc', +for instance, will start the python implementation. +Alternatively, if you use a session manager, you can add this +line to ~/.wmii/wmiirc (which must be executable): + + wmiir xwrite /ctl spawn python/wmiirc + + Index + ------------- ---------------------------------------------------- + python/ A pure Python wmiirc implementation. + plan9port/ A Plan 9 Port/rc shell based wmiirc implementation + ruby/ A pure-ruby wmiirc implementation, by Suraj Kurapati + diff --git a/alternative_wmiircs/plan9port/Makefile b/alternative_wmiircs/plan9port/Makefile new file mode 100644 index 0000000..e582ccf --- /dev/null +++ b/alternative_wmiircs/plan9port/Makefile @@ -0,0 +1,9 @@ +ROOT=../.. +include $(ROOT)/mk/hdr.mk +include $(ROOT)/mk/wmii.mk + +DOCS = README +EXECS = wmiirc + +DIR = $(ETC)/wmii$(CONFVERSION)/plan9port +DOCDIR = $(DOC)/alternative_wmiircs/plan9port diff --git a/alternative_wmiircs/plan9port/README b/alternative_wmiircs/plan9port/README new file mode 100644 index 0000000..37d388d --- /dev/null +++ b/alternative_wmiircs/plan9port/README @@ -0,0 +1,10 @@ +plan9port wmiirc +================ + +This directory contains a Plan 9 based wmiirc script. This script was +traditionally the default wmiirc for wmii, but has been moved for +portability reasons. To run this script, either Plan 9 from User +Space[1] (plan9port for short) or 9base[2] is required. Modifications +can be placed in $home/.wmii@CONFVERSION@/wmiirc_local.rc, which must +be executable. + diff --git a/alternative_wmiircs/plan9port/wmiirc b/alternative_wmiircs/plan9port/wmiirc new file mode 100755 index 0000000..1724bdf --- /dev/null +++ b/alternative_wmiircs/plan9port/wmiirc @@ -0,0 +1,284 @@ +#!/bin/sh -f +p="$PATH" +which rc >/dev/null || PATH="$PLAN9:$p" +which rc >/dev/null || PATH="/usr/local/plan9/bin:$p" +which rc >/dev/null || PATH="/usr/local/9/bin:$p" +which rc >/dev/null || PATH="/opt/plan9/bin:$p" +which rc >/dev/null || PATH="/opt/9/bin:$p" +which rc >/dev/null || PATH="/usr/plan9/bin:$p" +which rc >/dev/null || PATH="/usr/9/bin:$p" +test $#* '=' 0 || exec rc $0 + +cd +scriptname=$0 +oldpath=$path; path=($PLAN9/bin $path) +. wmii.rc wmiirc # Include utility functions + +# WMII Configuration + +# Keys +MODKEY=Mod4 +UP=k +DOWN=j +LEFT=h +RIGHT=l + +# Bars +noticetimeout=5 +noticebar=/rbar/!notice + +# Theme +wmiifont='drift,-*-fixed-*-*-*-*-9-*-*-*-*-*-*-*' +wmiifont='-*-fixed-medium-r-*-*-13-*-*-*-*-*-*-*' +wmiinormcol=`{echo '#000000 #c1c48b #81654f'} +wmiifocuscol=`{echo '#000000 #81654f #000000'} +wmiibackground='#333333' +wmiifloatbackground='#222222' +fn setbackground { xsetroot -solid $* } + +# Programs +WMII_TERM=(xterm) + +# Column Rules +wmiir write /colrules <<! +/gimp/ -> 17+83+41 +/.*/ -> 62+38 # Golden Ratio +! + +# Tagging Rules +wmiir write /tagrules <<! +/MPlayer|VLC/ -> ~ +! + +# Status Bar Info +fn status { + echo -n `{uptime | sed 's/.*://; s/,//g'} \ + '|' `{date} +} + +# End Configuration + +# For the time being, this file follows the lisp bracing +# convention. i.e.: +# if(frob this) { +# frob that +# if(frob theother) { +# unfrob this +# unfrob that}} +# Comments welcome. + +confpath=`{echo $WMII_CONFPATH | sed 'y/:/ /'} + +# Events +fn sigexit { + rm -f $progs_file + wi_cleankeys +} + +fn Event-CreateTag { + echo $wmiinormcol $* | wmiir create /lbar/$"*} +fn Event-DestroyTag { + wmiir remove /lbar/$"*} +fn Event-FocusTag { + wmiir xwrite /lbar/$"* $wmiifocuscol $*} +fn Event-UnfocusTag { + wmiir xwrite /lbar/$"* $wmiinormcol $*} +fn Event-UrgentTag { + shift + wmiir xwrite /lbar/$"* '*'$"*} +fn Event-NotUrgentTag { + shift + wmiir xwrite /lbar/$"* $"*} +fn Event-AreaFocus { + if(~ $1 '~') + setbackground $wmiifloatbackground + if not + setbackground $wmiibackground } + +fn Event-Unresponsive { + client = $1; shift + @{ + msg = 'The following client is not responding. What would you like to do?' + resp = `{wihack -transient $client \ + xmessage -nearmouse -buttons Kill,Wait -print \ + $msg $wi_newline '' `{wmiir read /client/$client/label}} + if(~ $resp Kill) + wmiir xwrite /client/$client/ctl slay + }&} +fn Event-Notice { + wmiir xwrite $noticebar $wi_arg + + /bin/kill $xpid >[2]/dev/null # Let's hope this isn't reused... + { sleep $noticetimeout; wmiir xwrite $noticebar ' ' }& # Bug... + xpid = $apid} + +fn Event-LeftBar^(Click DND) { + shift; wmiir xwrite /ctl view $*} + +fn ClientMenu-3-Delete { + wmiir xwrite /client/$1/ctl kill} +fn ClientMenu-3-Kill { + wmiir xwrite /client/$1/ctl slay} +fn ClientMenu-3-Fullscreen { + wmiir xwrite /client/$1/ctl Fullscreen on} +fn Event-ClientMouseDown { + wi_fnmenu Client $2 $1 &} + +fn LBarMenu-3-Delete { + tag=$1; clients=`{wmiir read /tag/$tag/index | awk '/[^#]/{print $2}'} + for(c in $clients) { + if(~ $tag `{wmiir read /client/$c/tags}) + wmiir xwrite /client/$c/ctl kill + if not + wmiir xwrite /client/$c/tags -$tag} + if(~ $tag `{wi_seltag}) { + newtag = `{wi_tags | awk -v't='$tag ' + $1 == t { if(!l) getline l + print l + exit } + { l = $0 }'} + wmiir xwrite /ctl view $newtag}} +fn Event-LeftBarMouseDown { + wi_fnmenu LBar $* &} + +# Actions +fn Action-rehash { + comm -23 <{ls `{namespace}^/proglist.* >[2]/dev/null | awk -F'.' '{print $NF}'} \ + <{ps | awk '{print $2}'} | + while(id=`{read}) + rm `{namespace}^/proglist.$id + wi_proglist $PATH >$progs_file} +fn Action-quit { + wmiir xwrite /ctl quit} +fn Action-exec { + wmiir xwrite /ctl exec $*} +fn Action-status { + flag x -; flag r - + if(wmiir remove /rbar/status >[2]/dev/null) + sleep 2 + echo $wmiinormcol | wmiir create /rbar/status + while(status | wmiir write /rbar/status) + sleep 1 +} + +# Source Variables, &c +if(~ $0 ('' */)wmiirc_local.rc) + wi_notice This file should not be named wmiirc_local.rc +if not + . `{wi_script -f wmiirc_local.rc} +echo $wmiinormcol | wmiir create $noticebar + +# Key Bindings +_keys = `{wi_getfuns Key} +fn key { + key=() + for(k) if(! ~ $k $_keys) key = ($key Key-$k) + ~ $#key 0} + +# This is... ugly. + +key $MODKEY-Control-t || fn $key { + switch(`{wmiir read /keys | wc -l}) { + case 0 1 + wmiir xwrite /keys $keys + wmiir xwrite /ctl grabmod $MODKEY + case * + ifs=() { keys=`{wmiir read /keys} } + wmiir xwrite /keys $MODKEY-Control-t + wmiir xwrite /ctl grabmod Mod3 + }} + +key $MODKEY-$LEFT || fn $key { + wmiir xwrite /tag/sel/ctl select left} +key $MODKEY-$RIGHT || fn $key { + wmiir xwrite /tag/sel/ctl select right} +key $MODKEY-$DOWN || fn $key { + wmiir xwrite /tag/sel/ctl select down} +key $MODKEY-$UP || fn $key { + wmiir xwrite /tag/sel/ctl select up} +key $MODKEY-Control-$DOWN || fn $key { + wmiir xwrite /tag/sel/ctl select down stack} +key $MODKEY-Control-$UP || fn $key { + wmiir xwrite /tag/sel/ctl select up stack} + +key $MODKEY-Shift-$LEFT || fn $key { + wmiir xwrite /tag/sel/ctl send sel left} +key $MODKEY-Shift-$RIGHT || fn $key { + wmiir xwrite /tag/sel/ctl send sel right} +key $MODKEY-Shift-$DOWN || fn $key { + wmiir xwrite /tag/sel/ctl send sel down} +key $MODKEY-Shift-$UP || fn $key { + wmiir xwrite /tag/sel/ctl send sel up} + +key $MODKEY-f || fn $key { + wmiir xwrite /client/sel/ctl Fullscreen toggle} + +key $MODKEY-space || fn $key { + wmiir xwrite /tag/sel/ctl select toggle} +key $MODKEY-Shift-space || fn $key { + wmiir xwrite /tag/sel/ctl send sel toggle} +key $MODKEY-d || fn $key { + wmiir xwrite /tag/sel/ctl colmode sel default-max} +key $MODKEY-s || fn $key { + wmiir xwrite /tag/sel/ctl colmode sel stack-max} +key $MODKEY-m || fn $key { + wmiir xwrite /tag/sel/ctl colmode sel stack+max} + +key $MODKEY-Shift-c || fn $key { + wmiir xwrite /client/sel/ctl kill} + +key $MODKEY-a || fn $key { + Action `{wi_actions | wimenu -h $hist.action -n $histlen} &} +key $MODKEY-p || fn $key { + ifs=() { cmd = `{wimenu -h $hist.prog -n $histlen <$progs_file} } + wi_runcmd $cmd & } + +key $MODKEY-Return || fn $key { + wi_runcmd $WMII_TERM &} + +key $MODKEY-t || fn $key { + tag=`{wi_tags | wimenu -h $hist.tag -n 50} && wmiir xwrite /ctl view $tag &} +key $MODKEY-Shift-t || fn $key { + sel=`{wi_selclient} { + tag=`{wi_tags | wimenu -h $hist.tag -n 50} && wmiir xwrite /client/$sel/tags $tag } &} + +key $MODKEY-^`{seq 0 9} || fn $key { + wmiir xwrite /ctl view `{echo $1 | sed 's/.*-//'}} +key Shift-$MODKEY-^`{seq 0 9} || fn $key { + wmiir xwrite /client/sel/tags `{echo $1 | sed 's/.*-//'}} + +#` WM Configuration +wmiir write /ctl <<! + grabmod $MODKEY + border 2 + font $wmiifont + focuscolors $wmiifocuscol + normcolors $wmiinormcol +! +setbackground $wmiibackground + +# Source Overrides +Action overridekeys + +# Misc Setup +progs_file=`{namespace}^/proglist.$pid +hist=`{echo $WMII_CONFPATH | sed 's,:.*,/,'}^/history +histlen=5000 +Action status & +Action rehash & + +# Tag Bar Setup +ifs=$wi_newline { + rc -c 'wmiir rm /lbar/^$*' >[2]/dev/null \ + `{comm -23 <{wmiir ls /lbar} \ + <{wi_tags}} + seltag=`{wi_seltag} + for(tag in `{wi_tags}) {{ + if(~ $tag $seltag) + echo $wmiifocuscol $tag + if not + echo $wmiinormcol $tag + } | wmiir create /lbar/$tag}} + +wi_eventloop + diff --git a/alternative_wmiircs/python/Makefile b/alternative_wmiircs/python/Makefile new file mode 100644 index 0000000..6c2a9ab --- /dev/null +++ b/alternative_wmiircs/python/Makefile @@ -0,0 +1,14 @@ +ROOT=../.. +include $(ROOT)/mk/hdr.mk +include $(ROOT)/mk/wmii.mk + +DOCS = README +EXECS = wmiirc +TEXT = wmiirc.py +DIRS = pygmi \ + pyxp + +DIR = $(ETC)/wmii$(CONFVERSION)/python +DOCDIR = $(DOC)/alternative_wmiircs/python + +include $(ROOT)/mk/dir.mk diff --git a/alternative_wmiircs/python/README b/alternative_wmiircs/python/README new file mode 100644 index 0000000..97bece9 --- /dev/null +++ b/alternative_wmiircs/python/README @@ -0,0 +1,16 @@ +Python wmiirc +============= + +This directory contains a pure Python implementation of +wmiirc. The two included libraries, pyxp and pygmi, are a 9P +client and wmii filesystem utility module, respectively. To +use this library, simply copy the contents of this direcctory +to ~/.wmii/. To customize it, either modify wmiirc.py +directly, or create wmii_local.py and store your modifications +there. The latter approach is preferable in that future +modifications to wmiirc.py can usually be painlessly +integrated. + +The documentation is sparse, but wmiirc.py should serve as a +fairly comprehensive example. + diff --git a/alternative_wmiircs/python/pygmi/Makefile b/alternative_wmiircs/python/pygmi/Makefile new file mode 100644 index 0000000..24bfa69 --- /dev/null +++ b/alternative_wmiircs/python/pygmi/Makefile @@ -0,0 +1,12 @@ +ROOT=../../.. +include $(ROOT)/mk/hdr.mk +include $(ROOT)/mk/wmii.mk + +BINARY = __init__.py \ + event.py \ + fs.py \ + menu.py \ + monitor.py \ + util.py + +DIR = $(ETC)/wmii$(CONFVERSION)/python/pygmi diff --git a/alternative_wmiircs/python/pygmi/__init__.py b/alternative_wmiircs/python/pygmi/__init__.py new file mode 100644 index 0000000..6ec1ddc --- /dev/null +++ b/alternative_wmiircs/python/pygmi/__init__.py @@ -0,0 +1,27 @@ +import os +import sys + +from pyxp.asyncclient import Client + +if 'WMII_ADDRESS' in os.environ: + client = Client(os.environ['WMII_ADDRESS']) +else: + client = Client(namespace='wmii') + +confpath = os.environ.get('WMII_CONFPATH', '%s/.wmii' % os.environ['HOME']).split(':') +shell = os.environ['SHELL'] + +sys.path += confpath + +from pygmi.util import * +from pygmi.event import * +from pygmi.fs import * +from pygmi.menu import * +from pygmi.monitor import * +from pygmi import util, event, fs, menu, monitor + +__all__ = (fs.__all__ + monitor.__all__ + event.__all__ + + menu.__all__ + util.__all__ + + ('client', 'confpath', 'shell')) + +# vim:se sts=4 sw=4 et: diff --git a/alternative_wmiircs/python/pygmi/event.py b/alternative_wmiircs/python/pygmi/event.py new file mode 100644 index 0000000..c56460a --- /dev/null +++ b/alternative_wmiircs/python/pygmi/event.py @@ -0,0 +1,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: diff --git a/alternative_wmiircs/python/pygmi/fs.py b/alternative_wmiircs/python/pygmi/fs.py new file mode 100644 index 0000000..1d05d8e --- /dev/null +++ b/alternative_wmiircs/python/pygmi/fs.py @@ -0,0 +1,699 @@ +import collections +from datetime import datetime, timedelta + +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') + +def constrain(min, max, val): + if val < min: + return min + if val > max: + return max + return val + +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. + """ + sentinel = {} + ctl_types = {} + ctl_hasid = False + + 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. + """ + client.awrite(self.ctl_path, ' '.join(args)) + + 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: + 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: + val = 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 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 + + 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 = 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[self.key] + 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, + } + 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._id or 'sel') + + @classmethod + def all(cls): + """ + Returns all of the objects that exist for this type of directory. + """ + return (cls(s.name) + for s in client.readdir(cls.base_path) + if s.name != 'sel') + + 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' + + fullscreen = Dir.toggle_property('Fullscreen') + urgent = Dir.toggle_property('Urgent') + + label = Dir.file_property('label', writable=True) + tags = Dir.file_property('tags', 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, None) is not None: + 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=None, width=None, height=None, frames=None): + 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): + return '%s:%s' % (self.screen, self.ord) + + def _get_mode(self): + for k, v in self.tag.iteritems(): + if k == 'colmode': + v = v.split(' ') + if v[0] == self.ord: + return v[1] + mode = property( + _get_mode, + lambda self, val: self.tag.set('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=None, ord=None, offset=None, height=None): + 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 + + def _set_selected(self, frame): + if not isinstance(frame, basestring) or ' ' not in frame: + frame = self.framespec(frame) + self['select'] = frame + selected = property(lambda self: tuple(self['select'].split(' ')), + _set_selected) + + def _get_selclient(self): + for k, v in self.iteritems(): + if k == 'select' and 'client' in v: + return Client(v.split(' ')[1]) + return None + selclient = property(_get_selclient, + lambda self, val: self.set('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, + 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 Button(object): + sides = { + 'left': 'lbar', + 'right': 'rbar', + } + def __init__(self, side, name, colors=None, label=None): + self.side = side + self.name = name + self.base_path = self.sides[side] + self.path = '%s/%s' % (self.base_path, self.name) + self.file = None + if colors or label: + self.create(colors, label) + + def create(self, colors=None, label=None): + def fail(resp, exc, tb): + self.file = None + if not self.file: + self.file = client.create(self.path, ORDWR) + if colors: + self.file.awrite(self.getval(colors, label), offset=0, fail=fail) + elif label: + self.file.awrite(label, offset=24, fail=fail) + + def remove(self): + if self.file: + self.file.aremove() + self.file = None + + def getval(self, colors=None, label=None): + if label is None: + label = self.label + if colors is None and re.match( + r'#[0-9a-f]{6} #[0-9a-f]{6} #[0-9a-f]{6}', label, re.I): + colors = self.colors + if not colors: + return str(label) + return ' '.join([Color(c).hex for c in colors] + [str(label)]) + + colors = property( + lambda self: self.file and + tuple(map(Color, self.file.read(offset=0).split(' ')[:3])) + or (), + lambda self, val: self.create(colors=val)) + + label = property( + lambda self: self.file and self.file.read(offset=0).split(' ', 3)[3] or '', + lambda self, val: self.create(label=val)) + + @classmethod + def all(cls, side): + return (Button(side, s.name) + for s in client.readdir(cls.sides[side]) + if s.name != 'sel') + +class Colors(object): + 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 __str__(self): + return str(unicode(self)) + 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 Color(object): + def __init__(self, colors): + if isinstance(colors, Color): + colors = colors.rgb + elif isinstance(colors, basestring): + match = re.match(r'^#(..)(..)(..)$', colors) + colors = tuple(int(match.group(group), 16) for group in range(1, 4)) + 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): + return '#%02x%02x%02x' % self.rgb + + def __str__(self): + return str(unicode(self)) + def __unicode__(self): + return 'rgb(%d, %d, %d)' % self.rgb + def __repr__(self): + return 'Color(%s)' % repr(self.rgb) + +class Rules(collections.MutableMapping): + regex = re.compile(r'^\s*/(.*?)/\s*(?:->)?\s*(.*)$') + + def __get__(self, obj, cls): + return self + def __set__(self, obj, val): + self.setitems(val) + + def __init__(self, path, rules=None): + self.path = path + if rules: + self.setitems(rules) + + def __getitem__(self, key): + for k, v in self.iteritems(): + if k == key: + return v + raise KeyError() + def __setitem__(self, key, val): + items = [] + for k, v in self.iteritems(): + if key == k: + v = val + key = None + items.append((k, v)) + if key is not None: + 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): + for k, v in self.iteritems(): + yield k + 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): + lines = [] + for k, v in items: + assert '/' not in k and '\n' not in v + lines.append('/%s/ -> %s' % (k, v)) + lines.append('') + client.awrite(self.path, '\n'.join(lines)) + + def iteritems(self): + for line in client.readlines(self.path): + match = self.regex.match(line) + if match: + yield match.groups() + def items(self): + return list(self.iteritems()) + +@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 = property(lambda self: Client.all()) + tags = property(lambda self: Tag.all()) + lbuttons = property(lambda self: Button.all('left')) + rbuttons = property(lambda self: Button.all('right')) + + tagrules = Rules('/tagrules') + colrules = Rules('/colrules') + +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.id) + for b in wmii.lbuttons: + 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.id not in self.ignore] + tags.append(tags[0]) + if reverse: + tags.reverse() + for i in range(0, len(tags)): + if tags[i] == self.sel: + return tags[i+1] + return self.sel + + def select(self, tag, take_client=None): + def goto(tag): + if take_client: + sel = Tag('sel').id + take_client.tags = '+%s' % tag + wmii['view'] = tag + if tag != sel: + take_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: diff --git a/alternative_wmiircs/python/pygmi/menu.py b/alternative_wmiircs/python/pygmi/menu.py new file mode 100644 index 0000000..4711576 --- /dev/null +++ b/alternative_wmiircs/python/pygmi/menu.py @@ -0,0 +1,54 @@ +from threading import Thread +from pygmi.util import call + +__all__ = 'Menu', 'ClickMenu' + +def inthread(args, action, **kwargs): + fn = lambda: call(*args, **kwargs) + if not action: + return fn() + t = Thread(target=lambda: action(fn())) + t.daemon = True + t.start() + +class Menu(object): + def __init__(self, choices=(), action=None, + histfile=None, nhist=None): + self.choices = choices + self.action = action + self.histfile = histfile + self.nhist = nhist + + def __call__(self, choices=None): + if choices is None: + choices = self.choices + if callable(choices): + choices = choices() + args = ['wimenu'] + if self.histfile: + args += ['-h', self.histfile] + if self.nhist: + args += ['-n', self.nhist] + return inthread(map(str, args), self.action, input='\n'.join(choices)) + call = __call__ + +class ClickMenu(object): + def __init__(self, choices=(), action=None, + histfile=None, nhist=None): + self.choices = choices + self.action = action + self.prev = None + + def __call__(self, choices=None): + if choices is None: + choices = self.choices + if callable(choices): + choices = choices() + args = ['wmii9menu'] + if self.prev: + args += ['-i', self.prev] + args += ['--'] + list(choices) + return inthread(map(str, args), self.action) + call = __call__ + +# vim:se sts=4 sw=4 et: diff --git a/alternative_wmiircs/python/pygmi/monitor.py b/alternative_wmiircs/python/pygmi/monitor.py new file mode 100644 index 0000000..528e9cd --- /dev/null +++ b/alternative_wmiircs/python/pygmi/monitor.py @@ -0,0 +1,118 @@ +from threading import Timer + +from pygmi import client +from pygmi.fs import * + +__all__ = 'monitors', 'defmonitor', 'Monitor' + +monitors = {} + +def defmonitor(*args, **kwargs): + """ + Defines a new monitor to appear in wmii's bar based on + the wrapped function. Creates a new Monitor object, + initialized with *args and **kwargs. The wrapped function + is assigned to the 'action' keyword argument for the + Monitor, its name is assigned to the 'name' argument. + + The new monitor is added to the 'monitors' dict in this + module. + """ + def monitor(fn): + kwargs['action'] = fn + if not args and 'name' not in kwargs: + kwargs['name'] = fn.__name__ + monitor = Monitor(*args, **kwargs) + monitors[monitor.name] = monitor + return monitor + if args and callable(args[0]): + fn = args[0] + args = args[1:] + return monitor(fn) + return monitor + +class Monitor(object): + """ + A class to manage status monitors for wmii's bar. The bar item + is updated on a fixed interval based on the values returned + by the 'action' method. + + Property active: When true, the monitor is updated at regular + intervals. When false, monitor is hidden. + Property name: The name of the monitor, which acts as the name + of the bar in wmii's filesystem. + Property interval: The update interval, in seconds. + Property side: The side of the bar on which to place the monitor. + Property action: A function of no arguments which returns the + value of the monitor. Called at each update interval. + May return a string, a tuple of (Color, string), or None + to hide the monitor for one iteration. + """ + side = 'right' + interval = 1.0 + + def __init__(self, name=None, interval=None, side=None, + action=None, colors=None, label=None): + """ + Initializes the new monitor. For parameter values, see the + corresponding property values in the class's docstring. + + Param colors: The initial colors for the monitor. + Param label: The initial label for the monitor. + """ + if side: + self.side = side + if name: + self.name = name + if interval: + self.interval = interval + if action: + self.action = action + + self.timer = None + self.button = Button(self.side, self.name, colors, label) + self.tick() + + def tick(self): + """ + Called internally at the interval defined by #interval. + Calls #action and updates the monitor based on the result. + """ + mon = monitors.get(self.name, None) + if self.timer and mon is not self: + return + if self.active: + label = self.getlabel() + if isinstance(label, basestring): + label = None, label + if label is None: + self.button.remove() + else: + self.button.create(*label) + + self.timer = Timer(self.interval, self.tick) + self.timer.daemon = True + self.timer.start() + + def getlabel(self): + """ + Calls #action and returns the result, ignoring any + exceptions. + """ + try: + return self.action(self) + except Exception: + return None + + _active = True + def _set_active(self, val): + self._active = bool(val) + self.tick() + if not val: + self.button.remove() + + active = property( + lambda self: self._active, + _set_active) + +# vim:se sts=4 sw=4 et: diff --git a/alternative_wmiircs/python/pygmi/util.py b/alternative_wmiircs/python/pygmi/util.py new file mode 100644 index 0000000..8821478 --- /dev/null +++ b/alternative_wmiircs/python/pygmi/util.py @@ -0,0 +1,66 @@ +import os +import subprocess + +import pygmi + +__all__ = 'call', 'message', 'program_list', 'curry', 'find_script', '_', 'prop' + +def _(): + pass + +def call(*args, **kwargs): + background = kwargs.pop('background', False) + input = kwargs.pop('input', None) + p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, cwd=os.environ['HOME'], + close_fds=True, **kwargs) + if not background: + return p.communicate(input)[0].rstrip('\n') + +def message(message): + args = ['xmessage', '-file', '-']; + font = pygmi.wmii['font'] + if not font.startswith('xft:'): + args += ['-fn', font.split(',')[0]] + call(*args, input=message) + +def program_list(path): + names = [] + for d in path: + try: + for f in os.listdir(d): + p = '%s/%s' % (d, f) + if f not in names and os.access(p, os.X_OK) and ( + os.path.isfile(p) or os.path.islink(p)): + names.append(f) + except Exception: + pass + return sorted(names) + +def curry(func, *args, **kwargs): + if _ in args: + blank = [i for i in range(0, len(args)) if args[i] is _] + def curried(*newargs, **newkwargs): + ary = list(args) + for k, v in zip(blank, newargs): + ary[k] = v + ary = tuple(ary) + newargs[len(blank):] + return func(*ary, **dict(kwargs, **newkwargs)) + else: + def curried(*newargs, **newkwargs): + return func(*(args + newargs), **dict(kwargs, **newkwargs)) + curried.__name__ = func.__name__ + '__curried__' + return curried + +def find_script(name): + for path in pygmi.confpath: + if os.access('%s/%s' % (path, name), os.X_OK): + return '%s/%s' % (path, name) + +def prop(**kwargs): + def prop_(wrapped): + kwargs['fget'] = wrapped + return property(**kwargs) + return prop_ + +# vim:se sts=4 sw=4 et: diff --git a/alternative_wmiircs/python/pyxp/Makefile b/alternative_wmiircs/python/pyxp/Makefile new file mode 100644 index 0000000..ad2edeb --- /dev/null +++ b/alternative_wmiircs/python/pyxp/Makefile @@ -0,0 +1,15 @@ +ROOT=../../.. +include $(ROOT)/mk/hdr.mk +include $(ROOT)/mk/wmii.mk + +BINARY = __init__.py \ + asyncclient.py \ + client.py \ + dial.py \ + fcall.py \ + fields.py \ + messages.py \ + mux.py \ + types.py + +DIR = $(ETC)/wmii$(CONFVERSION)/python/pyxp diff --git a/alternative_wmiircs/python/pyxp/__init__.py b/alternative_wmiircs/python/pyxp/__init__.py new file mode 100644 index 0000000..2ba7400 --- /dev/null +++ b/alternative_wmiircs/python/pyxp/__init__.py @@ -0,0 +1,7 @@ +from pyxp.client import Client +from pyxp.dial import dial +from pyxp.types import Qid, Stat + +VERSION = '9P2000' + +# vim:se sts=4 sw=4 et: diff --git a/alternative_wmiircs/python/pyxp/asyncclient.py b/alternative_wmiircs/python/pyxp/asyncclient.py new file mode 100644 index 0000000..b7ebc08 --- /dev/null +++ b/alternative_wmiircs/python/pyxp/asyncclient.py @@ -0,0 +1,193 @@ +from pyxp import client, fcall +from pyxp.client import * + +def awithfile(*oargs, **okwargs): + def wrapper(fn): + def next(self, path, *args, **kwargs): + def next(file, exc, tb): + fn(self, (file, exc, tb), *args, **kwargs) + self.aopen(path, next, *oargs, **okwargs) + return next + return wrapper +def wrap_callback(fn, file): + def callback(data, exc, tb): + file.close() + Client.respond(fn, data, exc, tb) + return callback + +class Client(client.Client): + ROOT_FID = 0 + + def _awalk(self, path, callback, fail=None): + ctxt = dict(path=path, fid=self._getfid(), ofid=ROOT_FID) + def next(resp=None, exc=None, tb=None): + if exc and ctxt['ofid'] != ROOT_FID: + self._aclunk(ctxt['fid']) + if not ctxt['path'] and resp or exc: + if exc and fail: + return self.respond(fail, None, exc, tb) + return self.respond(callback, ctxt['fid'], exc, tb) + wname = ctxt['path'][:fcall.MAX_WELEM] + ofid = ctxt['ofid'] + ctxt['path'] = ctxt['path'][fcall.MAX_WELEM:] + if resp: + ctxt['ofid'] = ctxt['fid'] + self._dorpc(fcall.Twalk(fid=ofid, + newfid=ctxt['fid'], + wname=wname), + next) + next() + + _file = property(lambda self: File) + def _aopen(self, path, mode, open, callback, fail=None, origpath=None): + resp = None + def next(fid, exc, tb): + def next(resp, exc, tb): + def cleanup(): + self._clunk(fid) + file = self._file(self, origpath or '/'.join(path), resp, fid, mode, cleanup) + self.respond(callback, file) + self._dorpc(open(fid), next, fail or callback) + self._awalk(path, next, fail or callback) + + def aopen(self, path, callback=True, fail=None, mode=OREAD): + assert callable(callback) + path = self._splitpath(path) + def open(fid): + return fcall.Topen(fid=fid, mode=mode) + return self._aopen(path, mode, open, fail or callback) + + def acreate(self, path, callback=True, fail=None, mode=OREAD, perm=0): + path = self._splitpath(path) + name = path.pop() + def open(fid): + return fcall.Tcreate(fid=fid, mode=mode, name=name, perm=perm) + if not callable(callback): + def callback(resp, exc, tb): + if resp: + resp.close() + return self._aopen(path, mode, open, callback, fail, + origpath='/'.join(path + [name])) + + def aremove(self, path, callback=True, fail=None): + path = self._splitpath(path) + def next(fid, exc, tb): + self._dorpc(fcall.Tremove(fid=fid), callback, fail) + self._awalk(path, next, callback, fail) + + def astat(self, path, callback, fail = None): + path = self._splitpath(path) + def next(fid, exc, tb): + def next(resp, exc, tb): + callback(resp.stat, exc, tb) + self._dorpc(fcall.Tstat(fid=fid), next, callback) + + @awithfile() + def aread(self, (file, exc, tb), callback, *args, **kwargs): + if exc: + callback(file, exc, tb) + else: + file.aread(wrap_callback(callback, file), *args, **kwargs) + @awithfile(mode=OWRITE) + def awrite(self, (file, exc, tb), data, callback=True, *args, **kwargs): + if exc: + self.respond(callback, file, exc, tb) + else: + file.awrite(data, wrap_callback(callback, file), *args, **kwargs) + @awithfile() + def areadlines(self, (file, exc, tb), fn): + def callback(resp): + if resp is None: + file.close() + if fn(resp) is False: + file.close() + return False + if exc: + callback(None) + else: + file.sreadlines(callback) + +class File(client.File): + @staticmethod + def respond(callback, data, exc=None, tb=None): + if callable(callback): + callback(data, exc, tb) + + def stat(self, callback): + def next(resp, exc, tb): + callback(resp.stat, exc, tb) + resp = self._dorpc(fcall.Tstat(), next, callback) + + def aread(self, callback, fail=None, count=None, offset=None, buf=''): + ctxt = dict(res=[], count=self.iounit, offset=self.offset) + if count is not None: + ctxt['count'] = count + if offset is not None: + ctxt['offset'] = offset + def next(resp=None, exc=None, tb=None): + if resp and resp.data: + ctxt['res'].append(resp.data) + ctxt['offset'] += len(resp.data) + if ctxt['count'] == 0: + if offset is None: + self.offset = ctxt['offset'] + return callback(''.join(ctxt['res']), exc, tb) + + n = min(ctxt['count'], self.iounit) + ctxt['count'] -= n + + self._dorpc(fcall.Tread(offset=ctxt['offset'], count=n), + next, fail or callback) + next() + + def areadlines(self, callback): + ctxt = dict(last=None) + def next(data, exc, tb): + res = True + if data: + lines = data.split('\n') + if ctxt['last']: + lines[0] = ctxt['last'] + lines[0] + for i in range(0, len(lines) - 1): + res = callback(lines[i]) + if res is False: + break + ctxt['last'] = lines[-1] + if res is not False: + self.aread(next) + else: + if ctxt['last']: + callback(ctxt['last']) + callback(None) + self.aread(next) + + def awrite(self, data, callback=True, fail=None, offset=None): + ctxt = dict(offset=self.offset, off=0) + if offset is not None: + ctxt['offset'] = offset + def next(resp=None, exc=None, tb=None): + if resp: + ctxt['off'] += resp.count + ctxt['offset'] += resp.count + if ctxt['off'] < len(data) or not (exc or resp): + n = min(len(data), self.iounit) + + self._dorpc(fcall.Twrite(offset=ctxt['offset'], + data=data[ctxt['off']:ctxt['off']+n]), + next, fail or callback) + else: + if offset is None: + self.offset = ctxt['offset'] + self.respond(callback, ctxt['off'], exc, tb) + next() + + def aremove(self, callback=True, fail=None): + def next(resp, exc, tb): + self.close() + if exc and fail: + self.respond(fail, resp and True, exc, tb) + else: + self.respond(callback, resp and True, exc, tb) + self._dorpc(fcall.Tremove(), next) + +# vim:se sts=4 sw=4 et: diff --git a/alternative_wmiircs/python/pyxp/client.py b/alternative_wmiircs/python/pyxp/client.py new file mode 100644 index 0000000..85b8e3e --- /dev/null +++ b/alternative_wmiircs/python/pyxp/client.py @@ -0,0 +1,346 @@ +# Copyright (C) 2009 Kris Maglione + +import operator +import os +import re +import sys +from threading import * +import traceback + +import pyxp +from pyxp import fcall, fields +from pyxp.mux import Mux +from pyxp.types import * + +if os.environ.get('NAMESPACE', None): + namespace = os.environ['NAMESPACE'] +else: + try: + namespace = '/tmp/ns.%s.%s' % ( + os.environ['USER'], + re.sub(r'\.0$', '', os.environ['DISPLAY'])) + except Exception: + pass +NAMESPACE = namespace + +OREAD = 0x00 +OWRITE = 0x01 +ORDWR = 0x02 +OEXEC = 0x03 +OEXCL = 0x04 +OTRUNC = 0x10 +OREXEC = 0x20 +ORCLOSE = 0x40 +OAPPEND = 0x80 + +ROOT_FID = 0 + +class ProtocolException(Exception): + pass +class RPCError(Exception): + pass + +class Client(object): + ROOT_FID = 0 + + @staticmethod + def respond(callback, data, exc=None, tb=None): + if callable(callback): + callback(data, exc, tb) + + + def __enter__(self): + return self + def __exit__(self, *args): + self._cleanup() + + def __init__(self, conn=None, namespace=None, root=None): + if not conn and namespace: + conn = 'unix!%s/%s' % (NAMESPACE, namespace) + try: + self.lastfid = ROOT_FID + self.fids = set() + self.lock = RLock() + + def process(data): + return fcall.Fcall.unmarshall(data)[1] + self.mux = Mux(conn, process, maxtag=256) + + resp = self._dorpc(fcall.Tversion(version=pyxp.VERSION, msize=65535)) + if resp.version != pyxp.VERSION: + raise ProtocolException, "Can't speak 9P version '%s'" % resp.version + self.msize = resp.msize + + self._dorpc(fcall.Tattach(fid=ROOT_FID, afid=fcall.NO_FID, + uname=os.environ['USER'], aname='')) + + if root: + path = self._splitpath(root) + resp = self._dorpc(fcall.Twalk(fid=ROOT_FID, + newfid=ROOT_FID, + wname=path)) + except Exception, e: + traceback.print_exc(sys.stdout) + if getattr(self, 'mux', None): + self.mux.fd.close() + raise e + + def _cleanup(self): + try: + for f in self.files: + f.close() + finally: + self.mux.fd.close() + self.mux = None + + def _dorpc(self, req, callback=None, error=None): + def doresp(resp): + if isinstance(resp, fcall.Rerror): + raise RPCError, "%s[%d] RPC returned error: %s" % ( + req.__class__.__name__, resp.tag, resp.ename) + if req.type != resp.type ^ 1: + raise ProtocolException, "Missmatched RPC message types: %s => %s" % ( + req.__class__.__name__, resp.__class__.__name__) + return resp + def next(mux, resp): + try: + res = doresp(resp) + except Exception, e: + if error: + self.respond(error, None, e, None) + else: + self.respond(callback, None, e, None) + else: + self.respond(callback, res) + if not callback: + return doresp(self.mux.rpc(req)) + self.mux.rpc(req, next) + + def _splitpath(self, path): + return [v for v in path.split('/') if v != ''] + + def _getfid(self): + with self.lock: + if self.fids: + return self.fids.pop() + self.lastfid += 1 + return self.lastfid + def _putfid(self, fid): + with self.lock: + self.fids.add(fid) + + def _aclunk(self, fid, callback=None): + def next(resp, exc, tb): + if resp: + self._putfid(fid) + self.respond(callback, resp, exc, tb) + self._dorpc(fcall.Tclunk(fid=fid), next) + + def _clunk(self, fid): + try: + self._dorpc(fcall.Tclunk(fid=fid)) + finally: + self._putfid(fid) + + def _walk(self, path): + fid = self._getfid() + ofid = ROOT_FID + while True: + self._dorpc(fcall.Twalk(fid=ofid, newfid=fid, + wname=path[0:fcall.MAX_WELEM])) + path = path[fcall.MAX_WELEM:] + ofid = fid + if len(path) == 0: + break + + @apply + class Res: + def __enter__(res): + return fid + def __exit__(res, exc_type, exc_value, traceback): + if exc_type: + self._clunk(fid) + return Res + + _file = property(lambda self: File) + def _open(self, path, mode, open, origpath=None): + resp = None + + with self._walk(path) as nfid: + fid = nfid + resp = self._dorpc(open(fid)) + + def cleanup(): + self._aclunk(fid) + file = self._file(self, origpath or '/'.join(path), resp, fid, mode, cleanup) + return file + + def open(self, path, mode=OREAD): + path = self._splitpath(path) + + def open(fid): + return fcall.Topen(fid=fid, mode=mode) + return self._open(path, mode, open) + + def create(self, path, mode=OREAD, perm=0): + path = self._splitpath(path) + name = path.pop() + + def open(fid): + return fcall.Tcreate(fid=fid, mode=mode, name=name, perm=perm) + return self._open(path, mode, open, origpath='/'.join(path + [name])) + + def remove(self, path): + path = self._splitpath(path) + + with self._walk(path) as fid: + self._dorpc(fcall.Tremove(fid=fid)) + + def stat(self, path): + path = self._splitpath(path) + + try: + with self._walk(path) as fid: + resp = self._dorpc(fcall.Tstat(fid= fid)) + st = resp.stat() + self._clunk(fid) + return st + except RPCError: + return None + + def read(self, path, *args, **kwargs): + with self.open(path) as f: + return f.read(*args, **kwargs) + def readlines(self, path, *args, **kwargs): + with self.open(path) as f: + for l in f.readlines(*args, **kwargs): + yield l + def readdir(self, path, *args, **kwargs): + with self.open(path) as f: + for s in f.readdir(*args, **kwargs): + yield s + def write(self, path, *args, **kwargs): + with self.open(path, OWRITE) as f: + return f.write(*args, **kwargs) + +class File(object): + + def __enter__(self): + return self + def __exit__(self, *args): + self.close() + + def __init__(self, client, path, fcall, fid, mode, cleanup): + self.lock = RLock() + self.client = client + self.path = path + self.fid = fid + self._cleanup = cleanup + self.mode = mode + self.iounit = fcall.iounit + self.qid = fcall.qid + self.closed = False + + self.offset = 0 + def __del__(self): + if not self.closed: + self._cleanup() + + def _dorpc(self, fcall, async=None, error=None): + if hasattr(fcall, 'fid'): + fcall.fid = self.fid + return self.client._dorpc(fcall, async, error) + + def stat(self): + resp = self._dorpc(fcall.Tstat()) + return resp.stat + + def read(self, count=None, offset=None, buf=''): + if count is None: + count = self.iounit + res = [] + with self.lock: + offs = self.offset + if offset is not None: + offs = offset + while count > 0: + n = min(count, self.iounit) + count -= n + + resp = self._dorpc(fcall.Tread(offset=offs, count=n)) + data = resp.data + + offs += len(data) + res.append(data) + + if len(data) < n: + break + if offset is None: + self.offset = offs + return ''.join(res) + def readlines(self): + last = None + while True: + data = self.read() + if not data: + break + lines = data.split('\n') + if last: + lines[0] = last + lines[0] + last = None + for i in range(0, len(lines) - 1): + yield lines[i] + last = lines[-1] + if last: + yield last + def write(self, data, offset=None): + if offset is None: + offset = self.offset + off = 0 + with self.lock: + offs = self.offset + if offset is not None: + offs = offset + while off < len(data): + n = min(len(data), self.iounit) + + resp = self._dorpc(fcall.Twrite(offset=offs, + data=data[off:off+n])) + off += resp.count + offs += resp.count + if resp.count < n: + break + if offset is None: + self.offset = offs + return off + def readdir(self): + if not self.qid.type & Qid.QTDIR: + raise Exception, "Can only call readdir on a directory" + off = 0 + while True: + data = self.read(self.iounit, off) + if not data: + break + off += len(data) + for s in Stat.unmarshall_list(data): + yield s + + def close(self): + assert not self.closed + self.closed = True + self._cleanup() + self.tg = None + self.fid = None + self.client = None + self.qid = None + + def remove(self): + try: + self._dorpc(fcall.Tremove()) + finally: + try: + self.close() + except Exception: + pass + +# vim:se sts=4 sw=4 et: diff --git a/alternative_wmiircs/python/pyxp/dial.py b/alternative_wmiircs/python/pyxp/dial.py new file mode 100644 index 0000000..55dcf9d --- /dev/null +++ b/alternative_wmiircs/python/pyxp/dial.py @@ -0,0 +1,35 @@ +from socket import * + +__all__ = 'dial', + +def dial_unix(address): + sock = socket(AF_UNIX, SOCK_STREAM, 0) + sock.connect(address) + return sock + +def dial_tcp(host): + host = host.split('!') + if len(host) != 2: + return + host, port = host + + res = getaddrinfo(host, port, AF_INET, SOCK_STREAM, 0, AI_PASSIVE) + for family, socktype, protocol, name, addr in res: + try: + sock = socket(family, socktype, protocol) + sock.connect(addr) + return sock + except error: + if sock: + sock.close() + +def dial(address): + proto, address = address.split('!', 1) + if proto == 'unix': + return dial_unix(address) + elif proto == 'tcp': + return dial_tcp(address) + else: + raise Exception('invalid protocol') + +# vim:se sts=4 sw=4 et: diff --git a/alternative_wmiircs/python/pyxp/fcall.py b/alternative_wmiircs/python/pyxp/fcall.py new file mode 100644 index 0000000..8e3c264 --- /dev/null +++ b/alternative_wmiircs/python/pyxp/fcall.py @@ -0,0 +1,131 @@ +from pyxp.messages import MessageBase, Message +from pyxp.fields import * +from types import Qid, Stat + +__all__ = 'Fcall', + +NO_FID = 1<<32 - 1 +MAX_WELEM = 16 + +class FcallBase(MessageBase): + idx = 99 + def __new__(cls, name, bases, attrs): + new_cls = super(FcallBase, cls).__new__(cls, name, bases, attrs) + new_cls.type = FcallBase.idx + if new_cls.type > 99: + new_cls.types[new_cls.type] = new_cls + FcallBase.idx += 1 + return new_cls + +class Fcall(Message): + __metaclass__ = FcallBase + types = {} + + def response(self, *args, **kwargs): + assert self.type % 2 == 0, "No respense type for response fcalls" + kwargs['tag'] = self.tag + return self.types[self.type + 1]() + + @classmethod + def unmarshall(cls, data, offset=0): + res = super(Fcall, cls).unmarshall(data, offset) + if cls.type < 100: + res = cls.types[res[1].type].unmarshall(data, offset) + return res + + size = Size(4, 4) + type = Int(1) + tag = Int(2) + +class Tversion(Fcall): + msize = Int(4) + version = String() +class Rversion(Fcall): + msize = Int(4) + version = String() + +class Tauth(Fcall): + afid = Int(4) + uname = String() + aname = String() +class Rauth(Fcall): + aqid = Qid.field() + +class Tattach(Fcall): + fid = Int(4) + afid = Int(4) + uname = String() + aname = String() +class Rattach(Fcall): + qid = Qid.field() + +class Terror(Fcall): + def __init__(self): + raise Exception("Illegal 9P tag 'Terror' encountered") +class Rerror(Fcall): + ename = String() + +class Tflush(Fcall): + oldtag = Int(2) +class Rflush(Fcall): + pass + +class Twalk(Fcall): + fid = Int(4) + newfid = Int(4) + wname = Array(2, String()) +class Rwalk(Fcall): + wqid = Array(2, Qid.field()) + +class Topen(Fcall): + fid = Int(4) + mode = Int(1) +class Ropen(Fcall): + qid = Qid.field() + iounit = Int(4) + +class Tcreate(Fcall): + fid = Int(4) + name = String() + perm = Int(4) + mode = Int(1) +class Rcreate(Fcall): + qid = Qid.field() + iounit = Int(4) + +class Tread(Fcall): + fid = Int(4) + offset = Int(8) + count = Int(4) +class Rread(Fcall): + data = Data(4) + +class Twrite(Fcall): + fid = Int(4) + offset = Int(8) + data = Data(4) +class Rwrite(Fcall): + count = Int(4) + +class Tclunk(Fcall): + fid = Int(4) +class Rclunk(Fcall): + pass + +class Tremove(Tclunk): + pass +class Rremove(Fcall): + pass + +class Tstat(Tclunk): + pass +class Rstat(Fcall): + sstat = Size(2) + stat = Stat.field() + +class Twstat(Rstat): + pass +class Rwstat(Fcall): + pass + +# vim:se sts=4 sw=4 et: diff --git a/alternative_wmiircs/python/pyxp/fields.py b/alternative_wmiircs/python/pyxp/fields.py new file mode 100644 index 0000000..ba61909 --- /dev/null +++ b/alternative_wmiircs/python/pyxp/fields.py @@ -0,0 +1,132 @@ +from datetime import datetime +import operator + +class Field(object): + idx = 0 + + def __init__(self): + Field.idx += 1 + self.id = Field.idx + + def repr(self): + return self.__class__.__name__ + + def __repr__(self): + if hasattr(self, 'name'): + return '<Field %s "%s">' % (self.repr(), self.name) + return super(Field, self).__repr__() + +class Int(Field): + encoders = {} + decoders = {} + @classmethod + def encoder(cls, n): + if n not in cls.encoders: + exec ('def enc(n):\n' + + ' assert n == n & 0x%s, "Arithmetic overflow"\n' % ('ff' * n) + + ' return "".join((' + ','.join( + 'chr((n >> %d) & 0xff)' % (i * 8) + for i in range(0, n)) + ',))\n') + cls.encoders[n] = enc + return cls.encoders[n] + @classmethod + def decoder(cls, n): + if n not in cls.decoders: + cls.decoders[n] = eval('lambda data, offset: ' + '|'.join( + 'ord(data[offset + %d]) << %d' % (i, i * 8) + for i in range(0, n))) + return cls.decoders[n] + + def __init__(self, size): + super(Int, self).__init__() + self.size = size + self.encode = self.encoder(size) + self.decode = self.decoder(size) + if self.__class__ == Int: + self.marshall = self.encode + + def unmarshall(self, data, offset): + return self.size, self.decode(data, offset) + def marshall(self, val): + return self.encode(val) + + def repr(self): + return '%s(%d)' % (self.__class__.__name__, self.size) + +class Size(Int): + def __init__(self, size, extra=0): + super(Size, self).__init__(size) + self.extra = extra + + def marshall(self, val): + return lambda vals, i: self.encode( + reduce(lambda n, i: n + len(vals[i]), + range(i + 1, len(vals)), + self.extra)) + +class Date(Int): + def __init__(self): + super(Date, self).__init__(4) + + def unmarshall(self, data, offset): + val = self.decode(data, offset) + return 4, datetime.fromtimestamp(val) + def marshall(self, val): + return self.encode(int(val.strftime('%s'))) + +class Data(Int): + def __init__(self, size=2): + super(Data, self).__init__(size) + def unmarshall(self, data, offset): + n = self.decode(data, offset) + offset += self.size + assert offset + n <= len(data), "String too long to unpack" + return self.size + n, data[offset:offset + n] + def marshall(self, val): + if isinstance(val, unicode): + val = val.encode('UTF-8') + return [self.encode(len(val)), val] + +# Note: Py3K strings are Unicode by default. They can't store binary +# data. +class String(Data): + def unmarshall(self, data, offset): + off, val = super(String, self).unmarshall(data, offset) + return off, val.decode('UTF-8') + def marshall(self, val): + if isinstance(val, str): + # Check for valid UTF-8 + str.decode('UTF-8') + else: + val = val.encode('UTF-8') + return super(String, self).marshall(val) + +class Array(Int): + def __init__(self, size, spec): + super(Array, self).__init__(size) + self.spec = spec + + def unmarshall(self, data, offset): + start = offset + n = self.decode(data, offset) + offset += self.size + res = [] + for i in range(0, n): + size, val = self.spec.unmarshall(data, offset) + if isinstance(val, list): + res += val + else: + res.append(val) + offset += size + return offset - start, res + def marshall(self, vals): + res = [self.encode(len(vals))] + for val in vals: + val = self.spec.marshall(val) + if isinstance(val, list): + res += val + else: + res.append(val) + return res + +# vim:se sts=4 sw=4 et: diff --git a/alternative_wmiircs/python/pyxp/messages.py b/alternative_wmiircs/python/pyxp/messages.py new file mode 100644 index 0000000..8498e50 --- /dev/null +++ b/alternative_wmiircs/python/pyxp/messages.py @@ -0,0 +1,73 @@ +from pyxp.fields import * + +class MessageBase(type): + idx = 0 + + def __new__(cls, name, bases, attrs): + fields = [] + fieldmap = {} + for k, v in attrs.items(): + if isinstance(v, Field): + attrs[k] = None + fields.append(v) + fieldmap[k] = v + v.name = k + fields.sort(lambda a, b: cmp(a.id, b.id)) + + new_cls = super(MessageBase, cls).__new__(cls, name, bases, attrs) + + map = getattr(new_cls, 'fieldmap', {}) + map.update(fieldmap) + new_cls.fields = getattr(new_cls, 'fields', ()) + tuple(fields) + new_cls.fieldmap = map + for f in fields: + f.message = new_cls + return new_cls + +class Message(object): + __metaclass__ = MessageBase + def __init__(self, *args, **kwargs): + if args: + args = dict(zip([f.name for f in self.fields], args)) + args.update(kwargs) + kwargs = args; + for k, v in kwargs.iteritems(): + assert k in self.fieldmap, "Invalid keyword argument" + setattr(self, k, v) + + @classmethod + def field(cls): + class MessageField(Field): + def repr(self): + return cls.__name__ + def unmarshall(self, data, offset): + return cls.unmarshall(data, offset) + def marshall(self, val): + return val.marshall() + return MessageField() + + @classmethod + def unmarshall(cls, data, offset=0): + vals = {} + start = offset + for field in cls.fields: + size, val = field.unmarshall(data, offset) + offset += size + vals[field.name] = val + return offset - start, cls(**vals) + def marshall(self): + res = [] + callbacks = [] + for field in self.fields: + val = field.marshall(getattr(self, field.name, None)) + if callable(val): + callbacks.append((val, len(res))) + if isinstance(val, list): + res += val + else: + res.append(val) + for fn, i in reversed(callbacks): + res[i] = fn(res, i) + return res + +# vim:se sts=4 sw=4 et: diff --git a/alternative_wmiircs/python/pyxp/mux.py b/alternative_wmiircs/python/pyxp/mux.py new file mode 100644 index 0000000..6e7babb --- /dev/null +++ b/alternative_wmiircs/python/pyxp/mux.py @@ -0,0 +1,195 @@ +# Derived from libmux, available in Plan 9 under /sys/src/libmux +# under the following terms: +# +# Copyright (C) 2003-2006 Russ Cox, Massachusetts Institute of Technology +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +import sys +import traceback + +from pyxp import fields +from pyxp.dial import dial +from threading import * +Condition = Condition().__class__ + +__all__ = 'Mux', + +class Mux(object): + def __init__(self, con, process, flush=None, mintag=0, maxtag=1<<16 - 1): + self.queue = set() + self.lock = RLock() + self.rendez = Condition(self.lock) + self.outlock = RLock() + self.inlock = RLock() + self.process = process + self.flush = flush + self.wait = {} + self.free = set(range(mintag, maxtag)) + self.mintag = mintag + self.maxtag = maxtag + self.muxer = None + + if isinstance(con, basestring): + con = dial(con) + self.fd = con + + if self.fd is None: + raise Exception("No connection") + + def mux(self, rpc): + try: + rpc.waiting = True + self.lock.acquire() + while self.muxer and self.muxer != rpc and rpc.data is None: + rpc.wait() + + if rpc.data is None: + assert not self.muxer or self.muxer is rpc + self.muxer = rpc + self.lock.release() + try: + while rpc.data is None: + data = self.recv() + if data is None: + self.lock.acquire() + self.queue.remove(rpc) + raise Exception("unexpected eof") + self.dispatch(data) + finally: + self.lock.acquire() + self.electmuxer() + except Exception, e: + traceback.print_exc(sys.stdout) + if self.flush: + self.flush(self, rpc.data) + raise e + finally: + if self.lock._is_owned(): + self.lock.release() + + if rpc.async: + if callable(rpc.async): + rpc.async(self, rpc.data) + else: + return rpc.data + + def rpc(self, dat, async=None): + rpc = self.newrpc(dat, async) + if async: + with self.lock: + if self.muxer is None: + self.electmuxer() + else: + return self.mux(rpc) + + def electmuxer(self): + async = None + for rpc in self.queue: + if self.muxer != rpc: + if rpc.async: + async = rpc + else: + self.muxer = rpc + rpc.notify() + return + self.muxer = None + if async: + self.muxer = async + t = Thread(target=self.mux, args=(async,)) + t.daemon = True + t.start() + + def dispatch(self, dat): + tag = dat.tag + rpc = None + with self.lock: + rpc = self.wait.get(tag, None) + if rpc is None or rpc not in self.queue: + #print "bad rpc tag: %u (no one waiting on it)" % dat.tag + return + self.puttag(rpc) + self.queue.remove(rpc) + rpc.dispatch(dat) + + def gettag(self, r): + tag = 0 + + while not self.free: + self.rendez.wait() + + tag = self.free.pop() + + if tag in self.wait: + raise Exception("nwait botch") + + self.wait[tag] = r + + r.tag = tag + r.orig.tag = r.tag + return r.tag + + def puttag(self, rpc): + if rpc.tag in self.wait: + del self.wait[rpc.tag] + self.free.add(rpc.tag) + self.rendez.notify() + + def send(self, dat): + data = ''.join(dat.marshall()) + n = self.fd.send(data) + return n == len(data) + def recv(self): + try: + with self.inlock: + data = self.fd.recv(4) + if data: + len = fields.Int.decoders[4](data, 0) + data += self.fd.recv(len - 4) + return self.process(data) + except Exception, e: + traceback.print_exc(sys.stdout) + print repr(data) + return None + + def newrpc(self, dat, async=None): + rpc = Rpc(self, dat, async) + tag = None + + with self.lock: + self.gettag(rpc) + self.queue.add(rpc) + + if rpc.tag >= 0 and self.send(dat): + return rpc + + with self.lock: + self.queue.remove(rpc) + self.puttag(rpc) + +class Rpc(Condition): + def __init__(self, mux, data, async=None): + super(Rpc, self).__init__(mux.lock) + self.mux = mux + self.orig = data + self.data = None + self.waiting = False + self.async = async + + def dispatch(self, data=None): + self.data = data + if not self.async or self.waiting: + self.notify() + elif callable(self.async): + Thread(target=self.async, args=(self.mux, data)).start() + +# vim:se sts=4 sw=4 et: diff --git a/alternative_wmiircs/python/pyxp/types.py b/alternative_wmiircs/python/pyxp/types.py new file mode 100644 index 0000000..a5ca1c0 --- /dev/null +++ b/alternative_wmiircs/python/pyxp/types.py @@ -0,0 +1,55 @@ +from pyxp.messages import Message +from pyxp.fields import * + +__all__ = 'Qid', 'Stat' + +class Qid(Message): + QTFILE = 0x00 + QTLINK = 0x01 + QTSYMLINK = 0x02 + QTTMP = 0x04 + QTAUTH = 0x08 + QTMOUNT = 0x10 + QTEXCL = 0x20 + QTAPPEND = 0x40 + QTDIR = 0x80 + + type = Int(1) + version = Int(4) + path = Int(8) + +class Stat(Message): + DMDIR = 0x80000000 + DMAPPEND = 0x40000000 + DMEXCL = 0x20000000 + DMMOUNT = 0x10000000 + DMAUTH = 0x08000000 + DMTMP = 0x04000000 + DMSYMLINK = 0x02000000 + DMDEVICE = 0x00800000 + DMNAMEDPIPE = 0x00200000 + DMSOCKET = 0x00100000 + DMSETUID = 0x00080000 + DMSETGID = 0x00040000 + + @classmethod + def unmarshall_list(cls, data, offset=0): + while offset < len(data): + n, stat = cls.unmarshall(data, offset) + offset += n + yield stat + + size = Size(2) + type = Int(2) + dev = Int(4) + qid = Qid.field() + mode = Int(4) + atime = Date() + mtime = Date() + length = Int(8) + name = String() + uid = String() + gid = String() + muid = String() + +# vim:se sts=4 sw=4 et: diff --git a/alternative_wmiircs/python/wmiirc b/alternative_wmiircs/python/wmiirc new file mode 100755 index 0000000..75a148a --- /dev/null +++ b/alternative_wmiircs/python/wmiirc @@ -0,0 +1,12 @@ +#!/usr/bin/env python +import os, sys +path = [] +for p in os.environ.get("WMII_CONFPATH", "").split(':'): + path += [p, p + '/python'] +sys.path = path + sys.path + +from pygmi import events +import wmiirc + +events.loop() + diff --git a/alternative_wmiircs/python/wmiirc.py b/alternative_wmiircs/python/wmiirc.py new file mode 100644 index 0000000..f385757 --- /dev/null +++ b/alternative_wmiircs/python/wmiirc.py @@ -0,0 +1,308 @@ +import datetime +import operator +import os +import re +import sys +import traceback +from threading import Thread, Timer + +import pygmi +from pygmi import * +from pygmi import event + +identity = lambda k: k + +# Begin Configuration +# +# Note: This file loads ~/.wmii/wmiirc_local.py if it exists. +# Configuration should be placed in that file, and this file +# left unmodified, if possible. wmiirc_local should import +# wmiirc or any other modules it needs. + +# Keys +keys.defs = dict( + mod='Mod4', + left='h', + down='j', + up='k', + right='l') + +# Bars +noticetimeout=5 +noticebar=('right', '!notice') + +# Theme +background = '#333333' +floatbackground='#222222' + +wmii['font'] = 'drift,-*-fixed-*-*-*-*-9-*-*-*-*-*-*-*' +wmii['normcolors'] = '#000000', '#c1c48b', '#81654f' +wmii['focuscolors'] = '#000000', '#81654f', '#000000' +wmii['grabmod'] = keys.defs['mod'] +wmii['border'] = 2 + +def setbackground(color): + call('xsetroot', '-solid', color) +setbackground(background) + +terminal = 'wmiir', 'setsid', '@TERMINAL@' +pygmi.shell = os.environ.get('SHELL', 'sh') + +@defmonitor +def load(self): + return wmii.cache['normcolors'], re.sub(r'^.*: ', '', call('uptime')).replace(', ', ' ') +@defmonitor +def time(self): + return wmii.cache['focuscolors'], datetime.datetime.now().strftime('%c') + +wmii.colrules = ( + ('gimp', '17+83+41'), + ('.*', '62+38 # Golden Ratio'), +) + +wmii.tagrules = ( + ('MPlayer|VLC', '~'), +) + +def unresponsive_client(client): + msg = 'The following client is not responding. What would you like to do?' + resp = call('wihack', '-transient', client.id, + 'xmessage', '-nearmouse', '-buttons', 'Kill,Wait', '-print', + '%s\n %s' % (msg, client.label)) + if resp == 'Kill': + client.slay() + +# End Configuration + +client.awrite('/event', 'Start wmiirc') + +tags = Tags() +events.bind({ + ('Quit', Match('Start', 'wmiirc')): lambda *a: sys.exit(), + 'CreateTag': tags.add, + 'DestroyTag': tags.delete, + 'FocusTag': tags.focus, + 'UnfocusTag': tags.unfocus, + 'UrgentTag': lambda args: tags.set_urgent(args.split()[1], True), + 'NotUrgentTag': lambda args: tags.set_urgent(args.split()[1], False), + + 'AreaFocus': lambda args: (args == '~' and + (setbackground(floatbackground), True) or + setbackground(background)), + + 'Unresponsive': lambda args: Thread(target=unresponsive_client, + args=(Client(args),)).start(), + + 'Notice': lambda args: notice.show(args), + + Match(('LeftBarClick', 'LeftBarDND'), '1'): lambda e, b, tag: tags.select(tag), + Match('LeftBarClick', '4'): lambda *a: tags.select(tags.next(True)), + Match('LeftBarClick', '5'): lambda *a: tags.select(tags.next()), + + Match('LeftBarMouseDown', 3): lambda e, n, tag: clickmenu(( + ('Delete', lambda t: Tag(t).delete()), + ), (tag,)), + Match('ClientMouseDown', _, 3): lambda e, client, n: clickmenu(( + ('Delete', lambda c: Client(c).kill()), + ('Kill', lambda c: Client(c).slay()), + ('Fullscreen', lambda c: Client(c).set('Fullscreen', 'on')), + ), (client,)), + + Match('ClientClick', _, 4): lambda e, c, n: Tag('sel').select('up'), + Match('ClientClick', _, 5): lambda e, c, n: Tag('sel').select('down'), +}) + +@apply +class Actions(event.Actions): + def rehash(self, args=''): + program_menu.choices = program_list(os.environ['PATH'].split(':')) + def showkeys(self, args=''): + message(keys.help) + def quit(self, args=''): + wmii.ctl('quit') + def eval_(self, args=''): + exec args + def exec_(self, args=''): + wmii['exec'] = args + def exit(self, args=''): + client.awrite('/event', 'Quit') + +program_menu = Menu(histfile='%s/history.progs' % confpath[0], nhist=5000, + action=curry(call, 'wmiir', 'setsid', + pygmi.shell, '-c', background=True)) +action_menu = Menu(histfile='%s/history.actions' % confpath[0], nhist=500, + choices=lambda: Actions._choices, + action=Actions._call) +tag_menu = Menu(histfile='%s/history.tags' % confpath[0], nhist=100, + choices=lambda: sorted(tags.tags.keys())) + +def clickmenu(choices, args): + ClickMenu(choices=(k for k, v in choices), + action=lambda choice: dict(choices).get(choice, identity)(*args) + ).call() + +class Notice(Button): + def __init__(self): + super(Notice, self).__init__(*noticebar, colors=wmii.cache['normcolors']) + self.timer = None + self.show(' ') + + def tick(self): + self.create(wmii.cache['normcolors'], ' ') + + def write(self, notice): + client.awrite('/event', 'Notice %s' % notice.replace('\n', ' ')) + + def show(self, notice): + if self.timer: + self.timer.cancel() + self.create(wmii.cache['normcolors'], notice) + self.timer = Timer(noticetimeout, self.tick) + self.timer.start() +notice = Notice() + +keys.bind('main', ( + "Moving around", + ('%(mod)s-%(left)s', "Select the client to the left", + lambda k: Tag('sel').select('left')), + ('%(mod)s-%(right)s', "Select the client to the right", + lambda k: Tag('sel').select('right')), + ('%(mod)s-%(up)s', "Select the client above", + lambda k: Tag('sel').select('up')), + ('%(mod)s-%(down)s', "Select the client below", + lambda k: Tag('sel').select('down')), + + ('%(mod)s-space', "Toggle between floating and managed layers", + lambda k: Tag('sel').select('toggle')), + + "Moving through stacks", + ('%(mod)s-Control-%(up)s', "Select the stack above", + lambda k: Tag('sel').select('up', stack=True)), + ('%(mod)s-Control-%(down)s', "Select the stack below", + lambda k: Tag('sel').select('down', stack=True)), + + + "Moving clients around", + ('%(mod)s-Shift-%(left)s', "Move selected client to the left", + lambda k: Tag('sel').send(Client('sel'), 'left')), + ('%(mod)s-Shift-%(right)s', "Move selected client to the right", + lambda k: Tag('sel').send(Client('sel'), 'right')), + ('%(mod)s-Shift-%(up)s', "Move selected client up", + lambda k: Tag('sel').send(Client('sel'), 'up')), + ('%(mod)s-Shift-%(down)s', "Move selected client down", + lambda k: Tag('sel').send(Client('sel'), 'down')), + + ('%(mod)s-Shift-space', "Toggle selected client between floating and managed layers", + lambda k: Tag('sel').send(Client('sel'), 'toggle')), + + "Client actions", + ('%(mod)s-f', "Toggle selected client's fullsceen state", + lambda k: Client('sel').set('Fullscreen', 'toggle')), + ('%(mod)s-Shift-c', "Close client", + lambda k: Client('sel').kill()), + + "Changing column modes", + ('%(mod)s-d', "Set column to default mode", + lambda k: setattr(Tag('sel').selcol, 'mode', 'default-max')), + ('%(mod)s-s', "Set column to stack mode", + lambda k: setattr(Tag('sel').selcol, 'mode', 'stack-max')), + ('%(mod)s-m', "Set column to max mode", + lambda k: setattr(Tag('sel').selcol, 'mode', 'stack+max')), + + "Running programs", + ('%(mod)s-a', "Open wmii actions menu", + lambda k: action_menu()), + ('%(mod)s-p', "Open program menu", + lambda k: program_menu()), + + ('%(mod)s-Return', "Launch a terminal", + lambda k: call(*terminal, background=True)), + + "Tag actions", + ('%(mod)s-t', "Change to another tag", + lambda k: tags.select(tag_menu())), + ('%(mod)s-Shift-t', "Retag the selected client", + lambda k: setattr(Client('sel'), 'tags', tag_menu())), + + ('%(mod)s-n', "Move to the view to the left", + lambda k: tags.select(tags.next())), + ('%(mod)s-b', "Move to the view to the right", + lambda k: tags.select(tags.next(True))), + ('%(mod)s-Shift-n', "Move to the view to the left, take along current client", + lambda k: tags.select(tags.next(), take_client=Client('sel'))), + ('%(mod)s-Shift-b', "Move to the view to the right, take along current client", + lambda k: tags.select(tags.next(True), take_client=Client('sel'))), + + ('%(mod)s-i', "Move to the newer tag in the tag stack", + lambda k: tags.select(tags.NEXT)), + ('%(mod)s-o', "Move to the older tag in the tag stack", + lambda k: tags.select(tags.PREV)), + ('%(mod)s-Shift-i', "Move to the newer tag in the tag stack, take along current client", + lambda k: tags.select(tags.NEXT, take_client=Client('sel'))), + ('%(mod)s-Shift-o', "Move to the older tag in the tag stack, take along current client", + lambda k: tags.select(tags.PREV, take_client=Client('sel'))), + +)) +def bind_num(i): + keys.bind('main', ( + "Tag actions", + ('%%(mod)s-%d' % i, "Move to view '%d'" % i, + lambda k: tags.select(str(i))), + ('%%(mod)s-Shift-%d' % i, "Retag selected client with tag '%d'" % i, + lambda k: setattr(Client('sel'), 'tags', i)), + )) +map(bind_num, range(0, 10)) + +keys.bind('main', ( + "Changing modes", + ('%(mod)s-Control-r', "Enter resize mode", + lambda k: setattr(keys, 'mode', 'resize')), + ('%(mod)s-Control-t', "Enter passthrough mode", + lambda k: setattr(keys, 'mode', 'passthrough')), +)); +keys.bind('passthrough', ( + "Changing modes", + ('%(mod)s-Control-t', "Leave passthrough mode", + lambda k: setattr(keys, 'mode', 'main')), +)); + +keys.bind('resize', ( + ('Escape', "Leave resize mode", + lambda k: setattr(keys, 'mode', 'main')), +), import_={'main': ('%(mod)s-%(left)s', '%(mod)s-%(right)s', + '%(mod)s-%(up)s', '%(mod)s-%(down)s', + '%(mod)s-Space')}) + +def addresize(mod, desc, cmd, *args): + keys.bind('resize', ( + (mod + '%(left)s', "%s selected client to the left" % desc, + lambda k: Tag('sel').ctl(cmd, 'sel sel', 'left', + *args)), + (mod + '%(right)s', "%s selected client to the right" % desc, + lambda k: Tag('sel').ctl(cmd, 'sel sel', 'right', + *args)), + (mod + '%(up)s', "%s selected client up" % desc, + lambda k: Tag('sel').ctl(cmd, 'sel sel', 'up', + *args)), + (mod + '%(down)s', "%s selected client down" % desc, + lambda k: Tag('sel').ctl(cmd, 'sel sel', 'down', + *args)), + )); +addresize('', 'Grow', 'grow') +addresize('Control-', 'Shrink', 'grow', '-1') +addresize('Shift-', 'Nudge', 'nudge') + +Thread(target=lambda: Actions.rehash()).start() + +if not os.environ.get('WMII_NOPLUGINS', ''): + dirs = filter(curry(os.access, _, os.R_OK), + ('%s/plugins' % dir for dir in confpath)) + files = filter(re.compile(r'\.py$').search, + reduce(operator.add, map(os.listdir, dirs), [])) + for f in ['wmiirc_local'] + ['plugins.%s' % file[:-3] for file in files]: + try: + exec 'import %s' % f + except Exception, e: + traceback.print_exc(sys.stdout) + +# vim:se sts=4 sw=4 et: diff --git a/alternative_wmiircs/ruby/HISTORY b/alternative_wmiircs/ruby/HISTORY new file mode 100644 index 0000000..d75f69d --- /dev/null +++ b/alternative_wmiircs/ruby/HISTORY @@ -0,0 +1,233 @@ += 2006-09-30 + +* Included 1.1.0 release of Ruby-IXP. + + += 2006-09-29 + +* Fixed bug in toggle_maximize method (in rc.rb) due + to accessing a nonexistent file in IXP file system. + + Thanks to Christian von Mueffling for reporting this bug. + +* Fixed problem with reading + index (Wmii::Client#index) of + currently selected client. + +* Wmii.find_client now accepts a variable number of places to be searched. + + += 2006-09-28 + +* Added number_view_buttons method (in rc.rb) which numbers + the view buttons displayed on the bar, from left to right. + + += 2006-09-27 + +* Included two main concurrency fixes for Ruby-IXP. + + += 2006-09-24 + +* Added two-stage event handling, + to minimize the number of events + missed while processing an event. + + += 2006-09-23 + +* Fixed event & status bar loop. It was forgotten when I transitioned + to the new Ixp::Node#method_missing behavior on 2006-09-22. + + Thanks to Fredrik Ternerot for reporting this bug. + +* When selecting views based on their first letter: if more than one + view matches, then they are cycled (adapted from Fredrik Ternerot). + +* Added focus_view_matching method in rc.rb. + +* Fixed errors that occurred when the tile and + diamond arrangements were applied to empty views. + + += 2006-09-22 + +* Ixp::Node#method_missing now only dereferences files. Also, + the ! notation has been removed, as you can see below. + + >> Wmii.fs.bar.status + => #<Ixp::Node:0xb7b5940c @path="/bar/status"> + >> Wmii.fs.bar.status.read + => ["colors", "data"] + >> Wmii.fs.bar.status.data + => "Fri Sep 22 18:46:11 PDT 2006 | 0.06 0.10 0.08 | 531M 100% /home" + >> Wmii.fs.bar.status.data! + => #<Ixp::Node:0xb7b377e4 @path="/bar/status/data!"> + + += 2006-09-21 + +* Fix some forgotten changes from show_menu() returning *nil*. + +* Exception error message (xmessage) now lets you restart *wmiirc*. + +* Updated event loop to generate less 9P traffic. + + += 2006-09-20 + +* Included code from upcoming Ruby-IXP 1.1.0 release. + +* Ixp::Node#method_missing now only dereferences a node + if the method is suffixed with an exclamation mark. + +* show_menu now returns *nil* if nothing was chosen. + +* Updated event loop for {wmii-3.1's /event overload bug + fix}[http://wmii.de/pipermail/wmii/2006-September/002718.html]. + +* Added explicit termination of already running instances + in *wmiirc* via Process.kill and `ps`, instead of using + /event as a means of coordinating said task. + + += 2006-09-19 + +* Included Ruby-IXP 1.0.3 release. + +* Added Ixp::Node#open method to reduce 9P traffic. + +* Added ability to fetch a sub-node + via Ixp::Node#method_missing, while + not dereferencing it (reading its + contents if it is a file), by adding + an exclamation to the file name. + + For example, consider the following output in *wmiish*. + + >> Wmii.fs.bar.status.data + => "Tue Sep 19 10:50:41 PDT 2006 | 0.30 0.43 0.29 | 1.7G 98% /home" + >> Wmii.fs.bar.status.data! + => #<Ixp::Node:0xb7bf1f18 @path="/bar/status/data"> + +* *wmiirc* no longer automatically resumes from error. Instead, + it throws you a terminal and shows you the error details so + you have a chance to fix it and restart *wmiirc* yourself. + + += 2006-09-18 + +* Included Ruby-IXP 1.0.2 release. + + += 2006-09-17 + +* Added Wmii::View#empty? and Wmii::Area#empty? methods. + +* change_tag_from_menu now returns the chosen tag. + +* Included Ruby-IXP 1.0.1 release. + + += 2006-09-16 + +* Fixed toggling of maximization + of currently focused client, + via toggle_maximize in rc.rb. + + Thanks to Fredrik Ternerot for reporting this bug. + + += 2006-09-15 + +* Added Wmii.get_view and Wmii.get_client + methods, to further minimize hard-coded + IXP file system paths. This will make it + easier to upgrade to wmii-4 later on. + +* Fixed ruby-ixp to be internally buffered for Ixp#read. + +* Event loop now uses Ixp#read instead of *wmiir*. + +* Already running configurations now correctly + exit when another instance starts up. + + += 2006-09-14 + +* Added ability to swap current client with the + currently focused client in any other column. + + += 2006-09-13 + +* Reverted to *wmiir* for event loop, because + Ixp#read isn't internally buffered! + +* Changed Wmii::View#each to Wmii::View#each_column because + floating area isn't a column (it doesn't have /mode file). + +* Added shortcuts for setting layouts of all columns in current view. + +* Added shortcuts for selection of current column. + +* Fixed ability to terminate multiple clients. + + += 2006-09-12 + +* Event loop now uses Ixp#read instead of *wmiir*. + + * Already running configurations now correctly + exit when another instance starts up. + +* Added Wmii::View#diamond! -- a diamond-shaped automated client arrangement. + +* Added Wmii::Area#length= for setting number of clients in a column. + + += 2006-09-11 + +* Added exception logging and recovery mechanism. + + * wmiirc is now split into a loader + file (wmiirc) and a configuration + file (wmiirc-config.rb), just + like in the ruby-wmii project. + +* IXPException' are no longer hidden away inside Ixp. + +* Moved support for destructive area-operations + from Wmii#with_selection into Array#each so + that it is generally available. + + += 2006-09-10 + +* Added wmiish--an interactive Ruby shell for controlling wmii. + +* Lots of major refactoring in Ixp and Wmii. + * Moved utility methods from wmiirc into rc.rb. + + += 2006-09-09 + +* Cleaned up IXP abstraction... now + multiple levels of method_missing + works, and so does self[sub_path] + +* Wmii#with_selection now supports destructive area-operations. + +* Update for compliance with new unique-client-id in filesystem patch. + + += 2006-08-31 + +* Added facility which sends the selection + to temporary view or switches back again. + + += 2006-08-30 + +* Add Wmii#with_selection method for operating on all clients in selection. diff --git a/alternative_wmiircs/ruby/LICENSE b/alternative_wmiircs/ruby/LICENSE new file mode 100644 index 0000000..893414a --- /dev/null +++ b/alternative_wmiircs/ruby/LICENSE @@ -0,0 +1,21 @@ +(the ISC license) + +Copyright 2006 Suraj N. Kurapati <sunaku@gmail.com> +Copyright 2007 Kris Maglione <jg@suckless.org> +Copyright 2007 Nick Stenning <nick@whiteink.com> +Copyright 2009 Daniel Wäber <waeber@inf.fu-berlin.de> +Copyright 2009 Michael Andrus <centyx@centyx.net> +Copyright 2009 Simon Hafner <hafnersimon@gmail.com> + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + diff --git a/alternative_wmiircs/ruby/Makefile b/alternative_wmiircs/ruby/Makefile new file mode 100644 index 0000000..5d8fd65 --- /dev/null +++ b/alternative_wmiircs/ruby/Makefile @@ -0,0 +1,13 @@ +ROOT=../.. +include $(ROOT)/mk/hdr.mk +include $(ROOT)/mk/wmii.mk + +DOCS = README \ + HISTORY \ + LICENSE +EXECS = wmiirc +TEXT = config.rb \ + config.yaml + +DIR = $(ETC)/wmii$(CONFVERSION)/ruby +DOCDIR = $(DOC)/alternative_wmiircs/ruby diff --git a/alternative_wmiircs/ruby/README b/alternative_wmiircs/ruby/README new file mode 100644 index 0000000..788e0b4 --- /dev/null +++ b/alternative_wmiircs/ruby/README @@ -0,0 +1,94 @@ + +This is a modified version of sunaku's wmiirc, designed for +his Rumai Ruby module. Minor configuration changes, namely to +the color scheme and default key bindings, as well as the +configuration search path, exist in this version. Builtin mpd +support has also been removed. Also added is support for +string interpolation in key bindings, as should be apparent in +the included config.yaml. + +In particular, not that there is no need to copy any files to +~/.wmii-hg or ~/.wmii other than config.yaml. The script will +happily load the requisite files from their default install +location. They can be loaded either by involing wmii as +follows: + + wmiir -r ruby/wmiirc + +or running the following after startup: + + wmiir xwrite /ctl spawn ruby/wmiirc + +The rumai gem is still required, as noted below. + +The original readme appears below unmodified: + +sunaku's Ruby wmiirc +==================== + +This is my wmii configuration, described in these articles: + + http://wmii.suckless.org/alternative_wmiirc_scripts + + http://snk.tuxfamily.org/lib/rumai/ + + http://article.gmane.org/gmane.comp.window-managers.wmii/1704 + + http://snk.tuxfamily.org/web/2006-07-01-wmii-3-1-configuration-in-ruby.html + +Dependencies: + + wmii 3.6 or newer (preferably wmii-hg) + + Ruby 1.8.6 or newer + + RubyGems 1.3.1 or newer + +Installation: + + # library + gem install rumai # required + gem install librmpd # optional + + # install + mv ~/.wmii-hg ~/.wmii-hg.backup + git clone git://github.com/sunaku/wmiirc.git ~/.wmii-hg + + # choose + cd ~/.wmii-hg + git checkout --track -b CHOICE origin/CHOICE # choices are: + + +--------+------------------------------------------------+ + | CHOICE | DESCRIPTION | + +--------+------------------------------------------------+ + | dvorak | sunaku's personal configuration; DSK friendly! | + | qwerty | QWERTY port of sunaku's personal configuration | + | strict | port of the default wmiirc shipped with wmii | + | master | barebones template for starting from scratch | + +--------+------------------------------------------------+ + + # run + ~/.wmii-hg/wmiirc + +Documentation: + + # see list of all key bindings + egrep '^ +\$\{\w+\}' ~/.wmii-hg/config.yaml + + # read the configuration file + less ~/.wmii-hg/config.yaml + +Configuration: + + Edit ~/.wmii-hg/config.yaml to your liking. + + Run ~/.wmii-hg/wmiirc to apply your changes. + +Contribution: + + Fork this project on GitHub and send pull requests. + +Questions: + + Send me an e-mail (see LICENSE for my address). + diff --git a/alternative_wmiircs/ruby/config.rb b/alternative_wmiircs/ruby/config.rb new file mode 100644 index 0000000..c86797a --- /dev/null +++ b/alternative_wmiircs/ruby/config.rb @@ -0,0 +1,547 @@ +# DSL for wmiirc configuration. +#-- +# Copyright protects this work. +# See LICENSE file for details. +#++ + +require 'shellwords' +require 'pathname' +require 'yaml' + +require 'rubygems' +gem 'rumai', '~> 3' +require 'rumai' + +include Rumai + +class Handler < Hash + def initialize + super {|h,k| h[k] = [] } + end + + ## + # If a block is given, registers a handler + # for the given key and returns the handler. + # + # Otherwise, executes all handlers registered for the given key. + # + def handle key, *args, &block + if block + self[key] << block + + elsif key? key + self[key].each do |block| + block.call(*args) + end + end + + block + end +end + +EVENTS = Handler.new +ACTIONS = Handler.new +KEYS = Handler.new + +## +# If a block is given, registers a handler +# for the given event and returns the handler. +# +# Otherwise, executes all handlers for the given event. +# +def event *a, &b + EVENTS.handle(*a, &b) +end + +## +# Returns a list of registered event names. +# +def events + EVENTS.keys +end + +## +# If a block is given, registers a handler for +# the given action and returns the handler. +# +# Otherwise, executes all handlers for the given action. +# +def action *a, &b + ACTIONS.handle(*a, &b) +end + +## +# Returns a list of registered action names. +# +def actions + ACTIONS.keys +end + +## +# If a block is given, registers a handler for +# the given keypress and returns the handler. +# +# Otherwise, executes all handlers for the given keypress. +# +def key *a, &b + KEYS.handle(*a, &b) +end + +## +# Returns a list of registered action names. +# +def keys + KEYS.keys +end + +## +# Shows a menu (where the user must press keys on their keyboard to +# make a choice) with the given items and returns the chosen item. +# +# If nothing was chosen, then nil is returned. +# +# ==== Parameters +# +# [prompt] +# Instruction on what the user should enter or choose. +# +def key_menu choices, prompt = nil + words = ['dmenu', '-fn', CONFIG['display']['font']] + + # show menu at the same location as the status bar + words << '-b' if CONFIG['display']['bar'] == 'bottom' + + words.concat %w[-nf -nb -sf -sb].zip( + [ + CONFIG['display']['color']['normal'], + CONFIG['display']['color']['focus'], + + ].map {|c| c.to_s.split[0,2] }.flatten + + ).flatten + + words.push '-p', prompt if prompt + + command = words.shelljoin + IO.popen(command, 'r+') do |menu| + menu.puts choices + menu.close_write + + choice = menu.read + choice unless choice.empty? + end +end + +## +# Shows a menu (where the user must click a menu +# item using their mouse to make a choice) with +# the given items and returns the chosen item. +# +# If nothing was chosen, then nil is returned. +# +# ==== Parameters +# +# [choices] +# List of choices to display in the menu. +# +# [initial] +# The choice that should be initially selected. +# +# If this choice is not included in the list +# of choices, then this item will be made +# into a makeshift title-bar for the menu. +# +def click_menu choices, initial = nil + words = ['wmii9menu'] + + if initial + words << '-i' + + unless choices.include? initial + initial = "<<#{initial}>>:" + words << initial + end + + words << initial + end + + words.concat choices + command = words.shelljoin + + choice = `#{command}`.chomp + choice unless choice.empty? +end + +## +# Shows a key_menu() containing the given +# clients and returns the chosen client. +# +# If nothing was chosen, then nil is returned. +# +# ==== Parameters +# +# [prompt] +# Instruction on what the user should enter or choose. +# +# [clients] +# List of clients to present as choices to the user. +# +# If this parameter is not specified, +# its default value will be a list of +# all currently available clients. +# +def client_menu prompt = nil, clients = Rumai.clients + choices = [] + + clients.each_with_index do |c, i| + choices << "%d. [%s] %s" % [i, c[:tags].read, c[:label].read.downcase] + end + + if target = key_menu(choices, prompt) + clients[target.scan(/\d+/).first.to_i] + end +end + +## +# Returns the basenames of executable files present in the given directories. +# +def find_programs *dirs + dirs.flatten. + map {|d| Pathname.new(d).expand_path.children rescue [] }.flatten. + map {|f| f.basename.to_s if f.file? and f.executable? }.compact.uniq.sort +end + +## +# Launches the command built from the given words in the background. +# +def launch *words + command = words.shelljoin + system "#{command} &" +end + +## +# A button on a bar. +# +class Button < Thread + ## + # Creates a new button at the given node and updates its label + # according to the given refresh rate (measured in seconds). The + # given block is invoked to calculate the label of the button. + # + # The return value of the given block can be either an + # array (whose first item is a wmii color sequence for the + # button, and the remaining items compose the label of the + # button) or a string containing the label of the button. + # + # If the given block raises a standard exception, then that will be + # rescued and displayed (using error colors) as the button's label. + # + def initialize fs_bar_node, refresh_rate, &button_label + raise ArgumentError, 'block must be given' unless block_given? + + super(fs_bar_node) do |button| + while true + label = + begin + Array(button_label.call) + rescue Exception => e + LOG.error e + [CONFIG['display']['color']['error'], e] + end + + # provide default color + unless label.first =~ /(?:#[[:xdigit:]]{6} ?){3}/ + label.unshift CONFIG['display']['color']['normal'] + end + + button.create unless button.exist? + button.write label.join(' ') + sleep refresh_rate + end + end + end + + ## + # Refreshes the label of this button. + # + alias refresh wakeup +end + +## +# Loads the given YAML configuration file. +# +def load_config config_file + Object.const_set :CONFIG, YAML.load_file(config_file) + + # script + eval CONFIG['script']['before'].to_s, TOPLEVEL_BINDING, + "#{config_file}:script:before" + + # display + fo = ENV['WMII_FONT'] = CONFIG['display']['font'] + fc = ENV['WMII_FOCUSCOLORS'] = CONFIG['display']['color']['focus'] + nc = ENV['WMII_NORMCOLORS'] = CONFIG['display']['color']['normal'] + + settings = { + 'font' => fo, + 'focuscolors' => fc, + 'normcolors' => nc, + 'border' => CONFIG['display']['border'], + 'bar on' => CONFIG['display']['bar'], + 'colmode' => CONFIG['display']['column']['mode'], + 'grabmod' => CONFIG['control']['grab'], + } + + begin + fs.ctl.write settings.map {|pair| pair.join(' ') }.join("\n") + + rescue Rumai::IXP::Error => e + # + # settings that are not supported in a particular wmii version + # are ignored, and those that are supported are (silently) + # applied. but a "bad command" error is raised nevertheless! + # + warn e.inspect + warn e.backtrace.join("\n") + end + + launch 'xsetroot', '-solid', CONFIG['display']['background'] + + # column + fs.colrules.write CONFIG['display']['column']['rule'] + + # client + event 'CreateClient' do |client_id| + client = Client.new(client_id) + + unless defined? @client_tags_by_regexp + @client_tags_by_regexp = CONFIG['display']['client'].map {|hash| + k, v = hash.to_a.first + [eval(k, TOPLEVEL_BINDING, "#{config_file}:display:client"), v] + } + end + + if label = client.props.read rescue nil + catch :found do + @client_tags_by_regexp.each do |regexp, tags| + if label =~ regexp + client.tags = tags + throw :found + end + end + + # force client onto current view + begin + client.tags = curr_tag + client.focus + rescue + # ignore + end + end + end + end + + # status + action 'status' do + fs.rbar.clear + + unless defined? @status_button_by_name + @status_button_by_name = {} + @status_button_by_file = {} + @on_click_by_status_button = {} + + CONFIG['display']['status'].each_with_index do |hash, position| + name, defn = hash.to_a.first + + # buttons appear in ASCII order of their IXP file name + file = "#{position}-#{name}" + + button = eval( + "Button.new(fs.rbar[#{file.inspect}], #{defn['refresh']}) { #{defn['content']} }", + TOPLEVEL_BINDING, "#{config_file}:display:status:#{name}" + ) + + @status_button_by_name[name] = button + @status_button_by_file[file] = button + + # mouse click handler + if code = defn['click'] + @on_click_by_status_button[button] = eval( + "lambda {|mouse_button| #{code} }", TOPLEVEL_BINDING, + "#{config_file}:display:status:#{name}:click" + ) + end + end + end + + @status_button_by_name.each_value {|b| b.refresh } + + end + + ## + # Returns the status button associated with the given name. + # + # ==== Parameters + # + # [name] + # Either the the user-defined name of + # the status button or the basename + # of the status button's IXP file. + # + def status_button name + @status_button_by_name[name] || @status_button_by_file[name] + end + + ## + # Refreshes the content of the status button with the given name. + # + # ==== Parameters + # + # [name] + # Either the the user-defined name of + # the status button or the basename + # of the status button's IXP file. + # + def status name + if button = status_button(name) + button.refresh + end + end + + ## + # Invokes the mouse click handler for the given mouse + # button on the status button that has the given name. + # + # ==== Parameters + # + # [name] + # Either the the user-defined name of + # the status button or the basename + # of the status button's IXP file. + # + # [mouse_button] + # The identification number of + # the mouse button (as defined + # by X server) that was clicked. + # + def status_click name, mouse_button + if button = status_button(name) and + handle = @on_click_by_status_button[button] + then + handle.call mouse_button.to_i + end + end + + # control + action 'reload' do + # reload this wmii configuration + reload_config + end + + action 'rehash' do + # scan for available programs and actions + @programs = find_programs(ENV['PATH'].squeeze(':').split(':')) + end + + # kill all currently open clients + action 'clear' do + # firefox's restore session feature does not + # work unless the whole process is killed. + system 'killall firefox firefox-bin thunderbird thunderbird-bin' + + # gnome-panel refuses to die by any other means + system 'killall -s TERM gnome-panel' + + Thread.pass until clients.each do |c| + begin + c.focus # XXX: client must be on current view in order to be killed + c.kill + rescue + # ignore + end + end.empty? + end + + # kill the window manager only; do not touch the clients! + action 'kill' do + fs.ctl.write 'quit' + end + + # kill both clients and window manager + action 'quit' do + action 'clear' + action 'kill' + end + + event 'Unresponsive' do |client_id| + client = Client.new(client_id) + + IO.popen('xmessage -nearmouse -file - -buttons Kill,Wait -print', 'w+') do |f| + f.puts 'The following client is not responding.', '' + f.puts client.inspect + f.puts client.label.read + + f.puts '', 'What would you like to do?' + f.close_write + + if f.read.chomp == 'Kill' + client.slay + end + end + end + + event 'Notice' do |*argv| + unless defined? @notice_mutex + require 'thread' + @notice_mutex = Mutex.new + end + + Thread.new do + # prevent notices from overwriting each other + @notice_mutex.synchronize do + button = fs.rbar['!notice'] + button.create unless button.exist? + + # display the notice + message = argv.join(' ') + + LOG.info message # also log it in case the user is AFK + button.write "#{CONFIG['display']['color']['notice']} #{message}" + + # clear the notice + sleep [1, CONFIG['display']['notice'].to_i].max + button.remove + end + end + end + + %w[key action event].each do |param| + if settings = CONFIG['control'][param] + settings.each do |name, code| + if param == 'key' + # expand ${...} expressions in shortcut key sequences + name = name.gsub(/\$\{(.+?)\}/) { CONFIG['control'][$1] } + end + + eval "#{param}(#{name.inspect}) {|*argv| #{code} }", + TOPLEVEL_BINDING, "#{config_file}:control:#{param}:#{name}" + end + end + end + + # script + action 'status' + action 'rehash' + + eval CONFIG['script']['after'].to_s, TOPLEVEL_BINDING, + "#{config_file}:script:after" + +end + +## +# Reloads the entire wmii configuration. +# +def reload_config + LOG.info 'reload' + exec $0 +end diff --git a/alternative_wmiircs/ruby/config.yaml b/alternative_wmiircs/ruby/config.yaml new file mode 100644 index 0000000..60065b3 --- /dev/null +++ b/alternative_wmiircs/ruby/config.yaml @@ -0,0 +1,536 @@ +# +# High-level wmii configuration. +# +# Ruby code in this file has access +# to a CONFIG constant which contains +# the data in this configuration file. +# +#-- +# Copyright protects this work. +# See LICENSE file for details. +#++ + + +## +# Program preferences. +# +program: + terminal: @TERMINAL@ + browser: firefox + editor: mousepad + filer: thunar + + +## +# Appearance settings. +# +display: + + ## + # Where to display the horizontal status bar? + # + # Possible choices are "top" and "bottom". + # + bar: bottom + + ## + # The font to use in all text drawn by wmii. + # + font: -*-fixed-medium-r-*-*-13-*-*-*-*-*-*-* + + ## + # Thickness of client border (measured in pixels). + # + border: 1 + + ## + # Number of seconds a notice should be displayed. + # + notice: 5 + + ## + # Color schemes for everything drawn by wmii. + # + # <scheme>: "<text> <background> <border>" + # + # You can find more color schemes here: + # + # http://wmii.suckless.org/scripts_n_snips/themes + # + color: + normal: "#000000 #c1c48b #81654f" + focus: "#000000 #81654f #000000" + error: "#000000 #81654f #000000" + notice: "#000000 #a1956d #413328" + success: "#000000 #c1c48b #81654f" + + ## + # Color of desktop background. + # + background: "#333333" + + ## + # Settings for columns drawn by wmii. + # + # mode: <the wmii "colmode" setting> + # rule: <the wmii "colrules" setting> + # + column: + mode: default + rule: | + /gimp/ -> 17+83+41 + /.*/ -> 62+38 # Golden Ratio + + ## + # Mapping of clients to views they must appear on. + # + # - <client props regular expression> : <tags to apply> + # + # These mappings are processed in top-to-bottom order. + # Processing stops after the first matching mapping is applied. + # + client: + - /MPlayer|VLC/ : ~ + + ## + # Self-refreshing buttons on the status bar. + # + # - <button name>: + # refresh: <number of seconds to wait before refreshing the content> + # content: <Ruby code whose result is displayed as the content> + # click: <Ruby code to handle mouse clicks on the status button. + # This code has access to a "mouse_button" variable which is + # an integer representing the mouse button that was clicked.> + # + # You can refresh a particular status button in Ruby using: + # + # status "your button name" + # + # The horizontal order in which these buttons appear on the status + # bar reflects the vertical order in which they are defined below. + # + status: + - system_load: + refresh: 10 + content: | + load_averages = File.read('/proc/loadavg').split.first(3) + current_load = load_averages.first.to_f + + # visually indicate the intensity of system load + color = case + when current_load > 3.0 then CONFIG['display']['color']['error'] + when current_load > 1.5 then CONFIG['display']['color']['notice'] + end + + [color, *load_averages] + + - clock: + refresh: 5 + content: Time.now.to_s + + +## +# Interaction settings. +# +control: + + ## + # The wmii "grabmod" setting. + # + grab: Mod4 + + ## + # Key sequence prefixes. + # + mod: Mod4 + move: Mod4-Shift + swap: Mod4-w + view: Mod4-v + group: Mod4-g + + ## + # Direction keys. + # + up: k + down: j + left: h + right: l + + ## + # Sequence keys. + # + prev: b + next: n + + ## + # Key bindings. + # + # <key sequence>: <Ruby code to execute> + # + # A key sequence may contain ${...} expressions which + # are replaced with the value corresponding to '...' + # in the 'control' section of this configuration file. + # + # For example, if the 'control' section of + # this configuration file appeared like this: + # + # control: + # foo: Mod4 + # bar: y + # + # and the following key sequence was used: + # + # ${foo}-${bar},${bar} + # + # then after ${...} expression replacement, + # that key sequence would appear like this: + # + # Mod4-y,y + # + key: + #--------------------------------------------------------------------------- + # focus + #--------------------------------------------------------------------------- + + ${mod}-${up}: | # focus above client + curr_view.select(:up) rescue nil + + ${mod}-${down}: | # focus below client + curr_view.select(:down) rescue nil + + ${mod}-${left}: | # focus left client + curr_view.select(:left) rescue nil + + ${mod}-${right}: | # focus right client + curr_view.select(:right) rescue nil + + ${mod}-space: | # focus floating area (toggle) + curr_view.select(:toggle) + + ${mod}-${prev}: | # focus previous view + prev_view.focus + + ${mod}-${next}: | # focus next view + next_view.focus + + # focus the view whose index or name equals the pressed number + ${mod}-1: focus_view tags[0] || 1 + ${mod}-2: focus_view tags[1] || 2 + ${mod}-3: focus_view tags[2] || 3 + ${mod}-4: focus_view tags[3] || 4 + ${mod}-5: focus_view tags[4] || 5 + ${mod}-6: focus_view tags[5] || 6 + ${mod}-7: focus_view tags[6] || 7 + ${mod}-8: focus_view tags[7] || 8 + ${mod}-9: focus_view tags[8] || 9 + ${mod}-0: focus_view tags[9] || 10 + + # focus the view whose name begins with the pressed alphabet + ${view},a: t = tags.grep(/^a/i).first and focus_view(t) + ${view},b: t = tags.grep(/^b/i).first and focus_view(t) + ${view},c: t = tags.grep(/^c/i).first and focus_view(t) + ${view},d: t = tags.grep(/^d/i).first and focus_view(t) + ${view},e: t = tags.grep(/^e/i).first and focus_view(t) + ${view},f: t = tags.grep(/^f/i).first and focus_view(t) + ${view},g: t = tags.grep(/^g/i).first and focus_view(t) + ${view},h: t = tags.grep(/^h/i).first and focus_view(t) + ${view},i: t = tags.grep(/^i/i).first and focus_view(t) + ${view},j: t = tags.grep(/^j/i).first and focus_view(t) + ${view},k: t = tags.grep(/^k/i).first and focus_view(t) + ${view},l: t = tags.grep(/^l/i).first and focus_view(t) + ${view},m: t = tags.grep(/^m/i).first and focus_view(t) + ${view},n: t = tags.grep(/^n/i).first and focus_view(t) + ${view},o: t = tags.grep(/^o/i).first and focus_view(t) + ${view},p: t = tags.grep(/^p/i).first and focus_view(t) + ${view},q: t = tags.grep(/^q/i).first and focus_view(t) + ${view},r: t = tags.grep(/^r/i).first and focus_view(t) + ${view},s: t = tags.grep(/^s/i).first and focus_view(t) + ${view},t: t = tags.grep(/^t/i).first and focus_view(t) + ${view},u: t = tags.grep(/^u/i).first and focus_view(t) + ${view},v: t = tags.grep(/^v/i).first and focus_view(t) + ${view},w: t = tags.grep(/^w/i).first and focus_view(t) + ${view},x: t = tags.grep(/^x/i).first and focus_view(t) + ${view},y: t = tags.grep(/^y/i).first and focus_view(t) + ${view},z: t = tags.grep(/^z/i).first and focus_view(t) + + #--------------------------------------------------------------------------- + # move + #--------------------------------------------------------------------------- + + ${move}-${up}: | # move grouping toward the top + grouping.each {|c| c.send(:up) rescue nil } + + ${move}-${down}: | # move grouping toward the bottom + grouping.each {|c| c.send(:down) rescue nil } + + ${move}-${left}: | # move grouping toward the left + grouping.each {|c| c.send(:left) rescue nil } + + ${move}-${right}: | # move grouping toward the right + grouping.each {|c| c.send(:right) rescue nil } + + ${move}-space: | # move grouping to floating area (toggle) + grouping.each {|c| c.send(:toggle) rescue nil } + + ${move}-t: | # move grouping to chosen view + # + # Changes the tag (according to a menu choice) of + # each grouped client and returns the chosen tag. + # + # The +tag -tag idea is from Jonas Pfenniger: + # + # http://zimbatm.oree.ch/articles/2006/06/15/wmii-3-and-ruby + # + choices = tags.map {|t| [t, "+#{t}", "-#{t}"] }.flatten + + if target = key_menu(choices, 'tag as:') + grouping.each {|c| c.tags = target } + end + + # move grouping to the view whose index or name equals the pressed number + ${move}-1: grouping.each {|c| c.tags = tags[0] || 1 } + ${move}-2: grouping.each {|c| c.tags = tags[1] || 2 } + ${move}-3: grouping.each {|c| c.tags = tags[2] || 3 } + ${move}-4: grouping.each {|c| c.tags = tags[3] || 4 } + ${move}-5: grouping.each {|c| c.tags = tags[4] || 5 } + ${move}-6: grouping.each {|c| c.tags = tags[5] || 6 } + ${move}-7: grouping.each {|c| c.tags = tags[6] || 7 } + ${move}-8: grouping.each {|c| c.tags = tags[7] || 8 } + ${move}-9: grouping.each {|c| c.tags = tags[8] || 9 } + ${move}-0: grouping.each {|c| c.tags = tags[9] || 10 } + + #--------------------------------------------------------------------------- + # group + #--------------------------------------------------------------------------- + + ${group},g: | # toggle current client from grouping + curr_client.group! + + ${group},c: | # add clients in current area to grouping + curr_area.group + + ${group},Shift-c: | # remove clients in current area from grouping + curr_area.ungroup + + ${group},f: | # add clients in floating area to grouping + Area.floating.group + + ${group},Shift-f: | # remove clients in floating area from grouping + Area.floating.ungroup + + ${group},m: | # add clients in managed areas to grouping + curr_view.managed_areas.each {|a| a.group } + + ${group},Shift-m: | # remove clients in managed areas from grouping + curr_view.managed_areas.each {|a| a.ungroup } + + ${group},v: | # add clients in current view to grouping + curr_view.group + + ${group},Shift-v: | # remove clients in current view from grouping + curr_view.ungroup + + ${group},i: | # invert the grouping in the current view + curr_view.group! + + ${group},Shift-i: | # invert the grouping in all views + Rumai.group! + + ${group},n: | # remove all clients everywhere from grouping + Rumai.ungroup + + #--------------------------------------------------------------------------- + # swap + #--------------------------------------------------------------------------- + + ${swap},${up}: | # swap with above client + curr_client.swap(:up) rescue nil + + ${swap},${down}: | # swap with below client + curr_client.swap(:down) rescue nil + + ${swap},${left}: | # swap with left client + curr_client.swap(:left) rescue nil + + ${swap},${right}: | # swap with right client + curr_client.swap(:right) rescue nil + + # swap current client with the column whose index equals the pressed number + ${swap},1: curr_client.swap 1 + ${swap},2: curr_client.swap 2 + ${swap},3: curr_client.swap 3 + ${swap},4: curr_client.swap 4 + ${swap},5: curr_client.swap 5 + ${swap},6: curr_client.swap 6 + ${swap},7: curr_client.swap 7 + ${swap},8: curr_client.swap 8 + ${swap},9: curr_client.swap 9 + ${swap},0: curr_client.swap 10 + + #--------------------------------------------------------------------------- + # client + #--------------------------------------------------------------------------- + + ${mod}-f: | # zoom client to fullscreen (toggle) + curr_client.fullscreen! + + ${mod}-Shift-c: | # kill the current client + curr_client.kill + + #--------------------------------------------------------------------------- + # column + #--------------------------------------------------------------------------- + + ${mod}-d: | # apply equal-spacing layout to current column + curr_area.layout = 'default-max' + + ${mod}-s: | # apply stacked layout to current column + curr_area.layout = 'stack-max' + + ${mod}-m: | # apply maximized layout to current column + curr_area.layout = 'stack+max' + + #--------------------------------------------------------------------------- + # menu + #--------------------------------------------------------------------------- + + ${mod}-a: | # run internal action chosen from a menu + if choice = key_menu(actions, 'run action:') + action choice + end + + ${mod}-p: | # run external program chosen from a menu + if choice = key_menu(@programs, 'run program:') + launch choice + end + + ${mod}-t: | # focus view chosen from a menu + if choice = key_menu(tags, 'show view:') + focus_view choice + end + + #--------------------------------------------------------------------------- + # launcher + #--------------------------------------------------------------------------- + + ${mod}-Return: | # launch a terminal + # + # Launch a new terminal and set its + # working directory to be the same + # as the currently focused terminal. + # + work = ENV['HOME'] + + label = curr_client.label.read rescue '' + + # iterate in reverse order because + # paths are usually at end of label + label.split(' ').reverse_each do |s| + path = File.expand_path(s) + + if File.exist? path + unless File.directory? path + path = File.dirname(path) + end + + work = path + break + end + end + + require 'fileutils' + FileUtils.cd work do + launch CONFIG['program']['terminal'] + end + + ## + # Event handlers. + # + # <event name>: <Ruby code to execute> + # + # The Ruby code has access to an "argv" variable which + # is a list of arguments that were passed to the event. + # + event: + CreateTag: | + tag = argv[0] + but = fs.lbar[tag] + but.create unless but.exist? + but.write "#{CONFIG['display']['color']['normal']} #{tag}" + + DestroyTag: | + tag = argv[0] + but = fs.lbar[tag] + but.remove if but.exist? + + FocusTag: | + tag = argv[0] + but = fs.lbar[tag] + but.write "#{CONFIG['display']['color']['focus']} #{tag}" if but.exist? + + UnfocusTag: | + tag = argv[0] + but = fs.lbar[tag] + but.write "#{CONFIG['display']['color']['normal']} #{tag}" if but.exist? + + UrgentTag: | + tag = argv[1] + but = fs.lbar[tag] + but.write "#{CONFIG['display']['color']['notice']} #{tag}" if but.exist? + + NotUrgentTag: | + tag = argv[1] + but = fs.lbar[tag] + color = curr_view.id == tag ? 'focus' : 'normal' + but.write "#{CONFIG['display']['color'][color]} #{tag}" if but.exist? + + LeftBarClick: &LeftBarClick | + mouse_button, view_id = argv + + if mouse_button == '1' # primary button + focus_view view_id + end + + ## + # allows the user to drag a file over a + # view button and activate that view while + # still holding on to their dragged file! + # + LeftBarDND: *LeftBarClick + + RightBarClick: | + status_click *argv.reverse + + ClientMouseDown: | + client_id, mouse_button = argv + + if mouse_button == '3' # secondary button + client = Client.new(client_id) + + case click_menu %w[stick group fullscreen kill slay], 'client' + when 'stick' then client.stick! + when 'group' then client.group! + when 'fullscreen' then client.fullscreen! + when 'kill' then client.kill + when 'slay' then client.slay + end + end + + ## + # Internal scripts. + # + # <action name>: <Ruby code to execute> + # + action: + + +## +# Arbitrary logic. +# +# script: +# before: <Ruby code to execute before processing this file> +# after: <Ruby code to execute after processing this file> +# +script: + before: + after: diff --git a/alternative_wmiircs/ruby/wmiirc b/alternative_wmiircs/ruby/wmiirc new file mode 100755 index 0000000..2545137 --- /dev/null +++ b/alternative_wmiircs/ruby/wmiirc @@ -0,0 +1,88 @@ +#!/usr/bin/env ruby +# +# Bootloader for wmii configuration. +# +#-- +# Copyright protects this work. +# See LICENSE file for details. +#++ + +# create a logger to aid debugging +require 'logger' +LOG = Logger.new(__FILE__ + '.log', 5) + +class << LOG + # emulate IO.write + alias write << + + def flush + # ignore + end +end + +# capture standard output in logger +$stdout = $stderr = LOG + +begin + LOG.info 'birth' + + # load configuration library + def find_config file + base_dirs = ENV['WMII_CONFPATH'].to_s.split(/:+/) + ruby_dirs = base_dirs.map {|dir| File.join(dir, 'ruby') } + + Dir["{#{base_dirs.zip(ruby_dirs).join(',')}}/#{file}"].first + end + + require find_config('config.rb') + + # terminate any existing wmiirc + fs.event.write 'Start wmiirc' + + event 'Start' do |arg| + exit if arg == 'wmiirc' + end + + # apply user configuration + load_config find_config('config.yaml') + + # setup tag bar (buttons that correspond to views) + fs.lbar.clear + tags.each {|t| event 'CreateTag', t } + event 'FocusTag', curr_tag + + # register key bindings + fs.keys.write keys.join("\n") + event('Key') {|*a| key(*a) } + + # the main event loop + fs.event.each_line do |line| + line.split("\n").each do |call| + name, args = call.split(' ', 2) + + argv = args.to_s.split(' ') + event name, *argv + end + end + +rescue SystemExit + # ignore it; the program wants to terminate + +rescue Exception => e + LOG.error e + + # allow the user to rescue themselves + system '@TERMINAL@ &' + + IO.popen('xmessage -nearmouse -file - -buttons Recover,Ignore -print', 'w+') do |f| + f.puts e.inspect, e.backtrace + f.close_write + + if f.read.chomp == 'Recover' + reload_config + end + end + +ensure + LOG.info 'death' +end |