shithub: lola

Download patch

ref: 1049552fb14efa811ca879b35244b8ea684995c1
author: aap <aap@papnet.eu>
date: Fri Jan 27 15:40:05 EST 2023

first commit

--- /dev/null
+++ b/TODO
@@ -1,0 +1,25 @@
+rethink resizing and repainting
+implement all Qids
+	wctl
+	tap
+tap
+border resize/move
+move mouse
+write text
+wctl
+release keys and buttons when unfocused
+...
+
+ideas:
+	case-insensitive 'look'
+	virtual screen (like fvwm)
+	cursor movement
+	decorations (at least make them possible)
+	tabbed window
+
+problems:
+	Borderwidth hardcoded in gengetwindow
+	originwindow doesn't work with gengetwindow
+	non-origin screen breaks samterm scrollbars
+	raw mode where?
+	initkeyboard with /dev/kbd support (also fix leaks in old code)
--- /dev/null
+++ b/fs.c
@@ -1,0 +1,868 @@
+#include "inc.h"
+
+enum {
+	Qroot,
+	Qwsys,
+
+	Qcons,
+	Qconsctl,
+	Qcursor,
+	Qwinid,
+	Qwinname,
+	Qlabel,
+	Qkbd,
+	Qmouse,
+	Qscreen,
+	Qsnarf,
+	Qtext,
+	Qwdir,
+//	Qwctl,
+	Qwindow,
+//	Qtap,		//
+
+	NQids
+};
+
+typedef struct Dirent Dirent;
+struct Dirent
+{
+	int path;
+	int type;
+	char *name;
+};
+
+Dirent dirents[] = {
+	Qroot,	QTDIR,	".",
+	Qwsys,	QTDIR,	"wsys",
+	Qwinid,	QTFILE,	"winid",
+	Qwinname,	QTFILE,	"winname",
+	Qwdir,	QTFILE,	"wdir",
+	Qlabel,	QTFILE,	"label",
+	Qsnarf,	QTFILE,	"snarf",
+	Qtext,	QTFILE,	"text",
+	Qcons,	QTFILE, "cons",
+	Qconsctl,	QTFILE, "consctl",
+	Qkbd,	QTFILE, "kbd",
+	Qmouse,	QTFILE, "mouse",
+	Qcursor,	QTFILE, "cursor",
+	Qscreen,	QTFILE, "screen",
+	Qwindow,	QTFILE, "window",
+};
+
+char Eperm[] = "permission denied";
+char Eexist[] = "file does not exist";
+char Enotdir[] = "not a directory";
+char Ebadfcall[] = "bad fcall type";
+char Eoffset[] = "illegal offset";
+char Enomem[] = "out of memory";
+
+char Eflush[] =		"interrupted";
+char Einuse[] =		"file in use";
+char Edeleted[] =	"window deleted";
+char Etooshort[] =	"buffer too small";
+char Eshort[] =		"short i/o request";
+char Elong[] = 		"snarf buffer too long";
+char Eunkid[] = 	"unknown id in attach";
+char Ebadrect[] = 	"bad rectangle in attach";
+char Ewindow[] = 	"cannot make window";
+char Enowindow[] = 	"window has no image";
+char Ebadmouse[] = 	"bad format on /dev/mouse";
+
+/* Extension of a Fid, fid->aux */
+typedef struct Xfid Xfid;
+struct Xfid
+{
+	Fid *fid;
+	Channel *xc;
+	Channel *flush;		// cancel read/write
+	Window *w;
+	RuneConvBuf cnv;
+	Xfid *next;
+};
+static Xfid *xfidfree;
+
+typedef struct XfidMsg XfidMsg;
+struct XfidMsg
+{
+	Req *r;
+	void (*f)(Req*);
+};
+
+static void
+xfidthread(void *a)
+{
+	Xfid *xf = a;
+	XfidMsg xm;
+
+	for(;;){
+		recv(xf->xc, &xm);
+		(*xm.f)(xm.r);
+	}
+}
+
+static void
+toxfid(Req *r, void (*f)(Req*))
+{
+	Xfid *xf;
+	XfidMsg xm;
+
+	xf = r->fid->aux;
+	xm.r = r;
+	xm.f = f;
+	send(xf->xc, &xm);
+}
+
+static Xfid*
+getxfid(Fid *fid, Window *w)
+{
+	Xfid *xf;
+	if(xfidfree){
+		xf = xfidfree;
+		xfidfree = xf->next;
+	}else{
+		xf = emalloc(sizeof(Xfid));
+		xf->xc = chancreate(sizeof(XfidMsg), 0);
+		xf->flush = chancreate(sizeof(int), 0);
+		threadcreate(xfidthread, xf, mainstacksize);
+	}
+	memset(&xf->cnv, 0, sizeof(xf->cnv));
+	xf->fid = fid;
+	xf->w = w;
+	incref(w);
+	xf->next = nil;
+	return xf;
+}
+
+static void
+freexfid(Xfid *xf)
+{
+	wrelease(xf->w);
+	free(xf->cnv.buf);
+	xf->fid = nil;
+	xf->w = nil;
+	xf->next = xfidfree;
+	xfidfree = xf;
+}
+
+#define QID(w, q) ((w)<<8|(q))
+#define QWIN(q) ((q)>>8)
+#define QFILE(q) ((q)&0xFF)
+
+static void
+fsattach(Req *r)
+{
+	Window *w;
+	char *end;
+	int id;
+
+	w = nil;
+	if(strcmp(r->ifcall.aname, "new") == 0){
+		w = wcreate(rectaddpt(newrect(), screen->r.min));
+wsetpid(w, -1, 1);
+wsetname(w);
+		flushimage(display, 1);
+		decref(w);	/* don't delete, xfid will take it */
+	}else if(id = strtol(r->ifcall.aname, &end, 10), *end == '\0'){
+		w = wfind(id);
+	}
+	if(w == nil){
+		respond(r, "bad attach name");
+		return;
+	}
+
+	r->fid->aux = getxfid(r->fid, w);
+	r->fid->qid = (Qid){QID(w->id,Qroot),0,QTDIR};
+	r->ofcall.qid = r->fid->qid;
+	respond(r, nil);
+}
+
+static char*
+fsclone(Fid *fid, Fid *newfid)
+{
+	Xfid *xf;
+
+	xf = fid->aux;
+	if(xf)
+		newfid->aux = getxfid(newfid, xf->w);
+	return nil;
+}
+
+int
+skipfile(char *name)
+{
+	return gotscreen && strcmp(name, "screen") == 0 ||
+	   snarffd >= 0 && strcmp(name, "snarf") == 0 ||
+	   !servekbd && strcmp(name, "kbd") == 0;
+}
+
+
+static char*
+fswalk1(Fid *fid, char *name, Qid *qid)
+{
+	int i;
+	Dirent *d;
+	Xfid *xf;
+	Window *w;
+	int dir;
+
+	xf = fid->aux;
+	w = xf->w;
+	dir = QFILE(fid->qid.path);
+	if(dir == Qroot){
+		if(strcmp(name, "..") == 0){
+			/* This sucks because we don't know which window we came from
+			 * error out for now */
+			return "vorwärts immer, rückwärts nimmer";
+		}
+		for(i = 0; i < nelem(dirents); i++){
+			d = &dirents[i];
+			if(!skipfile(d->name) && strcmp(name, d->name) == 0){
+				fid->qid = (Qid){QID(w->id,d->path), 0, d->type};
+				*qid = fid->qid;
+				return nil;
+			}
+		}
+	}else if(dir == Qwsys){
+		char *end;
+		int id;
+		if(strcmp(name, "..") == 0){
+			fid->qid = (Qid){QID(w->id,Qroot), 0, QTDIR};
+			*qid = fid->qid;
+			return nil;
+		}
+		if(id = strtol(name, &end, 10), *end == '\0'){
+			w = wfind(id);
+			if(w){
+				incref(w);
+				wrelease(xf->w);
+				xf->w = w;
+				fid->qid = (Qid){QID(w->id,Qroot), 0, QTDIR};
+				*qid = fid->qid;
+				return nil;
+			}
+		}
+	}
+	return "no such file";
+}
+
+static int
+genrootdir(int n, Dir *d, void *a)
+{
+	Window *w = a;
+	int i;
+
+	n++;	/* -1 is root dir */
+	i = 0;
+	while(n--){
+		i++;
+		if(i >= nelem(dirents))
+			return -1;
+		/* we know the last file is never skipped */
+		while(skipfile(dirents[i].name)) i++;
+	}
+
+	d->atime = time(nil);
+	d->mtime = d->atime;
+	d->uid = estrdup9p(getuser());
+	d->gid = estrdup9p(d->uid);
+	d->muid = estrdup9p(d->uid);
+	d->qid = (Qid){QID(w->id,dirents[i].path), 0, dirents[i].type};
+	d->mode = 0664;
+	if(dirents[i].type & QTDIR)
+		d->mode |= 0111;
+	d->name = estrdup9p(dirents[i].name);
+	d->length = 0;
+	return 0;
+}
+
+static int
+genwsysdir(int n, Dir *d, void*)
+{
+	d->atime = time(nil);
+	d->mtime = d->atime;
+	d->uid = estrdup9p(getuser());
+	d->gid = estrdup9p(d->uid);
+	d->muid = estrdup9p(d->uid);
+
+	if(n == -1){
+		d->qid = (Qid){Qwsys, 0, QTDIR};
+		d->mode = 0775;
+		d->name = estrdup9p("wsys");
+		d->length = 0;
+		return 0;
+	}
+	if(n < nwindows){
+		int id = windows[n]->id;
+		d->qid = (Qid){QID(id,Qroot), 0, QTDIR};
+		d->mode = 0775;
+		d->name = smprint("%d", id);
+		d->length = 0;
+		return 0;
+	}
+
+	return -1;
+}
+
+static int ntsnarf;
+static char *tsnarf;
+
+static void
+xfopen(Req *r)
+{
+	Xfid *xf;
+	Window *w;
+
+	xf = r->fid->aux;
+	w = xf->w;
+
+	if(w == nil || w->deleted){
+		respond(r, Edeleted);
+		return;
+	}
+
+	switch(QFILE(xf->fid->qid.path)){
+	case Qsnarf:
+		r->ifcall.mode &= ~OTRUNC;
+		if(r->ifcall.mode==ORDWR || r->ifcall.mode==OWRITE)
+			ntsnarf = 0;
+		break;
+
+	case Qconsctl:
+		if(w->consctlopen){
+			respond(r, Einuse);
+			return;
+		}
+		w->consctlopen = TRUE;
+		break;
+
+	case Qkbd:
+		if(w->kbdopen){
+			respond(r, Einuse);
+			return;
+		}
+		w->kbdopen = TRUE;
+		break;
+
+	case Qmouse:
+		if(w->mouseopen){
+			respond(r, Einuse);
+			return;
+		}
+// TODO: copy comment from rio
+		w->resized = FALSE;
+		w->mouseopen = TRUE;
+		break;
+	}
+
+	respond(r, nil);
+}
+
+static void
+xfclose(Xfid *xf)
+{
+	Window *w;
+	Text *x;
+
+	w = xf->w;
+	x = &w->text;
+
+	switch(QFILE(xf->fid->qid.path)){
+	/* replace snarf buffer when /dev/snarf is closed */
+	case Qsnarf:
+		if(xf->fid->omode==ORDWR || xf->fid->omode==OWRITE){
+			setsnarf(tsnarf, ntsnarf);
+			ntsnarf = 0;
+		}
+		break;
+
+	case Qconsctl:
+		if(x->rawmode){
+			x->rawmode = 0;
+			wsendmsg(w, Rawoff, ZR, nil);
+		}
+		if(w->holdmode > 0){
+			w->holdmode = 1;
+			wsendmsg(w, Holdoff, ZR, nil);
+		}
+		w->consctlopen = FALSE;
+		break;
+
+	case Qkbd:
+		w->kbdopen = FALSE;
+		break;
+
+	case Qmouse:
+		w->mouseopen = FALSE;
+		w->resized = FALSE;
+		wsendmsg(w, Refresh, ZR, nil);
+		break;
+
+	case Qcursor:
+		w->cursorp = nil;
+		wsetcursor(w);
+		break;
+	}
+}
+
+static int
+readimgdata(Image *i, char *t, Rectangle r, int offset, int n)
+{
+	int ww, oo, y, m;
+	uchar *tt;
+
+	ww = bytesperline(r, i->depth);
+	r.min.y += offset/ww;
+	if(r.min.y >= r.max.y)
+		return 0;
+	y = r.min.y + (n + ww-1)/ww;
+	if(y < r.max.y)
+		r.max.y = y;
+	m = ww * Dy(r);
+	oo = offset % ww;
+	if(oo == 0 && n >= m)
+		return unloadimage(i, r, (uchar*)t, n);
+	if((tt = malloc(m)) == nil)
+		return -1;
+	m = unloadimage(i, r, tt, m) - oo;
+	if(m > 0){
+		if(n < m) m = n;
+		memmove(t, tt + oo, m);
+	}
+	free(tt);
+	return m;
+}
+
+/* Fill request from image,
+ * returns only either header or data */
+char*
+readimg(Req *r, Image *img)
+{
+	char *head;
+	char cbuf[30];
+	Rectangle rect;
+	int n;
+
+	rect = img->r;
+	if(r->ifcall.offset < 5*12){
+		head = smprint("%11s %11d %11d %11d %11d ",
+			chantostr(cbuf, img->chan),
+			rect.min.x, rect.min.y, rect.max.x, rect.max.y);
+		readstr(r, head);
+		free(head);
+	}else{
+		/* count is unsigned, so check with n */
+		n = readimgdata(img, r->ofcall.data, rect, r->ifcall.offset-5*12, r->ifcall.count);
+		if(n < 0)
+			return Enomem;
+		r->ofcall.count = n;
+	}
+	return nil;
+}
+
+static char*
+readblocking(Req *r, Channel *readchan)
+{
+	Xfid *xf;
+	Window *w;
+	Channel *chan;
+	Stringpair pair;
+	enum { Adata, Agone, Aflush, NALT };
+	Alt alts[NALT+1];
+
+	xf = r->fid->aux;
+	w = xf->w;
+
+	alts[Adata] = ALT(readchan, &chan, CHANRCV);
+	alts[Agone] = ALT(w->gone, nil, CHANRCV);
+	alts[Aflush] = ALT(xf->flush, nil, CHANRCV);
+	alts[NALT].op = CHANEND;
+	switch(alt(alts)){
+	case Adata:
+		pair.s = r->ofcall.data;
+		pair.ns = r->ifcall.count;
+		send(chan, &pair);
+		recv(chan, &pair);
+		r->ofcall.count = pair.ns;
+		return nil;
+	case Agone:
+		return Edeleted;
+	case Aflush:
+		return Eflush;
+	}
+	return nil;	/* can't happen */
+}
+
+static void
+xfread(Req *r)
+{
+	Xfid *xf;
+	Window *w;
+	char *data;
+
+	xf = r->fid->aux;
+	w = xf->w;
+
+	if(w == nil || w->deleted){
+		respond(r, Edeleted);
+		return;
+	}
+
+	switch(QFILE(xf->fid->qid.path)){
+	case Qwinid:
+		data = smprint("%11d ", w->id);
+		readstr(r, data);
+		free(data);
+		break;
+	case Qwinname:
+		readstr(r, w->name);
+		break;
+	case Qlabel:
+		readstr(r, w->label);
+		break;
+	case Qsnarf:
+		data = smprint("%.*S", nsnarf, snarf);
+		readstr(r, data);
+		free(data);
+		break;
+	case Qtext:
+		data = smprint("%.*S", w->text.nr, w->text.r);
+		readstr(r, data);
+		free(data);
+		break;
+	case Qcons:
+		respond(r, readblocking(r, w->consread));
+		return;
+	case Qkbd:
+		respond(r, readblocking(r, w->kbdread));
+		return;
+	case Qmouse:
+		respond(r, readblocking(r, w->mouseread));
+		return;
+	case Qcursor:
+		respond(r, "cursor read not implemented");
+		return;
+	case Qscreen:
+		respond(r, readimg(r, screen));
+		return;
+	case Qwindow:
+		respond(r, readimg(r, w->img));
+		return;
+	default:
+		respond(r, "cannot read");
+		return;
+	}
+	respond(r, nil);
+}
+
+static void
+xfwrite(Req *r)
+{
+	Xfid *xf;
+	Window *w;
+	Text *x;
+	vlong offset;
+	u32int count;
+	char *data, *p;
+	Channel *kbd;
+	Stringpair pair;
+	enum { Adata, Agone, Aflush, NALT };
+	Alt alts[NALT+1];
+
+	xf = r->fid->aux;
+	w = xf->w;
+	x = &w->text;
+	offset = r->ifcall.offset;
+	count = r->ifcall.count;
+	data = r->ifcall.data;
+
+	if(w == nil || w->deleted){
+		respond(r, Edeleted);
+		return;
+	}
+	int f = QFILE(r->fid->qid.path);
+	switch(f){
+	case Qcons:
+		alts[Adata] = ALT(w->conswrite, &kbd, CHANRCV);
+		alts[Agone] = ALT(w->gone, nil, CHANRCV);
+		alts[Aflush] = ALT(xf->flush, nil, CHANRCV);
+		alts[NALT].op = CHANEND;
+		switch(alt(alts)){
+		case Adata:
+			cnvsize(&xf->cnv, count);
+			memmove(xf->cnv.buf+xf->cnv.n, data, count);
+			xf->cnv.n += count;
+			pair = b2r(&xf->cnv);
+			r->ofcall.count = r->ifcall.count;
+			send(kbd, &pair);
+			break;
+		case Agone:
+			respond(r, Edeleted);
+			return;
+		case Aflush:
+			respond(r, Eflush);
+			return;
+		}
+		break;
+
+	case Qconsctl:
+		if(strncmp(data, "holdon", 6) == 0){
+			wsendmsg(w, Holdon, ZR, nil);
+			break;
+		}
+		if(strncmp(data, "holdoff", 7) == 0){
+			wsendmsg(w, Holdoff, ZR, nil);
+			break;
+		}
+		if(strncmp(data, "rawon", 5) == 0){
+// TODO: apparently we turn of hold mode here
+			if(x->rawmode++ == 0)
+				wsendmsg(w, Rawon, ZR, nil);
+			break;
+		}
+		if(strncmp(data, "rawoff", 6) == 0){
+			if(--x->rawmode == 0)
+				wsendmsg(w, Rawoff, ZR, nil);
+			break;
+		}
+		respond(r, "unknown control message");
+		return;
+
+	case Qcursor:
+		if(count < 2*4+2*2*16)
+			w->cursorp = nil;
+		else{
+			w->cursor.offset.x = BGLONG(data+0*4);
+			w->cursor.offset.y = BGLONG(data+1*4);
+			memmove(w->cursor.clr, data+2*4, 2*2*16);
+			w->cursorp = &w->cursor;
+		}
+		cursor = (void*)(uintptr)~0;	/* invalide cache */
+		wsetcursor(w);
+		break;
+
+	case Qlabel:
+		if(offset != 0){
+			respond(r, "non-zero offset writing label");
+			return;
+		}
+		w->label = realloc(w->label, count+1);
+		memmove(w->label, data, count);
+		w->label[count] = 0;
+		break;
+
+	case Qsnarf:
+		if(count == 0)
+			break;
+		/* always append only */
+		if(ntsnarf > MAXSNARF){	/* avoid thrashing when people cut huge text */
+			respond(r, Elong);
+			return;
+		}
+		p = realloc(tsnarf, ntsnarf+count);
+		if(p == nil){
+			respond(r, Enomem);
+			return;
+		}
+		tsnarf = p;
+		memmove(tsnarf+ntsnarf, data, count);
+		ntsnarf += count;
+		break;
+
+	case Qwdir:
+		if(count > 0 && data[count-1] == '\n')
+			data[--count] = '\0';
+		if(count == 0)
+			break;
+		/* assume data comes in a single write */
+		if(data[0] == '/')
+			p = smprint("%.*s", count, data);
+		else
+			p = smprint("%s/%.*s", w->dir, count, data);
+		if(p == nil){
+			respond(r, Enomem);
+			return;
+		}
+		free(w->dir);
+		w->dir = cleanname(p);
+		break;
+
+	default:
+		respond(r, "cannot write");
+		return;
+	}
+	respond(r, nil);
+}
+
+static void
+fsopen(Req *r)
+{
+	toxfid(r, xfopen);
+}
+
+static void
+freefid(Fid *fid)
+{
+	Xfid *xf;
+
+	xf = fid->aux;
+	if(xf){
+		xfclose(xf);
+		freexfid(xf);
+	}
+	fid->aux = nil;
+}
+
+static void
+fsread(Req *r)
+{
+	Xfid *xf;
+
+	if((r->fid->qid.type & QTDIR) == 0){
+		toxfid(r, xfread);
+		return;
+	}
+
+	switch(QFILE(r->fid->qid.path)){
+	case Qroot:
+		xf = r->fid->aux;
+		dirread9p(r, genrootdir, xf->w);
+		break;
+	case Qwsys:
+		dirread9p(r, genwsysdir, nil);
+		break;
+	}
+	respond(r, nil);
+}
+
+static void
+fswrite(Req *r)
+{
+	toxfid(r, xfwrite);
+}
+
+static void
+fsflush(Req *r)
+{
+	Req *or;
+	Xfid *xf;
+	int dummy = 0;
+
+	or = r->oldreq;
+	xf = or->fid->aux;
+	assert(xf);
+
+	/* TODO: not entirely sure this is right.
+	 * is it possible no-one is listening? */
+	send(xf->flush, &dummy);
+	respond(r, nil);
+}
+
+static void
+fsstat(Req *r)
+{
+	Xfid *xf;
+	int f;
+
+	xf = r->fid->aux;
+	f = QFILE(r->fid->qid.path);
+	genrootdir(f-1, &r->d, xf->w);
+	respond(r, nil);
+}
+
+Srv fsys = {
+	.attach		fsattach,
+	.open		fsopen,
+	.read		fsread,
+	.write		fswrite,
+	.stat		fsstat,
+	.flush		fsflush,
+	.walk1		fswalk1,
+	.clone		fsclone,
+	.destroyfid	freefid,
+	nil
+};
+
+void
+post(char *name, int srvfd)
+{
+	char buf[80];
+	int fd;
+
+	snprint(buf, sizeof buf, "/srv/%s", name);
+	fd = create(buf, OWRITE|ORCLOSE|OCEXEC, 0600);
+	if(fd < 0)
+		panic(buf);
+	if(fprint(fd, "%d", srvfd) < 0)
+		panic("post");
+	putenv("wsys", buf);
+	/* leave fd open */
+}
+
+static Ioproc *io9p;
+
+/* copy & paste from /sys/src/libc/9sys/read9pmsg.c
+ * changed to use ioreadn instead of readn */
+int
+read9pmsg(int fd, void *abuf, uint n)
+{
+	int m, len;
+	uchar *buf;
+
+	buf = abuf;
+
+	/* read count */
+	m = ioreadn(io9p, fd, buf, BIT32SZ);
+	if(m != BIT32SZ){
+		if(m < 0)
+			return -1;
+		return 0;
+	}
+
+	len = GBIT32(buf);
+	if(len <= BIT32SZ || len > n){
+		werrstr("bad length in 9P2000 message header");
+		return -1;
+	}
+	len -= BIT32SZ;
+	m = ioreadn(io9p, fd, buf+BIT32SZ, len);
+	if(m < len)
+		return 0;
+	return BIT32SZ+m;
+}
+
+int fsysfd;
+char	srvpipe[64];
+//char	srvwctl[64];
+
+void
+fs(void)
+{
+	io9p = ioproc();
+
+	int fd[2];
+	if(pipe(fd) < 0)
+		panic("pipe");
+	fsysfd = fd[0];		/* don't close for children */
+	fsys.infd = fsys.outfd = fd[1];
+	snprint(srvpipe, sizeof(srvpipe), "lola.%s.%lud", getuser(), (ulong)getpid());
+	post(srvpipe, fd[0]);
+//	chatty9p++;
+	srv(&fsys);
+}
+
+int
+fsmount(int id)
+{	char buf[32];
+
+	close(fsys.infd);	/* close server end so mount won't hang if exiting */
+	snprint(buf, sizeof buf, "%d", id);
+	if(mount(fsysfd, -1, "/mnt/wsys", MREPL, buf) == -1){
+		fprint(2, "mount failed: %r\n");
+		return -1;
+	}
+	if(bind("/mnt/wsys", "/dev", MBEFORE) == -1){
+		fprint(2, "bind failed: %r\n");
+		return -1;
+	}
+	return 0;
+}
--- /dev/null
+++ b/inc.h
@@ -1,0 +1,284 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <keyboard.h>
+#include <mouse.h>
+#include <cursor.h>
+#include <frame.h>
+#include <fcall.h>
+#include <9p.h>
+#include <complete.h>
+#include <plumb.h>
+
+enum {
+	FALSE = 0,
+	TRUE = 1
+};
+
+#define ALT(c, v, t) (Alt){ c, v, t, nil, nil, 0 };
+
+#define CTRL(c) ((c)&0x1F)
+
+extern Rune *snarf;
+extern int nsnarf;
+extern int snarfversion;
+extern int snarffd;
+enum { MAXSNARF = 100*1024 };
+void putsnarf(void);
+void getsnarf(void);
+void setsnarf(char *s, int ns);
+
+typedef struct Text Text;
+struct Text
+{
+	Frame;
+	Rectangle scrollr, lastsr;
+	Image *i;
+	Rune *r;
+	uint nr;
+	uint maxr;
+	uint org;	/* start of Frame's text */
+	uint q0, q1;	/* selection */
+	uint qh;	/* host point, output here */
+
+	/* not entirely happy with this in here */
+	int rawmode;
+	Rune *raw;
+	int nraw;
+
+	int posx;
+};
+
+void xinit(Text *x, Rectangle textr, Rectangle scrollr, Font *ft, Image *b, Image **cols);
+void xsetrects(Text *x, Rectangle textr, Rectangle scrollr);
+void xclear(Text *x);
+void xredraw(Text *x);
+uint xinsert(Text *x, Rune *r, int n, uint q0);
+void xfill(Text *x);
+void xdelete(Text *x, uint q0, uint q1);
+void xsetselect(Text *x, uint q0, uint q1);
+void xselect(Text *x, Mousectl *mc);
+void xscrdraw(Text *x);
+void xscroll(Text *x, Mousectl *mc, int but);
+void xscrolln(Text *x, int n);
+void xtickupdn(Text *x, int d);
+void xshow(Text *x, uint q0);
+void xplacetick(Text *x, uint q);
+void xtype(Text *x, Rune r);
+int xninput(Text *x);
+void xaddraw(Text *x, Rune *r, int nr);
+void xlook(Text *x);
+void xsnarf(Text *x);
+void xcut(Text *x);
+void xpaste(Text *x);
+void xsend(Text *x);
+int xplumb(Text *w, char *dir, int maxsize);
+
+enum
+{
+	// NCOL is defined by libframe, add more after it
+	TITLE = NCOL,
+	LTITLE,
+	TITLEHOLD,
+	LTITLEHOLD,
+
+	PALETEXT,
+	HOLDTEXT,
+	PALEHOLDTEXT,
+
+	NumColors
+};
+
+extern Image *colors[NumColors];
+extern Screen *wscreen;
+extern Mousectl *mctl;
+extern int scrolling;
+extern char *startdir;
+extern int shiftdown;
+extern int gotscreen;
+extern int servekbd;
+
+extern Cursor whitearrow;
+extern Cursor *cursor;
+void setcursoroverride(Cursor *c, int ov);
+void setcursornormal(Cursor *c);
+
+
+typedef struct RuneConvBuf RuneConvBuf;
+struct RuneConvBuf
+{
+	char *buf;
+	int maxbuf;	// allocated size
+	int nb;		// size
+	int n;		// filled
+};
+
+typedef struct Stringpair Stringpair;
+struct Stringpair	/* rune and nrune or byte and nbyte */
+{
+	void		*s;
+	int		ns;
+};
+
+typedef struct Mousestate Mousestate;
+struct Mousestate
+{
+	Mouse;
+	ulong	counter;	/* serial no. of mouse event */
+};
+
+typedef struct Mousequeue Mousequeue;
+struct Mousequeue
+{
+	Mousestate	q[16];
+	int	ri;	/* read index into queue */
+	int	wi;	/* write index */
+	ulong	counter;	/* serial no. of last mouse event we received */
+	ulong	lastcounter;	/* serial no. of last mouse event sent to client */
+	int	lastb;	/* last button state we received */
+	uchar	full;	/* filled the queue; no more recording until client comes back */	
+};
+
+typedef struct Kbdqueue Kbdqueue;
+struct Kbdqueue
+{
+	char *q[32];
+	int ri;
+	int wi;
+	uchar full;
+};
+
+enum
+{
+	Closed,
+	Reshaped,
+	Deleted,
+	Refresh,
+	Holdon,
+	Holdoff,
+	Rawon,
+	Rawoff,
+	Wakeup
+};
+
+typedef struct Wctlmesg Wctlmesg;
+struct Wctlmesg
+{
+	int type;
+	Rectangle r;
+	void *p;
+};
+
+typedef struct Window Window;
+struct Window
+{
+	Ref;
+	int deleted;
+	int hidden;
+	Window *lower;
+	Window *higher;
+	Image *img;
+	int id;
+	char name[32];
+	int namecount;
+	char *label;
+	Rectangle contrect;
+	int notefd;
+	char *dir;
+
+	Text text;
+	Rectangle scrollr;
+	Rectangle textr;
+	int scrolling;
+	int holdmode;
+
+	Mousectl mc;
+	Mousequeue mq;
+	int mouseopen;
+	int resized;
+
+	Cursor *cursorp;
+	Cursor cursor;
+
+	Channel *kbd;
+	Kbdqueue kq;
+	int consctlopen;
+	int kbdopen;
+
+	Channel *gone;		// window gone
+	Channel *ctl;		// Wctlmesg
+	/* channels to xfids */
+	Channel *conswrite;
+	Channel *consread;
+	Channel *kbdread;
+	Channel *mouseread;
+	Channel *complete;
+};
+
+extern Window *bottomwin, *topwin;
+extern Window *windows[1000];	// TMP
+extern int nwindows;
+extern Window *hidden[1000];
+extern int nhidden;
+extern Window *focused, *cursorwin;
+
+void wdecor(Window *w);
+void wresize(Window *w, Rectangle r);
+Window *wcreate(Rectangle r);
+int wrelease(Window *w);
+void wsendmsg(Window *w, int type, Rectangle r, void *p);
+Window *wfind(int id);
+Window *wpointto(Point pt);
+void wsetcursor(Window *w);
+void wsetlabel(Window *w, char *label);
+void wmove(Window *w, Point pos);
+void wrmove(Window *w, Point delta);
+void wrmovescreen(Point delta);
+void wraise(Window *w);
+void wlower(Window *w);
+void wfocus(Window *w);
+void whide(Window *w);
+void wunhide(Window *w);
+void wsethold(Window *w, int hold);
+void wtype(Window *w, Rune r);
+void wsetname(Window *w);
+void wsetpid(Window *w, int pid, int dolabel);
+void winshell(void *args);
+
+
+Rectangle newrect(void);
+
+extern Srv fsys;
+void fs(void);
+int fsmount(int id);
+
+#define	runemalloc(n)		malloc((n)*sizeof(Rune))
+#define	runerealloc(a, n)	realloc(a, (n)*sizeof(Rune))
+#define	runemove(a, b, n)	memmove(a, b, (n)*sizeof(Rune))
+#define min(a, b)	((a) < (b) ? (a) : (b))
+#define max(a, b)	((a) > (b) ? (a) : (b))
+
+void panic(char *s);
+void *emalloc(ulong size);
+void *erealloc(void *p, ulong size);
+char *estrdup(char *s);
+int handlebs(Stringpair *pair);
+void cnvsize(RuneConvBuf *cnv, int nb);
+int r2bfill(RuneConvBuf *cnv, Rune *rp, int nr);
+void r2bfinish(RuneConvBuf *cnv, Stringpair *pair);
+Stringpair b2r(RuneConvBuf *cnv);
+
+
+typedef struct Timer Timer;
+struct Timer
+{
+	int		dt;
+	int		cancel;
+	Channel	*c;	/* chan(int) */
+	Timer	*next;
+};
+void timerinit(void);
+Timer *timerstart(int dt);
+void timerstop(Timer *t);
+void timercancel(Timer *t);
--- /dev/null
+++ b/main.c
@@ -1,0 +1,799 @@
+#include "inc.h"
+
+Cursor whitearrow = {
+	{0, 0},
+	{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFC, 
+	 0xFF, 0xF0, 0xFF, 0xF0, 0xFF, 0xF8, 0xFF, 0xFC, 
+	 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFC, 
+	 0xF3, 0xF8, 0xF1, 0xF0, 0xE0, 0xE0, 0xC0, 0x40, },
+	{0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x06, 0xC0, 0x1C, 
+	 0xC0, 0x30, 0xC0, 0x30, 0xC0, 0x38, 0xC0, 0x1C, 
+	 0xC0, 0x0E, 0xC0, 0x07, 0xCE, 0x0E, 0xDF, 0x1C, 
+	 0xD3, 0xB8, 0xF1, 0xF0, 0xE0, 0xE0, 0xC0, 0x40, }
+};
+
+Cursor query = {
+	{-7,-7},
+	{0x0f, 0xf0, 0x1f, 0xf8, 0x3f, 0xfc, 0x7f, 0xfe, 
+	 0x7c, 0x7e, 0x78, 0x7e, 0x00, 0xfc, 0x01, 0xf8, 
+	 0x03, 0xf0, 0x07, 0xe0, 0x07, 0xc0, 0x07, 0xc0, 
+	 0x07, 0xc0, 0x07, 0xc0, 0x07, 0xc0, 0x07, 0xc0, },
+	{0x00, 0x00, 0x0f, 0xf0, 0x1f, 0xf8, 0x3c, 0x3c, 
+	 0x38, 0x1c, 0x00, 0x3c, 0x00, 0x78, 0x00, 0xf0, 
+	 0x01, 0xe0, 0x03, 0xc0, 0x03, 0x80, 0x03, 0x80, 
+	 0x00, 0x00, 0x03, 0x80, 0x03, 0x80, 0x00, 0x00, }
+};
+
+Cursor crosscursor = {
+	{-7, -7},
+	{0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0,
+	 0x03, 0xC0, 0x03, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF,
+	 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0xC0, 0x03, 0xC0,
+	 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, },
+	{0x00, 0x00, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80,
+	 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x7F, 0xFE,
+	 0x7F, 0xFE, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80,
+	 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x00, 0x00, }
+};
+
+Cursor boxcursor = {
+	{-7, -7},
+	{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+	 0xFF, 0xFF, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F,
+	 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xFF, 0xFF,
+	 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, },
+	{0x00, 0x00, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE,
+	 0x70, 0x0E, 0x70, 0x0E, 0x70, 0x0E, 0x70, 0x0E,
+	 0x70, 0x0E, 0x70, 0x0E, 0x70, 0x0E, 0x70, 0x0E,
+	 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x00, 0x00, }
+};
+
+Cursor sightcursor = {
+	{-7, -7},
+	{0x1F, 0xF8, 0x3F, 0xFC, 0x7F, 0xFE, 0xFB, 0xDF,
+	 0xF3, 0xCF, 0xE3, 0xC7, 0xFF, 0xFF, 0xFF, 0xFF,
+	 0xFF, 0xFF, 0xFF, 0xFF, 0xE3, 0xC7, 0xF3, 0xCF,
+	 0x7B, 0xDF, 0x7F, 0xFE, 0x3F, 0xFC, 0x1F, 0xF8, },
+	{0x00, 0x00, 0x0F, 0xF0, 0x31, 0x8C, 0x21, 0x84,
+	 0x41, 0x82, 0x41, 0x82, 0x41, 0x82, 0x7F, 0xFE,
+	 0x7F, 0xFE, 0x41, 0x82, 0x41, 0x82, 0x41, 0x82,
+	 0x21, 0x84, 0x31, 0x8C, 0x0F, 0xF0, 0x00, 0x00, }
+};
+
+typedef struct RKeyboardctl RKeyboardctl;
+struct RKeyboardctl
+{
+	Keyboardctl;
+	int kbdfd;
+};
+
+RKeyboardctl *kbctl;
+Mousectl *mctl;
+int scrolling = 1;
+char *startdir;
+int shiftdown;
+int gotscreen;
+int servekbd;
+
+Image *background;
+Image *colors[NumColors];
+Screen *wscreen;
+
+void
+killprocs(void)
+{
+	int i;
+
+	for(i = 0; i < nwindows; i++)
+		if(windows[i]->notefd >= 0)
+			write(windows[i]->notefd, "hangup", 6);
+}
+
+/*
+ * /dev/snarf updates when the file is closed, so we must open our own
+ * fd here rather than use snarffd
+ */
+void
+putsnarf(void)
+{
+	int fd, i, n;
+
+	if(snarffd<0 || nsnarf==0)
+		return;
+	fd = open("/dev/snarf", OWRITE|OCEXEC);
+	if(fd < 0)
+		return;
+	/* snarf buffer could be huge, so fprint will truncate; do it in blocks */
+	for(i=0; i<nsnarf; i+=n){
+		n = nsnarf-i;
+		if(n >= 256)
+			n = 256;
+		if(fprint(fd, "%.*S", n, snarf+i) < 0)
+			break;
+	}
+	close(fd);
+}
+
+void
+setsnarf(char *s, int ns)
+{
+	free(snarf);
+	snarf = runesmprint("%.*s", ns, s);
+	nsnarf = runestrlen(snarf);
+	snarfversion++;
+}
+
+void
+getsnarf(void)
+{
+	int i, n;
+	char *s, *sn;
+
+	if(snarffd < 0)
+		return;
+	sn = nil;
+	i = 0;
+	seek(snarffd, 0, 0);
+	for(;;){
+		if(i > MAXSNARF)
+			break;
+		if((s = realloc(sn, i+1024+1)) == nil)
+			break;
+		sn = s;
+		if((n = read(snarffd, sn+i, 1024)) <= 0)
+			break;
+		i += n;
+	}
+	if(i == 0)
+		return;
+	sn[i] = 0;
+	setsnarf(sn, i);
+	free(sn);
+}
+
+Rectangle
+newrect(void)
+{
+	static int i = 0;
+	int minx, miny, dx, dy;
+
+//	dx = min(600, Dx(screen->r) - 2*Borderwidth);
+//	dy = min(400, Dy(screen->r) - 2*Borderwidth);
+	dx = 600;
+	dy = 400;
+	minx = 32 + 16*i;
+	miny = 32 + 16*i;
+	i++;
+	i %= 10;
+
+	return Rect(minx, miny, minx+dx, miny+dy);
+}
+
+static int overridecursor;
+static Cursor *ovcursor;
+static Cursor *normalcursor;
+Cursor *cursor;
+
+void
+setmousecursor(Cursor *c)
+{
+	if(cursor == c)
+		return;
+	cursor = c;
+	setcursor(mctl, c);
+}
+
+void
+setcursoroverride(Cursor *c, int ov)
+{
+	overridecursor = ov;
+	ovcursor = c;
+	setmousecursor(overridecursor ? ovcursor : normalcursor);
+}
+
+void
+setcursornormal(Cursor *c)
+{
+	normalcursor = c;
+	setmousecursor(overridecursor ? ovcursor : normalcursor);
+}
+
+char *rcargv[] = { "rc", "-i", nil };
+
+Window*
+new(Rectangle r)
+{
+	Window *w;
+	Channel *cpid;
+	void *args[5];
+	int pid;
+
+	w = wcreate(r);
+	assert(w);
+	w->scrolling = scrolling;
+	cpid = chancreate(sizeof(int), 0);
+	assert(cpid);
+
+	args[0] = w;
+	args[1] = cpid;
+	args[2] = "/bin/rc";
+	args[3] = rcargv;
+	args[4] = nil;
+	proccreate(winshell, args, mainstacksize);
+	pid = recvul(cpid);
+	chanfree(cpid);
+
+	if(pid == 0){
+		print("proc create failed\n");
+		return nil;
+	}
+
+	wsetpid(w, pid, 1);
+	wsetname(w);
+
+	return w;
+}
+
+void
+drainmouse(Mousectl *mc, Channel *c)
+{
+	if(c) send(c, &mc->Mouse);
+	while(mc->buttons){
+		readmouse(mc);
+		if(c) send(c, &mc->Mouse);
+	}
+}
+
+Window*
+clickwindow(int but, Mousectl *mc)
+{
+	Window *w;
+
+	but = 1<<(but-1);
+	setcursoroverride(&sightcursor, TRUE);
+	drainmouse(mc, nil);
+	while(!(mc->buttons & but)){
+		readmouse(mc);
+		if(mc->buttons & (7^but)){
+			setcursoroverride(nil, FALSE);
+			drainmouse(mc, nil);
+			return nil;
+		}
+	}
+	w = wpointto(mc->xy);
+	return w;
+}
+
+Rectangle
+dragrect(int but, Rectangle r, Mousectl *mc)
+{
+	Rectangle rc;
+	Point start, end;
+
+	but = 1<<(but-1);
+	setcursoroverride(&boxcursor, TRUE);
+	start = mc->xy;
+	end = mc->xy;
+	do{
+		rc = rectaddpt(r, subpt(end, start));
+		drawgetrect(rc, 1);
+		readmouse(mc);
+		drawgetrect(rc, 0);
+		end = mc->xy;
+	}while(mc->buttons == but);
+
+	setcursoroverride(nil, FALSE);
+	if(mc->buttons & (7^but)){
+		rc.min.x = rc.max.x = 0;
+		rc.min.y = rc.max.y = 0;
+		drainmouse(mc, nil);
+	}
+	return rc;
+}
+
+Rectangle
+sweeprect(int but, Mousectl *mc)
+{
+	Rectangle r, rc;
+
+	but = 1<<(but-1);
+	setcursoroverride(&crosscursor, TRUE);
+	drainmouse(mc, nil);
+	while(!(mc->buttons & but)){
+		readmouse(mc);
+		if(mc->buttons & (7^but))
+			goto Return;
+	}
+	r.min = mc->xy;
+	r.max = mc->xy;
+	do{
+		rc = canonrect(r);
+		drawgetrect(rc, 1);
+		readmouse(mc);
+		drawgetrect(rc, 0);
+		r.max = mc->xy;
+	}while(mc->buttons == but);
+
+    Return:
+	setcursoroverride(nil, FALSE);
+	if(mc->buttons & (7^but)){
+		rc.min.x = rc.max.x = 0;
+		rc.min.y = rc.max.y = 0;
+		drainmouse(mc, nil);
+	}
+	return rc;
+}
+
+Window*
+pick(void)
+{
+	Window *w1, *w2;
+
+	w1 = clickwindow(3, mctl);
+	drainmouse(mctl, nil);
+	setcursoroverride(nil, FALSE);
+	w2 = wpointto(mctl->xy);
+	if(w1 != w2)
+		return nil;
+	return w1;
+}
+
+void
+grab(void)
+{
+	Window *w = clickwindow(3, mctl);
+	if(w == nil)
+		setcursoroverride(nil, FALSE);
+	else{
+		Rectangle r = dragrect(3, w->img->r, mctl);
+		if(Dx(r) > 0 || Dy(r) > 0){
+			wmove(w, r.min);
+			wfocus(w);
+			flushimage(display, 1);
+		}
+	}
+}
+
+void
+sweep(Window *w)
+{
+	Rectangle r = sweeprect(3, mctl);
+	if(Dx(r) > 10 && Dy(r) > 10){
+		if(w){
+			wresize(w, r);
+			wraise(w);
+		}else{
+			w = new(r);
+		}
+		wfocus(w);
+		flushimage(display, 1);
+	}
+}
+
+int
+obscured(Window *w, Rectangle r, Window *t)
+{
+	if(Dx(r) < font->height || Dy(r) < font->height)
+		return 1;
+	if(!rectclip(&r, screen->r))
+		return 1;
+	for(; t; t = t->higher){
+		if(t->hidden || Dx(t->img->r) == 0 || Dy(t->img->r) == 0 || rectXrect(r, t->img->r) == 0)
+			continue;
+		if(r.min.y < t->img->r.min.y)
+			if(!obscured(w, Rect(r.min.x, r.min.y, r.max.x, t->img->r.min.y), t))
+				return 0;
+		if(r.min.x < t->img->r.min.x)
+			if(!obscured(w, Rect(r.min.x, r.min.y, t->img->r.min.x, r.max.y), t))
+				return 0;
+		if(r.max.y > t->img->r.max.y)
+			if(!obscured(w, Rect(r.min.x, t->img->r.max.y, r.max.x, r.max.y), t))
+				return 0;
+		if(r.max.x > t->img->r.max.x)
+			if(!obscured(w, Rect(t->img->r.max.x, r.min.y, r.max.x, r.max.y), t))
+				return 0;
+		return 1;
+	}
+	return 0;
+}
+
+enum {
+	Cut,
+	Paste,
+	Snarf,
+	Plumb,
+	Look,
+	Send,
+	Scroll
+};
+char *menu2str[] = {
+	"cut",
+	"paste",
+	"snarf",
+	"plumb",
+	"look",
+	"send",
+	"scroll",
+	nil
+};
+Menu menu2 = { menu2str };
+
+enum {
+	New,
+	Reshape,
+	Move,
+	Delete,
+	Hide,
+	Exit
+};
+int Hidden = Exit+1;
+char *menu3str[7 + nelem(hidden)] = {
+	"New",
+	"Resize",
+	"Move",
+	"Delete",
+	"Hide",
+	"Exit",
+	nil
+};
+Menu menu3 = { menu3str };
+
+void
+btn2menu(Window *w)
+{
+	int sel;
+	Text *x;
+	Cursor *c;
+
+	x = &w->text;
+	menu2str[Scroll] = w->scrolling ? "noscroll" : "scroll";
+	sel = menuhit(2, mctl, &menu2, wscreen);
+	switch(sel){
+	case Cut:
+		xsnarf(x);
+		xcut(x);
+		xscrdraw(x);			// TODO let cut handle this?
+		break;
+	case Paste:
+		xpaste(x);
+		break;
+	case Snarf:
+		xsnarf(x);
+		xscrdraw(x);			// TODO let snarf handle this?
+		break;
+	case Plumb:
+		if(xplumb(x, w->dir, fsys.msize-1024)){
+			c = cursor;
+			setcursoroverride(&query, TRUE);
+			sleep(300);
+			setcursoroverride(c, FALSE);
+		}
+		break;
+	case Look:
+		xlook(x);
+		break;
+	case Send:
+		xsend(x);
+		break;
+	case Scroll:
+		w->scrolling = !w->scrolling;
+		if(w->scrolling)
+			xshow(x, x->nr);
+		break;
+	}
+	wsendmsg(w, Wakeup, ZR, nil);
+}
+
+void
+btn3menu(void)
+{
+	Window *w, *t;
+	int i, sel;
+
+	nhidden = 0;
+	for(i = 0; i < nwindows; i++){
+		t = windows[i];
+		if(t->hidden || obscured(t, t->img->r, t->higher)){
+			hidden[nhidden] = windows[i];
+			menu3str[nhidden+Hidden] = windows[i]->label;
+			nhidden++;	
+		}
+	}
+	menu3str[nhidden+Hidden] = nil;
+
+	sel = menuhit(3, mctl, &menu3, wscreen);
+	switch(sel){
+	case New:
+		sweep(nil);
+		break;
+	case Reshape:
+		w = pick();
+		if(w) sweep(w);
+		break;
+	case Move:
+		grab();
+		break;
+	case Delete:
+		w = pick();
+		if(w) wsendmsg(w, Deleted, ZR, nil);
+		break;
+	case Hide:
+		w = pick();
+		if(w) whide(w);
+		break;
+	case Exit:
+		killprocs();
+		threadexitsall(nil);
+	default:
+		if(sel >= Hidden){
+			w = hidden[sel-Hidden];
+			if(w->hidden)
+				wunhide(w);
+			else{
+				wraise(w);
+				wfocus(w);
+			}
+		}
+		break;
+	}
+}
+
+void
+mthread(void*)
+{
+	Window *w;
+	int inside;
+
+	while(readmouse(mctl) != -1){
+		w = wpointto(mctl->xy);
+again:
+		inside = w && w == focused && ptinrect(mctl->xy, w->contrect);
+
+		cursorwin = w;
+		if(w)
+			wsetcursor(w);
+		else
+			setcursornormal(nil);
+
+/* TODO: handle borders */
+		if(inside && w->mouseopen){
+			drainmouse(mctl, w->mc.c);
+		}else if(inside){
+// TODO: this can't happen with rio, but maybe we should support it
+if(mctl->buttons && topwin != w)
+wraise(w);
+			if(mctl->buttons & (1|8|16) || ptinrect(mctl->xy, w->text.scrollr))
+				drainmouse(mctl, w->mc.c);
+			if(mctl->buttons & 2){
+				incref(w);
+				btn2menu(w);
+				wrelease(w);
+			}
+			if(mctl->buttons & 4)
+				btn3menu();
+		}else if(w){
+			if(mctl->buttons & 7 ||
+			   mctl->buttons & (8|16) && focused->mouseopen){
+				wraise(w);
+				wfocus(w);
+				if(ptinrect(mctl->xy, w->contrect)){	// temp hack for borders
+				if(mctl->buttons & 1)
+					drainmouse(mctl, nil);
+				else
+					goto again;
+				}
+			}
+		}else{
+			if(mctl->buttons & 4)
+				btn3menu();
+		}
+	}
+}
+
+void
+resthread(void*)
+{
+	Window *w;
+	Rectangle or, nr;
+	Point delta;
+
+	for(;;){
+		recvul(mctl->resizec);
+		or = screen->clipr;
+		if(getwindow(display, Refnone) < 0)
+			sysfatal("resize failed: %r");
+		nr = screen->clipr;
+
+		freescreen(wscreen);
+		wscreen = allocscreen(screen, background, 0);
+		draw(screen, screen->r, background, nil, ZP);
+
+		delta = subpt(nr.min, or.min);
+		for(w = bottomwin; w; w = w->higher){
+			Rectangle r = w->img->r;
+			freeimage(w->img);
+			w->img = nil;
+			wresize(w, rectaddpt(r, delta));
+			if(w->hidden)
+				originwindow(w->img, w->img->r.min, screen->r.max);
+		}
+
+		flushimage(display, 1);
+	}
+}
+
+static void
+_ioproc(void *arg)
+{
+	int m, n, nerr;
+	char buf[1024], *e, *p;
+	Rune r;
+	RKeyboardctl *kc;
+
+	kc = arg;
+	threadsetname("kbdproc");
+	n = 0;
+	nerr = 0;
+	if(kc->kbdfd >= 0){
+		while(kc->kbdfd >= 0){
+			m = read(kc->kbdfd, buf, sizeof(buf)-1);
+			if(m <= 0){
+				yield();	/* if error is due to exiting, we'll exit here */
+				if(kc->kbdfd < 0)
+					break;
+				fprint(2, "keyboard: short read: %r\n");
+				if(m<0 || ++nerr>10)
+					threadexits("read error");
+				continue;
+			}
+			/* one read can return multiple messages, delimited by NUL
+			 * split them up for sending on the channel */
+			e = buf+m;
+			e[-1] = 0;
+			e[0] = 0;
+			for(p = buf; p < e; p += strlen(p)+1)
+				chanprint(kc->c, "%s", p);
+		}
+	}else{
+		while(kc->consfd >= 0){
+			m = read(kc->consfd, buf+n, sizeof buf-n);
+			if(m <= 0){
+				yield();	/* if error is due to exiting, we'll exit here */
+				if(kc->consfd < 0)
+					break;
+				fprint(2, "keyboard: short read: %r\n");
+				if(m<0 || ++nerr>10)
+					threadexits("read error");
+				continue;
+			}
+			nerr = 0;
+			n += m;
+			while(n>0 && fullrune(buf, n)){
+				m = chartorune(&r, buf);
+				n -= m;
+				memmove(buf, buf+m, n);
+				if(chanprint(kc->c, "c%C", r) < 0)
+					break;
+			}
+		}
+	}
+	chanfree(kc->c);
+	free(kc->file);
+	free(kc);
+}
+
+RKeyboardctl*
+initkbd(char *file, char *kbdfile)
+{
+	RKeyboardctl *kc;
+	char *t;
+
+	if(file == nil)
+		file = "/dev/cons";
+	if(kbdfile == nil)
+		kbdfile = "/dev/kbd";
+
+	kc = mallocz(sizeof(RKeyboardctl), 1);
+	if(kc == nil)
+		return nil;
+	kc->file = strdup(file);
+// TODO: handle file == nil
+	kc->consfd = open(file, ORDWR|OCEXEC);
+	t = malloc(strlen(file)+16);
+	if(kc->consfd<0 || t==nil)
+		goto Error1;
+	sprint(t, "%sctl", file);
+	kc->ctlfd = open(t, OWRITE|OCEXEC);
+	if(kc->ctlfd < 0){
+		fprint(2, "initkeyboard: can't open %s: %r\n", t);
+		goto Error2;
+	}
+	if(ctlkeyboard(kc, "rawon") < 0){
+		fprint(2, "initkeyboard: can't turn on raw mode on %s: %r\n", t);
+		close(kc->ctlfd);
+		goto Error2;
+	}
+	free(t);
+	kc->kbdfd = open(kbdfile, OREAD|OCEXEC);
+	kc->c = chancreate(sizeof(char*), 20);
+	kc->pid = proccreate(_ioproc, kc, 4096);
+	return kc;
+
+Error2:
+	close(kc->consfd);
+Error1:
+	free(t);
+	free(kc->file);
+	free(kc);
+	return nil;
+}
+
+void
+kbthread(void*)
+{
+	char *s;
+
+	for(;;){
+		recv(kbctl->c, &s);
+		if(*s == 'k' || *s == 'K')
+			shiftdown = utfrune(s+1, Kshift) != nil;
+		if(focused)
+			send(focused->kbd, &s);
+		else
+			free(s);
+	}
+}
+
+void
+threadmain(int, char *[])
+{
+	char buf[256];
+//rfork(RFENVG);
+//newwindow("-dx 1280 -dy 800");
+
+	if(getwd(buf, sizeof(buf)) == nil)
+		startdir = estrdup(".");
+	else
+		startdir = estrdup(buf);
+	if(initdraw(nil, nil, "lola") < 0)
+		sysfatal("initdraw: %r");
+	kbctl = initkbd(nil, nil);
+	if(kbctl == nil)
+		sysfatal("inikeyboard: %r");
+	mctl = initmouse(nil, screen);
+	if(mctl == nil)
+		sysfatal("initmouse: %r");
+
+	servekbd = kbctl->kbdfd >= 0;
+	snarffd = open("/dev/snarf", OREAD|OCEXEC);
+	gotscreen = access("/dev/screen", AEXIST)==0;
+
+	background = allocimage(display, Rect(0,0,1,1), screen->chan, 1, 0x777777FF);
+	colors[BACK] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, 0xFFFFFFFF);
+	colors[HIGH] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, 0xCCCCCCFF);
+	colors[BORD] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, 0x999999FF);
+	colors[TEXT] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, 0x000000FF);
+	colors[HTEXT] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, 0x000000FF);
+
+	colors[TITLE] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, DGreygreen);
+	colors[LTITLE] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, DPalegreygreen);
+	colors[TITLEHOLD] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, DMedblue);
+	colors[LTITLEHOLD] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, DPalegreyblue);
+
+	colors[PALETEXT] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, 0x666666FF);
+	colors[HOLDTEXT] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, DMedblue);
+	colors[PALEHOLDTEXT] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, DGreyblue);
+
+	wscreen = allocscreen(screen, background, 0);
+	draw(screen, screen->r, background, nil, ZP);
+
+	timerinit();
+	threadcreate(mthread, nil, mainstacksize);
+	threadcreate(kbthread, nil, mainstacksize);
+	threadcreate(resthread, nil, mainstacksize);
+
+	flushimage(display, 1);
+
+	fs();
+	// not reached
+}
--- /dev/null
+++ b/mkfile
@@ -1,0 +1,16 @@
+< /$objtype/mkfile
+
+TARG=drawtest
+OFILES=\
+	main.$O \
+	text.$O \
+	wind.$O \
+	fs.$O \
+	util.$O \
+	time.$O
+
+HFILES=inc.h
+
+BIN=$home/bin/$objtype
+
+< /sys/src/cmd/mkone
--- /dev/null
+++ b/text.c
@@ -1,0 +1,956 @@
+#include "inc.h"
+
+enum
+{
+	HiWater	= 640000,	/* max size of history */
+	LoWater	= 400000,	/* min size of history after max'ed */
+	MinWater	= 20000,	/* room to leave available when reallocating */
+};
+
+void
+xinit(Text *x, Rectangle textr, Rectangle scrollr, Font *ft, Image *b, Image **cols)
+{
+	frinit(x, textr, ft, b, cols);
+	x->i = b;
+	x->scrollr = scrollr;
+	x->lastsr = ZR;
+	xfill(x);
+	xsetselect(x, x->q0, x->q1);
+	xscrdraw(x);
+}
+
+void
+xsetrects(Text *x, Rectangle textr, Rectangle scrollr)
+{
+	frsetrects(x, textr, x->b);
+	x->scrollr = scrollr;
+}
+
+void
+xclear(Text *x)
+{
+	free(x->r);
+	x->r = nil;
+	x->nr = 0;
+	free(x->raw);
+	x->r = nil;
+	x->nraw = 0;
+	frclear(x, TRUE);
+};
+
+void
+xredraw(Text *x)
+{
+	frredraw(x);
+	xscrdraw(x);
+}
+
+uint
+xinsert(Text *w, Rune *r, int n, uint q0)
+{
+	uint m;
+
+	if(n == 0)
+		return q0;
+	if(w->nr+n>HiWater && q0>=w->org && q0>=w->qh){
+		m = min(HiWater-LoWater, min(w->org, w->qh));
+		w->org -= m;
+		w->qh -= m;
+		if(w->q0 > m)
+			w->q0 -= m;
+		else
+			w->q0 = 0;
+		if(w->q1 > m)
+			w->q1 -= m;
+		else
+			w->q1 = 0;
+		w->nr -= m;
+		runemove(w->r, w->r+m, w->nr);
+		q0 -= m;
+	}
+	if(w->nr+n > w->maxr){
+		/*
+		 * Minimize realloc breakage:
+		 *	Allocate at least MinWater
+		 * 	Double allocation size each time
+		 *	But don't go much above HiWater
+		 */
+		m = max(min(2*(w->nr+n), HiWater), w->nr+n)+MinWater;
+		if(m > HiWater)
+			m = max(HiWater+MinWater, w->nr+n);
+		if(m > w->maxr){
+			w->r = runerealloc(w->r, m);
+			w->maxr = m;
+		}
+	}
+	runemove(w->r+q0+n, w->r+q0, w->nr-q0);
+	runemove(w->r+q0, r, n);
+	w->nr += n;
+	/* if output touches, advance selection, not qh; works best for keyboard and output */
+	if(q0 <= w->q1)
+		w->q1 += n;
+	if(q0 <= w->q0)
+		w->q0 += n;
+	if(q0 < w->qh)
+		w->qh += n;
+	if(q0 < w->org)
+		w->org += n;
+	else if(q0 <= w->org+w->nchars){
+for(int i = 0; i < n; i++)
+if(r[i] == 0)
+abort();
+		frinsert(w, r, r+n, q0-w->org);
+}
+	return q0;
+}
+
+void
+xfill(Text *w)
+{
+	Rune *rp;
+	int i, n, m, nl;
+
+	while(w->lastlinefull == FALSE){
+		n = w->nr-(w->org+w->nchars);
+		if(n == 0)
+			break;
+		if(n > 2000)	/* educated guess at reasonable amount */
+			n = 2000;
+		rp = w->r+(w->org+w->nchars);
+
+		/*
+		 * it's expensive to frinsert more than we need, so
+		 * count newlines.
+		 */
+		nl = w->maxlines-w->nlines;
+		m = 0;
+		for(i=0; i<n; ){
+			if(rp[i++] == '\n'){
+				m++;
+				if(m >= nl)
+					break;
+			}
+		}
+		frinsert(w, rp, rp+i, w->nchars);
+	}
+}
+
+void
+xdelete(Text *w, uint q0, uint q1)
+{
+	uint n, p0, p1;
+
+	n = q1-q0;
+	if(n == 0)
+		return;
+	runemove(w->r+q0, w->r+q1, w->nr-q1);
+	w->nr -= n;
+	if(q0 < w->q0)
+		w->q0 -= min(n, w->q0-q0);
+	if(q0 < w->q1)
+		w->q1 -= min(n, w->q1-q0);
+	if(q1 < w->qh)
+		w->qh -= n;
+	else if(q0 < w->qh)
+		w->qh = q0;
+	if(q1 <= w->org)
+		w->org -= n;
+	else if(q0 < w->org+w->nchars){
+		p1 = q1 - w->org;
+		if(p1 > w->nchars)
+			p1 = w->nchars;
+		if(q0 < w->org){
+			w->org = q0;
+			p0 = 0;
+		}else
+			p0 = q0 - w->org;
+		frdelete(w, p0, p1);
+		xfill(w);
+	}
+}
+
+void
+xsetselect(Text *w, uint q0, uint q1)
+{
+	int p0, p1;
+
+	w->posx = -1;
+	/* w->p0 and w->p1 are always right; w->q0 and w->q1 may be off */
+	w->q0 = q0;
+	w->q1 = q1;
+	/* compute desired p0,p1 from q0,q1 */
+	p0 = q0-w->org;
+	p1 = q1-w->org;
+	if(p0 < 0)
+		p0 = 0;
+	if(p1 < 0)
+		p1 = 0;
+	if(p0 > w->nchars)
+		p0 = w->nchars;
+	if(p1 > w->nchars)
+		p1 = w->nchars;
+	if(p0==w->p0 && p1==w->p1)
+		return;
+	/* screen disagrees with desired selection */
+	if(w->p1<=p0 || p1<=w->p0 || p0==p1 || w->p1==w->p0){
+		/* no overlap or too easy to bother trying */
+		frdrawsel(w, frptofchar(w, w->p0), w->p0, w->p1, 0);
+		frdrawsel(w, frptofchar(w, p0), p0, p1, 1);
+		goto Return;
+	}
+	/* overlap; avoid unnecessary painting */
+	if(p0 < w->p0){
+		/* extend selection backwards */
+		frdrawsel(w, frptofchar(w, p0), p0, w->p0, 1);
+	}else if(p0 > w->p0){
+		/* trim first part of selection */
+		frdrawsel(w, frptofchar(w, w->p0), w->p0, p0, 0);
+	}
+	if(p1 > w->p1){
+		/* extend selection forwards */
+		frdrawsel(w, frptofchar(w, w->p1), w->p1, p1, 1);
+	}else if(p1 < w->p1){
+		/* trim last part of selection */
+		frdrawsel(w, frptofchar(w, p1), p1, w->p1, 0);
+	}
+
+    Return:
+	w->p0 = p0;
+	w->p1 = p1;
+}
+
+static void
+xsetorigin(Text *w, uint org, int exact)
+{
+	int i, a, fixup;
+	Rune *r;
+	uint n;
+
+	if(org>0 && !exact){
+		/* org is an estimate of the char posn; find a newline */
+		/* don't try harder than 256 chars */
+		for(i=0; i<256 && org<w->nr; i++){
+			if(w->r[org] == '\n'){
+				org++;
+				break;
+			}
+			org++;
+		}
+	}
+	a = org-w->org;
+	fixup = 0;
+	if(a>=0 && a<w->nchars){
+		frdelete(w, 0, a);
+		fixup = 1;	/* frdelete can leave end of last line in wrong selection mode; it doesn't know what follows */
+	}else if(a<0 && -a<w->nchars){
+		n = w->org - org;
+		r = w->r+org;
+		frinsert(w, r, r+n, 0);
+	}else
+		frdelete(w, 0, w->nchars);
+	w->org = org;
+	xfill(w);
+	xscrdraw(w);
+	xsetselect(w, w->q0, w->q1);
+	if(fixup && w->p1 > w->p0)
+		frdrawsel(w, frptofchar(w, w->p1-1), w->p1-1, w->p1, 1);
+}
+
+
+/*
+ * Scrolling
+ */
+
+static Image *scrtmp;
+enum { BIG = 3 };
+
+static Image*
+scrtemps(void)
+{
+	int h;
+
+	if(scrtmp == nil){
+		h = BIG*Dy(screen->r);
+		scrtmp = allocimage(display, Rect(0, 0, 32, h), screen->chan, 0, DNofill);
+	}
+	return scrtmp;
+}
+
+/*
+void
+freescrtemps(void)
+{
+	if(scrtmp){
+		freeimage(scrtmp);
+		scrtmp = nil;
+	}
+}
+*/
+
+static Rectangle
+scrpos(Rectangle r, uint p0, uint p1, uint tot)
+{
+	Rectangle q;
+	int h;
+
+	q = r;
+	h = q.max.y-q.min.y;
+	if(tot == 0)
+		return q;
+	if(tot > 1024*1024){
+		tot>>=10;
+		p0>>=10;
+		p1>>=10;
+	}
+	if(p0 > 0)
+		q.min.y += h*p0/tot;
+	if(p1 < tot)
+		q.max.y -= h*(tot-p1)/tot;
+	if(q.max.y < q.min.y+2){
+		if(q.min.y+2 <= r.max.y)
+			q.max.y = q.min.y+2;
+		else
+			q.min.y = q.max.y-2;
+	}
+	return q;
+}
+
+void
+xscrdraw(Text *w)
+{
+	Rectangle r, r1, r2;
+	Image *b;
+
+	b = scrtemps();
+	if(b == nil || w->i == nil)
+		return;
+	r = w->scrollr;
+	r1 = r;
+	r1.min.x = 0;
+	r1.max.x = Dx(r);
+	r2 = scrpos(r1, w->org, w->org+w->nchars, w->nr);
+	if(!eqrect(r2, w->lastsr)){
+		w->lastsr = r2;
+		/* move r1, r2 to (0,0) to avoid clipping */
+		r2 = rectsubpt(r2, r1.min);
+		r1 = rectsubpt(r1, r1.min);
+		draw(b, r1, w->cols[BORD], nil, ZP);
+		draw(b, r2, w->cols[BACK], nil, ZP);
+		r2.min.x = r2.max.x-1;
+		draw(b, r2, w->cols[BORD], nil, ZP);
+		draw(w->i, r, b, nil, Pt(0, r1.min.y));
+	}
+}
+
+static uint
+xbacknl(Text *w, uint p, uint n)
+{
+	int i, j;
+
+	/* look for start of this line if n==0 */
+	if(n==0 && p>0 && w->r[p-1]!='\n')
+		n = 1;
+	i = n;
+	while(i-->0 && p>0){
+		--p;	/* it's at a newline now; back over it */
+		if(p == 0)
+			break;
+		/* at 128 chars, call it a line anyway */
+		for(j=128; --j>0 && p>0; p--)
+			if(w->r[p-1]=='\n')
+				break;
+	}
+	return p;
+}
+
+static void
+xscrsleep(Mousectl *mc, uint dt)
+{
+	Timer	*timer;
+	int y, b;
+	static Alt alts[3];
+
+	if(display->bufp > display->buf)
+		flushimage(display, 1);
+	timer = timerstart(dt);
+	y = mc->xy.y;
+	b = mc->buttons;
+	alts[0] = ALT(timer->c, nil, CHANRCV);
+	alts[1] = ALT(mc->c, &mc->Mouse, CHANRCV);
+	alts[2].op = CHANEND;
+	for(;;)
+		switch(alt(alts)){
+		case 0:
+			timerstop(timer);
+			return;
+		case 1:
+			if(abs(mc->xy.y-y)>2 || mc->buttons!=b){
+				timercancel(timer);
+				return;
+			}
+			break;
+		}
+}
+
+void
+xscroll(Text *w, Mousectl *mc, int but)
+{
+	uint p0, oldp0;
+	Rectangle s;
+	int y, my, h, first;
+
+	s = insetrect(w->scrollr, 1);
+	h = s.max.y-s.min.y;
+	oldp0 = ~0;
+	first = TRUE;
+	do{
+		my = mc->xy.y;
+		if(my < s.min.y)
+			my = s.min.y;
+		if(my >= s.max.y)
+			my = s.max.y;
+		if(but == 2){
+			y = my;
+			if(y > s.max.y-2)
+				y = s.max.y-2;
+			if(w->nr > 1024*1024)
+				p0 = ((w->nr>>10)*(y-s.min.y)/h)<<10;
+			else
+				p0 = w->nr*(y-s.min.y)/h;
+			if(oldp0 != p0)
+				xsetorigin(w, p0, FALSE);
+			oldp0 = p0;
+			readmouse(mc);
+			continue;
+		}
+		if(but == 1 || but == 4){
+			y = max(1, (my-s.min.y)/w->font->height);
+			p0 = xbacknl(w, w->org, y);
+		}else{
+			y = max(my, s.min.y+w->font->height);
+			p0 = w->org+frcharofpt(w, Pt(s.max.x, y));
+		}
+		if(oldp0 != p0)
+			xsetorigin(w, p0, TRUE);
+		oldp0 = p0;
+		/* debounce */
+		if(first){
+			if(display->bufp > display->buf)
+				flushimage(display, 1);
+			if(but > 3)
+				return;
+			sleep(200);
+			nbrecv(mc->c, &mc->Mouse);
+			first = FALSE;
+		}
+		xscrsleep(mc, 100);
+	}while(mc->buttons & (1<<(but-1)));
+	while(mc->buttons)
+		readmouse(mc);
+}
+
+void
+xscrolln(Text *x, int n)
+{
+	uint q0;
+
+	if(n < 0)
+		q0 = xbacknl(x, x->org, -n);
+	else
+		q0 = x->org+frcharofpt(x, Pt(x->Frame.r.min.x, x->Frame.r.min.y+n*x->font->height));
+	xsetorigin(x, q0, TRUE);
+}
+
+/* move tick up or down while staying at the same x position */
+void
+xtickupdn(Text *x, int d)
+{
+	Point p;
+	int py;
+	uint q0;
+
+	xshow(x, x->q0);
+	p = frptofchar(x, x->q0-x->org);
+	if(x->posx >= 0)
+		p.x = x->posx;
+	py = p.y;
+	p.y += d*x->font->height;
+	if(p.y < x->Frame.r.min.y ||
+	   p.y > x->Frame.r.max.y-x->font->height){
+		xscrolln(x, d);
+		p.y = py;
+	}
+	q0 = x->org+frcharofpt(x, p);
+	xsetselect(x, q0, q0);
+	x->posx = p.x;
+}
+
+static Text	*selecttext;
+static Mousectl *selectmc;
+static uint	selectq;
+
+static void
+xframescroll(Text *x, int dl)
+{
+	uint endq;
+
+	if(dl == 0){
+		xscrsleep(selectmc, 100);
+		return;
+	}
+	if(dl < 0){
+		endq = x->org+x->p0;
+	}else{
+		if(x->org+x->nchars == x->nr)
+			return;
+		endq = x->org+x->p1;
+	}
+	xscrolln(x, dl);
+	xsetselect(x, min(selectq, endq), max(selectq, endq));
+}
+
+static void
+framescroll(Frame *f, int dl)
+{
+	if(f != &selecttext->Frame)
+		panic("frameselect not right frame");
+	xframescroll(selecttext, dl);
+}
+
+/*
+ * Selection and deletion helpers
+ */
+
+int
+iswordrune(Rune r)
+{
+	return isalpharune(r) || isdigitrune(r);
+}
+
+static int
+xbswidth(Text *w, Rune c)
+{
+	uint q, stop;
+	Rune r;
+	int wd, inword;
+
+	/* there is known to be at least one character to erase */
+	if(c == Kbs)	/* ^H: erase character */
+		return 1;
+	q = w->q0;
+	stop = 0;
+	if(q > w->qh)
+		stop = w->qh;
+	inword = FALSE;
+	while(q > stop){
+		r = w->r[q-1];
+		if(r == '\n'){		/* eat at most one more character */
+			if(q == w->q0)	/* eat the newline */
+				--q;
+			break; 
+		}
+		/* ^W: erase word.
+		 * delete a bunch of non-word characters
+		 * followed by word characters */
+		if(c == CTRL('W')){
+			wd = iswordrune(r);
+			if(wd && !inword)
+				inword = TRUE;
+			else if(!wd && inword)
+				break;
+		}
+		--q;
+	}
+	return w->q0-q;
+}
+
+static Rune left1[] =  { L'{', L'[', L'(', L'<', L'«', 0 };
+static Rune right1[] = { L'}', L']', L')', L'>', L'»', 0 };
+static Rune left2[] =  { L'\n', 0 };
+static Rune left3[] =  { L'\'', L'"', L'`', 0 };
+
+static Rune *left[] = {
+	left1,
+	left2,
+	left3,
+	nil
+};
+static Rune *right[] = {
+	right1,
+	left2,
+	left3,
+	nil
+};
+
+static int
+xclickmatch(Text *x, int cl, int cr, int dir, uint *q)
+{
+	Rune c;
+	int nest;
+
+	nest = 1;
+	for(;;){
+		if(dir > 0){
+			if(*q == x->nr)
+				break;
+			c = x->r[*q];
+			(*q)++;
+		}else{
+			if(*q == 0)
+				break;
+			(*q)--;
+			c = x->r[*q];
+		}
+		if(c == cr){
+			if(--nest==0)
+				return 1;
+		}else if(c == cl)
+			nest++;
+	}
+	return cl=='\n' && nest==1;
+}
+
+static int
+inmode(Rune r, int mode)
+{
+	return (mode == 1) ? iswordrune(r) : r && !isspacerune(r);
+}
+
+static void
+xstretchsel(Text *x, uint pt, uint *q0, uint *q1, int mode)
+{
+	int c, i;
+	Rune *r, *l, *p;
+	uint q;
+
+	*q0 = pt;
+	*q1 = pt;
+	for(i=0; left[i]!=nil; i++){
+		q = *q0;
+		l = left[i];
+		r = right[i];
+		/* try matching character to left, looking right */
+		if(q == 0)
+			c = '\n';
+		else
+			c = x->r[q-1];
+		p = runestrchr(l, c);
+		if(p != nil){
+			if(xclickmatch(x, c, r[p-l], 1, &q))
+				*q1 = q-(c!='\n');
+			return;
+		}
+		/* try matching character to right, looking left */
+		if(q == x->nr)
+			c = '\n';
+		else
+			c = x->r[q];
+		p = runestrchr(r, c);
+		if(p != nil){
+			if(xclickmatch(x, c, l[p-r], -1, &q)){
+				*q1 = *q0+(*q0<x->nr && c=='\n');
+				*q0 = q;
+				if(c!='\n' || q!=0 || x->r[0]=='\n')
+					(*q0)++;
+			}
+			return;
+		}
+	}
+	/* try filling out word to right */
+	while(*q1<x->nr && inmode(x->r[*q1], mode))
+		(*q1)++;
+	/* try filling out word to left */
+	while(*q0>0 && inmode(x->r[*q0-1], mode))
+		(*q0)--;
+}
+
+static Mouse	lastclick;
+static Text	*clickfrm;
+static uint	clickcount;
+
+/* should be called with button 1 down */
+void
+xselect(Text *x, Mousectl *mc)
+{
+	uint q0, q1;
+	int dx, dy, dt, b;
+
+	/* reset click state if mouse is too different from last time */
+	dx = abs(mc->xy.x - lastclick.xy.x);
+	dy = abs(mc->xy.y - lastclick.xy.y);
+	dt = mc->msec - lastclick.msec;
+	if(x != clickfrm || dx > 3 || dy > 3 || dt >= 500)
+		clickcount = 0;
+
+	/* first button down can be a dragging selection or a click.
+	 * subsequent buttons downs can only be clicks.
+	 * both cases can be ended by chording. */
+	selectq = x->org+frcharofpt(x, mc->xy);
+	if(clickcount == 0){
+		/* what a kludge - can this be improved? */
+		selecttext = x;
+		selectmc = mc;
+		x->scroll = framescroll;
+		frselect(x, mc);
+		/* this is correct if the whole selection is visible */
+		q0 = x->org + x->p0;
+		q1 = x->org + x->p1;
+		/* otherwise replace one end with selectq */
+		if(selectq < x->org)
+			q0 = selectq;
+		if(selectq > x->org+x->nchars)
+			q1 = selectq;
+		xsetselect(x, q0, q1);
+
+		/* figure out whether it was a click */
+		if(q0 == q1 && mc->buttons == 0){
+			clickcount = 1;
+			clickfrm = x;
+		}
+	}else{
+		clickcount++;
+		xstretchsel(x, selectq, &q0, &q1, min(clickcount-1, 2));
+		xsetselect(x, q0, q1);
+		if(clickcount >= 3)
+			clickcount = 0;
+		b = mc->buttons;
+		while(mc->buttons == b)
+			readmouse(mc);
+	}
+	lastclick = mc->Mouse;		/* a bit unsure if this is correct */
+
+	/* chording */
+	while(mc->buttons){
+		clickcount = 0;
+		b = mc->buttons;
+		if(b & 6){
+			if(b & 2){
+				xsnarf(x);
+				xcut(x);
+			}else{
+				xpaste(x);
+			}
+			xscrdraw(x);			// TODO let cut/paste handle this?
+		}
+		while(mc->buttons == b)
+			readmouse(mc);
+	}
+}
+
+void
+xshow(Text *w, uint q0)
+{
+	int qe;
+	int nl;
+	uint q;
+
+	qe = w->org+w->nchars;
+	if(w->org<=q0 && (q0<qe || (q0==qe && qe==w->nr)))
+		xscrdraw(w);
+	else{
+		nl = 4*w->maxlines/5;
+		q = xbacknl(w, q0, nl);
+		/* avoid going backwards if trying to go forwards - long lines! */
+		if(!(q0>w->org && q<w->org))
+			xsetorigin(w, q, TRUE);
+		while(q0 > w->org+w->nchars)
+			xsetorigin(w, w->org+1, FALSE);
+	}
+}
+
+void
+xplacetick(Text *x, uint q)
+{
+	xsetselect(x, q, q);
+	xshow(x, q);
+}
+
+void
+xtype(Text *x, Rune r)
+{
+	uint q0, q1;
+	int nb;
+
+	switch(r){
+	case CTRL('H'):	/* erase character */
+	case CTRL('W'):	/* erase word */
+	case CTRL('U'):	/* erase line */
+		if(x->q0==0 || x->q0==x->qh)
+			return;
+		nb = xbswidth(x, r);
+		q1 = x->q0;
+		q0 = q1-nb;
+		if(q0 < x->org){
+			q0 = x->org;
+			nb = q1-q0;
+		}
+		if(nb > 0){
+			xdelete(x, q0, q0+nb);
+			xsetselect(x, q0, q0);
+		}
+		break;
+	default:
+		xdelete(x, x->q0, x->q1);
+		xinsert(x, &r, 1, x->q0);
+		xshow(x, x->q0);
+		break;
+	}
+}
+
+int
+xninput(Text *x)
+{
+	uint q;
+	Rune r;
+
+	for(q = x->qh; q < x->nr; q++){
+		r = x->r[q];
+		if(r == '\n')
+			return q - x->qh + 1;
+		if(r == CTRL('D'))
+			return q - x->qh;
+	}
+	return -1;
+}
+
+void
+xaddraw(Text *x, Rune *r, int nr)
+{
+	x->raw = runerealloc(x->raw, x->nraw+nr);
+	runemove(x->raw+x->nraw, r, nr);
+	x->nraw += nr;
+}
+
+/* TODO: maybe pass what we're looking for in a string */
+void
+xlook(Text *x)
+{
+	int i, n, e;
+
+	i = x->q1;
+	n = i - x->q0;
+	e = x->nr - n;
+	if(n <= 0 || e < n)
+		return;
+
+	if(i > e)
+		i = 0;
+
+	while(runestrncmp(x->r+x->q0, x->r+i, n) != 0){
+		if(i < e)
+			i++;
+		else
+			i = 0;
+	}
+
+	xsetselect(x, i, i+n);
+	xshow(x, i);
+}
+
+Rune *snarf;
+int nsnarf;
+int snarfversion;
+int snarffd;
+
+void
+xsnarf(Text *x)
+{
+	if(x->q1 == x->q0)
+		return;
+	nsnarf = x->q1-x->q0;
+	snarf = runerealloc(snarf, nsnarf);
+	snarfversion++;
+	runemove(snarf, x->r+x->q0, nsnarf);
+	putsnarf();
+}
+
+void
+xcut(Text *x)
+{
+	if(x->q1 == x->q0)
+		return;
+	xdelete(x, x->q0, x->q1);
+	xsetselect(x, x->q0, x->q0);
+}
+
+void
+xpaste(Text *x)
+{
+	uint q0;
+
+	getsnarf();
+	if(nsnarf == 0)
+		return;
+	xcut(x);
+	q0 = x->q0;
+	if(x->rawmode && q0==x->nr){
+		xaddraw(x, snarf, nsnarf);
+		xsetselect(x, q0, q0);
+	}else{
+		q0 = xinsert(x, snarf, nsnarf, x->q0);
+		xsetselect(x, q0, q0+nsnarf);
+	}
+}
+
+void
+xsend(Text *x)
+{
+	getsnarf();
+	xsnarf(x);
+	if(nsnarf == 0)
+		return;
+	if(x->rawmode){
+		xaddraw(x, snarf, nsnarf);
+		if(snarf[nsnarf-1]!='\n' && snarf[nsnarf-1]!=CTRL('D'))
+			xaddraw(x, L"\n", 1);
+	}else{
+		xinsert(x, snarf, nsnarf, x->nr);
+		if(snarf[nsnarf-1]!='\n' && snarf[nsnarf-1]!=CTRL('D'))
+			xinsert(x, L"\n", 1, x->nr);
+	}
+	xplacetick(x, x->nr);
+}
+
+int
+xplumb(Text *w, char *dir, int maxsize)
+{
+	Plumbmsg *m;
+	static int fd = -2;
+	char buf[32];
+	uint p0, p1;
+
+	if(fd == -2)
+		fd = plumbopen("send", OWRITE|OCEXEC);
+	if(fd < 0)
+		return 0;
+	m = emalloc(sizeof(Plumbmsg));
+	m->src = estrdup("rio");
+	m->dst = nil;
+	m->wdir = estrdup(dir);
+	m->type = estrdup("text");
+	p0 = w->q0;
+	p1 = w->q1;
+	if(w->q1 > w->q0)
+		m->attr = nil;
+	else{
+		while(p0>0 && w->r[p0-1]!=' ' && w->r[p0-1]!='\t' && w->r[p0-1]!='\n')
+			p0--;
+		while(p1<w->nr && w->r[p1]!=' ' && w->r[p1]!='\t' && w->r[p1]!='\n')
+			p1++;
+		snprint(buf, sizeof(buf), "click=%d", w->q0-p0);
+		m->attr = plumbunpackattr(buf);
+	}
+	if(p1-p0 > maxsize){
+		plumbfree(m);
+		return 0;	/* too large for 9P */
+	}
+//	m->data = runetobyte(w->r+p0, p1-p0, &m->ndata);
+	m->data = smprint("%.*S", p1-p0, w->r+p0);
+	m->ndata = strlen(m->data);
+	if(plumbsend(fd, m) < 0){
+		plumbfree(m);
+		return 1;
+	}
+	plumbfree(m);
+	return 0;
+}
--- /dev/null
+++ b/time.c
@@ -1,0 +1,114 @@
+#include "inc.h"
+
+/* taken from rio */
+
+static Channel*	ctimer;	/* chan(Timer*)[100] */
+static Timer *timer;
+
+static uint
+msec(void)
+{
+	return nsec()/1000000;
+}
+
+void
+timerstop(Timer *t)
+{
+	t->next = timer;
+	timer = t;
+}
+
+void
+timercancel(Timer *t)
+{
+	t->cancel = TRUE;
+}
+
+static void
+timerproc(void*)
+{
+	int i, nt, na, dt, del;
+	Timer **t, *x;
+	uint old, new;
+
+	rfork(RFFDG);
+	threadsetname("TIMERPROC");
+	t = nil;
+	na = 0;
+	nt = 0;
+	old = msec();
+	for(;;){
+		sleep(1);	/* will sleep minimum incr */
+		new = msec();
+		dt = new-old;
+		old = new;
+		if(dt < 0)	/* timer wrapped; go around, losing a tick */
+			continue;
+		for(i=0; i<nt; i++){
+			x = t[i];
+			x->dt -= dt;
+			del = 0;
+			if(x->cancel){
+				timerstop(x);
+				del = 1;
+			}else if(x->dt <= 0){
+				/*
+				 * avoid possible deadlock if client is
+				 * now sending on ctimer
+				 */
+				if(nbsendul(x->c, 0) > 0)
+					del = 1;
+			}
+			if(del){
+				memmove(&t[i], &t[i+1], (nt-i-1)*sizeof t[0]);
+				--nt;
+				--i;
+			}
+		}
+		if(nt == 0){
+			x = recvp(ctimer);
+	gotit:
+			if(nt == na){
+				na += 10;
+				t = realloc(t, na*sizeof(Timer*));
+				if(t == nil)
+					abort();
+			}
+			t[nt++] = x;
+			old = msec();
+		}
+		if(nbrecv(ctimer, &x) > 0)
+			goto gotit;
+	}
+}
+
+void
+timerinit(void)
+{
+	ctimer = chancreate(sizeof(Timer*), 100);
+	proccreate(timerproc, nil, mainstacksize);
+}
+
+/*
+ * timeralloc() and timerfree() don't lock, so can only be
+ * called from the main proc.
+ */
+
+Timer*
+timerstart(int dt)
+{
+	Timer *t;
+
+	t = timer;
+	if(t)
+		timer = timer->next;
+	else{
+		t = emalloc(sizeof(Timer));
+		t->c = chancreate(sizeof(int), 0);
+	}
+	t->next = nil;
+	t->dt = dt;
+	t->cancel = FALSE;
+	sendp(ctimer, t);
+	return t;
+}
--- /dev/null
+++ b/util.c
@@ -1,0 +1,128 @@
+#include "inc.h"
+
+void
+panic(char *s)
+{
+	fprint(2, "error: %s: %r\n", s);
+	threadexitsall("error");
+}
+
+void*
+emalloc(ulong size)
+{
+	void *p;
+
+	p = malloc(size);
+	if(p == nil)
+		panic("malloc failed");
+	memset(p, 0, size);
+	return p;
+}
+
+void*
+erealloc(void *p, ulong size)
+{
+	p = realloc(p, size);
+	if(p == nil)
+		panic("realloc failed");
+	return p;
+}
+
+char*
+estrdup(char *s)
+{
+	char *p;
+
+	p = malloc(strlen(s)+1);
+	if(p == nil)
+		panic("strdup failed");
+	strcpy(p, s);
+	return p;
+}
+
+/* Handle backspaces in a rune string.
+ * Set number of final runes,
+ * return number of runes to be deleted initially */
+int
+handlebs(Stringpair *pair)
+{
+	int initial;
+	Rune *start, *rp, *wp;
+	int i;
+
+	initial = 0;
+	start = rp = wp = pair->s;
+	for(i = 0; i < pair->ns; i++){
+		if(*rp == '\b'){
+			if(wp == start)
+				initial++;
+			else
+				wp--;
+		}else
+			*wp++ = *rp;
+		rp++;
+	}
+	pair->ns = wp - start;
+	return initial;
+}
+
+
+void
+cnvsize(RuneConvBuf *cnv, int nb)
+{
+	cnv->nb = nb;
+	if(cnv->maxbuf < nb+UTFmax){
+		cnv->maxbuf = nb+UTFmax;
+		cnv->buf = erealloc(cnv->buf, cnv->maxbuf);
+	}
+}
+
+int
+r2bfill(RuneConvBuf *cnv, Rune *rp, int nr)
+{
+	int i;
+	for(i = 0; cnv->n < cnv->nb && i < nr; i++)
+		cnv->n += runetochar(&cnv->buf[cnv->n], &rp[i]);
+	return i;
+}
+void
+r2bfinish(RuneConvBuf *cnv, Stringpair *pair)
+{
+	int nb;
+
+	nb = pair->ns;
+	pair->ns = min(nb, cnv->n);
+	memmove(pair->s, cnv->buf, pair->ns);
+	cnv->n = max(0, cnv->n-nb);
+	memmove(cnv->buf, cnv->buf+nb, cnv->n);
+}
+
+// TODO: not sure about the signature of this...
+// maybe pass in allocated pair?
+// don't include null runes
+Stringpair
+b2r(RuneConvBuf *cnv)
+{
+	Stringpair pair;
+	Rune *rp;
+	int i;
+
+	rp = runemalloc(cnv->n);
+	pair.s = rp;
+	pair.ns = 0;
+	i = 0;
+	// TODO: optimize this
+	// we know there are full runes until the end
+	while(fullrune(cnv->buf+i, cnv->n-i)){
+		i += chartorune(rp, cnv->buf+i);
+		if(*rp){
+			rp++;
+			pair.ns++;
+		}
+	}
+	memmove(cnv->buf, cnv->buf+i, cnv->n-i);
+	cnv->n -= i;
+
+	return pair;
+}
+
--- /dev/null
+++ b/wind.c
@@ -1,0 +1,982 @@
+#include "inc.h"
+
+Window *bottomwin, *topwin;
+Window *windows[1000];	// TMP
+int nwindows;
+Window *hidden[1000];
+int nhidden;
+Window *focused, *cursorwin;
+
+static void winthread(void *arg);
+
+static void
+wlistpushback(Window *w)
+{
+	w->higher = bottomwin;
+	if(bottomwin) bottomwin->lower = w;
+	w->lower = nil;
+	bottomwin = w;
+}
+
+static void
+wlistpushfront(Window *w)
+{
+	w->lower = topwin;
+	if(topwin) topwin->higher = w;
+	w->higher = nil;
+	topwin = w;
+}
+
+static void
+wlistremove(Window *w)
+{
+	if(w->lower)
+		w->lower->higher = w->higher;
+	else
+		bottomwin = w->higher;
+	if(w->higher)
+		w->higher->lower = w->lower;
+	else
+		topwin = w->lower;
+	w->higher = nil;
+	w->lower = nil;
+}
+
+void
+wcalcrects(Window *w)
+{
+	w->contrect = insetrect(w->img->r, Borderwidth);
+	Rectangle r = insetrect(w->contrect, 1);
+	w->scrollr = r;
+	w->scrollr.max.x = w->scrollr.min.x + 12;
+	w->textr = r;
+	w->textr.min.x = w->scrollr.max.x + 4;
+}
+
+void
+wdecor(Window *w)
+{
+	int c = w->holdmode ?
+		w == focused ? TITLEHOLD : LTITLEHOLD :
+		w == focused ? TITLE : LTITLE;
+	border(w->img, w->img->r, Borderwidth, colors[c], ZP);
+}
+
+void
+wsetcolors(Window *w)
+{
+	int c = w->holdmode ?
+		w == focused ? HOLDTEXT : PALEHOLDTEXT :
+		w == focused ? TEXT : PALETEXT;
+	w->text.cols[TEXT] = colors[c];
+}
+
+static void
+wsetsize(Window *w, Rectangle r)
+{
+	if(w->img)
+		freeimage(w->img);
+	w->img = allocwindow(wscreen, r, Refbackup, DNofill);
+	wcalcrects(w);
+// might be worth a try!
+//replclipr(w->img,0,w->contrect);
+	draw(w->img, w->img->r, colors[BACK], nil, ZP);
+	xinit(&w->text, w->textr, w->scrollr, font, w->img, colors);
+	wdecor(w);
+}
+
+static int id = 1;
+
+Window*
+wcreate(Rectangle r)
+{
+	Window *w;
+
+	w = emalloc(sizeof(Window));
+	incref(w);
+	w->id = id++;
+	w->notefd = -1;
+	w->label = estrdup("<unnamed>");
+	w->dir = estrdup(startdir);
+	wsetsize(w, r);
+	wlistpushfront(w);
+	// TMP - make dynamic
+	windows[nwindows++] = w;
+
+	w->mc.c = chancreate(sizeof(Mouse), 16);
+	w->mc.image = w->img;
+
+	w->gone = chancreate(sizeof(int), 0);
+	w->kbd = chancreate(sizeof(char*), 16);
+	w->ctl = chancreate(sizeof(Wctlmesg), 0);
+	w->conswrite = chancreate(sizeof(Channel**), 0);
+	w->consread = chancreate(sizeof(Channel**), 0);
+	w->kbdread = chancreate(sizeof(Channel**), 0);
+	w->mouseread = chancreate(sizeof(Channel**), 0);
+	w->complete = chancreate(sizeof(Completion*), 0);
+	threadcreate(winthread, w, mainstacksize);
+
+	return w;
+}
+
+/* called from winthread when it exits */
+static void
+wfree(Window *w)
+{
+	if(w->notefd >= 0)
+		close(w->notefd);
+	xclear(&w->text);
+	chanclose(w->mc.c);
+	chanclose(w->gone);
+	chanclose(w->kbd);
+	chanclose(w->ctl);
+	chanclose(w->conswrite);
+	chanclose(w->consread);
+	chanclose(w->kbdread);
+	chanclose(w->mouseread);
+	chanclose(w->complete);
+	free(w->label);
+	free(w);
+}
+
+static void
+wclose(Window *w)
+{
+	int i;
+
+	if(w->deleted)
+		return;
+	w->deleted = TRUE;
+	if(focused == w)
+		wfocus(nil);
+	wlistremove(w);
+	for(i = 0; i < nwindows; i++)
+		if(windows[i] == w){
+			nwindows--;
+			memmove(&windows[i], &windows[i+1], (nwindows-i)*sizeof(Window*));
+			break;
+		}
+
+	if(w->img){
+// rio does this, useful?
+//		originwindow(w->img, w->img->r.min, screen->r.max);
+		freeimage(w->img);
+	}
+	w->img = nil;
+	flushimage(display, 1);
+}
+
+int
+wrelease(Window *w)
+{
+	int i;
+
+	i = decref(w);
+	if(i > 0)
+		return 0;
+	if(i < 0)
+		panic("negative ref count");
+	wclose(w);
+	wsendmsg(w, Closed, ZR, nil);
+	return 1;
+}
+
+void
+wsendmsg(Window *w, int type, Rectangle r, void *p)
+{
+	Wctlmesg cm;
+
+	cm.type = type;
+	cm.r = r;
+	cm.p = p;
+	send(w->ctl, &cm);
+}
+
+Window*
+wfind(int id)
+{
+	int i;
+
+	for(i = 0; i < nwindows; i++)
+		if(windows[i]->id == id)
+			return windows[i];
+	return nil;
+}
+
+Window*
+wpointto(Point pt)
+{
+	Window *w;
+
+	for(w = topwin; w; w = w->lower)
+		if(!w->hidden && ptinrect(pt, w->img->r))
+			return w;
+	return nil;
+}
+
+void
+wsetcursor(Window *w)
+{
+	if(w != cursorwin)
+		return;
+
+	if(w->holdmode)
+		setcursornormal(&whitearrow);
+	else
+		setcursornormal(w->cursorp);
+}
+
+void
+wrepaint(Window *w)
+{
+	wsetcolors(w);
+	if(!w->mouseopen)
+		xredraw(&w->text);
+	wdecor(w);
+}
+
+void
+wsetlabel(Window *w, char *label)
+{
+	free(w->label);
+	w->label = estrdup(label);
+}
+
+void
+wresize(Window *w, Rectangle r)
+{
+// TODO: maybe call wsetsize from Reshaped handler?
+	wsetsize(w, r);
+	wsendmsg(w, Reshaped, w->img->r, nil);
+}
+
+void
+wmove(Window *w, Point pos)
+{
+	/* BUG: originwindow causes the old window rect to be drawn onto the new one
+	 * with backing store of allocscreen
+	 * happens in _freeimage1(*winp); in libdraw/init.c:gengetwindow
+	 * where *winp has the old rectangle
+	 *
+	 * We don't care if we're handling resizing ourselves though */
+
+	if(w->mouseopen){
+		Point delta = subpt(pos, w->img->r.min);
+		wresize(w, rectaddpt(w->img->r, delta));
+	}else{
+		originwindow(w->img, pos, pos);
+		wcalcrects(w);
+		xsetrects(&w->text, w->textr, w->scrollr);
+
+// TODO: Reshaped changes winname, don't want that
+		w->resized = TRUE;
+		w->mc.buttons = 0;	/* avoid re-triggering clicks on resize */
+		w->mq.counter++;	/* cause mouse to be re-read */
+	}
+}
+
+void
+wrmove(Window *w, Point delta)
+{
+	wmove(w, addpt(w->img->r.min, delta));
+}
+
+/* currently UNUSED */
+void
+wrmovescreen(Point delta)
+{
+	Point pos;
+	Window *w;
+
+	for(w = bottomwin; w; w = w->higher){
+		pos = addpt(w->img->r.min, delta);
+		originwindow(w->img, pos, pos);
+		wcalcrects(w);
+		xsetrects(&w->text, w->textr, w->scrollr);
+	}
+	flushimage(display, 1);
+}
+
+void
+wraise(Window *w)
+{
+	wlistremove(w);
+	wlistpushfront(w);
+	topwindow(w->img);
+	flushimage(display, 1);
+}
+
+void
+wlower(Window *w)
+{
+	wlistremove(w);
+	wlistpushback(w);
+	bottomwindow(w->img);
+	flushimage(display, 1);
+}
+
+void
+wfocus(Window *w)
+{
+	Window *prev;
+
+	if(w == focused)
+		return;
+	prev = focused;
+	focused = w;
+	if(prev)
+		wrepaint(prev);
+	if(focused)
+		wrepaint(focused);
+}
+
+void
+whide(Window *w)
+{
+	if(w->hidden)
+		return;
+	incref(w);
+	if(w == focused)
+		wfocus(nil);
+	w->hidden = TRUE;
+	originwindow(w->img, w->img->r.min, screen->r.max);
+	wrelease(w);
+}
+
+void
+wunhide(Window *w)
+{
+	if(!w->hidden)
+		return;
+	incref(w);
+	w->hidden = FALSE;
+	originwindow(w->img, w->img->r.min, w->img->r.min);
+	wfocus(w);
+	wrelease(w);
+}
+
+void
+wsethold(Window *w, int hold)
+{
+	int prev;
+
+	if(hold)
+		prev = w->holdmode++;
+	else
+		prev = --w->holdmode;
+	if(prev == 0){
+		wsetcursor(w);
+		wrepaint(w);
+	}
+}
+
+/*
+ * Need to do this in a separate proc because if process we're interrupting
+ * is dying and trying to print tombstone, kernel is blocked holding p->debug lock.
+ */
+static void
+interruptproc(void *v)
+{
+	int *notefd;
+
+	notefd = v;
+	write(*notefd, "interrupt", 9);
+	close(*notefd);
+	free(notefd);
+}
+
+/*
+ * Filename completion
+ */
+
+typedef struct Completejob Completejob;
+struct Completejob
+{
+	char	*dir;
+	char	*str;
+	Window	*win;
+};
+
+static void
+completeproc(void *arg)
+{
+	Completejob *job;
+	Completion *c;
+
+	job = arg;
+	threadsetname("namecomplete %s", job->dir);
+
+	c = complete(job->dir, job->str);
+	if(c != nil && sendp(job->win->complete, c) <= 0)
+		freecompletion(c);
+
+	wrelease(job->win);
+
+	free(job->dir);
+	free(job->str);
+	free(job);
+}
+
+static int
+windfilewidth(Window *w, uint q0, int oneelement)
+{
+	uint q;
+	Rune r;
+
+	q = q0;
+	while(q > 0){
+		r = w->text.r[q-1];
+		if(r<=' ' || r=='=' || r=='^' || r=='(' || r=='{')
+			break;
+		if(oneelement && r=='/')
+			break;
+		--q;
+	}
+	return q0-q;
+}
+
+static void
+namecomplete(Window *w)
+{
+	Text *x;
+	int nstr, npath;
+	Rune *path, *str;
+	char *dir, *root;
+	Completejob *job;
+
+	x = &w->text;
+	/* control-f: filename completion; works back to white space or / */
+	if(x->q0<x->nr && x->r[x->q0]>' ')	/* must be at end of word */
+		return;
+	nstr = windfilewidth(w, x->q0, TRUE);
+	str = x->r+(x->q0-nstr);
+	npath = windfilewidth(w, x->q0-nstr, FALSE);
+	path = x->r+(x->q0-nstr-npath);
+
+	/* is path rooted? if not, we need to make it relative to window path */
+	if(npath>0 && path[0]=='/')
+		dir = smprint("%.*S", npath, path);
+	else {
+		if(strcmp(w->dir, "") == 0)
+			root = ".";
+		else
+			root = w->dir;
+		dir = smprint("%s/%.*S", root, npath, path);
+	}
+	if(dir == nil)
+		return;
+
+	/* run in background, winctl will collect the result on w->complete chan */
+	job = emalloc(sizeof *job);
+	job->str = smprint("%.*S", nstr, str);
+	job->dir = cleanname(dir);
+	job->win = w;
+	incref(w);
+	proccreate(completeproc, job, mainstacksize);
+}
+
+static void
+showcandidates(Window *w, Completion *c)
+{
+	Text *x;
+	int i;
+	Fmt f;
+	Rune *rp;
+	uint nr, qline;
+	char *s;
+
+	x = &w->text;
+	runefmtstrinit(&f);
+	if (c->nmatch == 0)
+		s = "[no matches in ";
+	else
+		s = "[";
+	if(c->nfile > 32)
+		fmtprint(&f, "%s%d files]\n", s, c->nfile);
+	else{
+		fmtprint(&f, "%s", s);
+		for(i=0; i<c->nfile; i++){
+			if(i > 0)
+				fmtprint(&f, " ");
+			fmtprint(&f, "%s", c->filename[i]);
+		}
+		fmtprint(&f, "]\n");
+	}
+	rp = runefmtstrflush(&f);
+	nr = runestrlen(rp);
+
+	/* place text at beginning of line before cursor and host point */
+	qline = min(x->qh, x->q0);
+	while(qline>0 && x->r[qline-1] != '\n')
+		qline--;
+
+	if(qline == x->qh){
+		/* advance host point to avoid readback */
+		x->qh = xinsert(x, rp, nr, qline)+nr;
+	}else{
+		xinsert(x, rp, nr, qline);
+	}
+	free(rp);
+}
+
+void
+wkeyctl(Window *w, Rune r)
+{
+	Text *x;
+	int nlines, n;
+	int *notefd;
+
+	x = &w->text;
+	nlines = x->maxlines;	/* need signed */
+	if(!w->mouseopen){
+		switch(r){
+
+		/* Scrolling */
+		case Kscrollonedown:
+			n = mousescrollsize(x->maxlines);
+			xscrolln(x, max(n, 1));
+			return;
+		case Kdown:
+			xscrolln(x, shiftdown ? 1 : nlines/3);
+//			xtickupdn(x, 1);
+			return;
+		case Kpgdown:
+			xscrolln(x, nlines*2/3);
+			return;
+		case Kscrolloneup:
+			n = mousescrollsize(x->maxlines);
+			xscrolln(x, -max(n, 1));
+			return;
+		case Kup:
+			xscrolln(x, -(shiftdown ? 1 : nlines/3));
+//			xtickupdn(x, -1);
+			return;
+		case Kpgup:
+			xscrolln(x, -nlines*2/3);
+			return;
+
+		case Khome:
+			xshow(x, 0);
+			return;
+		case Kend:
+			xshow(x, x->nr);
+			return;
+
+		/* Cursor movement */
+		case Kleft:
+			if(x->q0 > 0)
+				xplacetick(x, x->q0-1);
+			return;
+		case Kright:
+			if(x->q1 < x->nr)
+				xplacetick(x, x->q1+1);
+			return;
+		case CTRL('A'):
+			while(x->q0 > 0 && x->r[x->q0-1] != '\n' &&
+			      x->q0 != x->qh)
+				x->q0--;
+			xplacetick(x, x->q0);
+			return;
+		case CTRL('E'):
+			while(x->q0 < x->nr && x->r[x->q0] != '\n')
+				x->q0++;
+			xplacetick(x, x->q0);
+			return;
+		case CTRL('B'):
+			xplacetick(x, x->qh);
+			return;
+
+		/* Hold mode */
+		case Kesc:
+			wsethold(w, !w->holdmode);
+			return;
+		case Kdel:
+			if(w->holdmode)
+				wsethold(w, 0);
+			break;
+		}
+	}
+
+	if(x->rawmode && (x->q0 == x->nr || w->mouseopen))
+		xaddraw(x, &r, 1);
+	else if(r == Kdel){
+		x->qh = x->nr;
+		xshow(x, x->qh);
+		if(w->notefd < 0)
+			return;
+		notefd = emalloc(sizeof(int));
+		*notefd = dup(w->notefd, -1);
+		proccreate(interruptproc, notefd, 4096);
+	}else if(r == CTRL('F') || r == Kins)
+		namecomplete(w);
+	else
+		xtype(x, r);
+}
+
+void
+wmousectl(Window *w)
+{
+	int but;
+
+	for(but = 1; but < 6; but++)
+		if(w->mc.buttons == 1<<(but-1))
+			goto found;
+	return;
+found:
+
+	incref(w);
+	if(shiftdown && but > 3)
+		wkeyctl(w, but == 4 ? Kscrolloneup : Kscrollonedown);
+	else if(ptinrect(w->mc.xy, w->text.scrollr) || but > 3)
+		xscroll(&w->text, &w->mc, but);
+	else if(but == 1)
+		xselect(&w->text, &w->mc);
+	wrelease(w);
+}
+
+int
+winctl(Window *w, int type, Rectangle r, void *p)
+{
+	Text *x;
+	int i;
+
+	x = &w->text;
+(void)p;
+(void)r;
+	switch(type){
+	case Closed:
+		wfree(w);
+		return 1;
+
+	case Deleted:
+		if(w->notefd >= 0)
+			write(w->notefd, "hangup", 6);
+		wclose(w);
+		break;
+
+	case Reshaped:
+/* TODO: all the resizing code is shit */
+		wsetname(w);
+		w->resized = TRUE;
+		w->mc.buttons = 0;	/* avoid re-triggering clicks on resize */
+		w->mq.counter++;	/* cause mouse to be re-read */
+		wdecor(w);
+		break;
+
+	case Refresh:
+/* TODO: clean this up? */
+		draw(w->img, w->img->r, x->cols[BACK], nil, ZP);
+		wdecor(w);
+		xfill(x);
+		x->ticked = 0;
+		if(x->p0 > 0)
+			frdrawsel(x, frptofchar(x, 0), 0, x->p0, 0);
+		if(x->p1 < x->nchars)
+			frdrawsel(x, frptofchar(x, x->p1), x->p1, x->nchars, 0);
+		frdrawsel(x, frptofchar(x, x->p0), x->p0, x->p1, 1);
+		x->lastsr = ZR;
+		xscrdraw(x);
+		break;
+
+	case Holdon:
+		wsethold(w, TRUE);
+		break;
+	case Holdoff:
+		wsethold(w, FALSE);
+		break;
+
+	case Rawon:
+		break;
+	case Rawoff:
+// TODO: better to remove one by one? not sure if wkeyctl is safe
+		for(i = 0; i < x->nraw; i++)
+			wkeyctl(w, x->raw[i]);
+		x->nraw = 0;
+		break;
+	}
+	return 0;
+}
+
+static void
+winthread(void *arg)
+{
+	Window *w;
+	Text *x;
+	Rune r, *rp;
+	char *s;
+	Wctlmesg cm;
+	enum { AKbd, AMouse, ACtl, AConsWrite, AConsRead, AKbdRead, AMouseRead, AComplete, Agone, NALT };
+	Alt alts[NALT+1];
+	Channel *fsc;
+	Stringpair pair;
+	int i, nb, nr, initial;
+	uint q0;
+	RuneConvBuf cnv;
+	Mousestate m;
+	Completion *comp;
+
+	w = arg;
+	x = &w->text;
+	nr = 0;
+	memset(&cnv, 0, sizeof(cnv));
+	fsc = chancreate(sizeof(Stringpair), 0);
+
+	alts[AKbd] = ALT(w->kbd, &s, CHANRCV);
+	alts[AMouse] = ALT(w->mc.c, &w->mc.Mouse, CHANRCV);
+	alts[ACtl] = ALT(w->ctl, &cm, CHANRCV);
+	alts[AConsWrite] = ALT(w->conswrite, &fsc, CHANSND);
+	alts[AConsRead] = ALT(w->consread, &fsc, CHANSND);
+	alts[AKbdRead] = ALT(w->kbdread, &fsc, CHANSND);
+	alts[AMouseRead] = ALT(w->mouseread, &fsc, CHANSND);
+	alts[AComplete] = ALT(w->complete, &comp, CHANRCV);
+	alts[Agone] = ALT(w->gone, nil, CHANNOP);
+	alts[NALT].op = CHANEND;
+
+	for(;;){
+		if(w->deleted){					// TODO? rio checks image here
+			alts[Agone].op = CHANSND;
+			alts[AConsWrite].op = CHANNOP;
+			alts[AConsRead].op = CHANNOP;
+			alts[AKbdRead].op = CHANNOP;
+			alts[AMouseRead].op = CHANNOP;
+		}else{
+			nr = xninput(x);
+			if(!w->holdmode && (nr >= 0 || cnv.n > 0 || x->rawmode && x->nraw > 0))
+				alts[AConsRead].op = CHANSND;
+			else
+				alts[AConsRead].op = CHANNOP;
+			if(w->scrolling || w->mouseopen || x->qh <= x->org+x->nchars)
+				alts[AConsWrite].op = CHANSND;
+			else
+				alts[AConsWrite].op = CHANNOP;
+			if(w->kbdopen && (w->kq.ri != w->kq.wi || w->kq.full))
+				alts[AKbdRead].op = CHANSND;
+			else
+				alts[AKbdRead].op = CHANNOP;
+			if(w->mouseopen && w->mq.counter != w->mq.lastcounter)
+				alts[AMouseRead].op = CHANSND;
+			else
+				alts[AMouseRead].op = CHANNOP;
+		}
+
+		switch(alt(alts)){
+		case AKbd:
+			if(!w->kq.full){
+				w->kq.q[w->kq.wi++] = s;
+				w->kq.wi %= nelem(w->kq.q);
+				w->kq.full = w->kq.wi == w->kq.ri;
+			}else
+				free(s);
+			if(!w->kbdopen)
+			while(w->kq.ri != w->kq.wi || w->kq.full){
+				s = w->kq.q[w->kq.ri++];
+				w->kq.ri %= nelem(w->kq.q);
+				w->kq.full = FALSE;
+				if(*s == 'c'){
+					chartorune(&r, s+1);
+					if(r)
+						wkeyctl(w, r);
+				}
+				free(s);
+			}
+			break;
+
+		case AKbdRead:
+			recv(fsc, &pair);
+			nb = 0;
+			while(w->kq.ri != w->kq.wi || w->kq.full){
+				s = w->kq.q[w->kq.ri];
+				i = strlen(s)+1;
+				if(nb+i > pair.ns)
+					break;
+				w->kq.ri = (w->kq.ri+1) % nelem(w->kq.q);
+				w->kq.full = FALSE;
+				memmove((char*)pair.s + nb, s, i);
+				free(s);
+				nb += i;
+			}
+			pair.ns = nb;
+			send(fsc, &pair);
+			break;
+
+		case AMouse:
+			if(w->mouseopen){
+				Mousestate *mp;
+				w->mq.counter++;
+				/* queue click events in ring buffer.
+				 * pure movement only in else branch of the case below */
+				if(!w->mq.full && w->mq.lastb != w->mc.buttons){
+					mp = &w->mq.q[w->mq.wi++];
+					w->mq.wi %= nelem(w->mq.q);
+					w->mq.full = w->mq.wi == w->mq.ri;
+					mp->Mouse = w->mc;
+					mp->counter = w->mq.counter;
+					w->mq.lastb = w->mc.buttons;
+				}
+			}else
+				wmousectl(w);
+			break;
+
+		case AMouseRead:
+			recv(fsc, &pair);
+			w->mq.full = FALSE;
+			/* first return queued clicks, then current state */
+			if(w->mq.wi != w->mq.ri){
+				m = w->mq.q[w->mq.ri++];
+				w->mq.ri %= nelem(w->mq.q);
+			}else
+				m = (Mousestate){w->mc.Mouse, w->mq.counter};
+			w->mq.lastcounter = m.counter;
+
+			nb = snprint(pair.s, pair.ns, "%c%11d %11d %11d %11ld ",
+				"mr"[w->resized], m.xy.x, m.xy.y, m.buttons, m.msec);
+			w->resized = FALSE;
+			pair.ns = min(nb, pair.ns);
+			send(fsc, &pair);
+			break;
+
+		case AConsWrite:
+			recv(fsc, &pair);
+			initial = handlebs(&pair);
+			if(initial){
+				initial = min(initial, x->qh);
+				xdelete(x, x->qh-initial, x->qh);
+			}
+			x->qh = xinsert(x, pair.s, pair.ns, x->qh) + pair.ns;
+			free(pair.s);
+			if(w->scrolling || w->mouseopen)
+				xshow(x, x->qh);
+			xscrdraw(x);
+			break;
+
+		case AConsRead:
+			recv(fsc, &pair);
+			cnvsize(&cnv, pair.ns);
+			nr = r2bfill(&cnv, x->r+x->qh, nr);
+			x->qh += nr;
+			/* if flushed by ^D, skip the ^D */
+			if(!(nr > 0 && x->r[x->qh-1] == '\n') &&
+			   x->qh < x->nr && x->r[x->qh] == CTRL('D'))
+				x->qh++;
+			if(x->rawmode){
+				nr = r2bfill(&cnv, x->raw, x->nraw);
+				x->nraw -= nr;
+				runemove(x->raw, x->raw+nr, x->nraw);
+			}
+			r2bfinish(&cnv, &pair);
+			send(fsc, &pair);
+			break;
+
+		case ACtl:
+			if(winctl(w, cm.type, cm.r, cm.p)){
+				free(cnv.buf);
+				return;
+			}
+			break;
+
+		case AComplete:
+			if(w->img!=nil){
+				if(!comp->advance)
+					showcandidates(w, comp);
+				if(comp->advance){
+					rp = runesmprint("%s", comp->string);
+					if(rp){
+						nr = runestrlen(rp);
+						q0 = x->q0;
+						q0 = xinsert(x, rp, nr, q0);
+						xshow(x, q0+nr);
+						free(rp);
+					}
+				}
+			}
+			freecompletion(comp);
+			break;
+		}
+		flushimage(display, 1);
+	}
+}
+
+void
+wsetname(Window *w)
+{
+	int i, n;
+	char err[ERRMAX];
+	
+	n = snprint(w->name, sizeof(w->name)-2, "window.%d.%d", w->id, w->namecount++);
+	for(i='A'; i<='Z'; i++){
+		if(nameimage(w->img, w->name, 1) > 0)
+			return;
+		errstr(err, sizeof err);
+		if(strcmp(err, "image name in use") != 0)
+			break;
+		w->name[n] = i;
+		w->name[n+1] = 0;
+	}
+	w->name[0] = 0;
+	fprint(2, "rio: setname failed: %s\n", err);
+}
+
+void
+wsetpid(Window *w, int pid, int dolabel)
+{
+	char buf[32];
+	int ofd;
+
+	ofd = w->notefd;
+	if(pid <= 0)
+		w->notefd = -1;
+	else {
+		if(dolabel){
+			snprint(buf, sizeof(buf), "rc %lud", (ulong)pid);
+			free(w->label);
+			w->label = estrdup(buf);
+		}
+		snprint(buf, sizeof(buf), "/proc/%lud/notepg", (ulong)pid);
+		w->notefd = open(buf, OWRITE|OCEXEC);
+	}
+	if(ofd >= 0)
+		close(ofd);
+}
+
+void
+winshell(void *args)
+{
+	Window *w;
+	Channel *pidc;
+	void **arg;
+	char *cmd, *dir;
+	char **argv;
+
+	arg = args;
+	w = arg[0];
+	pidc = arg[1];
+	cmd = arg[2];
+	argv = arg[3];
+	dir = arg[4];
+	rfork(RFNAMEG|RFFDG|RFENVG);
+	if(fsmount(w->id) < 0){
+		fprint(2, "mount failed: %r\n");
+		sendul(pidc, 0);
+		threadexits("mount failed");
+	}
+	close(0);
+	if(open("/dev/cons", OREAD) < 0){
+		fprint(2, "can't open /dev/cons: %r\n");
+		sendul(pidc, 0);
+		threadexits("/dev/cons");
+	}
+	close(1);
+	if(open("/dev/cons", OWRITE) < 0){
+		fprint(2, "can't open /dev/cons: %r\n");
+		sendul(pidc, 0);
+		threadexits("open");	/* BUG? was terminate() */
+	}
+	if(wrelease(w) == 0){	/* remove extra ref hanging from creation */
+		notify(nil);
+		dup(1, 2);
+		if(dir)
+			chdir(dir);
+		procexec(pidc, cmd, argv);
+		_exits("exec failed");
+	}
+}