summaryrefslogtreecommitdiff
path: root/alternative_wmiircs
diff options
context:
space:
mode:
Diffstat (limited to 'alternative_wmiircs')
-rw-r--r--alternative_wmiircs/Makefile13
-rw-r--r--alternative_wmiircs/README23
-rw-r--r--alternative_wmiircs/plan9port/Makefile9
-rw-r--r--alternative_wmiircs/plan9port/README10
-rwxr-xr-xalternative_wmiircs/plan9port/wmiirc284
-rw-r--r--alternative_wmiircs/python/Makefile14
-rw-r--r--alternative_wmiircs/python/README16
-rw-r--r--alternative_wmiircs/python/pygmi/Makefile12
-rw-r--r--alternative_wmiircs/python/pygmi/__init__.py27
-rw-r--r--alternative_wmiircs/python/pygmi/event.py304
-rw-r--r--alternative_wmiircs/python/pygmi/fs.py699
-rw-r--r--alternative_wmiircs/python/pygmi/menu.py54
-rw-r--r--alternative_wmiircs/python/pygmi/monitor.py118
-rw-r--r--alternative_wmiircs/python/pygmi/util.py66
-rw-r--r--alternative_wmiircs/python/pyxp/Makefile15
-rw-r--r--alternative_wmiircs/python/pyxp/__init__.py7
-rw-r--r--alternative_wmiircs/python/pyxp/asyncclient.py193
-rw-r--r--alternative_wmiircs/python/pyxp/client.py346
-rw-r--r--alternative_wmiircs/python/pyxp/dial.py35
-rw-r--r--alternative_wmiircs/python/pyxp/fcall.py131
-rw-r--r--alternative_wmiircs/python/pyxp/fields.py132
-rw-r--r--alternative_wmiircs/python/pyxp/messages.py73
-rw-r--r--alternative_wmiircs/python/pyxp/mux.py195
-rw-r--r--alternative_wmiircs/python/pyxp/types.py55
-rwxr-xr-xalternative_wmiircs/python/wmiirc12
-rw-r--r--alternative_wmiircs/python/wmiirc.py308
-rw-r--r--alternative_wmiircs/ruby/HISTORY233
-rw-r--r--alternative_wmiircs/ruby/LICENSE21
-rw-r--r--alternative_wmiircs/ruby/Makefile13
-rw-r--r--alternative_wmiircs/ruby/README94
-rw-r--r--alternative_wmiircs/ruby/config.rb547
-rw-r--r--alternative_wmiircs/ruby/config.yaml536
-rwxr-xr-xalternative_wmiircs/ruby/wmiirc88
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