shithub: fork

Download patch

ref: b804f62dceead0d0398f427b8e58cf50ef390f2d
parent: 1270ba4418294c69d04242d13621d24b10a4120f
author: qwx <[email protected]>
date: Sat Aug 19 04:53:26 EDT 2023

more theming

diff: cannot open b/sys/src/cmd/audio/zuke//null: file does not exist: 'b/sys/src/cmd/audio/zuke//null' diff: cannot open b/sys/src/cmd/audio//null: file does not exist: 'b/sys/src/cmd/audio//null' diff: cannot open b/sys/src/cmd/faces//null: file does not exist: 'b/sys/src/cmd/faces//null' diff: cannot open b/sys/src/cmd/spred//null: file does not exist: 'b/sys/src/cmd/spred//null'
--- /dev/null
+++ b/sys/src/cmd/audio/zuke/zuke.c
@@ -1,0 +1,1659 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include <draw.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <plumb.h>
+#include <ctype.h>
+#include "plist.h"
+#include "icy.h"
+
+#define MAX(a,b) ((a)>=(b)?(a):(b))
+#define MIN(a,b) ((a)<=(b)?(a):(b))
+#define CLAMP(x,min,max) MAX(min, MIN(max, x))
+
+typedef struct Color Color;
+typedef struct Player Player;
+typedef struct Playlist Playlist;
+
+enum
+{
+	Cstart = 1,
+	Cstop,
+	Ctoggle,
+	Cseekrel,
+
+	Rgdisabled = 0,
+	Rgtrack,
+	Rgalbum,
+	Numrg,
+
+	Everror = 1,
+	Evready,
+
+	Seek = 10, /* 10 seconds */
+	Seekfast = 60, /* a minute */
+
+	Bps = 44100*2*2, /* 44100KHz, stereo, s16 for a sample */
+	Relbufsz = Bps/2, /* 0.5s */
+
+	Dback = 0,
+	Dfhigh,
+	Dfmed,
+	Dflow,
+	Dfinv,
+	Dbmed,
+	Dblow,
+	Dbinv,
+	Numcolors,
+
+	Ncol = 10,
+};
+
+struct Color {
+	u32int rgb;
+	Image *im;
+};
+
+struct Player
+{
+	Channel *ctl;
+	Channel *ev;
+	Channel *img;
+	Channel *icytitlec;
+	char *icytitle;
+	double seek;
+	double gain;
+	int pcur;
+};
+
+struct Playlist
+{
+	Meta *m;
+	int n;
+	char *raw;
+	int rawsz;
+};
+
+int mainstacksize = 65536;
+
+static int debug;
+static int audio = -1;
+static int volume, rg;
+static int pnotifies;
+static Playlist *pl;
+static Player *playernext;
+static Player *playercurr;
+static vlong byteswritten;
+static int pcur, pcurplaying;
+static int scroll, scrollsz;
+static Font *f;
+static Image *cover;
+static Channel *playc;
+static Channel *redrawc;
+static Mousectl *mctl;
+static Keyboardctl kctl;
+static int shiftdown;
+static int colwidth[Ncol];
+static int mincolwidth[Ncol];
+static char *cols = "AatD";
+static int colspath;
+static int *shuffle;
+static int repeatone;
+static int stopafter;
+static Rectangle seekbar;
+static int seekmx, newseekmx = -1;
+static double seekoff; /* ms */
+static Lock audiolock;
+static int audioerr = 0;
+static Biobuf out;
+static char *covers[] =
+{
+	"art", "folder", "cover", "Cover", "scans/CD", "Scans/Front", "Covers/Front"
+};
+
+static Color colors[Numcolors] =
+{
+	[Dback]  = {0xf0f0f0},
+	[Dfhigh] = {0xffffff},
+	[Dfmed]  = {0x343434},
+	[Dflow]  = {0xa5a5a5},
+	[Dfinv]  = {0x323232},
+	[Dbmed]  = {0x72dec2},
+	[Dblow]  = {0x404040},
+	[Dbinv]  = {0xffb545},
+};
+
+static int Scrollwidth;
+static int Scrollheight;
+static int Seekthicc;
+static int Coversz;
+
+static char *
+matchvname(char **s)
+{
+	char *names[] = {"mix", "master", "pcm out"};
+	int i, l;
+
+	for(i = 0; i < nelem(names); i++){
+		l = strlen(names[i]);
+		if(strncmp(*s, names[i], l) == 0){
+			*s += l;
+			return names[i];
+		}
+	}
+
+	return nil;
+}
+
+static void
+chvolume(int d)
+{
+	int f, x, ox, want, try;
+	char *s, *e;
+	Biobuf b;
+	char *n;
+
+	if((f = open("/dev/volume", ORDWR|OCEXEC)) < 0)
+		return;
+	Binit(&b, f, OREAD);
+
+	want = x = -1;
+	ox = 0;
+	for(try = 0; try < 10; try++){
+		for(n = nil; (s = Brdline(&b, '\n')) != nil;){
+			if((n = matchvname(&s)) != nil && (ox = strtol(s, &e, 10)) >= 0 && s != e)
+				break;
+			n = nil;
+		}
+
+		if(want < 0){
+			want = CLAMP(ox+d, 0, 100);
+			x = ox;
+		}
+		if(n == nil || (d > 0 && ox >= want) || (d < 0 && ox <= want))
+			break;
+		x = CLAMP(x+d, 0, 100);
+		if(fprint(f, "%s %d\n", n, x) < 0)
+			break;
+		/* go to eof and back */
+		while(Brdline(&b, '\n') != nil);
+		Bseek(&b, 0, 0);
+	}
+
+	volume = CLAMP(ox, 0, 100);
+
+	Bterm(&b);
+	close(f);
+}
+
+static void
+audioon(void)
+{
+	lock(&audiolock);
+	if(audio < 0){
+		if((audio = open("/dev/audio", OWRITE|OCEXEC)) < 0 && audioerr == 0){
+			fprint(2, "%r\n");
+			audioerr = 1;
+		}else{
+			chvolume(0);
+		}
+	}
+	unlock(&audiolock);
+}
+
+static void
+audiooff(void)
+{
+	lock(&audiolock);
+	close(audio);
+	audio = -1;
+	audioerr = 0;
+	unlock(&audiolock);
+}
+
+#pragma varargck type "P" uvlong
+static int
+positionfmt(Fmt *f)
+{
+	char *s, tmp[16];
+	u64int sec;
+
+	s = tmp;
+	sec = va_arg(f->args, uvlong);
+	if(sec >= 3600){
+		s = seprint(s, tmp+sizeof(tmp), "%02lld:", sec/3600);
+		sec %= 3600;
+	}
+	s = seprint(s, tmp+sizeof(tmp), "%02lld:", sec/60);
+	sec %= 60;
+	seprint(s, tmp+sizeof(tmp), "%02lld", sec);
+
+	return fmtstrcpy(f, tmp);
+}
+
+static char *
+getcol(Meta *m, int c)
+{
+	static char tmp[32];
+	char *s;
+
+	s = nil;
+	switch(c){
+	case Palbum: s = m->album; break;
+	case Partist: s = m->artist[0]; break;
+	case Pdate: s = m->date; break;
+	case Ptitle: s = (!colspath && (m->title == nil || *m->title == 0)) ? m->basename : m->title; break;
+	case Ptrack: snprint(tmp, sizeof(tmp), "%4s", m->track); s = m->track ? tmp : nil; break;
+	case Ppath: s = m->path; break;
+	case Pduration:
+		tmp[0] = 0;
+		if(m->duration > 0)
+			snprint(tmp, sizeof(tmp), "%8P", m->duration/1000);
+		s = tmp;
+		break;
+	default: sysfatal("invalid column '%c'", c);
+	}
+
+	return s ? s : "";
+}
+
+static Meta *
+getmeta(int i)
+{
+	return &pl->m[shuffle != nil ? shuffle[i] : i];
+}
+
+static void
+updatescrollsz(void)
+{
+	scrollsz = Dy(screen->r)/f->height - 2;
+}
+
+static void
+redraw_(int full)
+{
+	static Image *back, *ocover;
+	static int oscrollcenter, opcur, opcurplaying;
+
+	int x, i, j, scrollcenter, w;
+	uvlong dur, msec;
+	Rectangle sel, r;
+	char tmp[32], *s;
+	Point p, sp, p₀, p₁;
+	Image *col;
+
+	/* seekbar playback/duration text */
+	i = snprint(tmp, sizeof(tmp), "%s%s%s%s",
+		rg ? (rg == Rgalbum ? "ᴬ" : "ᵀ") : "",
+		stopafter ? "ⁿ" : repeatone ? "¹" : "",
+		shuffle != nil ? "∫" : "",
+		(rg || stopafter || repeatone || shuffle != nil) ? " " : ""
+	);
+	msec = 0;
+	dur = 0;
+	w = stringwidth(f, tmp);
+	if(pcurplaying >= 0){
+		msec = byteswritten*1000/Bps;
+		dur = getmeta(pcurplaying)->duration;
+		if(dur > 0){
+			snprint(tmp+i, sizeof(tmp)-i, "%P/%P ", dur/1000, dur/1000);
+			w += stringwidth(f, tmp+i);
+			msec = MIN(msec, dur);
+			i += snprint(tmp+i, sizeof(tmp)-i, "%P/%P ",
+				(uvlong)(newseekmx >= 0 ? seekoff : msec)/1000,
+				dur/1000
+			);
+		}else{
+			j = snprint(tmp+i, sizeof(tmp)-i, "%P ", msec/1000);
+			w += stringwidth(f, tmp+i);
+			i += j;
+		}
+	}
+	snprint(tmp+i, sizeof(tmp)-i, "%d%%", 100);
+	w += stringwidth(f, tmp+i);
+	snprint(tmp+i, sizeof(tmp)-i, "%d%%", volume);
+
+	lockdisplay(display);
+
+	if(back == nil || Dx(screen->r) != Dx(back->r) || Dy(screen->r) != Dy(back->r)){
+		freeimage(back);
+		back = allocimage(display, Rpt(ZP,subpt(screen->r.max, screen->r.min)), XRGB32, 0, DNofill);
+		full = 1;
+	}
+
+	r = screen->r;
+
+	/* scrollbar */
+	p₀ = Pt(r.min.x, r.min.y);
+	p₁ = Pt(r.min.x+Scrollwidth, r.max.y-Seekthicc);
+	if(scroll < 1)
+		scrollcenter = 0;
+	else
+		scrollcenter = (p₁.y-p₀.y-Scrollheight/2 - Seekthicc)*scroll / (pl->n - scrollsz);
+	if(full || oscrollcenter != scrollcenter){
+		draw(screen, Rpt(p₀, Pt(p₁.x, p₁.y)), colors[Dback].im, nil, ZP);
+		line(screen, Pt(p₁.x, p₀.y), p₁, Endsquare, Endsquare, 0, colors[Dflow].im, ZP);
+		r = Rpt(
+			Pt(p₀.x+1, p₀.y + scrollcenter + Scrollheight/4),
+			Pt(p₁.x-1, p₀.y + scrollcenter + Scrollheight/4 + Scrollheight)
+		);
+		/* scrollbar handle */
+		draw(screen, r, colors[Dblow].im, nil, ZP);
+	}
+
+	/* seek bar rectangle */
+	r = Rpt(Pt(p₀.x, p₁.y), Pt(screen->r.max.x-w-4, screen->r.max.y));
+
+	/* playback/duration text */
+	draw(screen, Rpt(Pt(r.max.x, p₁.y), screen->r.max), colors[Dblow].im, nil, ZP);
+	p = addpt(Pt(screen->r.max.x - stringwidth(f, tmp) - 4, p₁.y), Pt(2, 2));
+	string(screen, p, colors[Dfhigh].im, ZP, f, tmp);
+
+	/* seek control */
+	if(pcurplaying >= 0 && dur > 0){
+		border(screen, r, 3, colors[Dblow].im, ZP);
+		r = insetrect(r, 3);
+		seekbar = r;
+		p = r.min;
+		x = p.x + Dx(r) * (double)msec / (double)dur;
+		r.min.x = x;
+		draw(screen, r, colors[Dback].im, nil, ZP);
+		r.min.x = p.x;
+		r.max.x = x;
+		draw(screen, r, colors[Dbmed].im, nil, ZP);
+	}else
+		draw(screen, r, colors[Dblow].im, nil, ZP);
+
+	Rectangle bp[2] = {
+		Rpt(addpt(screen->r.min, Pt(Scrollwidth+1, 0)), subpt(screen->r.max, Pt(0, Seekthicc))), 
+		ZR,
+	};
+
+	if(cover != nil){
+		r.min.x = screen->r.max.x - Dx(cover->r) - 8;
+		r.min.y = p₁.y - Dy(cover->r) - 6;
+		r.max.x = screen->r.max.x;
+		r.max.y = p₁.y + 2;
+		if(full || cover != ocover){
+			border(screen, r, 4, colors[Dblow].im, ZP);
+			draw(screen, insetrect(r, 4), cover, nil, ZP);
+		}
+		bp[1] = bp[0];
+		bp[0].max.y = r.min.y;
+		bp[1].max.x = r.min.x;
+		bp[1].min.y = r.min.y;
+	}else if(ocover != nil)
+		full = 1;
+
+	/* playlist */
+	if(full || oscrollcenter != scrollcenter || pcur != opcur || pcurplaying != opcurplaying){
+		draw(back, back->r, colors[Dback].im, nil, ZP);
+
+		p.x = sp.x = Scrollwidth;
+		p.y = 0;
+		sp.y = back->r.max.y;
+		for(i = 0; cols[i+1] != 0; i++){
+			p.x += colwidth[i] + 4;
+			sp.x = p.x;
+			line(back, p, sp, Endsquare, Endsquare, 0, colors[Dflow].im, ZP);
+			p.x += 4;
+		}
+
+		sp.x = sp.y = 0;
+		p.x = Scrollwidth + 2;
+		p.y = back->r.min.y + 2;
+
+		for(i = scroll; i < pl->n; i++, p.y += f->height){
+			if(i < 0)
+				continue;
+			if(p.y > back->r.max.y)
+				break;
+
+			if(pcur == i){
+				sel.min.x = Scrollwidth;
+				sel.min.y = p.y;
+				sel.max.x = back->r.max.x;
+				sel.max.y = p.y + f->height;
+				replclipr(back, 0, back->r);
+				draw(back, sel, colors[Dbinv].im, nil, ZP);
+				col = colors[Dfinv].im;
+			}else{
+				col = colors[Dfmed].im;
+			}
+
+			sel = back->r;
+
+			p.x = Scrollwidth + 2 + 3;
+			for(j = 0; cols[j] != 0; j++){
+				sel.max.x = cols[j+1] ? (p.x + colwidth[j] - 1) : back->r.max.x;
+				replclipr(back, 0, sel);
+				if(playercurr != nil && playercurr->icytitle != nil && pcurplaying == i && cols[j] == Ptitle)
+					s = playercurr->icytitle;
+				else
+					s = getcol(getmeta(i), cols[j]);
+				string(back, p, col, sp, f, s);
+				p.x += colwidth[j] + 8;
+			}
+
+			if(pcurplaying == i){
+				Point rightp, leftp;
+				leftp.y = rightp.y = p.y - 1;
+				leftp.x = Scrollwidth;
+				rightp.x = back->r.max.x;
+				replclipr(back, 0, back->r);
+				line(back, leftp, rightp, 0, 0, 0, colors[Dflow].im, sp);
+				leftp.y = rightp.y = p.y + f->height;
+				line(back, leftp, rightp, 0, 0, 0, colors[Dflow].im, sp);
+			}
+		}
+
+		for(i = 0; bp[i].max.x ; i++)
+			draw(screen, bp[i], back, nil, subpt(bp[i].min, screen->r.min));
+	}
+	oscrollcenter = scrollcenter;
+	opcurplaying = pcurplaying;
+	ocover = cover;
+	opcur = pcur;
+
+	flushimage(display, 1);
+	unlockdisplay(display);
+}
+
+static void
+redrawproc(void *)
+{
+	ulong full, nbfull, another;
+
+	threadsetname("redraw");
+	while(recv(redrawc, &full) == 1){
+Again:
+		redraw_(full);
+		another = 0;
+		full = 0;
+		while(nbrecv(redrawc, &nbfull) > 0){
+			full |= nbfull;
+			another = 1;
+		}
+		if(another)
+			goto Again;
+	}
+
+	threadexits(nil);
+}
+
+static void
+redraw(int full)
+{
+	sendul(redrawc, full);
+}
+
+static void
+coverload(void *player_)
+{
+	int p[2], pid, fd, i;
+	char *prog, *path, *s, tmp[64];
+	Meta *m;
+	Channel *ch;
+	Player *player;
+	Image *newcover;
+
+	threadsetname("cover");
+	player = player_;
+	m = getmeta(player->pcur);
+	pid = -1;
+	ch = player->img;
+	fd = -1;
+	prog = nil;
+
+	if(m->imagefmt != nil)
+		prog = "audio/readtags -i";
+	else{
+		path = strdup(m->path);
+		if(path != nil && (s = utfrrune(path, '/')) != nil){
+			*s = 0;
+
+			for(i = 0; i < nelem(covers) && prog == nil; i++){
+				if((s = smprint("%s/%s.jpg", path, covers[i])) != nil && (fd = open(s, OREAD|OCEXEC)) >= 0)
+					prog = "jpg -9t";
+				free(s);
+				s = nil;
+				if(fd < 0 && (s = smprint("%s/%s.png", path, covers[i])) != nil && (fd = open(s, OREAD|OCEXEC)) >= 0)
+					prog = "png -9t";
+				free(s);
+			}
+		}
+		free(path);
+	}
+
+	if(prog == nil)
+		goto done;
+	if(fd < 0)
+		fd = open(m->path, OREAD|OCEXEC);
+	snprint(tmp, sizeof(tmp), "%s | resample -x%d", prog, Coversz);
+	pipe(p);
+	if((pid = rfork(RFPROC|RFFDG|RFNOTEG|RFCENVG|RFNOWAIT)) == 0){
+		dup(fd, 0); close(fd);
+		dup(p[1], 1); close(p[1]);
+		if(!debug){
+			dup(fd = open("/dev/null", OWRITE), 2);
+			close(fd);
+		}
+		execl("/bin/rc", "rc", "-c", tmp, nil);
+		sysfatal("execl: %r");
+	}
+	close(fd);
+	close(p[1]);
+
+	if(pid > 0){
+		newcover = readimage(display, p[0], 1);
+		/* if readtags fails, readimage will also fail, and we send nil over ch */
+		sendp(ch, newcover);
+	}
+	close(p[0]);
+done:
+	if(pid < 0)
+		sendp(ch, nil);
+	chanclose(ch);
+	chanfree(ch);
+	postnote(PNGROUP, pid, "die");
+	threadexits(nil);
+}
+
+static int
+playerret(Player *player)
+{
+	return recvul(player->ev) == Everror ? -1 : 0;
+}
+
+static void
+pnotify(Player *p)
+{
+	Meta *m;
+	int i;
+
+	if(!pnotifies)
+		return;
+
+	if(p != nil){
+		m = getmeta(p->pcur);
+		for(i = 0; cols[i] != 0; i++)
+			Bprint(&out, "%s\t", getcol(m, cols[i]));
+	}
+	Bprint(&out, "\n");
+	Bflush(&out);
+}
+
+static void
+stop(Player *player)
+{
+	if(player == nil)
+		return;
+
+	if(player == playernext)
+		playernext = nil;
+	if(!getmeta(player->pcur)->filefmt[0])
+		playerret(player);
+	if(player == playercurr)
+		pnotify(nil);
+	sendul(player->ctl, Cstop);
+}
+
+static void
+start(Player *player)
+{
+	if(player == nil)
+		return;
+	if(!getmeta(player->pcur)->filefmt[0])
+		playerret(player);
+	pnotify(player);
+	sendul(player->ctl, Cstart);
+}
+
+static void playerthread(void *player_);
+
+static void
+setgain(Player *player)
+{
+	if(player == nil)
+		return;
+	if(rg == Rgdisabled)
+		player->gain = 0.0;
+	if(rg == Rgtrack)
+		player->gain = getmeta(player->pcur)->rgtrack;
+	else if(rg == Rgalbum)
+		player->gain = getmeta(player->pcur)->rgalbum;
+	player->gain = pow(10.0, player->gain/20.0);
+}
+
+static Player *
+newplayer(int pcur, int loadnext)
+{
+	Player *player;
+
+	if(playernext != nil && loadnext){
+		if(pcur == playernext->pcur){
+			player = playernext;
+			playernext = nil;
+			goto done;
+		}
+		stop(playernext);
+		playernext = nil;
+	}
+
+	player = mallocz(sizeof(*player), 1);
+	player->ctl = chancreate(sizeof(ulong), 0);
+	player->ev = chancreate(sizeof(ulong), 0);
+	player->pcur = pcur;
+	setgain(player);
+
+	threadcreate(playerthread, player, 32768);
+	if(getmeta(pcur)->filefmt[0] && playerret(player) < 0)
+		return nil;
+
+done:
+	if(pcur < pl->n-1 && playernext == nil && loadnext)
+		playernext = newplayer(pcur+1, 0);
+
+	return player;
+}
+
+static long
+iosetname(va_list *)
+{
+	procsetname("player/io");
+	return 0;
+}
+
+static int
+clip16(int v)
+{
+	if(v > 0x7fff)
+		return 0x7fff;
+	if(v < -0x8000)
+		return -0x8000;
+	return v;
+}
+
+static void
+gain(double g, char *buf, long n)
+{
+	s16int *f;
+
+	if(g != 1.0)
+		for(f = (s16int*)buf; n >= 4; n -= 4){
+			*f++ = clip16(*f * g);
+			*f++ = clip16(*f * g);
+		}
+}
+
+static void
+playerthread(void *player_)
+{
+	char *buf, cmd[64], seekpos[12], *fmt, *path, *icytitle;
+	Player *player;
+	Ioproc *io;
+	Image *thiscover;
+	ulong c;
+	int p[2], q[2], fd, pid, noinit, trycoverload;
+	long n, r;
+	vlong boffset, boffsetlast;
+	Meta *cur;
+
+	threadsetname("player");
+	player = player_;
+	noinit = 0;
+	boffset = 0;
+	buf = nil;
+	trycoverload = 1;
+	io = nil;
+
+restart:
+	cur = getmeta(player->pcur);
+	fmt = cur->filefmt;
+	path = cur->path;
+	fd = -1;
+	q[0] = -1;
+	pid = -1;
+	if(*fmt){
+		if((fd = open(cur->path, OREAD)) < 0){
+			fprint(2, "%r\n");
+			sendul(player->ev, Everror);
+			chanclose(player->ev);
+			goto freeplayer;
+		}
+	}else{
+		sendul(player->ev, Evready);
+		chanclose(player->ev);
+		if(strncmp(cur->path, "http://", 7) == 0){ /* try icy */
+			pipe(q);
+			if(icyget(cur, q[0], &player->icytitlec) == 0){
+				fd = q[1];
+				path = nil;
+			}else{
+				close(q[0]); q[0] = -1;
+				close(q[1]);
+			}
+		}
+	}
+
+	pipe(p);
+	if((pid = rfork(RFPROC|RFFDG|RFNOTEG|RFCENVG|RFNOWAIT)) == 0){
+		close(q[0]);
+		close(p[1]);
+		if(fd < 0)
+			fd = open("/dev/null", OREAD);
+		dup(fd, 0); close(fd); /* fd == q[1] when it's Icy */
+		dup(p[0], 1); close(p[0]);
+		if(!debug){
+			dup(fd = open("/dev/null", OWRITE), 2);
+			close(fd);
+		}
+		if(*fmt){
+			snprint(cmd, sizeof(cmd), "/bin/audio/%sdec", fmt);
+			snprint(seekpos, sizeof(seekpos), "%g", (double)boffset/Bps);
+			execl(cmd, cmd, boffset ? "-s" : nil, seekpos, nil);
+		}else{
+			execl("/bin/play", "play", "-o", "/fd/1", path, nil);
+		}
+		exits("%r");
+	}
+	if(pid < 0)
+		sysfatal("rfork: %r");
+	/* fd is q[1] when it's Icy */
+	close(fd);
+	close(p[0]);
+
+	c = 0;
+	if(!noinit){
+		if(*fmt){
+			sendul(player->ev, Evready);
+			chanclose(player->ev);
+		}
+		buf = malloc(Relbufsz);
+		if((io = ioproc()) == nil)
+			sysfatal("player: %r");
+		iocall(io, iosetname);
+		if((n = ioreadn(io, p[1], buf, Relbufsz)) < 0)
+			fprint(2, "player: %r\n");
+		if(recv(player->ctl, &c) < 0 || c != Cstart)
+			goto freeplayer;
+		if(n < 1)
+			goto next;
+		audioon();
+		gain(player->gain, buf, n);
+		boffset = iowrite(io, audio, buf, n);
+		noinit = 1;
+	}
+
+	boffsetlast = boffset;
+	byteswritten = boffset;
+	pcurplaying = player->pcur;
+	redraw(1);
+
+	while(1){
+		n = ioread(io, p[1], buf, Relbufsz);
+		if(n <= 0){
+			if(stopafter){
+				audiooff();
+				goto stop;
+			}else if(repeatone){
+				c = Cseekrel;
+				boffset = 0;
+			}
+			break;
+		}
+		if(player->icytitlec != nil && nbrecv(player->icytitlec, &icytitle) != 0){
+			free(player->icytitle);
+			player->icytitle = icytitle;
+			redraw(1);
+		}
+		thiscover = nil;
+		if(player->img != nil && nbrecv(player->img, &thiscover) != 0){
+			freeimage(cover);
+			cover = thiscover;
+			redraw(0);
+			player->img = nil;
+		}
+		r = nbrecv(player->ctl, &c);
+		if(r < 0){
+			audiooff();
+			goto stop;
+		}else if(r != 0){
+			if(c == Ctoggle){
+				audiooff();
+				if(recv(player->ctl, &c) < 0)
+					goto stop;
+			}
+			if(c == Cseekrel && *fmt){
+				boffset = MAX(0, boffset + player->seek*Bps);
+				n = 0;
+				break;
+			}else if(c == Cstop){
+				audiooff();
+				goto stop;
+			}
+		}
+
+		boffset += n;
+		byteswritten = boffset;
+		audioon();
+		gain(player->gain, buf, n);
+		iowrite(io, audio, buf, n);
+		if(trycoverload){
+			trycoverload = 0;
+			player->img = chancreate(sizeof(Image*), 0);
+			proccreate(coverload, player, 4096);
+		}
+		if(labs(boffset/Relbufsz - boffsetlast/Relbufsz) > 0){
+			boffsetlast = boffset;
+			redraw(0);
+		}
+	}
+
+	if(n < 1){ /* seeking backwards or end of the song */
+		close(p[1]);
+		p[1] = -1;
+		postnote(PNGROUP, pid, "die");
+		if(c != Cseekrel || (getmeta(pcurplaying)->duration && boffset >= getmeta(pcurplaying)->duration/1000*Bps)){
+next:
+			playercurr = nil;
+			playercurr = newplayer((player->pcur+1) % pl->n, 1);
+			start(playercurr);
+			goto stop;
+		}
+		goto restart;
+	}
+
+stop:
+	if(player->img != nil)
+		freeimage(recvp(player->img));
+freeplayer:
+	close(p[1]);
+	closeioproc(io);
+	postnote(PNGROUP, pid, "die");
+	if(player->icytitlec != nil){
+		while((icytitle = recvp(player->icytitlec)) != nil)
+			free(icytitle);
+		chanfree(player->icytitlec);
+	}
+	chanfree(player->ctl);
+	chanfree(player->ev);
+	if(player == playercurr)
+		playercurr = nil;
+	if(player == playernext)
+		playernext = nil;
+	free(buf);
+	free(player->icytitle);
+	free(player);
+	threadexits(nil);
+}
+
+static int
+toggle(Player *player)
+{
+	return (player != nil && sendul(player->ctl, Ctoggle) == 1) ? 0 : -1;
+}
+
+static void
+seekrel(Player *player, double off)
+{
+	if(player != nil){
+		player->seek = off;
+		sendul(player->ctl, Cseekrel);
+	}
+}
+
+static void
+freeplist(Playlist *pl)
+{
+	if(pl != nil){
+		free(pl->m);
+		free(pl->raw);
+	}
+	free(pl);
+}
+
+static char *
+readall(int f)
+{
+	int bufsz, sz, n;
+	char *s;
+
+	bufsz = 1023;
+	s = nil;
+	for(sz = 0;; sz += n){
+		if(bufsz-sz < 1024){
+			bufsz *= 2;
+			s = realloc(s, bufsz);
+		}
+		if((n = readn(f, s+sz, bufsz-sz-1)) < 1)
+			break;
+	}
+	if(n < 0 || sz < 1){
+		if(n == 0)
+			werrstr("empty");
+		free(s);
+		return nil;
+	}
+	s[sz] = 0;
+
+	return s;
+}
+
+static int
+cmpint(void *a, void *b)
+{
+	return *(int*)a - *(int*)b;
+}
+
+static Playlist *
+readplist(int fd, int mincolwidth[Ncol])
+{
+	char *raw, *s, *e, *a[5], *b;
+	int plsz, i, *w;
+	Playlist *pl;
+	Meta *m;
+
+	if((raw = readall(fd)) == nil)
+		return nil;
+
+	plsz = 0;
+	for(s = raw; (s = strchr(s, '\n')) != nil; s++){
+		if(*(++s) == '\n')
+			plsz++;
+	}
+
+	if((pl = calloc(1, sizeof(*pl))) == nil || (pl->m = calloc(plsz+1, sizeof(Meta))) == nil){
+		freeplist(pl);
+		werrstr("no memory");
+		return nil;
+	}
+
+	pl->raw = raw;
+	for(s = pl->raw, m = pl->m, e = s; e != nil; s = e){
+		if((e = strchr(s, '\n')) == nil)
+			goto addit;
+		s += 2;
+		*e++ = 0;
+		switch(s[-2]){
+		case 0:
+addit:
+			if(m->path != nil){
+				if(m->filefmt == nil)
+					m->filefmt = "";
+				pl->n++;
+				m++;
+			}
+			break;
+		case Pimage:
+			if(tokenize(s, a, nelem(a)) >= 4){
+				m->imageoffset = atoi(a[0]);
+				m->imagesize = atoi(a[1]);
+				m->imagereader = atoi(a[2]);
+				m->imagefmt = a[3];
+			}
+			break;
+		case Pduration:
+			m->duration = strtoull(s, nil, 0);
+			break;
+		case Partist:
+			if(m->numartist < Maxartist)
+				m->artist[m->numartist++] = s;
+			break;
+		case Pfilefmt: m->filefmt = s; break;
+		case Palbum:   m->album = s; break;
+		case Pdate:    m->date = s; break;
+		case Ptitle:   m->title = s; break;
+		case Ptrack:   m->track = s; break;
+		case Prgtrack: m->rgtrack = atof(s); break;
+		case Prgalbum: m->rgalbum = atof(s); break;
+		case Ppath:
+			m->path = s;
+			m->basename = (b = utfrrune(s, '/')) == nil ? s : b+1;
+			break;
+		}
+	}
+
+	w = malloc(sizeof(int)*pl->n);
+	for(i = 0; cols[i] != 0; i++){
+		for(m = pl->m; m != pl->m + pl->n; m++)
+			w[m - pl->m] = stringwidth(f, getcol(m, cols[i]));
+		qsort(w, pl->n, sizeof(*w), cmpint);
+		mincolwidth[i] = w[93*(pl->n-1)/100];
+	}
+	free(w);
+
+	return pl;
+}
+
+static void
+recenter(void)
+{
+	updatescrollsz();
+	scroll = pcur - scrollsz/2 + 1;
+}
+
+static void
+seekto(char *s)
+{
+	vlong p;
+	char *e;
+
+	for(p = 0; *s; s = e){
+		p += strtoll(s, &e, 10);
+		if(s == e)
+			break;
+		if(*e == ':'){
+			p *= 60;
+			e++;
+		}
+	}
+
+	seekrel(playercurr, p - byteswritten/Bps);
+}
+
+static void
+search(char d)
+{
+	Meta *m;
+	static char buf[64];
+	static int sz;
+	int inc, i, a, cycle;
+
+	inc = (d == '/' || d == 'n') ? 1 : -1;
+	if(d == '/' || d == '?')
+		sz = enter(inc > 0 ? "forward:" : "backward:", buf, sizeof(buf), mctl, &kctl, screen->screen);
+	if(sz < 1){
+		redraw(1);
+		return;
+	}
+
+	cycle = 1;
+	for(i = pcur+inc; i >= 0 && i < pl->n;){
+		m = getmeta(i);
+		for(a = 0; a < m->numartist; a++){
+			if(cistrstr(m->artist[a], buf) != nil)
+				break;
+		}
+		if(a < m->numartist)
+			break;
+		if(m->album != nil && cistrstr(m->album, buf) != nil)
+			break;
+		if(m->title != nil && cistrstr(m->title, buf) != nil)
+			break;
+		if(cistrstr(m->path, buf) != nil)
+			break;
+onemore:
+		i += inc;
+	}
+	if(i >= 0 && i < pl->n){
+		pcur = i;
+		recenter();
+	}else if(cycle && i+inc < 0){
+		cycle = 0;
+		i = pl->n;
+		goto onemore;
+	}else if(cycle && i+inc >= pl->n){
+		cycle = 0;
+		i = -1;
+		goto onemore;
+	}
+	redraw(1);
+}
+
+static void
+toggleshuffle(void)
+{
+	int i, m, xi, a, c, pcurnew, pcurplayingnew;
+
+	if(shuffle == nil){
+		if(pl->n < 2)
+			return;
+
+		m = pl->n;
+		if(pl->n < 4){
+			a = 1;
+			c = 3;
+			m = 7;
+		}else{
+			m += 1;
+			m |= m >> 1;
+			m |= m >> 2;
+			m |= m >> 4;
+			m |= m >> 8;
+			m |= m >> 16;
+			a = 1 + nrand(m/4)*4;     /* 1 ≤ a < m   && a mod 4 = 1 */
+			c = 3 + nrand((m-2)/2)*2; /* 3 ≤ c < m-1 && c mod 2 = 1 */
+		}
+
+		shuffle = malloc(pl->n*sizeof(*shuffle));
+		xi = pcurplaying < 0 ? pcur : pcurplaying;
+		pcurplayingnew = -1;
+		pcurnew = 0;
+		for(i = 0; i < pl->n;){
+			if(xi < pl->n){
+				if(pcur == xi)
+					pcurnew = i;
+				if(pcurplaying == xi)
+					pcurplayingnew = i;
+				shuffle[i++] = xi;
+			}
+			xi = (a*xi + c) & m;
+		}
+		pcur = pcurnew;
+		pcurplaying = pcurplayingnew;
+	}else{
+		pcur = shuffle[pcur];
+		if(pcurplaying >= 0)
+			pcurplaying = shuffle[pcurplaying];
+		free(shuffle);
+		shuffle = nil;
+	}
+
+	stop(playernext);
+	if(pcur < pl->n-1)
+		playernext = newplayer(pcur+1, 0);
+}
+
+static void
+adjustcolumns(void)
+{
+	int i, n, total, width;
+
+	total = 0;
+	n = 0;
+	width = Dx(screen->r);
+	for(i = 0; cols[i] != 0; i++){
+		if(cols[i] == Pduration || cols[i] == Pdate || cols[i] == Ptrack)
+			width -= mincolwidth[i] + 8;
+		else{
+			total += mincolwidth[i];
+			n++;
+		}
+	}
+	colspath = 0;
+	for(i = 0; cols[i] != 0; i++){
+		if(cols[i] == Ppath || cols[i] == Pbasename)
+			colspath = 1;
+		if(cols[i] == Pduration || cols[i] == Pdate || cols[i] == Ptrack)
+			colwidth[i] = mincolwidth[i];
+		else
+			colwidth[i] = (width - Scrollwidth - n*8) * mincolwidth[i] / total;
+	}
+}
+
+static void
+plumbaudio(void *kbd)
+{
+	int i, f, pf, mcw[Ncol], playing, shuffled;
+	Playlist *p;
+	Plumbmsg *m;
+	char *s, *e;
+	Rune c;
+
+	threadsetname("audio/plumb");
+	if((f = plumbopen("audio", OREAD)) >= 0){
+		while((m = plumbrecv(f)) != nil){
+			s = m->data;
+			if(strncmp(s, "key", 3) == 0 && isspace(s[3])){
+				for(s = s+4; isspace(*s); s++);
+				for(; (i = chartorune(&c, s)) > 0 && c != Runeerror; s += i)
+					sendul(kbd, c);
+				continue;
+			}
+			if(*s != '/' && m->wdir != nil)
+				s = smprint("%s/%.*s", m->wdir, m->ndata, m->data);
+
+			if((e = strrchr(s, '.')) != nil && strcmp(e, ".plist") == 0 && (pf = open(s, OREAD|OCEXEC)) >= 0){
+				p = readplist(pf, mcw);
+				close(pf);
+				if(p == nil)
+					continue;
+				playing = pcurplaying;
+				if(shuffled = (shuffle != nil))
+					sendul(kbd, 's');
+				/* make sure nothing is playing */
+				while(pcurplaying >= 0){
+					sendul(kbd, 'v');
+					sleep(100);
+				}
+				freeplist(pl);
+				pl = p;
+				memmove(mincolwidth, mcw, sizeof(mincolwidth));
+				adjustcolumns();
+				pcur = 0;
+				if(shuffled){
+					pcur = nrand(pl->n);
+					sendul(kbd, 's');
+				}
+				redraw(1);
+				if(playing >= 0)
+					sendul(kbd, '\n');
+			}else{
+				for(i = 0; i < pl->n; i++){
+					if(strcmp(pl->m[i].path, s) == 0){
+						sendul(playc, i);
+						break;
+					}
+				}
+			}
+
+			if(s != m->data)
+				free(s);
+			plumbfree(m);
+		}
+	}
+
+	threadexits(nil);
+}
+
+static void
+kbproc(void *cchan)
+{
+	char *s, buf[128], buf2[128];
+	int kbd, n;
+	Rune r;
+
+	threadsetname("kbproc");
+	if((kbd = open("/dev/kbd", OREAD|OCEXEC)) < 0)
+		sysfatal("/dev/kbd: %r");
+
+	buf2[0] = 0;
+	buf2[1] = 0;
+	buf[0] = 0;
+	for(;;){
+		if(buf[0] != 0){
+			n = strlen(buf)+1;
+			memmove(buf, buf+n, sizeof(buf)-n);
+		}
+		if(buf[0] == 0){
+			n = read(kbd, buf, sizeof(buf)-1);
+			if(n <= 0)
+				break;
+			buf[n-1] = 0;
+			buf[n] = 0;
+		}
+
+		switch(buf[0]){
+		case 'k':
+			for(s = buf+1; *s;){
+				s += chartorune(&r, s);
+				if(utfrune(buf2+1, r) == nil){
+					if(r == Kshift)
+						shiftdown = 1;
+				}
+			}
+			break;
+		case 'K':
+			for(s = buf2+1; *s;){
+				s += chartorune(&r, s);
+				if(utfrune(buf+1, r) == nil){
+					if(r == Kshift)
+						shiftdown = 0;
+				}
+			}
+			break;
+		case 'c':
+			if(chartorune(&r, buf+1) > 0 && r != Runeerror)
+				nbsend(cchan, &r);
+		default:
+			continue;
+		}
+
+		strcpy(buf2, buf);
+	}
+
+	close(kbd);
+	threadexits(nil);
+}
+
+static void
+usage(void)
+{
+	fprint(2, "usage: %s [-s] [-c aAdDtTp]\n", argv0);
+	sysfatal("usage");
+}
+
+void
+threadmain(int argc, char **argv)
+{
+	Rune key;
+	Mouse m;
+	ulong ind;
+	enum {
+		Emouse,
+		Eresize,
+		Ekey,
+		Eplay,
+	};
+	Alt a[] = {
+		{ nil, &m, CHANRCV },
+		{ nil, nil, CHANRCV },
+		{ nil, &key, CHANRCV },
+		{ nil, &ind, CHANRCV },
+		{ nil, nil, CHANEND },
+	};
+	int n, scrolling, oldpcur, oldbuttons, pnew, shuffled;
+	int seekmx, full;
+	char buf[64];
+
+	shuffled = 0;
+	ARGBEGIN{
+	case 'd':
+		debug++;
+		break;
+	case 's':
+		shuffled = 1;
+		break;
+	case 'c':
+		cols = EARGF(usage());
+		if(strlen(cols) >= nelem(colwidth))
+			sysfatal("max %d columns allowed", nelem(colwidth));
+		break;
+	default:
+		usage();
+		break;
+	}ARGEND;
+
+	Binit(&out, 1, OWRITE);
+	pnotifies = fd2path(1, buf, sizeof(buf)) == 0 && strcmp(buf, "/dev/cons") != 0;
+
+	if(initdraw(nil, nil, "zuke") < 0)
+		sysfatal("initdraw: %r");
+	unlockdisplay(display);
+	display->locking = 1;
+	f = display->defaultfont;
+	Scrollwidth = MAX(14, stringwidth(f, "#"));
+	Scrollheight = MAX(16, f->height);
+	Seekthicc = Scrollheight + 2;
+	Coversz = MAX(64, stringwidth(f, "¹∫ 00:00:00/00:00:00 100%"));
+	if((mctl = initmouse(nil, screen)) == nil)
+		sysfatal("initmouse: %r");
+
+	kctl.c = chancreate(sizeof(Rune), 20);
+	proccreate(kbproc, kctl.c, 4096);
+	playc = chancreate(sizeof(ind), 0);
+
+	a[Emouse].c = mctl->c;
+	a[Eresize].c = mctl->resizec;
+	a[Ekey].c = kctl.c;
+	a[Eplay].c = playc;
+
+	redrawc = chancreate(sizeof(ulong), 8);
+	proccreate(redrawproc, nil, 8192);
+
+ 	Theme th[Numcolors] = {
+ 		[Dback] { "back",	0xf0f0f0 },
+ 		[Dfhigh] { "high",	0xffffff },
+ 		[Dfmed] { "text",	0x343434 },
+ 		[Dflow] { "border",	0xa5a5a5 },
+ 		[Dfinv] { "hold",	0x323232 },
+ 		[Dbmed] { "menubar",	0x72dec2 },
+ 		[Dblow] { "ltitle",	0x404040 },
+ 		[Dbinv] { "paletext",	0xffb545 },
+ 	};
+ 	readtheme(th, nelem(th), nil);
+ 	for(n = 0; n < Numcolors; n++){
+ 		colors[n].rgb = th[n].c >> 8 & ~(0xff<<24);
+ 		colors[n].im = allocimage(display, Rect(0,0,1,1), RGB24, 1, th[n].c);
+ 	}
+
+	srand(time(0));
+	pcurplaying = -1;
+	chvolume(0);
+	fmtinstall('P', positionfmt);
+	threadsetname("zuke");
+
+	if((pl = readplist(0, mincolwidth)) == nil){
+		fprint(2, "playlist: %r\n");
+		sysfatal("playlist error");
+	}
+
+	m.buttons = 0;
+	scrolling = 0;
+	seekmx = 0;
+	adjustcolumns();
+
+	proccreate(plumbaudio, kctl.c, 4096);
+
+	if(shuffled){
+		pcur = nrand(pl->n);
+		toggleshuffle();
+	}
+	full = 1;
+
+	for(;;){
+		updatescrollsz();
+		scroll = CLAMP(scroll, 0, pl->n - scrollsz);
+		redraw(full);
+
+		oldpcur = pcur;
+		full = 0;
+		if(seekmx != newseekmx){
+			seekmx = newseekmx;
+			redraw(0);
+		}
+
+		oldbuttons = m.buttons;
+		switch(alt(a)){
+		case Emouse:
+			if(ptinrect(m.xy, seekbar)){
+				seekoff = getmeta(pcurplaying)->duration * (double)(m.xy.x-1-seekbar.min.x) / (double)Dx(seekbar);
+				if(seekoff < 0)
+					seekoff = 0;
+				newseekmx = m.xy.x;
+			}else{
+				newseekmx = -1;
+			}
+			if(oldbuttons == m.buttons && m.buttons == 0)
+				continue;
+
+			if(m.buttons != 2)
+				scrolling = 0;
+			if(m.buttons == 0)
+				break;
+			if(m.buttons == 8){
+				scroll -= (shiftdown ? 0 : scrollsz/4)+1;
+				break;
+			}else if(m.buttons == 16){
+				scroll += (shiftdown ? 0 : scrollsz/4)+1;
+				break;
+			}
+
+			n = (m.xy.y - screen->r.min.y)/f->height;
+
+			if(m.xy.x <= screen->r.min.x+Scrollwidth && m.xy.y <= screen->r.max.y-Seekthicc){
+				if(m.buttons == 1){
+					scroll -= n+1;
+					break;
+				}else if(m.buttons == 4){
+					scroll += n+1;
+					break;
+				}else if(m.buttons == 2){
+					scrolling = 1;
+				}
+			}
+
+			if(!scrolling && ptinrect(m.xy, insetrect(seekbar, -4))){
+				if(ptinrect(m.xy, seekbar))
+					seekrel(playercurr, seekoff/1000.0 - byteswritten/Bps);
+				break;
+			}
+
+			if(scrolling){
+				if(scrollsz >= pl->n)
+					break;
+				scroll = (m.xy.y - screen->r.min.y)*(pl->n-scrollsz) / (Dy(screen->r)-Seekthicc);
+			}else if(m.buttons == 1 || m.buttons == 2){
+				n += scroll;
+				if(n < pl->n){
+					pcur = n;
+					if(m.buttons == 2 && oldbuttons == 0){
+						stop(playercurr);
+						playercurr = newplayer(pcur, 1);
+						start(playercurr);
+					}
+				}
+			}
+			break;
+		case Eresize: /* resize */
+			if(getwindow(display, Refnone) < 0)
+				sysfatal("getwindow: %r");
+			adjustcolumns();
+			redraw(1);
+			break;
+		case Ekey:
+			switch(key){
+			default:
+				if(isdigit(key) && pcurplaying >= 0 && getmeta(pcurplaying)->duration > 0){
+					buf[0] = key;
+					buf[1] = 0;
+					if(enter("seek:", buf, sizeof(buf), mctl, &kctl, screen->screen) < 1)
+						redraw(1);
+					else
+						seekto(buf);
+				}
+				break;
+			case Kleft:
+				seekrel(playercurr, -(double)Seek);
+				break;
+			case Kright:
+				seekrel(playercurr, Seek);
+				break;
+			case ',':
+				seekrel(playercurr, -(double)Seekfast);
+				break;
+			case '.':
+				seekrel(playercurr, Seekfast);
+				break;
+			case Kup:
+				pcur--;
+				break;
+			case Kpgup:
+				pcur -= scrollsz;
+				break;
+			case Kdown:
+				pcur++;
+				break;
+			case Kpgdown:
+				pcur += scrollsz;
+				break;
+			case Kend:
+				pcur = pl->n-1;
+				scroll = pl->n-scrollsz;
+				break;
+			case Khome:
+				pcur = 0;
+				break;
+			case '\n':
+playcur:
+				stop(playercurr);
+				playercurr = newplayer(pcur, 1);
+				start(playercurr);
+				break;
+			case 'q':
+			case Kdel:
+				stop(playercurr);
+				stop(playernext);
+				threadexitsall(nil);
+			case 'i':
+			case 'o':
+				if(pcur == pcurplaying)
+					oldpcur = -1;
+				pcur = pcurplaying;
+				recenter();
+				break;
+			case 'b':
+			case '>':
+				if(playercurr == nil)
+					break;
+				pnew = pcurplaying;
+				if(++pnew >= pl->n)
+					pnew = 0;
+				stop(playercurr);
+				playercurr = newplayer(pnew, 1);
+				start(playercurr);
+				break;
+			case 'z':
+			case '<':
+				if(playercurr == nil)
+					break;
+				pnew = pcurplaying;
+				if(--pnew < 0)
+					pnew = pl->n-1;
+				stop(playercurr);
+				playercurr = newplayer(pnew, 1);
+				start(playercurr);
+				break;
+			case '-':
+				chvolume(-1);
+				redraw(0);
+				continue;
+			case '+':
+			case '=':
+				chvolume(+1);
+				redraw(0);
+				continue;
+			case 'v':
+				stop(playercurr);
+				stop(playernext);
+				playercurr = nil;
+				playernext = nil;
+				pcurplaying = -1;
+				freeimage(cover);
+				cover = nil;
+				full = 1;
+				break;
+			case 'g':
+				rg = (rg+1) % Numrg;
+				setgain(playercurr);
+				setgain(playernext);
+				redraw(0);
+				break;
+			case 's':
+				toggleshuffle();
+				recenter();
+				full = 1;
+				break;
+			case 'r':
+				repeatone ^= 1;
+				redraw(0);
+				break;
+			case 'x':
+				stopafter ^= 1;
+				redraw(0);
+				break;
+			case 'c':
+			case 'p':
+			case ' ':
+				if(toggle(playercurr) != 0)
+					goto playcur;
+				break;
+			case '/':
+			case '?':
+			case 'n':
+			case 'N':
+				search(key);
+				break;
+			}
+			break;
+		case Eplay:
+			pcur = ind;
+			recenter();
+			if(playercurr != nil)
+				goto playcur;
+			break;
+		}
+
+		if(pcur != oldpcur){
+			pcur = CLAMP(pcur, 0, pl->n-1);
+			if(pcur < scroll)
+				scroll = pcur;
+			else if(pcur > scroll + scrollsz)
+				scroll = pcur - scrollsz;
+		}
+	}
+}
--- /dev/null
+++ b/sys/src/cmd/bar.c
@@ -1,0 +1,371 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <draw.h>
+#include <keyboard.h>
+#include <mouse.h>
+#include <thread.h>
+#include <tos.h>
+
+#define MAX(a,b) ((a)>=(b)?(a):(b))
+
+enum {
+	Off = 3,
+};
+
+static int wctl = -1, owidth, width, twidth, bottom, bat, minheight, seplen, sepw, hlitem;
+static int lastpx;
+static char sep[16], bats[16], *aux;
+static char *pos = "rb", *dfmt = "YYYY/MM/DD WW hh:mm:ss", *items[64];
+static int itemw[64], nitems;
+static Image *cback, *ctext;
+static Tzone *local;
+static Font *f;
+
+#pragma varargck type "|" char*
+static int
+sepfmt(Fmt *f)
+{
+	return fmtstrcpy(f, va_arg(f->args, char*)[0] ? sep : "");
+}
+
+/*
+ * nsec() is wallclock and can be adjusted by timesync
+ * so need to use cycles() instead, but fall back to
+ * nsec() in case we can't
+ */
+static uvlong
+nanosec(void)
+{
+	static uvlong fasthz, xstart;
+	uvlong x, div;
+
+	if(fasthz == ~0ULL)
+		return nsec() - xstart;
+
+	if(fasthz == 0){
+		fasthz = _tos->cyclefreq;
+		if(fasthz == 0){
+			fasthz = ~0ULL;
+			xstart = nsec();
+			return 0;
+		}else{
+			cycles(&xstart);
+		}
+	}
+	cycles(&x);
+	x -= xstart;
+
+	/* this is ugly */
+	for(div = 1000000000ULL; x < 0x1999999999999999ULL && div > 1 ; div /= 10ULL, x *= 10ULL);
+
+	return x / (fasthz / div);
+}
+
+static void
+place(void)
+{
+	int w, h, minx, miny, maxx, maxy;
+	char t[64];
+	static int ow, oh;
+
+	if(wctl < 0 && (wctl = open("/dev/wctl", OWRITE)) < 0)
+		return;
+
+	fprint(wctl, bottom ? "bottom" : "top");
+	w = Dx(display->image->r);
+	h = Dy(display->image->r);
+
+	if(ow != w || oh != h || owidth < width){
+		if(pos[0] == 't' || pos[1] == 't'){
+			miny = 0;
+			maxy = minheight;
+		}else{
+			miny = h - minheight;
+			maxy = h;
+		}
+		if(pos[0] == 'l' || pos[1] == 'l'){
+			minx = 0;
+			maxx = MAX(100, Borderwidth+Off+width+Off+Borderwidth);
+		}else if(pos[0] == 'r' || pos[1] == 'r'){
+			minx = MAX(100, w-(Borderwidth+Off+width+Off+Borderwidth));
+			maxx = w;
+		}else{
+			minx = (w-MAX(100, Borderwidth+Off+width+Off+Borderwidth))/2;
+			maxx = (w+MAX(100, Borderwidth+Off+width+Off+Borderwidth))/2;
+		}
+		snprint(t, sizeof(t), "resize -r %d %d %d %d", minx, miny, maxx, maxy);
+		write(wctl, "current", 7);
+		if(fprint(wctl, "%s", t) < 0)
+			fprint(2, "%s: %r\n", t);
+		ow = w;
+		oh = h;
+		owidth = width;
+	}
+}
+
+static void
+split(char *s)
+{
+	char *i;
+
+	for(nitems = 0, i = s; nitems < nelem(items); s += seplen, i = s){
+		if((s = strstr(s, sep)) != nil)
+			*s = 0;
+		if(*i == 0)
+			continue;
+		items[nitems] = i;
+		itemw[nitems++] = stringwidth(f, i);
+		if(s == nil)
+			break;
+	}
+	
+}
+
+static void
+redraw(void)
+{
+	static char s[1024];
+	char tmp[1024];
+	Rectangle r;
+	Tmfmt tf;
+	Point p;
+	Tm tm;
+	int i;
+
+	r = screen->r;
+
+	tf = tmfmt(tmnow(&tm, local), dfmt);
+	p.x = r.min.x + Off;
+	p.y = (pos[0] == 't' || pos[1] == 't') ? r.max.y - (f->height + Off) : r.min.y + Off;
+	if(pos[0] == 'l' || pos[1] == 'l'){
+		snprint(s, sizeof(s), "%τ%|%s%|%s", tf, bats, bats, aux, aux);
+	}else{
+		snprint(s, sizeof(s), "%s%|%s%|%τ", aux, aux, bats, bats, tf);
+		if(pos[0] == 'r' || pos[1] == 'r')
+			p.x = r.max.x - (stringwidth(f, s) + Off);
+	}
+	lastpx = p.x;
+	draw(screen, r, cback, nil, ZP);
+	string(screen, p, ctext, ZP, f, s);
+	if(hlitem >= 0){
+		r.min.x = lastpx;
+		for(i = 0; i < hlitem; i++)
+			r.min.x += itemw[i] + sepw;
+		r.max.x = r.min.x + itemw[i];
+		replclipr(screen, 0, r);
+		stringbg(screen, p, cback, ZP, f, s, ctext, ZP);
+		replclipr(screen, 0, screen->r);
+	}
+	split(s);
+
+	flushimage(display, 1);
+
+	snprint(tmp, sizeof(tmp), "%τ", tf);
+	twidth = MAX(twidth, stringwidth(f, tmp));
+	snprint(tmp, sizeof(tmp), "%|%s%|%s", bats, bats[0] ? "100%" : "", aux, aux);
+	width = twidth + stringwidth(f, tmp);
+	if(owidth != width)
+		place();
+}
+
+static void
+readbattery(void)
+{
+	char *s, tmp[16];
+
+	s = bat < 0 || pread(bat, tmp, 4, 0) < 4 ? nil : strchr(tmp, ' ');
+	if(s != nil){
+		*s = 0;
+		snprint(bats, sizeof(bats), "%s%%", tmp);
+	}else{
+		bats[0] = 0;
+	}
+}
+
+static void
+timerproc(void *c)
+{
+	threadsetname("timer");
+	for(;;){
+		sleep(990);
+		sendul(c, 0);
+	}
+}
+
+static void
+auxproc(void *c)
+{
+	Biobuf b;
+	char *s;
+
+	threadsetname("aux");
+	Binit(&b, 0, OREAD);
+	for(;;){
+		s = Brdstr(&b, '\n', 1);
+		if(s == nil)
+			break;
+		sendp(c, s);
+	}
+	Bterm(&b);
+
+	threadexits(nil);
+}
+
+static void
+usage(void)
+{
+	fprint(2, "usage: %s [-b] [-d dateformat] [-p lt|t|rt|lb|b|rb] [-s separator]\n", argv0);
+	threadexitsall("usage");
+}
+
+static void
+clicked(int x, int buttons)
+{
+	int i, ix;
+
+	x -= lastpx;
+	for(i = ix = 0; i < nitems; i++){
+		if(x >= ix && x <= ix+itemw[i]){
+			fprint(1, "%d\t%s\n", buttons, items[i]);
+			hlitem = i;
+			break;
+		}
+		ix += itemw[i] + sepw;
+	}
+}
+
+void
+threadmain(int argc, char **argv)
+{
+	Keyboardctl *kctl;
+	Mousectl *mctl;
+	uvlong t, oldt;
+	int oldbuttons;
+	char *s;
+	Rune key;
+	Mouse m;
+	enum {
+		Emouse,
+		Eresize,
+		Ekeyboard,
+		Eaux,
+		Etimer,
+		Eend,
+	};
+	Alt a[] = {
+		[Emouse] = { nil, &m, CHANRCV },
+		[Eresize] = { nil, nil, CHANRCV },
+		[Ekeyboard] = { nil, &key, CHANRCV },
+		[Eaux] = { nil, &s, CHANRCV },
+		[Etimer] = { nil, nil, CHANRCV },
+		[Eend] = { nil, nil, CHANEND },
+	};
+
+	strcpy(sep, " │ ");
+	ARGBEGIN{
+	case 'b':
+		bottom = 1;
+		break;
+	case 'd':
+		dfmt = EARGF(usage());
+		break;
+	case 'p':
+		pos = EARGF(usage());
+		break;
+	case 's':
+		snprint(sep, sizeof(sep), "%s", EARGF(usage()));
+		break;
+	default:
+		usage();
+	}ARGEND
+	seplen = strlen(sep);
+
+	fmtinstall('|', sepfmt);
+	tmfmtinstall();
+	if((local = tzload("local")) == nil)
+		sysfatal("zone: %r");
+
+	if((bat = open("/mnt/pm/battery", OREAD)) < 0)
+		bat = open("/dev/battery", OREAD);
+	if(initdraw(nil, nil, "bar") < 0)
+		sysfatal("initdraw: %r");
+	f = display->defaultfont;
+	minheight = 2*(Borderwidth+1) + f->height;
+	sepw = stringwidth(f, sep);
+	if((mctl = initmouse(nil, screen)) == nil)
+		sysfatal("initmouse: %r");
+	if((kctl = initkeyboard(nil)) == nil)
+		sysfatal("initkeyboard: %r");
+	Theme th[2] = {
+		{ "back",	DPalegreygreen },
+		{ "text",	DBlack },
+	};
+	readtheme(th, nelem(th), nil);
+	cback = allocimage(display, Rect(0,0,1,1), screen->chan, 1, th[0].c);
+	ctext = allocimage(display, Rect(0,0,1,1), screen->chan, 1, th[1].c);
+
+	a[Emouse].c = mctl->c;
+	a[Eresize].c = mctl->resizec;
+	a[Ekeyboard].c = kctl->c;
+	a[Eaux].c = chancreate(sizeof(s), 0);
+	a[Etimer].c = chancreate(sizeof(ulong), 0);
+
+	hlitem = -1;
+	aux = strdup("");
+	readbattery();
+	redraw();
+	proccreate(timerproc, a[Etimer].c, 4096);
+	proccreate(auxproc, a[Eaux].c, 16384);
+
+	m.buttons = 0;
+	oldt = nanosec();
+	for(;;){
+		oldbuttons = m.buttons;
+
+		switch(alt(a)){
+		case Ekeyboard:
+			if(key == Kdel){
+				close(wctl);
+				threadexitsall(nil);
+			}
+			break;
+
+		case Emouse:
+			if(m.buttons == oldbuttons)
+				break;
+			if(m.buttons == 0)
+				hlitem = -1;
+			else
+				clicked(m.xy.x, m.buttons);
+			/* wet floor */
+
+		if(0){
+		case Eresize:
+			if(getwindow(display, Refnone) < 0)
+				threadexitsall(nil);
+			owidth = 0;
+			/* wet floor */
+		}
+
+		if(0){
+		case Eaux:
+			free(aux);
+			aux = s;
+			/* wet floor */
+		}
+
+		if(0){
+		case Etimer:
+			t = nanosec();
+			if(t - oldt >= 30000000000ULL){
+				readbattery();
+				oldt = t;
+			}
+		}
+			place();
+			redraw();
+			break;
+		}
+	}
+}
--- /dev/null
+++ b/sys/src/cmd/faces/main.c
@@ -1,0 +1,744 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <plumb.h>
+#include <regexp.h>
+#include <event.h>	/* for support routines only */
+#include <bio.h>
+#include "faces.h"
+
+int	history = 0;	/* use old interface, showing history of mailbox rather than current state */
+int	initload = 0;	/* initialize program with contents of mail box */
+int	clickrm = 0;	/* allows removing mail faces by left clicking */
+
+enum
+{
+	Facesep = 6,	/* must be even to avoid damaging background stipple */
+	Infolines = 9,
+
+	HhmmTime = 18*60*60,	/* max age of face to display hh:mm time */
+};
+
+enum
+{
+	Mainp,
+	Timep,
+	Mousep,
+	NPROC
+};
+
+int pids[NPROC];
+char *procnames[] = {
+	"main",
+	"time",
+	"mouse"
+};
+
+Rectangle leftright = {0, 0, 20, 15};
+
+uchar leftdata[] = {
+	0x00, 0x80, 0x00, 0x01, 0x80, 0x00, 0x03, 0x80,
+	0x00, 0x07, 0x80, 0x00, 0x0f, 0x00, 0x00, 0x1f,
+	0xff, 0xf0, 0x3f, 0xff, 0xf0, 0xff, 0xff, 0xf0,
+	0x3f, 0xff, 0xf0, 0x1f, 0xff, 0xf0, 0x0f, 0x00,
+	0x00, 0x07, 0x80, 0x00, 0x03, 0x80, 0x00, 0x01,
+	0x80, 0x00, 0x00, 0x80, 0x00
+};
+
+uchar rightdata[] = {
+	0x00, 0x10, 0x00, 0x00, 0x18, 0x00, 0x00, 0x1c,
+	0x00, 0x00, 0x1e, 0x00, 0x00, 0x0f, 0x00, 0xff,
+	0xff, 0x80, 0xff, 0xff, 0xc0, 0xff, 0xff, 0xf0,
+	0xff, 0xff, 0xc0, 0xff, 0xff, 0x80, 0x00, 0x0f,
+	0x00, 0x00, 0x1e, 0x00, 0x00, 0x1c, 0x00, 0x00,
+	0x18, 0x00, 0x00, 0x10, 0x00
+};
+
+enum{
+	Cbgrnd,
+	Carrow,
+	Csmallfg,
+	Cfacebg,
+	Ctext,
+	Csmalltext,
+	Ncols,
+};
+Image *cols[Ncols];
+Image	*left;		/* left-pointing cols[Carrow] mask */
+Image	*right;		/* right-pointing cols[Carrow] mask */
+Font	*tinyfont;
+Font	*mediumfont;
+Font	*datefont;
+int	first, last;	/* first and last visible face; last is first invisible */
+int	nfaces;
+int	mousefd;
+int	nacross;
+int	ndown;
+
+char	date[64];
+Face	**faces;
+char	*maildir = "/mail/fs/mbox";
+ulong	now;
+
+Point	datep = { 8, 6 };
+Point	facep = { 8, 6+0+4 };	/* 0 updated to datefont->height in init() */
+Point	enddate;			/* where date ends on display; used to place cols[Carrow]s */
+Rectangle	leftr;			/* location of left cols[Carrow] on display */
+Rectangle	rightr;		/* location of right cols[Carrow] on display */
+void updatetimes(void);
+
+void
+setdate(void)
+{
+	now = time(nil);
+	strcpy(date, ctime(now));
+	date[4+4+3+5] = '\0';	/* change from Thu Jul 22 14:28:43 EDT 1999\n to Thu Jul 22 14:28 */
+}
+
+void
+init(void)
+{	int i;
+
+	mousefd = open("/dev/mouse", OREAD);
+	if(mousefd < 0){
+		fprint(2, "faces: can't open mouse: %r\n");
+		exits("mouse");
+	}
+	initplumb();
+
+	Theme th[nelem(cols)] = {
+		[Cbgrnd] { "rioback",		DBlack },
+		[Carrow] { "palehold",		0x008888FF },
+		[Csmallfg] { "paletext",	DBlack },
+		[Cfacebg] { "white",		DWhite },
+		[Ctext] { "hold",		DWhite },
+		[Csmalltext] { "white",		DWhite },
+	};
+	readtheme(th, nelem(th), nil);
+	for(i=0; i<nelem(cols); i++)
+ 		cols[i] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, th[i].c);
+	left = allocimage(display, leftright, GREY1, 0, DWhite);
+	right = allocimage(display, leftright, GREY1, 0, DWhite);
+	if(cols[Cbgrnd]==nil || cols[Carrow]==nil || cols[Csmallfg] == nil || cols[Cfacebg] == nil || cols[Ctext] == nil || left==nil || right==nil){
+		fprint(2, "faces: can't create images: %r\n");
+		exits("image");
+	}
+	loadimage(left, leftright, leftdata, sizeof leftdata);
+	loadimage(right, leftright, rightdata, sizeof rightdata);
+
+	/* initialize little fonts */
+	tinyfont = openfont(display, "/lib/font/bit/misc/ascii.5x7.font");
+	if(tinyfont == nil)
+		tinyfont = font;
+ 	mediumfont = openfont(display, "/lib/font/bit/vga/unicode.font");
+ 	if(mediumfont == nil)
+ 		mediumfont = font;
+	datefont = font;
+
+	facep.y += datefont->height;
+	if(datefont->height & 1)	/* stipple parity */
+		facep.y++;
+	faces = nil;
+}
+
+void
+drawtime(void)
+{
+	Rectangle r;
+
+	r.min = addpt(screen->r.min, datep);
+	if(eqpt(enddate, ZP)){
+		enddate = r.min;
+		enddate.x += stringwidth(datefont, "Wed May 30 22:54");	/* nice wide string */
+		enddate.x += Facesep;	/* for safety */
+	}
+	r.max.x = enddate.x;
+	r.max.y = enddate.y+datefont->height;
+	draw(screen, r, cols[Cbgrnd], nil, ZP);
+	string(screen, r.min, cols[Csmallfg], ZP, datefont, date);
+}
+
+void
+timeproc(void)
+{
+	for(;;){
+		lockdisplay(display);
+		drawtime();
+		updatetimes();
+		flushimage(display, 1);
+		unlockdisplay(display);
+		now = time(nil);
+		sleep(((60 - now%60) + 1)*1000); /* wait for minute to change */
+		setdate();
+	}
+}
+
+int
+alreadyseen(char *digest)
+{
+	int i;
+	Face *f;
+
+	if(!digest)
+		return 0;
+
+	/* can do accurate check */
+	for(i=0; i<nfaces; i++){
+		f = faces[i];
+		if(f->str[Sdigest]!=nil && strcmp(digest, f->str[Sdigest])==0)
+			return 1;
+	}
+	return 0;
+}
+
+int
+torune(Rune *r, char *s, int nr)
+{
+	int i;
+
+	for(i=0; i<nr-1 && *s!='\0'; i++)
+		s += chartorune(r+i, s);
+	r[i] = L'\0';
+	return i;
+}
+
+void
+center(Font *f, Point p, char *s, Image *color)
+{
+	int i, n, dx;
+	Rune rbuf[32];
+	char sbuf[32*UTFmax+1];
+
+	dx = stringwidth(f, s);
+	if(dx > Facesize){
+		n = torune(rbuf, s, nelem(rbuf));
+		for(i=0; i<n; i++){
+			dx = runestringnwidth(f, rbuf, i+1);
+			if(dx > Facesize)
+				break;
+		}
+		sprint(sbuf, "%.*S", i, rbuf);
+		s = sbuf;
+		dx = stringwidth(f, s);
+	}
+	p.x += (Facesize-dx)/2;
+	string(screen, p, color, ZP, f, s);
+}
+
+Rectangle
+facerect(int index)	/* index is geometric; 0 is always upper left face */
+{
+	Rectangle r;
+	int x, y;
+
+	x = index % nacross;
+	y = index / nacross;
+	r.min = addpt(screen->r.min, facep);
+	r.min.x += x*(Facesize+Facesep);
+	r.min.y += y*(Facesize+Facesep+2*mediumfont->height);
+	r.max = addpt(r.min, Pt(Facesize, Facesize));
+	r.max.y += 2*mediumfont->height;
+	/* simple fix to avoid drawing off screen, allowing customers to use position */
+	if(index<0 || index>=nacross*ndown)
+		r.max.x = r.min.x;
+	return r;
+}
+
+static char *mon = "JanFebMarAprMayJunJulAugSepOctNovDec";
+char*
+facetime(Face *f, int *recent)
+{
+	static char buf[30];
+
+	if((long)(now - f->time) > HhmmTime){
+		*recent = 0;
+		sprint(buf, "%.3s %2d", mon+3*f->tm.mon, f->tm.mday);
+		return buf;
+	}else{
+		*recent = 1;
+		sprint(buf, "%02d:%02d", f->tm.hour, f->tm.min);
+		return buf;
+	}
+}
+
+void
+drawface(Face *f, int i)
+{
+	char *tstr;
+	Rectangle r;
+	Point p;
+
+	if(f == nil)
+		return;
+	if(i<first || i>=last)
+		return;
+	r = facerect(i-first);
+	draw(screen, r, cols[Cbgrnd], nil, ZP);
+	draw(screen, Rpt(r.min, addpt(r.min, Pt(Facesize, Facesize))), cols[Cfacebg], nil, ZP);
+	draw(screen, r, f->bit, f->mask, ZP);
+	r.min.y += Facesize;
+	center(mediumfont, r.min, f->str[Suser], cols[Ctext]);
+	r.min.y += mediumfont->height;
+	tstr = facetime(f, &f->recent);
+	center(mediumfont, r.min, tstr, cols[Ctext]);
+	if(f->unknown){
+		r.min.y -= mediumfont->height + tinyfont->height + 2;
+		for(p.x=-1; p.x<=1; p.x++)
+			for(p.y=-1; p.y<=1; p.y++)
+				center(tinyfont, addpt(r.min, p), f->str[Sdomain], cols[Cbgrnd]);
+		center(tinyfont, r.min, f->str[Sdomain], cols[Csmalltext]);
+	}
+}
+
+void
+updatetimes(void)
+{
+	int i;
+	Face *f;
+
+	for(i=0; i<nfaces; i++){
+		f = faces[i];
+		if(f == nil)
+			continue;
+		if(((long)(now - f->time) <= HhmmTime) != f->recent)
+			drawface(f, i);
+	}	
+}
+
+void
+setlast(void)
+{
+	last = first+nacross*ndown;
+	if(last > nfaces)
+		last = nfaces;
+}
+
+void
+drawarrows(void)
+{
+	Point p;
+
+	p = enddate;
+	p.x += Facesep;
+	if(p.x & 1)
+		p.x++;	/* align background texture */
+	leftr = rectaddpt(leftright, p);
+	p.x += Dx(leftright) + Facesep;
+	rightr = rectaddpt(leftright, p);
+	draw(screen, leftr, first>0? cols[Carrow] : cols[Cbgrnd], left, leftright.min);
+	draw(screen, rightr, last<nfaces? cols[Carrow] : cols[Cbgrnd], right, leftright.min);
+}
+
+void
+addface(Face *f)	/* always adds at 0 */
+{
+	Face **ofaces;
+	Rectangle r0, r1, r;
+	int y, nx, ny;
+
+	if(f == nil)
+		return;
+	lockdisplay(display);
+	if(first != 0){
+		first = 0;
+		resized();
+	}
+	findbit(f);
+
+	nx = nacross;
+	ny = (nfaces+(nx-1)) / nx;
+
+	for(y=ny; y>=0; y--){
+		/* move them along */
+		r0 = facerect(y*nx+0);
+		r1 = facerect(y*nx+1);
+		r = r1;
+		r.max.x = r.min.x + (nx - 1)*(Facesize+Facesep);
+		draw(screen, r, screen, nil, r0.min);
+		/* copy one down from row above */
+		if(y != 0){
+			r = facerect((y-1)*nx+nx-1);
+			draw(screen, r0, screen, nil, r.min);
+		}
+	}
+
+	ofaces = faces;
+	faces = emalloc((nfaces+1)*sizeof(Face*));
+	memmove(faces+1, ofaces, nfaces*(sizeof(Face*)));
+	free(ofaces);
+	nfaces++;
+	setlast();
+	drawarrows();
+	faces[0] = f;
+	drawface(f, 0);
+	flushimage(display, 1);
+	unlockdisplay(display);
+}
+
+void
+loadmboxfaces(char *maildir)
+{
+	int dirfd;
+	Dir *d;
+	int i, n;
+
+	dirfd = open(maildir, OREAD);
+	if(dirfd >= 0){
+		chdir(maildir);
+		while((n = dirread(dirfd, &d)) > 0){
+			for(i=0; i<n; i++)
+				addface(dirface(maildir, d[i].name));
+			free(d);
+		}
+		close(dirfd);
+	}
+}
+
+void
+freeface(Face *f)
+{
+	int i;
+
+	if(f->file==nil || f->bit!=f->file->image)
+		freeimage(f->bit);
+	freefacefile(f->file);
+	for(i=0; i<Nstring; i++)
+		free(f->str[i]);
+	free(f);
+}
+
+void
+delface(int j)
+{
+	Rectangle r0, r1, r;
+	int nx, ny, x, y;
+
+	if(j < first)
+		first--;
+	else if(j < last){
+		nx = nacross;
+		ny = (nfaces+(nx-1)) / nx;
+		x = (j-first)%nx;
+		for(y=(j-first)/nx; y<ny; y++){
+			if(x != nx-1){
+				/* move them along */
+				r0 = facerect(y*nx+x);
+				r1 = facerect(y*nx+x+1);
+				r = r0;
+				r.max.x = r.min.x + (nx - x - 1)*(Facesize+Facesep);
+				draw(screen, r, screen, nil, r1.min);
+			}
+			if(y != ny-1){
+				/* copy one up from row below */
+				r = facerect((y+1)*nx);
+				draw(screen, facerect(y*nx+nx-1), screen, nil, r.min);
+			}
+			x = 0;
+		}
+		if(last < nfaces)	/* first off-screen becomes visible */
+			drawface(faces[last], last-1);
+		else{
+			/* clear final spot */
+			r = facerect(last-first-1);
+			draw(screen, r, cols[Cbgrnd], nil, r.min);
+		}
+	}
+	freeface(faces[j]);
+	memmove(faces+j, faces+j+1, (nfaces-(j+1))*sizeof(Face*));
+	nfaces--;
+	setlast();
+	drawarrows();
+}
+
+void
+dodelete(int i)
+{
+	Face *f;
+
+	f = faces[i];
+	if(history){
+		free(f->str[Sshow]);
+		f->str[Sshow] = estrdup("");
+	}else{
+		delface(i);
+		flushimage(display, 1);
+	}
+}
+
+void
+delete(char *s, char *digest)
+{
+	int i;
+	Face *f;
+
+	lockdisplay(display);
+	for(i=0; i<nfaces; i++){
+		f = faces[i];
+		if(digest != nil){
+			if(f->str[Sdigest]!=nil && strcmp(digest, f->str[Sdigest]) == 0){
+				dodelete(i);
+				break;
+			}
+		}else{
+			if(f->str[Sshow] && strcmp(s, f->str[Sshow]) == 0){
+				dodelete(i);
+				break;
+			}
+		}
+	}
+	unlockdisplay(display);
+}
+
+void
+faceproc(void)
+{
+	for(;;)
+		addface(nextface());
+}
+
+void
+resized(void)
+{
+	int i;
+
+	nacross = (Dx(screen->r)-2*facep.x+Facesep)/(Facesize+Facesep);
+	for(ndown=1; rectinrect(facerect(ndown*nacross), screen->r); ndown++)
+		;
+	setlast();
+	draw(screen, screen->r, cols[Cbgrnd], nil, ZP);
+	enddate = ZP;
+	drawtime();
+	for(i=0; i<nfaces; i++)
+		drawface(faces[i], i);
+	drawarrows();
+	flushimage(display, 1);
+}
+
+void
+eresized(int new)
+{
+	lockdisplay(display);
+	if(new && getwindow(display, Refnone) < 0) {
+		fprint(2, "can't reattach to window\n");
+		killall("reattach");
+	}
+	resized();
+	unlockdisplay(display);
+}
+
+int
+getmouse(Mouse *m)
+{
+	int n;
+	static int eof;
+	char buf[128];
+
+	if(eof)
+		return 0;
+	for(;;){
+		n = read(mousefd, buf, sizeof(buf));
+		if(n <= 0){
+			/* so callers needn't check return value every time */
+			eof = 1;
+			m->buttons = 0;
+			return 0;
+		}
+		n = eatomouse(m, buf, n);
+		if(n > 0)
+			return 1;
+	}
+}
+
+enum
+{
+	Clicksize	= 3,		/* pixels */
+};
+
+int
+scroll(int but, Point p)
+{
+	int delta;
+
+	delta = 0;
+	lockdisplay(display);
+	if(ptinrect(p, leftr) && first>0){
+		if(but == 2)
+			delta = -first;
+		else{
+			delta = nacross;
+			if(delta > first)
+				delta = first;
+			delta = -delta;
+		}
+	}else if(ptinrect(p, rightr) && last<nfaces){
+		if(but == 2)
+			delta = (nfaces-nacross*ndown) - first;
+		else{
+			delta = nacross;
+			if(delta > nfaces-last)
+				delta = nfaces-last;
+		}
+	}
+	first += delta;
+	last += delta;
+	unlockdisplay(display);
+	if(delta)
+		eresized(0);
+	return delta;
+}
+
+void
+click(int button, Mouse *m)
+{
+	Point p;
+	int i;
+
+	p = m->xy;
+	while(m->buttons == (1<<(button-1)))
+		getmouse(m);
+	if(m->buttons)
+		return;
+	if(abs(p.x-m->xy.x)>Clicksize || abs(p.y-m->xy.y)>Clicksize)
+		return;
+	switch(button){
+	case 1:
+		if(scroll(1, p))
+			break;
+		if(history){
+			/* click clears display */
+			lockdisplay(display);
+			for(i=0; i<nfaces; i++)
+				freeface(faces[i]);
+			free(faces);
+			faces=nil;
+			nfaces = 0;
+			unlockdisplay(display);
+			eresized(0);
+			return;
+		}else{
+			for(i=first; i<last; i++)	/* clear vwhois faces */
+				if(ptinrect(p, facerect(i-first)) 
+				&& (clickrm || strstr(faces[i]->str[Sshow], "/XXXvwhois"))){
+					delface(i);
+					flushimage(display, 1);
+				}
+		}
+		break;
+	case 2:
+		scroll(2, p);
+		break;
+	case 3:
+		scroll(3, p);
+		lockdisplay(display);
+		for(i=first; i<last; i++)
+			if(ptinrect(p, facerect(i-first))){
+				showmail(faces[i]);
+				break;
+			}
+		unlockdisplay(display);
+		break;
+	}
+}
+
+void
+mouseproc(void)
+{
+	Mouse mouse;
+
+	while(getmouse(&mouse)){
+		if(mouse.buttons == 1)
+			click(1, &mouse);
+		else if(mouse.buttons == 2)
+			click(2, &mouse);
+		else if(mouse.buttons == 4)
+			click(3, &mouse);
+
+		while(mouse.buttons)
+			getmouse(&mouse);
+	}
+}
+
+void
+killall(char *s)
+{
+	int i, pid;
+
+	pid = getpid();
+	for(i=0; i<NPROC; i++)
+		if(pids[i] && pids[i]!=pid)
+			postnote(PNPROC, pids[i], "kill");
+	exits(s);
+}
+
+void
+startproc(void (*f)(void), int index)
+{
+	int pid;
+
+	switch(pid = rfork(RFPROC|RFMEM|RFNOWAIT)){
+	case -1:
+		fprint(2, "faces: fork failed: %r\n");
+		killall("fork failed");
+	case 0:
+		f();
+		fprint(2, "faces: %s process exits\n", procnames[index]);
+		if(index >= 0)
+			killall("process died");
+		exits(nil);
+	}
+	if(index >= 0)
+		pids[index] = pid;
+}
+
+void
+usage(void)
+{
+	fprint(2, "usage: faces [-chi] [-m maildir]\n");
+	exits("usage");
+}
+
+void
+main(int argc, char *argv[])
+{
+	int i;
+
+	ARGBEGIN{
+	case 'h':
+		history++;
+		break;
+	case 'i':
+		initload++;
+		break;
+	case 'm':
+		addmaildir(EARGF(usage()));
+		maildir = nil;
+		break;
+	case 'c':
+		clickrm++;
+		break;
+	default:
+		usage();
+	}ARGEND
+
+	if(initdraw(nil, nil, "faces") < 0){
+		fprint(2, "faces: initdraw failed: %r\n");
+		exits("initdraw");
+	}
+	if(maildir)
+		addmaildir(maildir);
+	init();
+	unlockdisplay(display);	/* initdraw leaves it locked */
+	display->locking = 1;	/* tell library we're using the display lock */
+	setdate();
+	eresized(0);
+
+	pids[Mainp] = getpid();
+	startproc(timeproc, Timep);
+	startproc(mouseproc, Mousep);
+	if(initload)
+		for(i = 0; i < nmaildirs; i++)
+		 loadmboxfaces(maildirs[i]);
+	faceproc();
+	fprint(2, "faces: %s process exits\n", procnames[Mainp]);
+	killall(nil);
+}
--- /dev/null
+++ b/sys/src/cmd/kbmap.c
@@ -1,0 +1,284 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <keyboard.h>
+
+typedef struct KbMap KbMap;
+struct KbMap {
+	char *name;
+	char *file;
+	Rectangle r;
+	int current;
+};
+
+KbMap *map;
+int nmap;
+Image *txt, *sel;
+
+enum {
+	PAD = 3,
+	MARGIN = 5
+};
+
+char *dir = "/sys/lib/kbmap";
+
+void*
+erealloc(void *v, ulong n)
+{
+	v = realloc(v, n);
+	if(v == nil)
+		sysfatal("out of memory reallocating %lud", n);
+	return v;
+}
+
+void*
+emalloc(ulong n)
+{
+	void *v;
+
+	v = malloc(n);
+	if(v == nil)
+		sysfatal("out of memory allocating %lud", n);
+	memset(v, 0, n);
+	return v;
+}
+
+char*
+estrdup(char *s)
+{
+	int l;
+	char *t;
+
+	if (s == nil)
+		return nil;
+	l = strlen(s)+1;
+	t = emalloc(l);
+	memcpy(t, s, l);
+
+	return t;
+}
+
+void
+init(void)
+{
+	int i, fd, nr;
+	Dir *pd;
+
+	if((fd = open(dir, OREAD)) < 0)
+		return;
+
+	nmap = nr = dirreadall(fd, &pd);
+	map = emalloc(nr * sizeof(KbMap));
+	for(i=0; i<nr; i++){
+		map[i].file = emalloc(strlen(dir) + strlen(pd[i].name) + 2);
+		sprint(map[i].file, "%s/%s", dir, pd[i].name);
+		map[i].name = estrdup(pd[i].name);
+		map[i].current = 0;
+	}
+	free(pd);
+
+	close(fd);
+}
+
+void
+drawmap(int i)
+{
+	if(map[i].current)
+		draw(screen, map[i].r, sel, nil, ZP);
+	else
+		draw(screen, map[i].r, display->black, nil, ZP);
+
+	_string(screen, addpt(map[i].r.min, Pt(2,0)), txt, ZP,
+		font, map[i].name, nil, strlen(map[i].name), 
+		map[i].r, nil, ZP, SoverD);
+	border(screen, map[i].r, 1, txt, ZP);	
+}
+
+void
+geometry(void)
+{
+	int i, rows, cols;
+	Rectangle r;
+
+	rows = (Dy(screen->r)-2*MARGIN+PAD)/(font->height+PAD);
+	if(rows < 1)
+		rows = 1;
+	cols = (nmap+rows-1)/rows;
+	if(cols < 1)
+		cols = 1;
+	r = Rect(0,0,(Dx(screen->r)-2*MARGIN+PAD)/cols-PAD, font->height);
+	for(i=0; i<nmap; i++)
+		map[i].r = rectaddpt(rectaddpt(r, Pt(MARGIN+(PAD+Dx(r))*(i/rows),
+					MARGIN+(PAD+Dy(r))*(i%rows))), screen->r.min);
+
+}
+
+void
+redraw(Image *screen)
+{
+	int i;
+
+	draw(screen, screen->r, display->black, nil, ZP);
+	for(i=0; i<nmap; i++)
+		drawmap(i);
+	flushimage(display, 1);
+}
+
+void
+eresized(int new)
+{
+	if(new && getwindow(display, Refmesg) < 0)
+		fprint(2,"can't reattach to window");
+	geometry();
+	redraw(screen);
+}
+
+int
+writemap(char *file)
+{
+	int i, fd, ofd;
+	char buf[8192];
+	int n;
+	char *p;
+
+	if((fd = open(file, OREAD)) < 0){
+		fprint(2, "cannot open %s: %r\n", file);
+		return -1;
+	}
+	if((ofd = open("/dev/kbmap", OWRITE|OTRUNC)) < 0){
+		fprint(2, "cannot open /dev/kbmap: %r\n");
+		close(fd);
+		return -1;
+	}
+	/* do not write half lines */
+	n = 0;
+	while((i = read(fd, buf + n, sizeof buf - 1 - n)) > 0){
+		n += i;
+		buf[n] = '\0';
+		p = strrchr(buf, '\n');
+		if(p == nil){
+			if(n == sizeof buf - 1){
+				fprint(2, "writing /dev/kbmap: line too long\n");
+				break;
+			}
+			continue;
+		}
+		p++;
+		if(write(ofd, buf, p - buf) !=  p - buf){
+			fprint(2, "writing /dev/kbmap: %r\n");
+			break;
+		}
+		n -= p - buf;
+		memmove(buf, p, n);
+	}
+
+	close(fd);
+	close(ofd);
+	return 0;
+}
+
+void
+click(Mouse m)
+{
+	int i, j;
+
+	if(m.buttons == 0 || (m.buttons & ~4))
+		return;
+
+	for(i=0; i<nmap; i++)
+		if(ptinrect(m.xy, map[i].r))
+			break;
+	if(i == nmap)
+		return;
+
+	do
+		m = emouse();
+	while(m.buttons == 4);
+
+	if(m.buttons != 0){
+		do
+			m = emouse();
+		while(m.buttons);
+		return;
+	}
+
+	for(j=0; j<nmap; j++)
+		if(ptinrect(m.xy, map[j].r))
+			break;
+	if(j != i)
+		return;
+
+	writemap(map[i].file);
+
+	/* clean the previous current map */
+	for(j=0; j<nmap; j++)
+		map[j].current = 0;
+
+	map[i].current = 1;
+
+	redraw(screen);
+}
+
+void
+usage(void)
+{
+	fprint(2, "usage: kbmap [file...]\n");
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	Event e;
+	char *c;
+
+	if(argc > 1) {
+		argv++; argc--;
+		map = emalloc((argc)*sizeof(KbMap));
+		while(argc--) {
+			map[argc].file = estrdup(argv[argc]);
+			c = strrchr(map[argc].file, '/');
+			map[argc].name = (c == nil ? map[argc].file : c+1);
+			map[argc].current = 0;
+			nmap++;
+		}
+	} else 
+		init();
+
+	if(initdraw(0, 0, "kbmap") < 0){
+		fprint(2, "kbmap: initdraw failed: %r\n");
+		exits("initdraw");
+	}
+	enum{
+		Ctxt,
+		Csel,
+		Ncols,
+	};
+	Theme th[Ncols] = {
+		[Ctxt] { "text",	0xEAFFFFFF },
+		[Csel] { "hold", 	DBlue },
+	};
+	readtheme(th, nelem(th), nil);
+	txt = allocimage(display, Rect(0,0,1,1), screen->chan, 1, th[Ctxt].c);
+	sel = allocimage(display, Rect(0,0,1,1), screen->chan, 1, th[Csel].c);
+	if(txt == nil || sel == nil)
+		sysfatal("allocimage: %r");
+
+	eresized(0);
+	einit(Emouse|Ekeyboard);
+
+	for(;;){
+		switch(eread(Emouse|Ekeyboard, &e)){
+		case Ekeyboard:
+			if(e.kbdc==Kdel || e.kbdc=='q')
+				exits(0);
+			break;
+		case Emouse:
+			if(e.mouse.buttons)
+				click(e.mouse);
+			break;
+		}
+	}
+}
+
--- /dev/null
+++ b/sys/src/cmd/spred/cmdw.c
@@ -1,0 +1,342 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include <draw.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <frame.h>
+#include "dat.h"
+#include "fns.h"
+
+static int
+cmdinit(Win *)
+{
+	return 0;
+}
+
+static void
+scrollbar(Win *w)
+{
+	int h, t0, t1;
+
+	h = Dy(w->inner);
+	draw(w->im, rectaddpt(Rect(0, 0, SCRBSIZ+1, h), w->inner.min), w->tab->cols[BORD], nil, ZP);
+	t0 = w->toprune * h;
+	t1 = (w->toprune + w->fr.nchars) * h;
+	if(w->nrunes == 0){
+		 t0 = 0;
+		 t1 = h;
+	}else{
+		t0 /= w->nrunes;
+		t1 /= w->nrunes;
+	}
+	draw(w->im, rectaddpt(Rect(0, t0, SCRBSIZ, t1), w->inner.min), w->tab->cols[BACK], nil, ZP);
+}
+
+static void
+cmddraw(Win *w)
+{
+	Rectangle r;
+	
+	frclear(&w->fr, 0);
+	r = insetrect(w->inner, 1);
+	r.min.x += SCRTSIZ;
+	scrollbar(w);
+	frinit(&w->fr, r, display->defaultfont, w->im, w->tab->cols);
+	frinsert(&w->fr, w->runes + w->toprune, w->runes + w->nrunes, 0);
+}
+
+void
+cmdscroll(Win *w, int l)
+{
+	int r;
+	
+	if(l == 0)
+		return;
+	if(l > 0){
+		for(r = w->toprune; r < w->nrunes && l != 0; r++)
+			if(w->runes[r] == '\n')
+				l--;
+		w->toprune = r;
+	}else{
+		for(r = w->toprune; r > 0; r--)
+			if(w->runes[r] == '\n' && ++l == 0){
+				r++;
+				break;
+			}
+		w->toprune = r;
+	
+	}
+	frdelete(&w->fr, 0, w->fr.nchars);
+	frinsert(&w->fr, w->runes + w->toprune, w->runes + w->nrunes, 0);
+	scrollbar(w);
+}
+
+static void
+cmdclick(Win *w, Mousectl *mc)
+{
+	if(mc->xy.x <= w->inner.min.x + SCRBSIZ){
+		cmdscroll(w, -5);
+		return;
+	}
+	frselect(&w->fr, mc);
+}
+
+static int
+cmdrmb(Win *w, Mousectl *mc)
+{
+	if(mc->xy.x > w->inner.min.x + SCRBSIZ)
+		return -1;
+	cmdscroll(w, 5);
+	return 0;
+}
+
+int
+cmdinsert(Win *w, Rune *r, int nr, int rp)
+{
+	Rune *s;
+
+	if(nr < 0)
+		for(nr = 0, s = r; *s++ != 0; nr++)
+			;
+	if(rp < 0 || rp > w->nrunes)
+		rp = w->nrunes;
+	if(w->nrunes + nr > w->arunes){
+		w->runes = realloc(w->runes, w->arunes = w->arunes + (nr + RUNEBLK - 1) & ~(RUNEBLK - 1));
+		if(w->runes == nil)
+			sysfatal("realloc: %r");
+	}
+	if(rp != w->nrunes)
+		memmove(w->runes + rp, w->runes + rp + nr, (w->nrunes - rp) * sizeof(Rune));
+	memmove(w->runes + rp, r, nr * sizeof(Rune));
+	w->nrunes += nr;
+	if(w->toprune > rp)
+		w->toprune += nr;
+	else{
+		frinsert(&w->fr, w->runes + rp, w->runes + rp + nr, rp - w->toprune);
+		if(rp == w->nrunes - nr){
+			if(w->fr.lastlinefull)
+				cmdscroll(w, 1);
+		}
+	}
+	if(w->opoint > rp)
+		w->opoint += nr;
+	return nr;
+}
+
+static void
+cmddel(Win *w, int a, int b)
+{
+	if(a >= b)
+		return;
+	memmove(w->runes + a, w->runes + b, w->nrunes - b);
+	w->nrunes -= b - a;
+	if(w->toprune >= b)
+		w->toprune -= b - a;
+	else{
+		frdelete(&w->fr, a - w->toprune, b - w->toprune);
+		if(w->toprune >= a)
+			w->toprune = a;
+	}
+	if(a <= w->opoint && w->opoint < b)
+		w->opoint = a;
+	else if(w->opoint >= b)
+		w->opoint -= b -  a;
+}
+
+static void
+setsel(Win *w, int p0, int p1)
+{
+	frdrawsel(&w->fr, frptofchar(&w->fr, w->fr.p0), w->fr.p0, w->fr.p1, 0);
+	w->fr.p0 = p0;
+	w->fr.p1 = p1;
+	frdrawsel(&w->fr, frptofchar(&w->fr, p0), p0, p1, 1);
+}
+
+static void
+cmdline(Win *w)
+{
+	static char buf[4096];
+	Rune *q;
+	char *p;
+
+	q = w->runes + w->opoint;
+	p = buf;
+	while(q < w->runes + w->nrunes && p < buf + nelem(buf) + 1)
+		p += runetochar(p, q++);
+	*p = 0;
+	w->opoint = w->nrunes;
+	docmd(buf);
+}
+
+static void
+cmdkey(Win *w, Rune r)
+{
+	switch(r){
+	case Kview:
+		cmdscroll(w, 3);
+		return;
+	case Kup:
+		cmdscroll(w, -3);
+		return;
+	case Kleft:
+		if(w->fr.p0 == 0)
+			return;
+		setsel(w, w->fr.p0 - 1, w->fr.p0 - 1);
+		return;
+	case Kright:
+		if(w->toprune + w->fr.p1 == w->nrunes)
+			return;
+		setsel(w, w->fr.p1 + 1, w->fr.p1 + 1);
+		return;
+	}
+	if(w->fr.p0 < w->fr.p1)
+		cmddel(w, w->toprune + w->fr.p0, w->toprune + w->fr.p1);
+	switch(r){
+	case 0x00:
+	case Kesc:
+		break;
+	case '\b':
+		if(w->fr.p0 > 0 && w->toprune + w->fr.p0 != w->opoint)
+			cmddel(w, w->toprune + w->fr.p0 - 1, w->toprune + w->fr.p0);
+		break;
+	case '\n':
+		cmdinsert(w, &r, 1, w->fr.p0 + w->toprune);
+		if(w->toprune + w->fr.p0 == w->nrunes)
+			cmdline(w);
+		break;
+	default:
+		cmdinsert(w, &r, 1, w->fr.p0 + w->toprune);
+	}
+}
+
+static int
+tosnarf(Win *w, int p0, int p1)
+{
+	int fd;
+	static char buf[512];
+	char *c, *ce;
+	Rune *rp, *re;
+	
+	if(p0 >= p1)
+		return 0;
+	fd = open("/dev/snarf", OWRITE|OTRUNC);
+	if(fd < 0){
+		cmdprint("tosnarf: %r");
+		return -1;
+	}
+	c = buf;
+	ce = buf + sizeof(buf);
+	rp = w->runes + p0;
+	re = w->runes + p1;
+	for(; rp < re; rp++){
+		if(c + UTFmax > ce){
+			write(fd, buf, c - buf);
+			c = buf;
+		}
+		c += runetochar(c, rp);
+	}
+	if(c > buf)
+		write(fd, buf, c - buf);
+	close(fd);
+	return 0;
+}
+
+static int
+fromsnarf(Win *w, int p0)
+{
+	int fd, rc;
+	char *buf, *p;
+	Rune *rbuf, *r;
+	int nc, end;
+	
+	fd = open("/dev/snarf", OREAD);
+	if(fd < 0){
+		cmdprint("fromsnarf: %r");
+		return -1;
+	}
+	buf = nil;
+	nc = 0;
+	for(;;){
+		buf = realloc(buf, nc + 4096);
+		rc = readn(fd, buf + nc, nc + 4096);
+		if(rc <= 0)
+			break;
+		nc += rc;
+		if(rc < 4096)
+			break;
+	}
+	close(fd);
+	rbuf = emalloc(sizeof(Rune) * nc);
+	r = rbuf;
+	for(p = buf; p < buf + nc; r++)
+		p += chartorune(r, p);
+	end = p0 == w->nrunes;
+	cmdinsert(w, rbuf, r - rbuf, p0);
+	if(end && r > rbuf && r[-1] == '\n')
+		cmdline(w);
+	return 0;
+}
+
+static void
+cmdmenu(Win *w, Mousectl *mc)
+{
+	enum {
+		CUT,
+		PASTE,
+		SNARF,
+	};
+	static char *ms[] = {
+		[CUT] "cut",
+		[PASTE] "paste",
+		[SNARF] "snarf",
+		nil,
+	};
+	static Menu m = {ms};
+	
+	switch(menuhit(2, mc, &m, nil)){
+	case CUT:
+		if(tosnarf(w, w->toprune + w->fr.p0, w->toprune + w->fr.p1) >= 0)
+			cmddel(w, w->toprune + w->fr.p0, w->toprune + w->fr.p1);
+		break;
+	case SNARF:
+		tosnarf(w, w->toprune + w->fr.p0, w->toprune + w->fr.p1);
+		break;
+	case PASTE:
+		if(w->fr.p0 < w->fr.p1)
+			cmddel(w, w->toprune + w->fr.p0, w->toprune + w->fr.p1);
+		fromsnarf(w, w->toprune + w->fr.p0);
+		break;
+	}
+}
+
+void
+cmdprint(char *fmt, ...)
+{
+	Rune *r;
+	va_list va;
+	
+	va_start(va, fmt);
+	r = runevsmprint(fmt, va);
+	va_end(va);
+	if(r != nil)
+		cmdw->opoint += cmdinsert(cmdw, r, -1, cmdw->opoint);
+}
+
+Wintab cmdtab = {
+	.init = cmdinit,
+	.draw = cmddraw,
+	.click = cmdclick,
+	.menu = cmdmenu,
+	.rmb = cmdrmb,
+	.key = cmdkey,
+	.hexcols = {
+		[BORD] 0x440000FF,
+		[TEXT] 0x770000FF,
+		[DISB] 0x111111FF,
+		[BACK] 0x000000FF,
+		[HIGH] 0x111111FF,
+		[HTEXT] 0x770000FF,
+	}
+};
--- /dev/null
+++ b/sys/src/cmd/spred/win.c
@@ -1,0 +1,283 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include <draw.h>
+#include <mouse.h>
+#include <cursor.h>
+#include <frame.h>
+#include "dat.h"
+#include "fns.h"
+
+Screen *scr;
+extern Wintab *tabs[];
+Win wlist;
+File flist;
+Win *actw, *actf, *cmdw;
+Image *invcol;
+
+void*
+emalloc(ulong sz)
+{
+	void *v;
+	
+	v = malloc(sz);
+	if(v == nil)
+		sysfatal("malloc: %r");
+	memset(v, 0, sz);
+	setmalloctag(v, getcallerpc(&sz));
+	return v;
+}
+
+void
+initwin(void)
+{
+	Rectangle r;
+	int i, j;
+
+	scr = allocscreen(screen, display->white, 0);
+	if(scr == nil)
+		sysfatal("allocscreen: %r");
+	draw(screen, screen->r, display->black, nil, ZP);
+	for(i = 0; i < NTYPES; i++)
+		for(j = 0; j < NCOLS; j++)
+			tabs[i]->cols[j] = allocimage(display, Rect(0, 0, 1, 1), screen->chan, 1, tabs[i]->hexcols[j]);
+	invcol = allocimage(display, Rect(0, 0, 2, 2), screen->chan, 1, 0);
+	draw(invcol, Rect(1, 0, 2, 1), display->white, nil, ZP);
+	draw(invcol, Rect(0, 1, 1, 2), display->white, nil, ZP);
+	wlist.next = wlist.prev = &wlist;
+	flist.next = flist.prev = &flist;
+	r = screen->r;
+	r.max.y = r.min.y + Dy(r) / 5;
+	cmdw = newwin(CMD, r, nil);
+	if(cmdw == nil)
+		sysfatal("newwin: %r");
+}
+
+Win *
+newwin(int t, Rectangle r, File *f)
+{
+	Win *w;
+	
+	w = emalloc(sizeof(*w));
+	w->next = &wlist;
+	w->prev = wlist.prev;
+	w->next->prev = w;
+	w->prev->next = w;
+	w->type = t;
+	w->tab = tabs[t];
+	w->entire = r;
+	w->inner = insetrect(r, BORDSIZ);
+	w->im = allocwindow(scr, r, Refbackup, 0);
+	draw(w->im, w->inner, w->tab->cols[BACK], nil, ZP);
+	if(f != nil){
+		incref(f);
+		w->wprev = f->wins.wprev;
+		w->wnext = &f->wins;
+		f->wins.wprev->wnext = w;
+		f->wins.wprev = w;
+		w->f = f;
+	}
+	w->tab->init(w);
+	setfocus(w);
+	w->tab->draw(w);
+	return w;
+}
+
+Win *
+newwinsel(int t, Mousectl *mc, File *f)
+{
+	Rectangle u;
+
+	u = getrect(3, mc);
+	if(Dx(u) < MINSIZ || Dy(u) < MINSIZ)
+		return nil;
+	rectclip(&u, screen->r);
+	return newwin(t, u, f);
+}
+
+void
+winzerox(Win *w, Mousectl *mc)
+{
+	Win *v;
+
+	if(w->tab->zerox == nil){
+		cmdprint("?\n");
+		return;
+	}
+	v = newwinsel(w->type, mc, w->f);
+	if(v == nil)
+		return;
+	w->tab->zerox(w, v);
+	v->tab->draw(v);
+}
+
+void
+winclose(Win *w)
+{
+	if(w->f == nil){
+		cmdprint("?\n");
+		return;
+	}
+	if(!decref(w->f)){
+		if(w->f->change > 0){
+			cmdprint("?\n");
+			incref(w->f);
+			w->f->change = -1;
+			return;
+		}
+		putfil(w->f);
+		w->f = nil;
+	}
+	freeimage(w->im);
+	if(w->f != nil){
+		w->wnext->wprev = w->wprev;
+		w->wprev->wnext = w->wnext;
+	}
+	w->next->prev = w->prev;
+	w->prev->next = w->next;
+	if(w == actw)
+		actw = nil;
+	if(w == actf)
+		actf = nil;
+	free(w);
+}
+
+void
+setfocus(Win *w)
+{
+	if(actw != nil)
+		border(actw->im, actw->entire, BORDSIZ, actw->tab->cols[DISB], ZP);
+	actw = w;
+	if(w != cmdw)
+		actf = w;
+	if(w == nil)
+		return;
+	if(w->im == nil)
+		sysfatal("setfocus: phase error");
+	topwindow(w->im);
+	w->prev->next = w->next;
+	w->next->prev = w->prev;
+	w->prev = wlist.prev;
+	w->next = &wlist;
+	w->prev->next = w;
+	w->next->prev = w;
+	border(w->im, w->entire, BORDSIZ, w->tab->cols[BORD], ZP);
+}
+
+static Win *
+winpoint(Point p)
+{
+	Win *w;
+	
+	for(w = wlist.prev; w != &wlist; w = w->prev)
+		if(ptinrect(p, w->entire))
+			return w;
+	return nil;
+}
+
+void
+winclick(Mousectl *mc)
+{
+	Win *w;
+	
+	w = winpoint(mc->xy);
+	if(w != nil){
+		if(w != actw)
+			setfocus(w);
+		w->tab->click(w, mc);
+	}
+	while((mc->buttons & 1) != 0)
+		readmouse(mc);
+}
+
+Win *
+winsel(Mousectl *mc, int but)
+{
+	extern Cursor crosscursor;
+	int m;
+	Win *w;
+	
+	m = 1 << but - 1;
+	setcursor(mc, &crosscursor);
+	for(;;){
+		readmouse(mc);
+		if((mc->buttons & ~m) != 0){
+			w = nil;
+			goto end;
+		}
+		if((mc->buttons & m) != 0)
+			break;
+	}
+	w = winpoint(mc->xy);
+end:
+	while(readmouse(mc), mc->buttons != 0)
+		;
+	setcursor(mc, nil);
+	return w;
+}
+
+void
+winresize(Win *w, Mousectl *mc)
+{
+	Rectangle r;
+	
+	if(w == nil)
+		return;
+	r = getrect(3, mc);
+	if(Dx(r) < MINSIZ || Dy(r) < MINSIZ)
+		return;
+	rectclip(&r, screen->r);
+	freeimage(w->im);
+	w->entire = r;
+	w->inner = insetrect(r, BORDSIZ);
+	w->im = allocwindow(scr, r, Refbackup, 0);
+	draw(w->im, w->inner, w->tab->cols[BACK], nil, ZP);
+	setfocus(w);
+	w->tab->draw(w);
+}
+
+void
+resize(void)
+{
+	Rectangle old, r;
+	int dxo, dyo, dxn, dyn;
+	Win *w;
+	
+	old = screen->r;
+	dxo = Dx(old);
+	dyo = Dy(old);
+	if(getwindow(display, Refnone) < 0)
+		sysfatal("resize failed: %r");
+	dxn = Dx(screen->r);
+	dyn = Dy(screen->r);
+	freescreen(scr);
+	scr = allocscreen(screen, display->white, 0);
+	if(scr == nil)
+		sysfatal("allocscreen: %r");
+	draw(screen, screen->r, display->black, nil, ZP);
+	for(w = wlist.next; w != &wlist; w = w->next){
+		r = rectsubpt(w->entire, old.min);
+		r.min.x = muldiv(r.min.x, dxn, dxo);
+		r.max.x = muldiv(r.max.x, dxn, dxo);
+		r.min.y = muldiv(r.min.y, dyn, dyo);
+		r.max.y = muldiv(r.max.y, dyn, dyo);
+		w->entire = rectaddpt(r, screen->r.min);
+		w->inner = insetrect(w->entire, BORDSIZ);
+		freeimage(w->im);
+		w->im = allocwindow(scr, w->entire, Refbackup, 0);
+		if(w->im == nil)
+			sysfatal("allocwindow: %r");
+		draw(w->im, w->inner, w->tab->cols[BACK], nil, ZP);
+		border(w->im, w->entire, BORDSIZ, w->tab->cols[w == actw ? BORD : DISB], ZP);
+		w->tab->draw(w);
+	}
+}
+
+extern Wintab cmdtab, paltab, sprtab;
+
+Wintab *tabs[] = {
+	[CMD] &cmdtab,
+	[PAL] &paltab,
+	[SPR] &sprtab,
+};
--- /dev/null
+++ b/sys/src/cmd/stats.c
@@ -1,0 +1,1463 @@
+#include <u.h>
+#include <libc.h>
+#include <ctype.h>
+#include <draw.h>
+#include <event.h>
+#include <keyboard.h>
+
+#define	MAXNUM	10	/* maximum number of numbers on data line */
+
+typedef struct Graph	Graph;
+typedef struct Machine	Machine;
+
+struct Graph
+{
+	int		colindex;
+	Rectangle	r;
+	int		*data;
+	int		ndata;
+	char		*label;
+	void		(*newvalue)(Machine*, uvlong*, uvlong*, int);
+	void		(*update)(Graph*, uvlong, uvlong);
+	Machine		*mach;
+	int		overflow;
+	Image		*overtmp;
+};
+
+enum
+{
+	/* /dev/swap */
+	Mem		= 0,
+	Maxmem,
+	Swap,
+	Maxswap,
+	Reclaim,
+	Maxreclaim,
+	Kern,
+	Maxkern,
+	Draw,
+	Maxdraw,
+
+	/* /dev/sysstats */
+	Procno	= 0,
+	Context,
+	Interrupt,
+	Syscall,
+	Fault,
+	TLBfault,
+	TLBpurge,
+	Load,
+	Idle,
+	InIntr,
+
+	/* /net/ether0/stats */
+	In		= 0,
+	Link,
+	Out,
+	Err0,
+};
+
+struct Machine
+{
+	char		*name;
+	char		*shortname;
+	int		remote;
+	int		statsfd;
+	int		swapfd;
+	int		etherfd;
+	int		ifstatsfd;
+	int		batteryfd;
+	int		bitsybatfd;
+	int		tempfd;
+	int		disable;
+
+	uvlong		devswap[10];
+	uvlong		devsysstat[10];
+	uvlong		prevsysstat[10];
+	int		nproc;
+	int		lgproc;
+	uvlong		netetherstats[8];
+	uvlong		prevetherstats[8];
+	uvlong		batterystats[2];
+	uvlong		netetherifstats[2];
+	uvlong		temp[10];
+
+	/* big enough to hold /dev/sysstat even with many processors */
+	char		buf[8*1024];
+	char		*bufp;
+	char		*ebufp;
+};
+
+enum
+{
+	Mainproc,
+	Inputproc,
+	NPROC,
+};
+
+enum
+{
+	Ncolor		= 6,
+	Ysqueeze	= 2,	/* vertical squeezing of label text */
+	Labspace	= 2,	/* room around label */
+	Dot		= 2,	/* height of dot */
+	Opwid		= 5,	/* strlen("add  ") or strlen("drop ") */
+	Nlab		= 3,	/* max number of labels on y axis */
+	Lablen		= 16,	/* max length of label */
+	Lx		= 4,	/* label tick length */
+};
+
+enum Menu2
+{
+	Mbattery,
+	Mcontext,
+	Mether,
+	Methererr,
+	Metherin,
+	Metherout,
+	Mfault,
+	Midle,
+	Minintr,
+	Mintr,
+	Mload,
+	Mmem,
+	Mswap,
+	Mreclaim,
+	Mkern,
+	Mdraw,
+	Msyscall,
+	Mtlbmiss,
+	Mtlbpurge,
+	Msignal,
+	Mtemp,
+	Nmenu2,
+};
+
+char	*menu2str[Nmenu2+1] = {
+	"add  battery ",
+	"add  context ",
+	"add  ether   ",
+	"add  ethererr",
+	"add  etherin ",
+	"add  etherout",
+	"add  fault   ",
+	"add  idle    ",
+	"add  inintr  ",
+	"add  intr    ",
+	"add  load    ",
+	"add  mem     ",
+	"add  swap    ",
+	"add  reclaim ",
+	"add  kern    ",
+	"add  draw    ",
+	"add  syscall ",
+	"add  tlbmiss ",
+	"add  tlbpurge",
+	"add  802.11b ",
+	"add  temp    ",
+	nil,
+};
+
+
+void	contextval(Machine*, uvlong*, uvlong*, int),
+	etherval(Machine*, uvlong*, uvlong*, int),
+	ethererrval(Machine*, uvlong*, uvlong*, int),
+	etherinval(Machine*, uvlong*, uvlong*, int),
+	etheroutval(Machine*, uvlong*, uvlong*, int),
+	faultval(Machine*, uvlong*, uvlong*, int),
+	intrval(Machine*, uvlong*, uvlong*, int),
+	inintrval(Machine*, uvlong*, uvlong*, int),
+	loadval(Machine*, uvlong*, uvlong*, int),
+	idleval(Machine*, uvlong*, uvlong*, int),
+	memval(Machine*, uvlong*, uvlong*, int),
+	swapval(Machine*, uvlong*, uvlong*, int),
+	reclaimval(Machine*, uvlong*, uvlong*, int),
+	kernval(Machine*, uvlong*, uvlong*, int),
+	drawval(Machine*, uvlong*, uvlong*, int),
+	syscallval(Machine*, uvlong*, uvlong*, int),
+	tlbmissval(Machine*, uvlong*, uvlong*, int),
+	tlbpurgeval(Machine*, uvlong*, uvlong*, int),
+	batteryval(Machine*, uvlong*, uvlong*, int),
+	signalval(Machine*, uvlong*, uvlong*, int),
+	tempval(Machine*, uvlong*, uvlong*, int);
+
+Menu	menu2 = {menu2str, nil};
+int	present[Nmenu2];
+void	(*newvaluefn[Nmenu2])(Machine*, uvlong*, uvlong*, int init) = {
+	batteryval,
+	contextval,
+	etherval,
+	ethererrval,
+	etherinval,
+	etheroutval,
+	faultval,
+	idleval,
+	inintrval,
+	intrval,
+	loadval,
+	memval,
+	swapval,
+	reclaimval,
+	kernval,
+	drawval,
+	syscallval,
+	tlbmissval,
+	tlbpurgeval,
+	signalval,
+	tempval,
+};
+
+enum{
+	Cback,
+	Cbord,
+	Ctext,
+	Cmix1,
+	Cmix2,
+	Nscolor = Cmix2,
+	Ccol1,
+	Ccol2,
+	Ccol3,
+	Ccol4,
+	Ccol5,
+	Ccol6,
+	Ntcolor,
+};
+
+Image	*cols[Ncolor][3];
+Image	*tcols[Nscolor];
+Graph	*graph;
+Machine	*mach;
+char	*mysysname;
+char	argchars[] = "8bcdeEfiIkmlnprstwz";
+int	pids[NPROC];
+int 	parity;	/* toggled to avoid patterns in textured background */
+int	nmach;
+int	ngraph;	/* totaly number is ngraph*nmach */
+double	scale = 1.0;
+int	logscale = 0;
+int	ylabels = 0;
+int	sleeptime = 1000;
+int	batteryperiod = 1000;
+int	tempperiod = 1000;
+
+char	*procnames[NPROC] = {"main", "input"};
+
+void
+killall(char *s)
+{
+	int i, pid;
+
+	pid = getpid();
+	for(i=0; i<NPROC; i++)
+		if(pids[i] && pids[i]!=pid)
+			postnote(PNPROC, pids[i], "kill");
+	exits(s);
+}
+
+void*
+emalloc(ulong sz)
+{
+	void *v;
+	v = malloc(sz);
+	if(v == nil) {
+		fprint(2, "stats: out of memory allocating %ld: %r\n", sz);
+		killall("mem");
+	}
+	memset(v, 0, sz);
+	return v;
+}
+
+void*
+erealloc(void *v, ulong sz)
+{
+	v = realloc(v, sz);
+	if(v == nil) {
+		fprint(2, "stats: out of memory reallocating %ld: %r\n", sz);
+		killall("mem");
+	}
+	return v;
+}
+
+char*
+estrdup(char *s)
+{
+	char *t;
+	if((t = strdup(s)) == nil) {
+		fprint(2, "stats: out of memory in strdup(%.10s): %r\n", s);
+		killall("mem");
+	}
+	return t;
+}
+
+void
+mkcol(int i, int mix, int mix2, int c)
+{
+	cols[i][0] = allocimagemix(display, c, mix);
+	cols[i][1] = allocimagemix(display, c, mix2);
+	cols[i][2] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, c);
+}
+
+void
+colinit(Theme th[Ntcolor])
+{
+	mkcol(0, th[Cmix1].c, th[Cmix2].c, th[Ccol1].c);
+	mkcol(1, th[Cmix1].c, th[Cmix2].c, th[Ccol2].c);
+	mkcol(2, th[Cmix1].c, th[Cmix2].c, th[Ccol3].c);
+	mkcol(3, th[Cmix1].c, th[Cmix2].c, th[Ccol4].c);
+	mkcol(4, th[Cmix1].c, th[Cmix2].c, th[Ccol5].c);
+	mkcol(5, th[Cmix1].c, th[Cmix2].c, th[Ccol6].c);
+}
+
+int
+loadbuf(Machine *m, int *fd)
+{
+	int n;
+
+
+	if(*fd < 0)
+		return 0;
+	seek(*fd, 0, 0);
+	n = read(*fd, m->buf, sizeof m->buf-1);
+	if(n <= 0){
+		close(*fd);
+		*fd = -1;
+		return 0;
+	}
+	m->bufp = m->buf;
+	m->ebufp = m->buf+n;
+	m->buf[n] = 0;
+	return 1;
+}
+
+void
+label(Point p, int dy, char *text)
+{
+	char *s;
+	Rune r[2];
+	int w, maxw, maxy;
+
+	p.x += Labspace;
+	maxy = p.y+dy;
+	maxw = 0;
+	r[1] = '\0';
+	for(s=text; *s; ){
+		if(p.y+font->height-Ysqueeze > maxy)
+			break;
+		w = chartorune(r, s);
+		s += w;
+		w = runestringwidth(font, r);
+		if(w > maxw)
+			maxw = w;
+		runestring(screen, p, tcols[Ctext], ZP, font, r);
+		p.y += font->height-Ysqueeze;
+	}
+}
+
+Point
+paritypt(int x)
+{
+	return Pt(x+parity, 0);
+}
+
+Point
+datapoint(Graph *g, int x, uvlong v, uvlong vmax)
+{
+	Point p;
+	double y;
+
+	p.x = x;
+	y = ((double)v)/(vmax*scale);
+	if(logscale){
+		/*
+		 * Arrange scale to cover a factor of 1000.
+		 * vmax corresponds to the 100 mark.
+		 * 10*vmax is the top of the scale.
+		 */
+		if(y <= 0.)
+			y = 0;
+		else{
+			y = log10(y);
+			/* 1 now corresponds to the top; -2 to the bottom; rescale */
+			y = (y+2.)/3.;
+		}
+	}
+	if(y >= 1.)
+		y = 1;
+	if(y <= 0.)
+		y = 0;
+	p.y = g->r.max.y - Dy(g->r)*y - Dot;
+	if(p.y < g->r.min.y)
+		p.y = g->r.min.y;
+	if(p.y > g->r.max.y-Dot)
+		p.y = g->r.max.y-Dot;
+	return p;
+}
+
+void
+drawdatum(Graph *g, int x, uvlong prev, uvlong v, uvlong vmax)
+{
+	int c;
+	Point p, q;
+
+	c = g->colindex;
+	p = datapoint(g, x, v, vmax);
+	q = datapoint(g, x, prev, vmax);
+	if(p.y < q.y){
+		draw(screen, Rect(p.x, g->r.min.y, p.x+1, p.y), cols[c][0], nil, paritypt(p.x));
+		draw(screen, Rect(p.x, p.y, p.x+1, q.y+Dot), cols[c][2], nil, ZP);
+		draw(screen, Rect(p.x, q.y+Dot, p.x+1, g->r.max.y), cols[c][1], nil, ZP);
+	}else{
+		draw(screen, Rect(p.x, g->r.min.y, p.x+1, q.y), cols[c][0], nil, paritypt(p.x));
+		draw(screen, Rect(p.x, q.y, p.x+1, p.y+Dot), cols[c][2], nil, ZP);
+		draw(screen, Rect(p.x, p.y+Dot, p.x+1, g->r.max.y), cols[c][1], nil, ZP);
+	}
+
+}
+
+void
+redraw(Graph *g, uvlong vmax)
+{
+	int i, c;
+
+	c = g->colindex;
+	draw(screen, g->r, cols[c][0], nil, paritypt(g->r.min.x));
+	for(i=1; i<Dx(g->r); i++)
+		drawdatum(g, g->r.max.x-i, g->data[i-1], g->data[i], vmax);
+	drawdatum(g, g->r.min.x, g->data[i], g->data[i], vmax);
+	g->overflow = 0;
+}
+
+void
+update1(Graph *g, uvlong v, uvlong vmax)
+{
+	char buf[48];
+	int overflow;
+
+	if(g->overflow && g->overtmp!=nil)
+		draw(screen, g->overtmp->r, g->overtmp, nil, g->overtmp->r.min);
+	draw(screen, g->r, screen, nil, Pt(g->r.min.x+1, g->r.min.y));
+	drawdatum(g, g->r.max.x-1, g->data[0], v, vmax);
+	memmove(g->data+1, g->data, (g->ndata-1)*sizeof(g->data[0]));
+	g->data[0] = v;
+	g->overflow = 0;
+	if(logscale)
+		overflow = (v>10*vmax*scale);
+	else
+		overflow = (v>vmax*scale);
+	if(overflow && g->overtmp!=nil){
+		g->overflow = 1;
+		draw(g->overtmp, g->overtmp->r, screen, nil, g->overtmp->r.min);
+		sprint(buf, "%llud", v);
+		string(screen, g->overtmp->r.min, tcols[Ctext], ZP, font, buf);
+	}
+}
+
+/* read one line of text from buffer and process integers */
+int
+readnums(Machine *m, int n, uvlong *a, int spanlines)
+{
+	int i;
+	char *p, *ep;
+
+	if(spanlines)
+		ep = m->ebufp;
+	else
+		for(ep=m->bufp; ep<m->ebufp; ep++)
+			if(*ep == '\n')
+				break;
+	p = m->bufp;
+	for(i=0; i<n && p<ep; i++){
+		while(p<ep && (!isascii(*p) || !isdigit(*p)) && *p!='-')
+			p++;
+		if(p == ep)
+			break;
+		a[i] = strtoull(p, &p, 10);
+	}
+	if(ep < m->ebufp)
+		ep++;
+	m->bufp = ep;
+	return i == n;
+}
+
+int
+readswap(Machine *m, uvlong *a)
+{
+	static int xxx = 0;
+
+	if(strstr(m->buf, "memory\n")){
+		/* new /dev/swap - skip first 3 numbers */
+		if(!readnums(m, 7, a, 1))
+			return 0;
+
+		a[Mem] = a[3];
+		a[Maxmem] = a[4];
+		a[Swap] = a[5];
+		a[Maxswap] = a[6];
+
+		a[Reclaim] = 0;
+		a[Maxreclaim] = 0;
+		if(m->bufp = strstr(m->buf, "reclaim")){
+			while(m->bufp > m->buf && m->bufp[-1] != '\n')
+				m->bufp--;
+			a[Reclaim] = strtoull(m->bufp, &m->bufp, 10);
+			while(*m->bufp++ == '/')
+				a[Maxreclaim] = strtoull(m->bufp, &m->bufp, 10);
+		}
+
+		a[Kern] = 0;
+		a[Maxkern] = 0;
+		if(m->bufp = strstr(m->buf, "kernel malloc")){
+			while(m->bufp > m->buf && m->bufp[-1] != '\n')
+				m->bufp--;
+			a[Kern] = strtoull(m->bufp, &m->bufp, 10);
+			while(*m->bufp++ == '/')
+				a[Maxkern] = strtoull(m->bufp, &m->bufp, 10);
+		}
+
+		a[Draw] = 0;
+		a[Maxdraw] = 0;
+		if(m->bufp = strstr(m->buf, "kernel draw")){
+			while(m->bufp > m->buf && m->bufp[-1] != '\n')
+				m->bufp--;
+			a[Draw] = strtoull(m->bufp, &m->bufp, 10);
+			while(*m->bufp++ == '/')
+				a[Maxdraw] = strtoull(m->bufp, &m->bufp, 10);
+		}
+
+		return 1;
+	}
+
+	a[Reclaim] = 0;
+	a[Maxreclaim] = 0;
+	a[Kern] = 0;
+	a[Maxkern] = 0;
+	a[Draw] = 0;
+	a[Maxdraw] = 0;
+
+	return readnums(m, 4, a, 0);
+}
+
+char*
+shortname(char *s)
+{
+	char *p, *e;
+
+	p = estrdup(s);
+	e = strchr(p, '.');
+	if(e)
+		*e = 0;
+	return p;
+}
+
+int
+ilog10(uvlong j)
+{
+	int i;
+
+	for(i = 0; j >= 10; i++)
+		j /= 10;
+	return i;
+}
+
+int
+initmach(Machine *m, char *name)
+{
+	int n;
+	uvlong a[MAXNUM];
+	char *p, mpt[256], buf[256];
+
+	p = strchr(name, '!');
+	if(p)
+		p++;
+	else
+		p = name;
+	m->name = estrdup(p);
+	m->shortname = shortname(p);
+	m->remote = (strcmp(p, mysysname) != 0);
+	if(m->remote == 0)
+		strcpy(mpt, "");
+	else{
+		Waitmsg *w;
+		int pid;
+
+		snprint(mpt, sizeof mpt, "/n/%s", p);
+
+		pid = fork();
+		switch(pid){
+		case -1:
+			fprint(2, "can't fork: %r\n");
+			return 0;
+		case 0:
+			execl("/bin/rimport", "rimport", name, "/", mpt, nil);
+			fprint(2, "can't exec: %r\n");
+			exits("exec");
+		}
+		w = wait();
+		if(w == nil || w->pid != pid || w->msg[0] != '\0'){
+			free(w);
+			return 0;
+		}
+		free(w);
+	}
+
+	snprint(buf, sizeof buf, "%s/dev/swap", mpt);
+	m->swapfd = open(buf, OREAD);
+	if(loadbuf(m, &m->swapfd) && readswap(m, a))
+		memmove(m->devswap, a, sizeof m->devswap);
+
+	snprint(buf, sizeof buf, "%s/dev/sysstat", mpt);
+	m->statsfd = open(buf, OREAD);
+	if(loadbuf(m, &m->statsfd)){
+		for(n=0; readnums(m, nelem(m->devsysstat), a, 0); n++)
+			;
+		m->nproc = n;
+	}else
+		m->nproc = 1;
+	m->lgproc = ilog10(m->nproc);
+
+	snprint(buf, sizeof buf, "%s/net/ether0/stats", mpt);
+	m->etherfd = open(buf, OREAD);
+	if(loadbuf(m, &m->etherfd) && readnums(m, nelem(m->netetherstats), a, 1))
+		memmove(m->netetherstats, a, sizeof m->netetherstats);
+
+	snprint(buf, sizeof buf, "%s/net/ether0/ifstats", mpt);
+	m->ifstatsfd = open(buf, OREAD);
+	if(loadbuf(m, &m->ifstatsfd)){
+		/* need to check that this is a wavelan interface */
+		if(strncmp(m->buf, "Signal: ", 8) == 0 && readnums(m, nelem(m->netetherifstats), a, 1))
+			memmove(m->netetherifstats, a, sizeof m->netetherifstats);
+	}
+
+	snprint(buf, sizeof buf, "%s/mnt/apm/battery", mpt);
+	m->batteryfd = open(buf, OREAD);
+	if(m->batteryfd < 0){
+		snprint(buf, sizeof buf, "%s/mnt/pm/battery", mpt);
+		m->batteryfd = open(buf, OREAD);
+	}
+	m->bitsybatfd = -1;
+	if(m->batteryfd >= 0){
+		batteryperiod = 10000;
+		if(loadbuf(m, &m->batteryfd) && readnums(m, nelem(m->batterystats), a, 0))
+			memmove(m->batterystats, a, sizeof(m->batterystats));
+	}else{
+		snprint(buf, sizeof buf, "%s/dev/battery", mpt);
+		m->bitsybatfd = open(buf, OREAD);
+		if(loadbuf(m, &m->bitsybatfd) && readnums(m, 1, a, 0))
+			memmove(m->batterystats, a, sizeof(m->batterystats));
+	}
+	snprint(buf, sizeof buf, "%s/dev/cputemp", mpt);
+	m->tempfd = open(buf, OREAD);
+	if(m->tempfd < 0){
+		tempperiod = 5000;
+		snprint(buf, sizeof buf, "%s/mnt/pm/cputemp", mpt);
+		m->tempfd = open(buf, OREAD);
+	}
+	if(loadbuf(m, &m->tempfd))
+		for(n=0; n < nelem(m->temp) && readnums(m, 2, a, 0); n++)
+			 m->temp[n] = a[0];
+	return 1;
+}
+
+jmp_buf catchalarm;
+
+int
+alarmed(void *a, char *s)
+{
+	if(strcmp(s, "alarm") == 0)
+		notejmp(a, catchalarm, 1);
+	return 0;
+}
+
+int
+needswap(int init)
+{
+	return init | present[Mmem] | present[Mswap] | present[Mreclaim] | present[Mkern] | present[Mdraw];
+}
+
+
+int
+needstat(int init)
+{
+	return init | present[Mcontext]  | present[Mfault] | present[Mintr] | present[Mload] | present[Midle] |
+		present[Minintr] | present[Msyscall] | present[Mtlbmiss] | present[Mtlbpurge];
+}
+
+
+int
+needether(int init)
+{
+	return init | present[Mether] | present[Metherin] | present[Metherout] | present[Methererr];
+}
+
+int
+needbattery(int init)
+{
+	static uint step = 0;
+
+	if(++step*sleeptime >= batteryperiod){
+		step = 0;
+		return init | present[Mbattery];
+	}
+
+	return 0;
+}
+
+int
+needsignal(int init)
+{
+	return init | present[Msignal];
+}
+
+int
+needtemp(int init)
+{
+	static uint step = 0;
+
+	if(++step*sleeptime >= tempperiod){
+		step = 0;
+		return init | present[Mtemp];
+	}
+
+	return 0;
+}
+
+void
+readmach(Machine *m, int init)
+{
+	int n, i;
+	uvlong a[nelem(m->devsysstat)];
+	char buf[32];
+
+	if(m->remote && (m->disable || setjmp(catchalarm))){
+		if (m->disable++ >= 5)
+			m->disable = 0; /* give it another chance */
+		memmove(m->devsysstat, m->prevsysstat, sizeof m->devsysstat);
+		memmove(m->netetherstats, m->prevetherstats, sizeof m->netetherstats);
+		return;
+	}
+	snprint(buf, sizeof buf, "%s", m->name);
+	if (strcmp(m->name, buf) != 0){
+		free(m->name);
+		m->name = estrdup(buf);
+		free(m->shortname);
+		m->shortname = shortname(buf);
+		if(display != nil)	/* else we're still initializing */
+			eresized(0);
+	}
+	if(m->remote){
+		atnotify(alarmed, 1);
+		alarm(5000);
+	}
+	if(needswap(init) && loadbuf(m, &m->swapfd) && readswap(m, a))
+		memmove(m->devswap, a, sizeof m->devswap);
+	if(needstat(init) && loadbuf(m, &m->statsfd)){
+		memmove(m->prevsysstat, m->devsysstat, sizeof m->devsysstat);
+		memset(m->devsysstat, 0, sizeof m->devsysstat);
+		for(n=0; n<m->nproc && readnums(m, nelem(m->devsysstat), a, 0); n++)
+			for(i=0; i<nelem(m->devsysstat); i++)
+				m->devsysstat[i] += a[i];
+	}
+	if(needether(init) && loadbuf(m, &m->etherfd) && readnums(m, nelem(m->netetherstats), a, 1)){
+		memmove(m->prevetherstats, m->netetherstats, sizeof m->netetherstats);
+		memmove(m->netetherstats, a, sizeof m->netetherstats);
+	}
+	if(needsignal(init) && loadbuf(m, &m->ifstatsfd) && strncmp(m->buf, "Signal: ", 8)==0 && readnums(m, nelem(m->netetherifstats), a, 1)){
+		memmove(m->netetherifstats, a, sizeof m->netetherifstats);
+	}
+	if(needbattery(init)){
+		if(loadbuf(m, &m->batteryfd) && readnums(m, nelem(m->batterystats), a, 0))
+			memmove(m->batterystats, a, sizeof(m->batterystats));
+		else if(loadbuf(m, &m->bitsybatfd) && readnums(m, 1, a, 0))
+			memmove(m->batterystats, a, sizeof(m->batterystats));
+	}
+	if(needtemp(init) && loadbuf(m, &m->tempfd))
+		for(n=0; n < nelem(m->temp) && readnums(m, 2, a, 0); n++)
+			 m->temp[n] = a[0];
+	if(m->remote){
+		alarm(0);
+		atnotify(alarmed, 0);
+	}
+}
+
+void
+memval(Machine *m, uvlong *v, uvlong *vmax, int)
+{
+	*v = m->devswap[Mem];
+	*vmax = m->devswap[Maxmem];
+	if(*vmax == 0)
+		*vmax = 1;
+}
+
+void
+swapval(Machine *m, uvlong *v, uvlong *vmax, int)
+{
+	*v = m->devswap[Swap];
+	*vmax = m->devswap[Maxswap];
+	if(*vmax == 0)
+		*vmax = 1;
+}
+
+void
+reclaimval(Machine *m, uvlong *v, uvlong *vmax, int)
+{
+	*v = m->devswap[Reclaim];
+	*vmax = m->devswap[Maxreclaim];
+	if(*vmax == 0)
+		*vmax = 1;
+}
+
+void
+kernval(Machine *m, uvlong *v, uvlong *vmax, int)
+{
+	*v = m->devswap[Kern];
+	*vmax = m->devswap[Maxkern];
+	if(*vmax == 0)
+		*vmax = 1;
+}
+
+void
+drawval(Machine *m, uvlong *v, uvlong *vmax, int)
+{
+	*v = m->devswap[Draw];
+	*vmax = m->devswap[Maxdraw];
+	if(*vmax == 0)
+		*vmax = 1;
+}
+
+void
+contextval(Machine *m, uvlong *v, uvlong *vmax, int init)
+{
+	*v = (m->devsysstat[Context]-m->prevsysstat[Context])&0xffffffff;
+	*vmax = sleeptime*m->nproc;
+	if(init)
+		*vmax = sleeptime;
+}
+
+/*
+ * bug: need to factor in HZ
+ */
+void
+intrval(Machine *m, uvlong *v, uvlong *vmax, int init)
+{
+	*v = (m->devsysstat[Interrupt]-m->prevsysstat[Interrupt])&0xffffffff;
+	*vmax = sleeptime*m->nproc*10;
+	if(init)
+		*vmax = sleeptime*10;
+}
+
+void
+syscallval(Machine *m, uvlong *v, uvlong *vmax, int init)
+{
+	*v = (m->devsysstat[Syscall]-m->prevsysstat[Syscall])&0xffffffff;
+	*vmax = sleeptime*m->nproc;
+	if(init)
+		*vmax = sleeptime;
+}
+
+void
+faultval(Machine *m, uvlong *v, uvlong *vmax, int init)
+{
+	*v = (m->devsysstat[Fault]-m->prevsysstat[Fault])&0xffffffff;
+	*vmax = sleeptime*m->nproc;
+	if(init)
+		*vmax = sleeptime;
+}
+
+void
+tlbmissval(Machine *m, uvlong *v, uvlong *vmax, int init)
+{
+	*v = (m->devsysstat[TLBfault]-m->prevsysstat[TLBfault])&0xffffffff;
+	*vmax = (sleeptime/1000)*10*m->nproc;
+	if(init)
+		*vmax = (sleeptime/1000)*10;
+}
+
+void
+tlbpurgeval(Machine *m, uvlong *v, uvlong *vmax, int init)
+{
+	*v = (m->devsysstat[TLBpurge]-m->prevsysstat[TLBpurge])&0xffffffff;
+	*vmax = (sleeptime/1000)*10*m->nproc;
+	if(init)
+		*vmax = (sleeptime/1000)*10;
+}
+
+void
+loadval(Machine *m, uvlong *v, uvlong *vmax, int init)
+{
+	*v = m->devsysstat[Load];
+	*vmax = 1000*m->nproc;
+	if(init)
+		*vmax = 1000;
+}
+
+void
+idleval(Machine *m, uvlong *v, uvlong *vmax, int)
+{
+	*v = m->devsysstat[Idle]/m->nproc;
+	*vmax = 100;
+}
+
+void
+inintrval(Machine *m, uvlong *v, uvlong *vmax, int)
+{
+	*v = m->devsysstat[InIntr]/m->nproc;
+	*vmax = 100;
+}
+
+void
+etherval(Machine *m, uvlong *v, uvlong *vmax, int)
+{
+	*v = m->netetherstats[In]-m->prevetherstats[In] + m->netetherstats[Out]-m->prevetherstats[Out];
+	*vmax = sleeptime;
+}
+
+void
+etherinval(Machine *m, uvlong *v, uvlong *vmax, int)
+{
+	*v = m->netetherstats[In]-m->prevetherstats[In];
+	*vmax = sleeptime;
+}
+
+void
+etheroutval(Machine *m, uvlong *v, uvlong *vmax, int)
+{
+	*v = m->netetherstats[Out]-m->prevetherstats[Out];
+	*vmax = sleeptime;
+}
+
+void
+ethererrval(Machine *m, uvlong *v, uvlong *vmax, int)
+{
+	int i;
+
+	*v = 0;
+	for(i=Err0; i<nelem(m->netetherstats); i++)
+		*v += m->netetherstats[i]-m->prevetherstats[i];
+	*vmax = (sleeptime/1000)*10;
+}
+
+void
+batteryval(Machine *m, uvlong *v, uvlong *vmax, int)
+{
+	*v = m->batterystats[0];
+	if(m->bitsybatfd >= 0)
+		*vmax = 184;		// at least on my bitsy...
+	else
+		*vmax = 100;
+}
+
+void
+signalval(Machine *m, uvlong *v, uvlong *vmax, int)
+{
+	ulong l;
+
+	*vmax = sleeptime;
+	l = m->netetherifstats[0];
+	/*
+	 * Range is seen to be from about -45 (strong) to -95 (weak); rescale
+	 */
+	if(l == 0){	/* probably not present */
+		*v = 0;
+		return;
+	}
+	*v = 20*(l+95);
+}
+
+void
+tempval(Machine *m, uvlong *v, uvlong *vmax, int)
+{
+	ulong l;
+
+	*vmax = 100;
+	l = m->temp[0];
+	if(l == ~0 || l == 0)
+		*v = 0;
+	else
+		*v = l;
+}
+
+void
+usage(void)
+{
+	fprint(2, "usage: stats [-O] [-S scale] [-LY] [-%s] [machine...]\n", argchars);
+	exits("usage");
+}
+
+void
+addgraph(int n)
+{
+	Graph *g, *ograph;
+	int i, j;
+	static int nadd;
+
+	if(n > nelem(menu2str))
+		abort();
+	/* avoid two adjacent graphs of same color */
+	if(ngraph>0 && graph[ngraph-1].colindex==nadd%Ncolor)
+		nadd++;
+	ograph = graph;
+	graph = emalloc(nmach*(ngraph+1)*sizeof(Graph));
+	for(i=0; i<nmach; i++)
+		for(j=0; j<ngraph; j++)
+			graph[i*(ngraph+1)+j] = ograph[i*ngraph+j];
+	free(ograph);
+	ngraph++;
+	for(i=0; i<nmach; i++){
+		g = &graph[i*ngraph+(ngraph-1)];
+		memset(g, 0, sizeof(Graph));
+		g->label = menu2str[n]+Opwid;
+		g->newvalue = newvaluefn[n];
+		g->update = update1;	/* no other update functions yet */
+		g->mach = &mach[i];
+		g->colindex = nadd%Ncolor;
+	}
+	present[n] = 1;
+	nadd++;
+}
+
+void
+dropgraph(int which)
+{
+	Graph *ograph;
+	int i, j, n;
+
+	if(which > nelem(menu2str))
+		abort();
+	/* convert n to index in graph table */
+	n = -1;
+	for(i=0; i<ngraph; i++)
+		if(strcmp(menu2str[which]+Opwid, graph[i].label) == 0){
+			n = i;
+			break;
+		}
+	if(n < 0){
+		fprint(2, "stats: internal error can't drop graph\n");
+		killall("error");
+	}
+	ograph = graph;
+	graph = emalloc(nmach*(ngraph-1)*sizeof(Graph));
+	for(i=0; i<nmach; i++){
+		for(j=0; j<n; j++)
+			graph[i*(ngraph-1)+j] = ograph[i*ngraph+j];
+		free(ograph[i*ngraph+j].data);
+		freeimage(ograph[i*ngraph+j].overtmp);
+		for(j++; j<ngraph; j++)
+			graph[i*(ngraph-1)+j-1] = ograph[i*ngraph+j];
+	}
+	free(ograph);
+	ngraph--;
+	present[which] = 0;
+}
+
+int
+addmachine(char *name)
+{
+	if(ngraph > 0){
+		fprint(2, "stats: internal error: ngraph>0 in addmachine()\n");
+		usage();
+	}
+	if(mach == nil)
+		nmach = 0;	/* a little dance to get us started with local machine by default */
+	mach = erealloc(mach, (nmach+1)*sizeof(Machine));
+	memset(mach+nmach, 0, sizeof(Machine));
+	if (initmach(mach+nmach, name)){
+		nmach++;
+		return 1;
+	} else
+		return 0;
+}
+
+void
+labelstrs(Graph *g, char strs[Nlab][Lablen], int *np)
+{
+	int j;
+	uvlong v, vmax;
+
+	g->newvalue(g->mach, &v, &vmax, 1);
+	if(vmax == 0)
+		vmax = 1;
+	if(logscale){
+		for(j=1; j<=2; j++)
+			sprint(strs[j-1], "%g", scale*pow(10., j)*(double)vmax/100.);
+		*np = 2;
+	}else{
+		for(j=1; j<=3; j++)
+			sprint(strs[j-1], "%g", scale*(double)j*(double)vmax/4.0);
+		*np = 3;
+	}
+}
+
+int
+labelwidth(void)
+{
+	int i, j, n, w, maxw;
+	char strs[Nlab][Lablen];
+
+	maxw = 0;
+	for(i=0; i<ngraph; i++){
+		/* choose value for rightmost graph */
+		labelstrs(&graph[ngraph*(nmach-1)+i], strs, &n);
+		for(j=0; j<n; j++){
+			w = stringwidth(font, strs[j]);
+			if(w > maxw)
+				maxw = w;
+		}
+	}
+	return maxw;
+}
+
+void
+resize(void)
+{
+	int i, j, k, n, startx, starty, x, y, dx, dy, ly, ondata, maxx, wid, nlab;
+	Graph *g;
+	Rectangle machr, r;
+	uvlong v, vmax;
+	char buf[128], labs[Nlab][Lablen];
+
+	draw(screen, screen->r, tcols[Cback], nil, ZP);
+
+	/* label left edge */
+	x = screen->r.min.x;
+	y = screen->r.min.y + Labspace+font->height+Labspace;
+	dy = (screen->r.max.y - y)/ngraph;
+	dx = Labspace+stringwidth(font, "0")+Labspace;
+	startx = x+dx+1;
+	starty = y;
+	for(i=0; i<ngraph; i++,y+=dy){
+		draw(screen, Rect(x, y-1, screen->r.max.x, y), tcols[Cbord], nil, ZP);
+		draw(screen, Rect(x, y, x+dx, screen->r.max.y), cols[graph[i].colindex][0], nil, paritypt(x));
+		label(Pt(x, y), dy, graph[i].label);
+		draw(screen, Rect(x+dx, y, x+dx+1, screen->r.max.y), cols[graph[i].colindex][2], nil, ZP);
+	}
+
+	/* label top edge */
+	dx = (screen->r.max.x - startx)/nmach;
+	for(x=startx, i=0; i<nmach; i++,x+=dx){
+		draw(screen, Rect(x-1, starty-1, x, screen->r.max.y), tcols[Cbord], nil, ZP);
+		j = dx/stringwidth(font, "0");
+		n = mach[i].nproc;
+		if(n>1 && j>=1+3+mach[i].lgproc){	/* first char of name + (n) */
+			j -= 3+mach[i].lgproc;
+			if(j <= 0)
+				j = 1;
+			snprint(buf, sizeof buf, "%.*s(%d)", j, mach[i].shortname, n);
+		}else
+			snprint(buf, sizeof buf, "%.*s", j, mach[i].shortname);
+		string(screen, Pt(x+Labspace, screen->r.min.y + Labspace), tcols[Ctext], ZP, font, buf);
+	}
+
+	maxx = screen->r.max.x;
+
+	/* label right, if requested */
+	if(ylabels && dy>Nlab*(font->height+1)){
+		wid = labelwidth();
+		if(wid < dx-10){
+			/* else there's not enough room */
+			maxx -= 1+Lx+wid;
+			draw(screen, Rect(maxx, starty, maxx+1, screen->r.max.y), tcols[Cbord], nil, ZP);
+			y = starty;
+			for(j=0; j<ngraph; j++, y+=dy){
+				/* choose value for rightmost graph */
+				g = &graph[ngraph*(nmach-1)+j];
+				labelstrs(g, labs, &nlab);
+				r = Rect(maxx+1, y, screen->r.max.x, y+dy-1);
+				if(j == ngraph-1)
+					r.max.y = screen->r.max.y;
+				draw(screen, r, cols[g->colindex][0], nil, paritypt(r.min.x));
+				for(k=0; k<nlab; k++){
+					ly = y + (dy*(nlab-k)/(nlab+1));
+					draw(screen, Rect(maxx+1, ly, maxx+1+Lx, ly+1), tcols[Cbord], nil, ZP);
+					ly -= font->height/2;
+					string(screen, Pt(maxx+1+Lx, ly), tcols[Ctext], ZP, font, labs[k]);
+				}
+			}
+		}
+	}
+
+	/* create graphs */
+	for(i=0; i<nmach; i++){
+		machr = Rect(startx+i*dx, starty, startx+(i+1)*dx - 1, screen->r.max.y);
+		if(i == nmach-1)
+			machr.max.x = maxx;
+		y = starty;
+		for(j=0; j<ngraph; j++, y+=dy){
+			g = &graph[i*ngraph+j];
+			/* allocate data */
+			ondata = g->ndata;
+			g->ndata = Dx(machr)+1;	/* may be too many if label will be drawn here; so what? */
+			g->data = erealloc(g->data, g->ndata*sizeof(ulong));
+			if(g->ndata > ondata)
+				memset(g->data+ondata, 0, (g->ndata-ondata)*sizeof(ulong));
+			/* set geometry */
+			g->r = machr;
+			g->r.min.y = y;
+			g->r.max.y = y+dy - 1;
+			if(j == ngraph-1)
+				g->r.max.y = screen->r.max.y;
+			draw(screen, g->r, cols[g->colindex][0], nil, paritypt(g->r.min.x));
+			g->overflow = 0;
+			r = g->r;
+			r.max.y = r.min.y+font->height;
+			r.max.x = r.min.x+stringwidth(font, "999999999999");
+			freeimage(g->overtmp);
+			g->overtmp = nil;
+			if(r.max.x <= g->r.max.x)
+				g->overtmp = allocimage(display, r, screen->chan, 0, -1);
+			g->newvalue(g->mach, &v, &vmax, 0);
+			if(vmax == 0)
+				vmax = 1;
+			redraw(g, vmax);
+		}
+	}
+
+	flushimage(display, 1);
+}
+
+void
+eresized(int new)
+{
+	lockdisplay(display);
+	if(new && getwindow(display, Refnone) < 0) {
+		fprint(2, "stats: can't reattach to window\n");
+		killall("reattach");
+	}
+	resize();
+	unlockdisplay(display);
+}
+
+void
+inputproc(void)
+{
+	Event e;
+	int i;
+
+	for(;;){
+		switch(eread(Emouse|Ekeyboard, &e)){
+		case Emouse:
+			if(e.mouse.buttons == 4){
+				lockdisplay(display);
+				for(i=0; i<Nmenu2; i++)
+					if(present[i])
+						memmove(menu2str[i], "drop ", Opwid);
+					else
+						memmove(menu2str[i], "add  ", Opwid);
+				i = emenuhit(3, &e.mouse, &menu2);
+				if(i >= 0){
+					if(!present[i])
+						addgraph(i);
+					else if(ngraph > 1)
+						dropgraph(i);
+					resize();
+				}
+				unlockdisplay(display);
+			}
+			break;
+		case Ekeyboard:
+			if(e.kbdc==Kdel || e.kbdc=='q')
+				killall(nil);
+			break;
+		}
+	}
+}
+
+void
+startproc(void (*f)(void), int index)
+{
+	int pid;
+
+	switch(pid = rfork(RFPROC|RFMEM|RFNOWAIT)){
+	case -1:
+		fprint(2, "stats: fork failed: %r\n");
+		killall("fork failed");
+	case 0:
+		f();
+		fprint(2, "stats: %s process exits\n", procnames[index]);
+		if(index >= 0)
+			killall("process died");
+		exits(nil);
+	}
+	if(index >= 0)
+		pids[index] = pid;
+}
+
+void
+main(int argc, char *argv[])
+{
+	int i, j;
+	double secs;
+	uvlong v, vmax, nargs;
+	char args[100];
+
+	quotefmtinstall();
+
+	nmach = 1;
+	mysysname = getenv("sysname");
+	if(mysysname == nil){
+		fprint(2, "stats: can't find $sysname: %r\n");
+		exits("sysname");
+	}
+
+	nargs = 0;
+	ARGBEGIN{
+	case 'T':
+		secs = atof(EARGF(usage()));
+		if(secs > 0)
+			sleeptime = 1000*secs;
+		break;
+	case 'S':
+		scale = atof(EARGF(usage()));
+		if(scale <= 0)
+			usage();
+		break;
+	case 'L':
+		logscale++;
+		break;
+	case 'Y':
+		ylabels++;
+		break;
+	case 'O':
+		break;
+	default:
+		if(nargs>=sizeof args || strchr(argchars, ARGC())==nil)
+			usage();
+		args[nargs++] = ARGC();
+	}ARGEND
+
+	if(argc == 0){
+		mach = emalloc(nmach*sizeof(Machine));
+		initmach(&mach[0], mysysname);
+		readmach(&mach[0], 1);
+	}else{
+		rfork(RFNAMEG);
+		for(i=j=0; i<argc; i++){
+			if (addmachine(argv[i]))
+				readmach(&mach[j++], 1);
+		}
+		if (j == 0)
+			exits("connect");
+	}
+
+	for(i=0; i<nargs; i++)
+	switch(args[i]){
+	default:
+		fprint(2, "stats: internal error: unknown arg %c\n", args[i]);
+		usage();
+	case 'b':
+		addgraph(Mbattery);
+		break;
+	case 'c':
+		addgraph(Mcontext);
+		break;
+	case 'e':
+		addgraph(Mether);
+		break;
+	case 'E':
+		addgraph(Metherin);
+		addgraph(Metherout);
+		break;
+	case 'f':
+		addgraph(Mfault);
+		break;
+	case 'i':
+		addgraph(Mintr);
+		break;
+	case 'I':
+		addgraph(Mload);
+		addgraph(Midle);
+		addgraph(Minintr);
+		break;
+	case 'l':
+		addgraph(Mload);
+		break;
+	case 'm':
+		addgraph(Mmem);
+		break;
+	case 'n':
+		addgraph(Metherin);
+		addgraph(Metherout);
+		addgraph(Methererr);
+		break;
+	case 'p':
+		addgraph(Mtlbpurge);
+		break;
+	case 'r':
+		addgraph(Mreclaim);
+		break;
+	case 's':
+		addgraph(Msyscall);
+		break;
+	case 't':
+		addgraph(Mtlbmiss);
+		addgraph(Mtlbpurge);
+		break;
+	case '8':
+		addgraph(Msignal);
+		break;
+	case 'w':
+		addgraph(Mswap);
+		break;
+	case 'k':
+		addgraph(Mkern);
+		break;
+	case 'd':
+		addgraph(Mdraw);
+		break;
+	case 'z':
+		addgraph(Mtemp);
+		break;
+	}
+
+	if(ngraph == 0)
+		addgraph(Mload);
+
+	for(i=0; i<nmach; i++)
+		for(j=0; j<ngraph; j++)
+			graph[i*ngraph+j].mach = &mach[i];
+
+	if(initdraw(nil, nil, "stats") < 0){
+		fprint(2, "stats: initdraw failed: %r\n");
+		exits("initdraw");
+	}
+	display->locking = 1;	/* tell library we're using the display lock */
+
+	Theme th[Ntcolor] = {
+		[Cback] { "back",	DWhite },
+		[Cbord] { "border",	DBlack },
+		[Ctext] { "text",	DBlack },
+		[Cmix1] { "rioback",	DWhite },
+		[Cmix2] { "palehold",	DWhite },
+		[Ccol1] { "htext",	0xFFAAAAFF },
+		[Ccol2] { "high",	DPurpleblue },
+		[Ccol3] { "hold",	DYellowgreen },
+		[Ccol4] { "size",	DDarkgreen },
+		[Ccol5] { "title",	0x0088CCFF },
+		[Ccol6] { "paletext",	0x888888FF },
+	};
+	readtheme(th, nelem(th), nil);
+	for(i=0; i<nelem(tcols); i++)
+ 		tcols[i] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, th[i].c);
+	colinit(th);
+	einit(Emouse|Ekeyboard);
+	startproc(inputproc, Inputproc);
+	pids[Mainproc] = getpid();
+
+	resize();
+
+	unlockdisplay(display); /* display is still locked from initdraw() */
+	for(;;){
+		for(i=0; i<nmach; i++)
+			readmach(&mach[i], 0);
+		lockdisplay(display);
+		parity = 1-parity;
+		for(i=0; i<nmach*ngraph; i++){
+			graph[i].newvalue(graph[i].mach, &v, &vmax, 0);
+			if(vmax == 0)
+				vmax = 1;
+			graph[i].update(&graph[i], v, vmax);
+		}
+		flushimage(display, 1);
+		unlockdisplay(display);
+		sleep(sleeptime);
+	}
+}