shithub: acme-themes

Download patch

ref: 28e8ccb5203de0d122310d70e5751c9778adbbaf
parent: 8b24786f2b7a6faf246874b4e50149c35628e2b8
author: jgstratton <[email protected]>
date: Sun Oct 3 11:29:56 EDT 2021

Initial Add

--- a/README.md
+++ b/README.md
@@ -1,2 +1,4 @@
 # plan9-acme-themes
-acme, but it uses themes from rio-themes if they are loaded. Otherwise, it will default to the traditional rio theme.
+This is plan’s acme text editor, but it will use themes from rio-themes (https://ftrv.se/14) if they are installed.
+
+Basically, this code is the functions from rio-themes, yanked out and dumped into acme. Currently it only reads the themes and execution. So if you change your theme, you will need to restart acme if you want it to reflect the new theme. If no theme is installed, then it defaults to traditional acme.
--- /dev/null
+++ b/acme.c
@@ -1,0 +1,1071 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <cursor.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <frame.h>
+#include <fcall.h>
+#include <plumb.h>
+#include "dat.h"
+#include "fns.h"
+	/* for generating syms in mkfile only: */
+	#include <bio.h>
+	#include "edit.h"
+
+void	mousethread(void*);
+void	keyboardthread(void*);
+void	waitthread(void*);
+void	xfidallocthread(void*);
+void	newwindowthread(void*);
+void 	plumbproc(void*);
+void 	themeload(char *s, int n);
+
+Reffont	**fontcache;
+int		nfontcache;
+char		wdir[512] = ".";
+Reffont	*reffonts[2];
+int		snarffd = -1;
+int		mainpid;
+int		plumbsendfd;
+int		plumbeditfd;
+
+enum{
+	NSnarf = 1000	/* less than 1024, I/O buffer size */
+};
+Rune	snarfrune[NSnarf+1];
+
+char		*fontnames[2];
+
+Command *command;
+
+void	acmeerrorinit(void);
+void	readfile(Column*, char*);
+int	shutdown(void*, char*);
+
+void
+derror(Display*, char *errorstr)
+{
+	error(errorstr);
+}
+
+void
+threadmain(int argc, char *argv[])
+{
+	int i;
+	char *p, *loadfile;
+	char buf[256];
+	Column *c;
+	int ncol;
+	Display *d;
+
+	rfork(RFENVG|RFNAMEG);
+
+	ncol = -1;
+
+	loadfile = nil;
+	ARGBEGIN{
+	case 'a':
+		globalindent[AUTOINDENT] = TRUE;
+		break;
+	case 'b':
+		bartflag = TRUE;
+		break;
+	case 'c':
+		p = ARGF();
+		if(p == nil)
+			goto Usage;
+		ncol = atoi(p);
+		if(ncol <= 0)
+			goto Usage;
+		break;
+	case 'f':
+		fontnames[0] = ARGF();
+		if(fontnames[0] == nil)
+			goto Usage;
+		break;
+	case 'F':
+		fontnames[1] = ARGF();
+		if(fontnames[1] == nil)
+			goto Usage;
+		break;
+	case 'i':
+		globalindent[SPACESINDENT] = TRUE;
+		break;
+	case 'l':
+		loadfile = ARGF();
+		if(loadfile == nil)
+			goto Usage;
+		break;
+	default:
+	Usage:
+		fprint(2, "usage: acme [-aib] [-c ncol] [-f font] [-F fixedfont] [-l loadfile | file...]\n");
+		exits("usage");
+	}ARGEND
+
+	if(fontnames[0] == nil)
+		fontnames[0] = getenv("font");
+	if(fontnames[0] == nil)
+		fontnames[0] = "/lib/font/bit/vga/unicode.font";
+	if(access(fontnames[0], 0) < 0){
+		fprint(2, "acme: can't access %s: %r\n", fontnames[0]);
+		exits("font open");
+	}
+	if(fontnames[1] == nil)
+		fontnames[1] = fontnames[0];
+	fontnames[0] = estrdup(fontnames[0]);
+	fontnames[1] = estrdup(fontnames[1]);
+
+	quotefmtinstall();
+	cputype = getenv("cputype");
+	objtype = getenv("objtype");
+	home = getenv("home");
+	p = getenv("tabstop");
+	if(p != nil){
+		maxtab = strtoul(p, nil, 0);
+		free(p);
+	}
+	if(maxtab == 0)
+		maxtab = 4; 
+	if(loadfile)
+		rowloadfonts(loadfile);
+	putenv("font", fontnames[0]);
+	snarffd = open("/dev/snarf", OREAD|OCEXEC);
+	if(cputype){
+		sprint(buf, "/acme/bin/%s", cputype);
+		bind(buf, "/bin", MBEFORE);
+	}
+	bind("/acme/bin", "/bin", MBEFORE);
+	getwd(wdir, sizeof wdir);
+
+	if(geninitdraw(nil, derror, fontnames[0], "acme", nil, Refnone) < 0){
+		fprint(2, "acme: can't open display: %r\n");
+		exits("geninitdraw");
+	}
+	d = display;
+	font = d->defaultfont;
+
+	reffont.f = font;
+	reffonts[0] = &reffont;
+	incref(&reffont);	/* one to hold up 'font' variable */
+	incref(&reffont);	/* one to hold up reffonts[0] */
+	fontcache = emalloc(sizeof(Reffont*));
+	nfontcache = 1;
+	fontcache[0] = &reffont;
+
+	iconinit();
+	timerinit();
+	rxinit();
+
+	cwait = threadwaitchan();
+	ccommand = chancreate(sizeof(Command**), 0);
+	ckill = chancreate(sizeof(Rune*), 0);
+	cxfidalloc = chancreate(sizeof(Xfid*), 0);
+	cxfidfree = chancreate(sizeof(Xfid*), 0);
+	cnewwindow = chancreate(sizeof(Channel*), 0);
+	cerr = chancreate(sizeof(char*), 0);
+	cedit = chancreate(sizeof(int), 0);
+	cexit = chancreate(sizeof(int), 0);
+	cwarn = chancreate(sizeof(void*), 1);
+	if(cwait==nil || ccommand==nil || ckill==nil || cxfidalloc==nil || cxfidfree==nil || cnewwindow==nil || cerr==nil || cedit==nil || cexit==nil || cwarn==nil){
+		fprint(2, "acme: can't create initial channels: %r\n");
+		threadexitsall("channels");
+	}
+
+	mousectl = initmouse(nil, screen);
+	if(mousectl == nil){
+		fprint(2, "acme: can't initialize mouse: %r\n");
+		threadexitsall("mouse");
+	}
+	mouse = mousectl;
+	keyboardctl = initkeyboard(nil);
+	if(keyboardctl == nil){
+		fprint(2, "acme: can't initialize keyboard: %r\n");
+		threadexitsall("keyboard");
+	}
+	mainpid = getpid();
+	plumbeditfd = plumbopen("edit", OREAD|OCEXEC);
+	if(plumbeditfd >= 0){
+		cplumb = chancreate(sizeof(Plumbmsg*), 0);
+		proccreate(plumbproc, nil, STACK);
+	}
+	plumbsendfd = plumbopen("send", OWRITE|OCEXEC);
+
+	fsysinit();
+
+	#define	WPERCOL	8
+	disk = diskinit();
+	if(!loadfile || !rowload(&row, loadfile, TRUE)){
+		rowinit(&row, screen->clipr);
+		if(ncol < 0){
+			if(argc == 0)
+				ncol = 2;
+			else{
+				ncol = (argc+(WPERCOL-1))/WPERCOL;
+				if(ncol < 2)
+					ncol = 2;
+			}
+		}
+		if(ncol == 0)
+			ncol = 2;
+		for(i=0; i<ncol; i++){
+			c = rowadd(&row, nil, -1);
+			if(c==nil && i==0)
+				error("initializing columns");
+		}
+		c = row.col[row.ncol-1];
+		if(argc == 0)
+			readfile(c, wdir);
+		else
+			for(i=0; i<argc; i++){
+				p = utfrrune(argv[i], '/');
+				if((p!=nil && strcmp(p, "/guide")==0) || i/WPERCOL>=row.ncol)
+					readfile(c, argv[i]);
+				else
+					readfile(row.col[i/WPERCOL], argv[i]);
+			}
+	}
+	flushimage(display, 1);
+
+	acmeerrorinit();
+	threadcreate(keyboardthread, nil, STACK);
+	threadcreate(mousethread, nil, STACK);
+	threadcreate(waitthread, nil, STACK);
+	threadcreate(xfidallocthread, nil, STACK);
+	threadcreate(newwindowthread, nil, STACK);
+
+	threadnotify(shutdown, 1);
+	recvul(cexit);
+	killprocs();
+	threadexitsall(nil);
+}
+
+void
+readfile(Column *c, char *s)
+{
+	Window *w;
+	Rune rb[256];
+	int nb, nr;
+	Runestr rs;
+
+	w = coladd(c, nil, nil, -1);
+	cvttorunes(s, strlen(s), rb, &nb, &nr, nil);
+	rs = cleanrname((Runestr){rb, nr});
+	winsetname(w, rs.r, rs.nr);
+	textload(&w->body, 0, s, 1);
+	w->body.file->mod = FALSE;
+	w->dirty = FALSE;
+	winsettag(w);
+	textscrdraw(&w->body);
+	textsetselect(&w->tag, w->tag.file->nc, w->tag.file->nc);
+	xfidlog(w, "new");
+}
+
+char *oknotes[] ={
+	"delete",
+	"hangup",
+	"kill",
+	"exit",
+	nil
+};
+
+int	dumping;
+
+int
+shutdown(void*, char *msg)
+{
+	int i;
+
+	killprocs();
+	if(!dumping && strcmp(msg, "kill")!=0 && strcmp(msg, "exit")!=0 && getpid()==mainpid){
+		dumping = TRUE;
+		rowdump(&row, nil);
+	}
+	for(i=0; oknotes[i]; i++)
+		if(strncmp(oknotes[i], msg, strlen(oknotes[i])) == 0)
+			threadexitsall(msg);
+	print("acme: %s\n", msg);
+	abort();
+	return 0;
+}
+
+void
+killprocs(void)
+{
+	Command *c;
+
+	fsysclose();
+//	if(display)
+//		flushimage(display, 1);
+
+	for(c=command; c; c=c->next)
+		postnote(PNGROUP, c->pid, "hangup");
+	remove(acmeerrorfile);
+}
+
+static int errorfd;
+
+void
+acmeerrorproc(void *)
+{
+	char *buf, *s;
+	int n;
+
+	threadsetname("acmeerrorproc");
+	buf = emalloc(8192+1);
+	while((n=read(errorfd, buf, 8192)) >= 0){
+		buf[n] = '\0';
+		s = estrdup(buf);
+		sendp(cerr, s);
+	}
+	free(buf);
+}
+
+void
+acmeerrorinit(void)
+{
+	int fd, pfd[2];
+	char buf[64];
+
+	if(pipe(pfd) < 0)
+		error("can't create pipe");
+	sprint(acmeerrorfile, "/srv/acme.%s.%d", user, mainpid);
+	fd = create(acmeerrorfile, OWRITE, 0666);
+	if(fd < 0){
+		remove(acmeerrorfile);
+  		fd = create(acmeerrorfile, OWRITE, 0666);
+		if(fd < 0)
+			error("can't create acmeerror file");
+	}
+	sprint(buf, "%d", pfd[0]);
+	write(fd, buf, strlen(buf));
+	close(fd);
+	/* reopen pfd[1] close on exec */
+	sprint(buf, "/fd/%d", pfd[1]);
+	errorfd = open(buf, OREAD|OCEXEC);
+	if(errorfd < 0)
+		error("can't re-open acmeerror file");
+	close(pfd[1]);
+	close(pfd[0]);
+	proccreate(acmeerrorproc, nil, STACK);
+}
+
+void
+plumbproc(void *)
+{
+	Plumbmsg *m;
+
+	threadsetname("plumbproc");
+	for(;;){
+		m = plumbrecv(plumbeditfd);
+		if(m == nil)
+			threadexits(nil);
+		sendp(cplumb, m);
+	}
+}
+
+void
+keyboardthread(void *)
+{
+	Rune r;
+	Timer *timer;
+	Text *t;
+	enum { KTimer, KKey, NKALT };
+	static Alt alts[NKALT+1];
+
+	alts[KTimer].c = nil;
+	alts[KTimer].v = nil;
+	alts[KTimer].op = CHANNOP;
+	alts[KKey].c = keyboardctl->c;
+	alts[KKey].v = &r;
+	alts[KKey].op = CHANRCV;
+	alts[NKALT].op = CHANEND;
+
+	timer = nil;
+	typetext = nil;
+	threadsetname("keyboardthread");
+	for(;;){
+		switch(alt(alts)){
+		case KTimer:
+			timerstop(timer);
+			t = typetext;
+			if(t!=nil && t->what==Tag){
+				winlock(t->w, 'K');
+				wincommit(t->w, t);
+				winunlock(t->w);
+				flushimage(display, 1);
+			}
+			alts[KTimer].c = nil;
+			alts[KTimer].op = CHANNOP;
+			break;
+		case KKey:
+		casekeyboard:
+			typetext = rowtype(&row, r, mouse->xy);
+			t = typetext;
+			if(t!=nil && t->col!=nil && !(r==Kdown || r==Kleft || r==Kright))	/* scrolling doesn't change activecol */
+				activecol = t->col;
+			if(t!=nil && t->w!=nil)
+				t->w->body.file->curtext = &t->w->body;
+			if(timer != nil)
+				timercancel(timer);
+			if(t!=nil && t->what==Tag) {
+				timer = timerstart(500);
+				alts[KTimer].c = timer->c;
+				alts[KTimer].op = CHANRCV;
+			}else{
+				timer = nil;
+				alts[KTimer].c = nil;
+				alts[KTimer].op = CHANNOP;
+			}
+			if(nbrecv(keyboardctl->c, &r) > 0)
+				goto casekeyboard;
+			flushimage(display, 1);
+			break;
+		}
+	}
+}
+
+void
+mousethread(void *)
+{
+	Text *t, *argt;
+	int but;
+	uint q0, q1;
+	Window *w;
+	Plumbmsg *pm;
+	Mouse m;
+	char *act;
+	enum { MResize, MMouse, MPlumb, MWarnings, NMALT };
+	static Alt alts[NMALT+1];
+
+	threadsetname("mousethread");
+	alts[MResize].c = mousectl->resizec;
+	alts[MResize].v = nil;
+	alts[MResize].op = CHANRCV;
+	alts[MMouse].c = mousectl->c;
+	alts[MMouse].v = &mousectl->Mouse;
+	alts[MMouse].op = CHANRCV;
+	alts[MPlumb].c = cplumb;
+	alts[MPlumb].v = &pm;
+	alts[MPlumb].op = CHANRCV;
+	alts[MWarnings].c = cwarn;
+	alts[MWarnings].v = nil;
+	alts[MWarnings].op = CHANRCV;
+	if(cplumb == nil)
+		alts[MPlumb].op = CHANNOP;
+	alts[NMALT].op = CHANEND;
+	
+	for(;;){
+		qlock(&row);
+		flushwarnings();
+		qunlock(&row);
+		flushimage(display, 1);
+		switch(alt(alts)){
+		case MResize:
+			if(getwindow(display, Refnone) < 0)
+				error("attach to window");
+			scrlresize();
+			rowresize(&row, screen->clipr);
+			break;
+		case MPlumb:
+			if(strcmp(pm->type, "text") == 0){
+				act = plumblookup(pm->attr, "action");
+				if(act==nil || strcmp(act, "showfile")==0)
+					plumblook(pm);
+				else if(strcmp(act, "showdata")==0)
+					plumbshow(pm);
+			}
+			plumbfree(pm);
+			break;
+		case MWarnings:
+			break;
+		case MMouse:
+			/*
+			 * Make a copy so decisions are consistent; mousectl changes
+			 * underfoot.  Can't just receive into m because this introduces
+			 * another race; see /sys/src/libdraw/mouse.c.
+			 */
+			m = mousectl->Mouse;
+			qlock(&row);
+			t = rowwhich(&row, m.xy);
+
+			if((t!=mousetext && t!=nil && t->w!=nil) &&
+				(mousetext==nil || mousetext->w==nil || t->w->id!=mousetext->w->id)) {
+				xfidlog(t->w, "focus");
+			}
+
+			if(t!=mousetext && mousetext!=nil && mousetext->w!=nil){
+				winlock(mousetext->w, 'M');
+				mousetext->eq0 = ~0;
+				wincommit(mousetext->w, mousetext);
+				winunlock(mousetext->w);
+			}
+			mousetext = t;
+			if(t == nil)
+				goto Continue;
+			w = t->w;
+			if(t==nil || m.buttons==0)
+				goto Continue;
+			but = 0;
+			if(m.buttons == 1)
+				but = 1;
+			else if(m.buttons == 2)
+				but = 2;
+			else if(m.buttons == 4)
+				but = 3;
+			barttext = t;
+			if(t->what==Body && ptinrect(m.xy, t->scrollr)){
+				if(but){
+					winlock(w, 'M');
+					t->eq0 = ~0;
+					textscroll(t, but);
+					winunlock(w);
+				}
+				goto Continue;
+			}
+			/* scroll buttons, wheels, etc. */
+			if(t->what==Body && w != nil && (m.buttons & (8|16))){
+				if(m.buttons & 8)
+					but = Kscrolloneup;
+				else
+					but = Kscrollonedown;
+				winlock(w, 'M');
+				t->eq0 = ~0;
+				texttype(t, but);
+				winunlock(w);
+				goto Continue;
+			}
+			if(ptinrect(m.xy, t->scrollr)){
+				if(but){
+					if(t->what == Columntag)
+						rowdragcol(&row, t->col, but);
+					else if(t->what == Tag){
+						coldragwin(t->col, t->w, but);
+						if(t->w)
+							barttext = &t->w->body;
+					}
+					if(t->col)
+						activecol = t->col;
+				}
+				goto Continue;
+			}
+			if(m.buttons){
+				if(w)
+					winlock(w, 'M');
+				t->eq0 = ~0;
+				if(w)
+					wincommit(w, t);
+				else
+					textcommit(t, TRUE);
+				if(m.buttons & 1){
+					textselect(t);
+					if(w)
+						winsettag(w);
+					argtext = t;
+					seltext = t;
+					if(t->col)
+						activecol = t->col;	/* button 1 only */
+					if(t->w!=nil && t==&t->w->body)
+						activewin = t->w;
+				}else if(m.buttons & 2){
+					if(textselect2(t, &q0, &q1, &argt))
+						execute(t, q0, q1, FALSE, argt);
+				}else if(m.buttons & 4){
+					if(textselect3(t, &q0, &q1))
+						look3(t, q0, q1, FALSE);
+				}
+				if(w)
+					winunlock(w);
+				goto Continue;
+			}
+    Continue:
+			qunlock(&row);
+			break;
+		}
+	}
+}
+
+/*
+ * There is a race between process exiting and our finding out it was ever created.
+ * This structure keeps a list of processes that have exited we haven't heard of.
+ */
+typedef struct Pid Pid;
+struct Pid
+{
+	int	pid;
+	char	msg[ERRMAX];
+	Pid	*next;
+};
+
+void
+waitthread(void *)
+{
+	Waitmsg *w;
+	Command *c, *lc;
+	uint pid;
+	int found, ncmd;
+	Rune *cmd;
+	char *err;
+	Text *t;
+	Pid *pids, *p, *lastp;
+	enum { WErr, WKill, WWait, WCmd, NWALT };
+	Alt alts[NWALT+1];
+
+	threadsetname("waitthread");
+	pids = nil;
+	alts[WErr].c = cerr;
+	alts[WErr].v = &err;
+	alts[WErr].op = CHANRCV;
+	alts[WKill].c = ckill;
+	alts[WKill].v = &cmd;
+	alts[WKill].op = CHANRCV;
+	alts[WWait].c = cwait;
+	alts[WWait].v = &w;
+	alts[WWait].op = CHANRCV;
+	alts[WCmd].c = ccommand;
+	alts[WCmd].v = &c;
+	alts[WCmd].op = CHANRCV;
+	alts[NWALT].op = CHANEND;
+
+	command = nil;
+	for(;;){
+		switch(alt(alts)){
+		case WErr:
+			qlock(&row);
+			warning(nil, "%s", err);
+			free(err);
+			flushimage(display, 1);
+			qunlock(&row);
+			break;
+		case WKill:
+			found = FALSE;
+			ncmd = runestrlen(cmd);
+			for(c=command; c; c=c->next){
+				/* -1 for blank */
+				if(runeeq(c->name, c->nname-1, cmd, ncmd) == TRUE){
+					if(postnote(PNGROUP, c->pid, "kill") < 0)
+						warning(nil, "kill %S: %r\n", cmd);
+					found = TRUE;
+				}
+			}
+			if(!found)
+				warning(nil, "Kill: no process %S\n", cmd);
+			free(cmd);
+			break;
+		case WWait:
+			pid = w->pid;
+			lc = nil;
+			for(c=command; c; c=c->next){
+				if(c->pid == pid){
+					if(lc)
+						lc->next = c->next;
+					else
+						command = c->next;
+					break;
+				}
+				lc = c;
+			}
+			qlock(&row);
+			t = &row.tag;
+			textcommit(t, TRUE);
+			if(c == nil){
+				/* helper processes use this exit status */
+				if(strncmp(w->msg, "libthread", 9) != 0){
+					p = emalloc(sizeof(Pid));
+					p->pid = pid;
+					strncpy(p->msg, w->msg, sizeof(p->msg));
+					p->next = pids;
+					pids = p;
+				}
+			}else{
+				if(search(t, c->name, c->nname)){
+					textdelete(t, t->q0, t->q1, TRUE);
+					textsetselect(t, 0, 0);
+				}
+				if(w->msg[0])
+					warning(c->md, "%s\n", w->msg);
+				flushimage(display, 1);
+			}
+			qunlock(&row);
+			free(w);
+    Freecmd:
+			if(c){
+				if(c->iseditcmd)
+					sendul(cedit, 0);
+				free(c->text);
+				free(c->name);
+				fsysdelid(c->md);
+				free(c);
+			}
+			break;
+		case WCmd:
+			/* has this command already exited? */
+			lastp = nil;
+			for(p=pids; p!=nil; p=p->next){
+				if(p->pid == c->pid){
+					if(p->msg[0])
+						warning(c->md, "%s\n", p->msg);
+					if(lastp == nil)
+						pids = p->next;
+					else
+						lastp->next = p->next;
+					free(p);
+					goto Freecmd;
+				}
+				lastp = p;
+			}
+			c->next = command;
+			command = c;
+			qlock(&row);
+			t = &row.tag;
+			textcommit(t, TRUE);
+			textinsert(t, 0, c->name, c->nname, TRUE);
+			textsetselect(t, 0, 0);
+			flushimage(display, 1);
+			qunlock(&row);
+			break;
+		}
+	}
+}
+
+void
+xfidallocthread(void*)
+{
+	Xfid *xfree, *x;
+	enum { Alloc, Free, N };
+	static Alt alts[N+1];
+
+	threadsetname("xfidallocthread");
+	alts[Alloc].c = cxfidalloc;
+	alts[Alloc].v = nil;
+	alts[Alloc].op = CHANRCV;
+	alts[Free].c = cxfidfree;
+	alts[Free].v = &x;
+	alts[Free].op = CHANRCV;
+	alts[N].op = CHANEND;
+
+	xfree = nil;
+	for(;;){
+		switch(alt(alts)){
+		case Alloc:
+			x = xfree;
+			if(x)
+				xfree = x->next;
+			else{
+				x = emalloc(sizeof(Xfid));
+				x->c = chancreate(sizeof(void(*)(Xfid*)), 0);
+				x->arg = x;
+				threadcreate(xfidctl, x->arg, STACK);
+			}
+			sendp(cxfidalloc, x);
+			break;
+		case Free:
+			x->next = xfree;
+			xfree = x;
+			break;
+		}
+	}
+}
+
+/* this thread, in the main proc, allows fsysproc to get a window made without doing graphics */
+void
+newwindowthread(void*)
+{
+	Window *w;
+
+	threadsetname("newwindowthread");
+
+	for(;;){
+		/* only fsysproc is talking to us, so synchronization is trivial */
+		recvp(cnewwindow);
+		w = makenewwindow(nil);
+		winsettag(w);
+		xfidlog(w, "new");
+		sendp(cnewwindow, w);
+	}
+}
+
+Reffont*
+rfget(int fix, int save, int setfont, char *name)
+{
+	Reffont *r;
+	Font *f;
+	int i;
+
+	r = nil;
+	if(name == nil){
+		name = fontnames[fix];
+		r = reffonts[fix];
+	}
+	if(r == nil){
+		for(i=0; i<nfontcache; i++)
+			if(strcmp(name, fontcache[i]->f->name) == 0){
+				r = fontcache[i];
+				goto Found;
+			}
+		f = openfont(display, name);
+		if(f == nil){
+			warning(nil, "can't open font file %s: %r\n", name);
+			return nil;
+		}
+		r = emalloc(sizeof(Reffont));
+		r->f = f;
+		fontcache = erealloc(fontcache, (nfontcache+1)*sizeof(Reffont*));
+		fontcache[nfontcache++] = r;
+	}
+    Found:
+	if(save){
+		incref(r);
+		if(reffonts[fix])
+			rfclose(reffonts[fix]);
+		reffonts[fix] = r;
+		if(name != fontnames[fix]){
+			free(fontnames[fix]);
+			fontnames[fix] = estrdup(name);
+		}
+	}
+	if(setfont){
+		reffont.f = r->f;
+		incref(r);
+		rfclose(reffonts[0]);
+		font = r->f;
+		reffonts[0] = r;
+		incref(r);
+		iconinit();
+	}
+	incref(r);
+	return r;
+}
+
+void
+rfclose(Reffont *r)
+{
+	int i;
+
+	if(decref(r) == 0){
+		for(i=0; i<nfontcache; i++)
+			if(r == fontcache[i])
+				break;
+		if(i >= nfontcache)
+			warning(nil, "internal error: can't find font in cache\n");
+		else{
+			nfontcache--;
+			memmove(fontcache+i, fontcache+i+1, (nfontcache-i)*sizeof(Reffont*));
+		}
+		freefont(r->f);
+		free(r);
+	}
+}
+
+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}
+};
+
+static char *
+readall(int f, int *osz)
+{
+	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){
+		free(s);
+		return nil;
+	}
+	s[sz] = 0;
+	*osz = sz;
+
+	return s;
+}
+
+void
+iconinit(void)
+{
+	Rectangle r;
+	Image *tmp;
+
+	/* jgs3 - Apply the themes */
+	int f, sz;
+	char *s;
+	if((f = open("/dev/theme", OREAD|OCEXEC)) >= 0){
+		if((s = readall(f, &sz)) != nil)
+			themeload(s, sz);
+		free(s);
+		close(f);
+
+		/* Menu */
+		tagcols[BACK] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, theme[Colmenuback].rgb<<8|0xff);
+		tagcols[HIGH] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, theme[Colmenuhigh].rgb<<8|0xff);
+		tagcols[BORD] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, theme[Colmenubord].rgb<<8|0xff);
+		tagcols[TEXT] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, theme[Colmenutext].rgb<<8|0xff);
+		tagcols[HTEXT] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, theme[Colmenuhtext].rgb<<8|0xff);
+
+		/* Body */
+		textcols[BACK] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, theme[Colback].rgb<<8|0xff);
+		textcols[HIGH] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, theme[Colhigh].rgb<<8|0xff);
+		textcols[BORD] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, theme[Colbord].rgb<<8|0xff);
+		textcols[TEXT] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, theme[Coltext].rgb<<8|0xff);
+		textcols[HTEXT] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, theme[Colhtext].rgb<<8|0xff);
+
+	} else {
+		/* Blue */
+		tagcols[BACK] = allocimagemix(display, DPalebluegreen, DWhite);
+		tagcols[HIGH] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, DPalegreygreen);
+		tagcols[BORD] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, DPurpleblue);
+		tagcols[TEXT] = display->black;
+		tagcols[HTEXT] = display->black;
+
+		/* Yellow */
+		textcols[BACK] = allocimagemix(display, DPaleyellow, DWhite);
+		textcols[HIGH] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, DDarkyellow);
+		textcols[BORD] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, DYellowgreen);
+		textcols[TEXT] = display->black;
+		textcols[HTEXT] = display->black;
+	}
+
+	if(button){
+		freeimage(button);
+		freeimage(modbutton);
+		freeimage(colbutton);
+	}
+
+	r = Rect(0, 0, Scrollwid+2, font->height+1);
+	button = allocimage(display, r, screen->chan, 0, DNofill);
+	draw(button, r, tagcols[BACK], nil, r.min);
+	r.max.x -= 2;
+	border(button, r, 2, tagcols[BORD], ZP);
+
+	r = button->r;
+	modbutton = allocimage(display, r, screen->chan, 0, DNofill);
+	draw(modbutton, r, tagcols[BACK], nil, r.min);
+	r.max.x -= 2;
+	border(modbutton, r, 2, tagcols[BORD], ZP);
+	r = insetrect(r, 2);
+	tmp = allocimage(display, Rect(0,0,1,1), screen->chan, 1, DMedblue);
+	draw(modbutton, r, tmp, nil, ZP);
+	freeimage(tmp);
+
+	r = button->r;
+	colbutton = allocimage(display, r, screen->chan, 0, DPurpleblue);
+
+	but2col = allocimage(display, r, screen->chan, 1, 0xAA0000FF);
+	but3col = allocimage(display, r, screen->chan, 1, 0x006600FF);
+}
+
+/*
+ * /dev/snarf updates when the file is closed, so we must open our own
+ * fd here rather than use snarffd
+ */
+
+/* rio truncates larges snarf buffers, so this avoids using the
+ * service if the string is huge */
+
+#define MAXSNARF 100*1024
+
+void
+putsnarf(void)
+{
+	int fd, i, n;
+
+	if(snarffd<0 || snarfbuf.nc==0)
+		return;
+	if(snarfbuf.nc > MAXSNARF)
+		return;
+	fd = open("/dev/snarf", OWRITE);
+	if(fd < 0)
+		return;
+	for(i=0; i<snarfbuf.nc; i+=n){
+		n = snarfbuf.nc-i;
+		if(n >= NSnarf)
+			n = NSnarf;
+		bufread(&snarfbuf, i, snarfrune, n);
+		if(fprint(fd, "%.*S", n, snarfrune) < 0)
+			break;
+	}
+	close(fd);
+}
+
+void
+getsnarf()
+{
+	int nulls;
+
+	if(snarfbuf.nc > MAXSNARF)
+		return;
+	if(snarffd < 0)
+		return;
+	seek(snarffd, 0, 0);
+	bufreset(&snarfbuf);
+	bufload(&snarfbuf, 0, snarffd, &nulls);
+}
+
+/* jgs1 - functions to load colors on startup */
+void
+themeload(char *s, int n)
+{
+	int i;
+	char *t, *a[2], *e;
+	Image *newc;
+	u32int rgb;
+
+	if((t = malloc(n+1)) == nil)
+		return;
+	memmove(t, s, n);
+	t[n] = 0;
+
+	for(s = t; s != nil && *s; s = e){
+		if((e = strchr(s, '\n')) != nil)
+			*e++ = 0;
+		if(tokenize(s, a, 2) == 2){
+			for(i = 0; i < nelem(theme); i++) {
+				if(strcmp(theme[i].id, a[0]) == 0) {
+					rgb = strtoul(a[1], nil, 16);
+					if((newc = allocimage(display, Rect(0, 0, 1, 1), RGB24, 1, rgb<<8 | 0xff)) != nil) {
+						theme[i].rgb = rgb;
+					}
+					if(new != nil){
+						freeimage(col[i]);
+						col[i] = newc;
+					}
+					break;
+				}
+			}
+		}
+	}
+	free(t);
+}
+
+char *
+themestring(int *n)
+{
+	char *s, *t, *e;
+	int i;
+
+	if((t = malloc(512)) != nil){
+		s = t;
+		e = s+512;
+		for(i = 0; i < nelem(theme); i++)
+			s = seprint(s, e, "%s\t%06ux\n", theme[i].id, theme[i].rgb);
+		*n = s - t;
+	}
+
+	return t;
+}
--- /dev/null
+++ b/addr.c
@@ -1,0 +1,291 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <cursor.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <frame.h>
+#include <fcall.h>
+#include <plumb.h>
+#include "dat.h"
+#include "fns.h"
+
+enum
+{
+	None = 0,
+	Fore = '+',
+	Back = '-',
+};
+
+enum
+{
+	Char,
+	Line,
+};
+
+int
+isaddrc(int r)
+{
+	if(r && utfrune("0123456789+-/$.#,;", r)!=nil)
+		return TRUE;
+	return FALSE;
+}
+
+/*
+ * quite hard: could be almost anything but white space, but we are a little conservative,
+ * aiming for regular expressions of alphanumerics and no white space
+ */
+int
+isregexc(int r)
+{
+	if(r == 0)
+		return FALSE;
+	if(isalnum(r))
+		return TRUE;
+	if(utfrune("^+-.*?#,;[]()$", r)!=nil)
+		return TRUE;
+	return FALSE;
+}
+
+// nlcounttopos starts at q0 and advances nl lines,
+// being careful not to walk past the end of the text,
+// and then nr chars, being careful not to walk past
+// the end of the current line.
+// It returns the final position.
+long
+nlcounttopos(Text *t, long q0, long nl, long nr)
+{
+	while(nl > 0 && q0 < t->file->nc) {
+		if(textreadc(t, q0++) == '\n')
+			nl--;
+	}
+	if(nl > 0)
+		return q0;
+	while(nr > 0 && q0 < t->file->nc && textreadc(t, q0) != '\n') {
+		q0++;
+		nr--;
+	}
+	return q0;
+}
+
+Range
+number(Mntdir *md, Text *t, Range r, int line, int dir, int size, int *evalp)
+{
+	uint q0, q1;
+
+	if(size == Char){
+		if(dir == Fore)
+			line = r.q1+line;
+		else if(dir == Back){
+			if(r.q0==0 && line>0)
+				r.q0 = t->file->nc;
+			line = r.q0 - line;
+		}
+		if(line<0 || line>t->file->nc)
+			goto Rescue;
+		*evalp = TRUE;
+		return (Range){line, line};
+	}
+	q0 = r.q0;
+	q1 = r.q1;
+	switch(dir){
+	case None:
+		q0 = 0;
+		q1 = 0;
+	Forward:
+		while(line>0 && q1<t->file->nc)
+			if(textreadc(t, q1++) == '\n' || q1==t->file->nc)
+				if(--line > 0)
+					q0 = q1;
+		if(line > 0)
+			goto Rescue;
+		break;
+	case Fore:
+		if(q1 > 0)
+			while(q1<t->file->nc && textreadc(t, q1-1) != '\n')
+				q1++;
+		q0 = q1;
+		goto Forward;
+	case Back:
+		if(q0 < t->file->nc)
+			while(q0>0 && textreadc(t, q0-1)!='\n')
+				q0--;
+		q1 = q0;
+		while(line>0 && q0>0){
+			if(textreadc(t, q0-1) == '\n'){
+				if(--line >= 0)
+					q1 = q0;
+			}
+			--q0;
+		}
+		/* :1-1 is :0 = #0, but :1-2 is an error */
+		if(line > 1)
+			goto Rescue;
+		while(q0>0 && textreadc(t, q0-1)!='\n')
+			--q0;
+	}
+	*evalp = TRUE;
+	return (Range){q0, q1};
+
+    Rescue:
+	if(md != nil)
+		warning(nil, "address out of range\n");
+	*evalp = FALSE;
+	return r;
+}
+
+
+Range
+regexp(Mntdir *md, Text *t, Range lim, Range r, Rune *pat, int dir, int *foundp)
+{
+	int found;
+	Rangeset sel;
+	int q;
+
+	if(pat[0] == '\0' && rxnull()){
+		warning(md, "no previous regular expression\n");
+		*foundp = FALSE;
+		return r;
+	}
+	if(pat[0] && rxcompile(pat) == FALSE){
+		*foundp = FALSE;
+		return r;
+	}
+	if(dir == Back)
+		found = rxbexecute(t, r.q0, &sel);
+	else{
+		if(lim.q0 < 0)
+			q = Infinity;
+		else
+			q = lim.q1;
+		found = rxexecute(t, nil, r.q1, q, &sel);
+	}
+	if(!found && md==nil)
+		warning(nil, "no match for regexp\n");
+	*foundp = found;
+	return sel.r[0];
+}
+
+Range
+address(Mntdir *md, Text *t, Range lim, Range ar, void *a, uint q0, uint q1, int (*getc)(void*, uint),  int *evalp, uint *qp)
+{
+	int dir, size, npat;
+	int prevc, c, nc, n;
+	uint q;
+	Rune *pat;
+	Range r, nr;
+
+	r = ar;
+	q = q0;
+	dir = None;
+	size = Line;
+	c = 0;
+	while(q < q1){
+		prevc = c;
+		c = (*getc)(a, q++);
+		switch(c){
+		default:
+			*qp = q-1;
+			return r;
+		case ';':
+			ar = r;
+			/* fall through */
+		case ',':
+			if(prevc == 0)	/* lhs defaults to 0 */
+				r.q0 = 0;
+			if(q>=q1 && t!=nil && t->file!=nil)	/* rhs defaults to $ */
+				r.q1 = t->file->nc;
+			else{
+				nr = address(md, t, lim, ar, a, q, q1, getc, evalp, &q);
+				r.q1 = nr.q1;
+			}
+			*qp = q;
+			return r;
+		case '+':
+		case '-':
+			if(*evalp && (prevc=='+' || prevc=='-'))
+				if((nc=(*getc)(a, q))!='#' && nc!='/' && nc!='?')
+					r = number(md, t, r, 1, prevc, Line, evalp);	/* do previous one */
+			dir = c;
+			break;
+		case '.':
+		case '$':
+			if(q != q0+1){
+				*qp = q-1;
+				return r;
+			}
+			if(*evalp)
+				if(c == '.')
+					r = ar;
+				else
+					r = (Range){t->file->nc, t->file->nc};
+			if(q < q1)
+				dir = Fore;
+			else
+				dir = None;
+			break;
+		case '#':
+			if(q==q1 || (c=(*getc)(a, q++))<'0' || '9'<c){
+				*qp = q-1;
+				return r;
+			}
+			size = Char;
+			/* fall through */
+		case '0': case '1': case '2': case '3': case '4':
+		case '5': case '6': case '7': case '8': case '9':
+			n = c -'0';
+			while(q<q1){
+				nc = (*getc)(a, q++);
+				if(nc<'0' || '9'<nc){
+					q--;
+					break;
+				}
+				n = n*10+(nc-'0');
+			}
+			if(*evalp)
+				r = number(md, t, r, n, dir, size, evalp);
+			dir = None;
+			size = Line;
+			break;
+		case '?':
+			dir = Back;
+			/* fall through */
+		case '/':
+			npat = 0;
+			pat = nil;
+			while(q<q1){
+				c = (*getc)(a, q++);
+				switch(c){
+				case '\n':
+					--q;
+					goto out;
+				case '\\':
+					pat = runerealloc(pat, npat+1);
+					pat[npat++] = c;
+					if(q == q1)
+						goto out;
+					c = (*getc)(a, q++);
+					break;
+				case '/':
+					goto out;
+				}
+				pat = runerealloc(pat, npat+1);
+				pat[npat++] = c;
+			}
+		    out:
+			pat = runerealloc(pat, npat+1);
+			pat[npat] = 0;
+			if(*evalp)
+				r = regexp(md, t, lim, r, pat, dir, evalp);
+			free(pat);
+			dir = None;
+			size = Line;
+			break;
+		}
+	}
+	if(*evalp && dir != None)
+		r = number(md, t, r, 1, dir, Line, evalp);	/* do previous one */
+	*qp = q;
+	return r;
+}
--- /dev/null
+++ b/buff.c
@@ -1,0 +1,322 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <cursor.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <frame.h>
+#include <fcall.h>
+#include <plumb.h>
+#include "dat.h"
+#include "fns.h"
+
+enum
+{
+	Slop = 100,	/* room to grow with reallocation */
+};
+
+static
+void
+sizecache(Buffer *b, uint n)
+{
+	if(n <= b->cmax)
+		return;
+	b->cmax = n+Slop;
+	b->c = runerealloc(b->c, b->cmax);
+}
+
+static
+void
+addblock(Buffer *b, uint i, uint n)
+{
+	if(i > b->nbl)
+		error("internal error: addblock");
+
+	b->bl = realloc(b->bl, (b->nbl+1)*sizeof b->bl[0]);
+	if(i < b->nbl)
+		memmove(b->bl+i+1, b->bl+i, (b->nbl-i)*sizeof(Block*));
+	b->bl[i] = disknewblock(disk, n);
+	b->nbl++;
+}
+
+static
+void
+delblock(Buffer *b, uint i)
+{
+	if(i >= b->nbl)
+		error("internal error: delblock");
+
+	diskrelease(disk, b->bl[i]);
+	b->nbl--;
+	if(i < b->nbl)
+		memmove(b->bl+i, b->bl+i+1, (b->nbl-i)*sizeof(Block*));
+	b->bl = realloc(b->bl, b->nbl*sizeof b->bl[0]);
+}
+
+/*
+ * Move cache so b->cq <= q0 < b->cq+b->cnc.
+ * If at very end, q0 will fall on end of cache block.
+ */
+
+static
+void
+flush(Buffer *b)
+{
+	if(b->cdirty || b->cnc==0){
+		if(b->cnc == 0)
+			delblock(b, b->cbi);
+		else
+			diskwrite(disk, &b->bl[b->cbi], b->c, b->cnc);
+		b->cdirty = FALSE;
+	}
+}
+
+static
+void
+setcache(Buffer *b, uint q0)
+{
+	Block **blp, *bl;
+	uint i, q;
+
+	if(q0 > b->nc)
+		error("internal error: setcache");
+	/*
+	 * flush and reload if q0 is not in cache.
+	 */
+	if(b->nc == 0 || (b->cq<=q0 && q0<b->cq+b->cnc))
+		return;
+	/*
+	 * if q0 is at end of file and end of cache, continue to grow this block
+	 */
+	if(q0==b->nc && q0==b->cq+b->cnc && b->cnc<Maxblock)
+		return;
+	flush(b);
+	/* find block */
+	if(q0 < b->cq){
+		q = 0;
+		i = 0;
+	}else{
+		q = b->cq;
+		i = b->cbi;
+	}
+	blp = &b->bl[i];
+	while(q+(*blp)->n <= q0 && q+(*blp)->n < b->nc){
+		q += (*blp)->n;
+		i++;
+		blp++;
+		if(i >= b->nbl)
+			error("block not found");
+	}
+	bl = *blp;
+	/* remember position */
+	b->cbi = i;
+	b->cq = q;
+	sizecache(b, bl->n);
+	b->cnc = bl->n;
+	/*read block*/
+	diskread(disk, bl, b->c, b->cnc);
+}
+
+void
+bufinsert(Buffer *b, uint q0, Rune *s, uint n)
+{
+	uint i, m, t, off;
+
+	if(q0 > b->nc)
+		error("internal error: bufinsert");
+
+	while(n > 0){
+		setcache(b, q0);
+		off = q0-b->cq;
+		if(b->cnc+n <= Maxblock){
+			/* Everything fits in one block. */
+			t = b->cnc+n;
+			m = n;
+			if(b->bl == nil){	/* allocate */
+				if(b->cnc != 0)
+					error("internal error: bufinsert1 cnc!=0");
+				addblock(b, 0, t);
+				b->cbi = 0;
+			}
+			sizecache(b, t);
+			runemove(b->c+off+m, b->c+off, b->cnc-off);
+			runemove(b->c+off, s, m);
+			b->cnc = t;
+			goto Tail;
+		}
+		/*
+		 * We must make a new block.  If q0 is at
+		 * the very beginning or end of this block,
+		 * just make a new block and fill it.
+		 */
+		if(q0==b->cq || q0==b->cq+b->cnc){
+			if(b->cdirty)
+				flush(b);
+			m = min(n, Maxblock);
+			if(b->bl == nil){	/* allocate */
+				if(b->cnc != 0)
+					error("internal error: bufinsert2 cnc!=0");
+				i = 0;
+			}else{
+				i = b->cbi;
+				if(q0 > b->cq)
+					i++;
+			}
+			addblock(b, i, m);
+			sizecache(b, m);
+			runemove(b->c, s, m);
+			b->cq = q0;
+			b->cbi = i;
+			b->cnc = m;
+			goto Tail;
+		}
+		/*
+		 * Split the block; cut off the right side and
+		 * let go of it.
+		 */
+		m = b->cnc-off;
+		if(m > 0){
+			i = b->cbi+1;
+			addblock(b, i, m);
+			diskwrite(disk, &b->bl[i], b->c+off, m);
+			b->cnc -= m;
+		}
+		/*
+		 * Now at end of block.  Take as much input
+		 * as possible and tack it on end of block.
+		 */
+		m = min(n, Maxblock-b->cnc);
+		sizecache(b, b->cnc+m);
+		runemove(b->c+b->cnc, s, m);
+		b->cnc += m;
+  Tail:
+		b->nc += m;
+		q0 += m;
+		s += m;
+		n -= m;
+		b->cdirty = TRUE;
+	}
+}
+
+void
+bufdelete(Buffer *b, uint q0, uint q1)
+{
+	uint m, n, off;
+
+	if(!(q0<=q1 && q0<=b->nc && q1<=b->nc))
+		error("internal error: bufdelete");
+	while(q1 > q0){
+		setcache(b, q0);
+		off = q0-b->cq;
+		if(q1 > b->cq+b->cnc)
+			n = b->cnc - off;
+		else
+			n = q1-q0;
+		m = b->cnc - (off+n);
+		if(m > 0)
+			runemove(b->c+off, b->c+off+n, m);
+		b->cnc -= n;
+		b->cdirty = TRUE;
+		q1 -= n;
+		b->nc -= n;
+	}
+}
+
+static int
+bufloader(void *v, uint q0, Rune *r, int nr)
+{
+	bufinsert(v, q0, r, nr);
+	return nr;
+}
+
+uint
+loadfile(int fd, uint q0, int *nulls, int(*f)(void*, uint, Rune*, int), void *arg)
+{
+	char *p;
+	Rune *r;
+	int l, m, n, nb, nr;
+	uint q1;
+
+	p = emalloc((Maxblock+UTFmax+1)*sizeof p[0]);
+	r = runemalloc(Maxblock);
+	m = 0;
+	n = 1;
+	q1 = q0;
+	/*
+	 * At top of loop, may have m bytes left over from
+	 * last pass, possibly representing a partial rune.
+	 */
+	while(n > 0){
+		n = read(fd, p+m, Maxblock);
+		if(n < 0){
+			warning(nil, "read error in Buffer.load");
+			break;
+		}
+		m += n;
+		p[m] = 0;
+		l = m;
+		if(n > 0)
+			l -= UTFmax;
+		cvttorunes(p, l, r, &nb, &nr, nulls);
+		memmove(p, p+nb, m-nb);
+		m -= nb;
+		q1 += (*f)(arg, q1, r, nr);
+	}
+	free(p);
+	free(r);
+	return q1-q0;
+}
+
+uint
+bufload(Buffer *b, uint q0, int fd, int *nulls)
+{
+	if(q0 > b->nc)
+		error("internal error: bufload");
+	return loadfile(fd, q0, nulls, bufloader, b);
+}
+
+void
+bufread(Buffer *b, uint q0, Rune *s, uint n)
+{
+	uint m;
+
+	if(!(q0<=b->nc && q0+n<=b->nc))
+		error("bufread: internal error");
+
+	while(n > 0){
+		setcache(b, q0);
+		m = min(n, b->cnc-(q0-b->cq));
+		runemove(s, b->c+(q0-b->cq), m);
+		q0 += m;
+		s += m;
+		n -= m;
+	}
+}
+
+void
+bufreset(Buffer *b)
+{
+	int i;
+
+	b->nc = 0;
+	b->cnc = 0;
+	b->cq = 0;
+	b->cdirty = 0;
+	b->cbi = 0;
+	/* delete backwards to avoid n² behavior */
+	for(i=b->nbl-1; --i>=0; )
+		delblock(b, i);
+}
+
+void
+bufclose(Buffer *b)
+{
+	bufreset(b);
+	free(b->c);
+	b->c = nil;
+	b->cnc = 0;
+	free(b->bl);
+	b->bl = nil;
+	b->nbl = 0;
+}
--- /dev/null
+++ b/cols.c
@@ -1,0 +1,561 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <cursor.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <frame.h>
+#include <fcall.h>
+#include <plumb.h>
+#include "dat.h"
+#include "fns.h"
+
+void
+colinit(Column *c, Rectangle r)
+{
+	Rectangle r1;
+	Text *t;
+
+	draw(screen, r, display->white, nil, ZP);
+	c->r = r;
+	c->w = nil;
+	c->nw = 0;
+	t = &c->tag;
+	t->w = nil;
+	t->col = c;
+	r1 = r;
+	r1.max.y = r1.min.y + font->height;
+	textinit(t, fileaddtext(nil, t), r1, &reffont, tagcols);
+	t->what = Columntag;
+	r1.min.y = r1.max.y;
+	r1.max.y += Border;
+	draw(screen, r1, display->black, nil, ZP);
+	textinsert(t, 0, L"New Cut Paste Snarf Sort Zerox Delcol ", 38, TRUE);
+	textsetselect(t, t->file->nc, t->file->nc);
+	draw(screen, t->scrollr, colbutton, nil, colbutton->r.min);
+	c->safe = TRUE;
+}
+
+Window*
+coladd(Column *c, Window *w, Window *clone, int y)
+{
+	Rectangle r, r1;
+	Window *v;
+	int i, t;
+
+	v = nil;
+	r = c->r;
+	r.min.y = c->tag.r.max.y+Border;
+	if(y<r.min.y && c->nw>0){	/* steal half of last window by default */
+		v = c->w[c->nw-1];
+		y = v->body.r.min.y+Dy(v->body.r)/2;
+	}
+	/* look for window we'll land on */
+	for(i=0; i<c->nw; i++){
+		v = c->w[i];
+		if(y < v->r.max.y)
+			break;
+	}
+	if(c->nw > 0){
+		if(i < c->nw)
+			i++;	/* new window will go after v */
+		/*
+		 * if v's too small, grow it first.
+		 */
+		if(!c->safe || v->body.maxlines<=3){
+			colgrow(c, v, 1);
+			y = v->body.r.min.y+Dy(v->body.r)/2;
+		}
+		r = v->r;
+		if(i == c->nw)
+			t = c->r.max.y;
+		else
+			t = c->w[i]->r.min.y-Border;
+		r.max.y = t;
+		draw(screen, r, textcols[BACK], nil, ZP);
+		r1 = r;
+		y = min(y, t-(v->tag.font->height+v->body.font->height+Border+1));
+		r1.max.y = min(y, v->body.r.min.y+v->body.nlines*v->body.font->height);
+		r1.min.y = winresize(v, r1, FALSE);
+		r1.max.y = r1.min.y+Border;
+		draw(screen, r1, display->black, nil, ZP);
+		r.min.y = r1.max.y;
+	}
+	if(w == nil){
+		w = emalloc(sizeof(Window));
+		w->rdselfd = -1;
+		w->col = c;
+		draw(screen, r, textcols[BACK], nil, ZP);
+		wininit(w, clone, r);
+	}else{
+		w->col = c;
+		winresize(w, r, FALSE);
+	}
+	w->tag.col = c;
+	w->tag.row = c->row;
+	w->body.col = c;
+	w->body.row = c->row;
+	c->w = realloc(c->w, (c->nw+1)*sizeof(Window*));
+	memmove(c->w+i+1, c->w+i, (c->nw-i)*sizeof(Window*));
+	c->nw++;
+	c->w[i] = w;
+	savemouse(w);
+	/* near but not on the button */
+	moveto(mousectl, addpt(w->tag.scrollr.max, Pt(3, 3)));
+	barttext = &w->body;
+	c->safe = TRUE;
+	return w;
+}
+
+void
+colclose(Column *c, Window *w, int dofree)
+{
+	Rectangle r;
+	int i, didmouse, up;
+
+	/* w is locked */
+	if(!c->safe)
+		colgrow(c, w, 1);
+	for(i=0; i<c->nw; i++)
+		if(c->w[i] == w)
+			goto Found;
+	error("can't find window");
+  Found:
+	r = w->r;
+	w->tag.col = nil;
+	w->body.col = nil;
+	w->col = nil;
+	didmouse = restoremouse(w);
+	if(dofree){
+		windelete(w);
+		winclose(w);
+	}
+	c->nw--;
+	memmove(c->w+i, c->w+i+1, (c->nw-i)*sizeof(Window*));
+	c->w = realloc(c->w, c->nw*sizeof(Window*));
+	if(c->nw == 0){
+		draw(screen, r, display->white, nil, ZP);
+		return;
+	}
+	up = 0;
+	if(i == c->nw){		/* extend last window down */
+		w = c->w[i-1];
+		r.min.y = w->r.min.y;
+		r.max.y = c->r.max.y;
+	}else{			/* extend next window up */
+		up = 1;
+		w = c->w[i];
+		r.max.y = w->r.max.y;
+	}
+	draw(screen, r, textcols[BACK], nil, ZP);
+	if(c->safe) {
+		if(!didmouse && up)
+			w->showdel = TRUE;
+		winresize(w, r, FALSE);
+		if(!didmouse && up)
+			movetodel(w);
+	}
+}
+
+void
+colcloseall(Column *c)
+{
+	int i;
+	Window *w;
+
+	if(c == activecol)
+		activecol = nil;
+	textclose(&c->tag);
+	for(i=0; i<c->nw; i++){
+		w = c->w[i];
+		winclose(w);
+	}
+	c->nw = 0;
+	free(c->w);
+	free(c);
+	clearmouse();
+}
+
+void
+colmousebut(Column *c)
+{
+	moveto(mousectl, divpt(addpt(c->tag.scrollr.min, c->tag.scrollr.max), 2));
+}
+
+void
+colresize(Column *c, Rectangle r)
+{
+	int i, old, new;
+	Rectangle r1, r2;
+	Window *w;
+
+	clearmouse();
+	r1 = r;
+	r1.max.y = r1.min.y + c->tag.font->height;
+	textresize(&c->tag, r1);
+	draw(screen, c->tag.scrollr, colbutton, nil, colbutton->r.min);
+	r1.min.y = r1.max.y;
+	r1.max.y += Border;
+	draw(screen, r1, display->black, nil, ZP);
+	r1.max.y = r.max.y;
+	new = Dy(r) - c->nw*(Border + font->height);
+	old = Dy(c->r) - c->nw*(Border + font->height);
+	for(i=0; i<c->nw; i++){
+		w = c->w[i];
+		w->maxlines = 0;
+		if(i == c->nw-1)
+			r1.max.y = r.max.y;
+		else {
+			r1.max.y = r1.min.y;
+			if(new > 0 && old > 0 && Dy(w->r) > font->height)
+				r1.max.y += (Dy(w->r)-font->height)*new/old + Border + font->height;
+		}
+		r2 = r1;
+		r2.max.y = r2.min.y+Border;
+		draw(screen, r2, display->black, nil, ZP);
+		r1.min.y = r2.max.y;
+		r1.min.y = winresize(w, r1, FALSE);
+	}
+	c->r = r;
+}
+
+static
+int
+colcmp(void *a, void *b)
+{
+	Rune *r1, *r2;
+	int i, nr1, nr2;
+
+	r1 = (*(Window**)a)->body.file->name;
+	nr1 = (*(Window**)a)->body.file->nname;
+	r2 = (*(Window**)b)->body.file->name;
+	nr2 = (*(Window**)b)->body.file->nname;
+	for(i=0; i<nr1 && i<nr2; i++){
+		if(*r1 != *r2)
+			return *r1-*r2;
+		r1++;
+		r2++;
+	}
+	return nr1-nr2;
+}
+
+void
+colsort(Column *c)
+{
+	int i, y;
+	Rectangle r, r1, *rp;
+	Window **wp, *w;
+
+	if(c->nw == 0)
+		return;
+	clearmouse();
+	rp = emalloc(c->nw*sizeof(Rectangle));
+	wp = emalloc(c->nw*sizeof(Window*));
+	memmove(wp, c->w, c->nw*sizeof(Window*));
+	qsort(wp, c->nw, sizeof(Window*), colcmp);
+	for(i=0; i<c->nw; i++)
+		rp[i] = wp[i]->r;
+	r = c->r;
+	r.min.y = c->tag.r.max.y;
+	draw(screen, r, textcols[BACK], nil, ZP);
+	y = r.min.y;
+	for(i=0; i<c->nw; i++){
+		w = wp[i];
+		r.min.y = y;
+		if(i == c->nw-1)
+			r.max.y = c->r.max.y;
+		else
+			r.max.y = r.min.y+Dy(w->r)+Border;
+		r1 = r;
+		r1.max.y = r1.min.y+Border;
+		draw(screen, r1, display->black, nil, ZP);
+		r.min.y = r1.max.y;
+		y = winresize(w, r, FALSE);
+	}
+	free(rp);
+	free(c->w);
+	c->w = wp;
+}
+
+void
+colgrow(Column *c, Window *w, int but)
+{
+	Rectangle r, cr;
+	int i, j, k, l, y1, y2, *nl, *ny, tot, nnl, onl, dnl, h;
+	Window *v;
+
+	for(i=0; i<c->nw; i++)
+		if(c->w[i] == w)
+			goto Found;
+	error("can't find window");
+
+  Found:
+	cr = c->r;
+	if(but < 0){	/* make sure window fills its own space properly */
+		r = w->r;
+		if(i==c->nw-1 || c->safe==FALSE)
+			r.max.y = cr.max.y;
+		else
+			r.max.y = c->w[i+1]->r.min.y;
+		winresize(w, r, FALSE);
+		return;
+	}
+	cr.min.y = c->w[0]->r.min.y;
+	if(but == 3){	/* full size */
+		if(i != 0){
+			v = c->w[0];
+			c->w[0] = w;
+			c->w[i] = v;
+		}
+		draw(screen, cr, textcols[BACK], nil, ZP);
+		winresize(w, cr, FALSE);
+		for(i=1; i<c->nw; i++)
+			c->w[i]->body.maxlines = 0;
+		c->safe = FALSE;
+		return;
+	}
+	/* store old #lines for each window */
+	onl = w->body.maxlines;
+	nl = emalloc(c->nw * sizeof(int));
+	ny = emalloc(c->nw * sizeof(int));
+	tot = 0;
+	for(j=0; j<c->nw; j++){
+		l = c->w[j]->body.maxlines;
+		nl[j] = l;
+		tot += l;
+	}
+	/* approximate new #lines for this window */
+	if(but == 2){	/* as big as can be */
+		memset(nl, 0, c->nw * sizeof(int));
+		goto Pack;
+	}
+	nnl = min(onl + max(min(5, w->maxlines), onl/2), tot);
+	if(nnl < w->maxlines)
+		nnl = (w->maxlines+nnl)/2;
+	if(nnl == 0)
+		nnl = 2;
+	dnl = nnl - onl;
+	/* compute new #lines for each window */
+	for(k=1; k<c->nw; k++){
+		/* prune from later window */
+		j = i+k;
+		if(j<c->nw && nl[j]){
+			l = min(dnl, max(1, nl[j]/2));
+			nl[j] -= l;
+			nl[i] += l;
+			dnl -= l;
+		}
+		/* prune from earlier window */
+		j = i-k;
+		if(j>=0 && nl[j]){
+			l = min(dnl, max(1, nl[j]/2));
+			nl[j] -= l;
+			nl[i] += l;
+			dnl -= l;
+		}
+	}
+    Pack:
+	/* pack everyone above */
+	y1 = cr.min.y;
+	for(j=0; j<i; j++){
+		v = c->w[j];
+		r = v->r;
+		r.min.y = y1;
+		r.max.y = y1+Dy(v->tag.all);
+		if(nl[j])
+			r.max.y += 1 + nl[j]*v->body.font->height;
+		if(!c->safe || !eqrect(v->r, r)){
+			draw(screen, r, textcols[BACK], nil, ZP);
+			winresize(v, r, c->safe);
+		}
+		r.min.y = v->r.max.y;
+		r.max.y += Border;
+		draw(screen, r, display->black, nil, ZP);
+		y1 = r.max.y;
+	}
+	/* scan to see new size of everyone below */
+	y2 = c->r.max.y;
+	for(j=c->nw-1; j>i; j--){
+		v = c->w[j];
+		r = v->r;
+		r.min.y = y2-Dy(v->tag.all);
+		if(nl[j])
+			r.min.y -= 1 + nl[j]*v->body.font->height;
+		r.min.y -= Border;
+		ny[j] = r.min.y;
+		y2 = r.min.y;
+	}
+	/* compute new size of window */
+	r = w->r;
+	r.min.y = y1;
+	r.max.y = r.min.y+Dy(w->tag.all);
+	h = w->body.font->height;
+	if(y2-r.max.y >= 1+h+Border){
+		r.max.y += 1;
+		r.max.y += h*((y2-r.max.y)/h);
+	}
+	/* draw window */
+	if(!c->safe || !eqrect(w->r, r)){
+		draw(screen, r, textcols[BACK], nil, ZP);
+		winresize(w, r, c->safe);
+	}
+	if(i < c->nw-1){
+		r.min.y = r.max.y;
+		r.max.y += Border;
+		draw(screen, r, display->black, nil, ZP);
+		for(j=i+1; j<c->nw; j++)
+			ny[j] -= (y2-r.max.y);
+	}
+	/* pack everyone below */
+	y1 = r.max.y;
+	for(j=i+1; j<c->nw; j++){
+		v = c->w[j];
+		r = v->r;
+		r.min.y = y1;
+		r.max.y = y1+Dy(v->tag.all);
+		if(nl[j])
+			r.max.y += 1 + nl[j]*v->body.font->height;
+		if(!c->safe || !eqrect(v->r, r)){
+			draw(screen, r, textcols[BACK], nil, ZP);
+			winresize(v, r, c->safe);
+		}
+		if(j < c->nw-1){	/* no border on last window */
+			r.min.y = v->r.max.y;
+			r.max.y = r.min.y + Border;
+			draw(screen, r, display->black, nil, ZP);
+		}
+		y1 = r.max.y;
+	}
+	r = w->r;
+	r.min.y = y1;
+	r.max.y = c->r.max.y;
+	draw(screen, r, textcols[BACK], nil, ZP);
+	free(nl);
+	free(ny);
+	c->safe = TRUE;
+	winmousebut(w);
+}
+
+void
+coldragwin(Column *c, Window *w, int but)
+{
+	Rectangle r;
+	int i, b;
+	Point p, op;
+	Window *v;
+	Column *nc;
+
+	clearmouse();
+	setcursor(mousectl, &boxcursor);
+	b = mouse->buttons;
+	op = mouse->xy;
+	while(mouse->buttons == b)
+		readmouse(mousectl);
+	setcursor(mousectl, nil);
+	if(mouse->buttons){
+		while(mouse->buttons)
+			readmouse(mousectl);
+		return;
+	}
+
+	for(i=0; i<c->nw; i++)
+		if(c->w[i] == w)
+			goto Found;
+	error("can't find window");
+
+  Found:
+	p = mouse->xy;
+	if(abs(p.x-op.x)<5 && abs(p.y-op.y)<5){
+		colgrow(c, w, but);
+		winmousebut(w);
+		return;
+	}
+	/* is it a flick to the right? */
+	if(abs(p.y-op.y)<10 && p.x>op.x+30 && rowwhichcol(c->row, p)==c)
+		p.x = op.x+Dx(w->r);	/* yes: toss to next column */
+	nc = rowwhichcol(c->row, p);
+	if(nc!=nil && nc!=c){
+		colclose(c, w, FALSE);
+		coladd(nc, w, nil, p.y);
+		winmousebut(w);
+		return;
+	}
+	if(i==0 && c->nw==1)
+		return;			/* can't do it */
+	if((i>0 && p.y<c->w[i-1]->r.min.y) || (i<c->nw-1 && p.y>w->r.max.y)
+	|| (i==0 && p.y>w->r.max.y)){
+		/* shuffle */
+		colclose(c, w, FALSE);
+		coladd(c, w, nil, p.y);
+		winmousebut(w);
+		return;
+	}
+	if(i == 0)
+		return;
+	v = c->w[i-1];
+	if(p.y < v->tag.all.max.y)
+		p.y = v->tag.all.max.y;
+	if(p.y > w->r.max.y-Dy(w->tag.all)-Border)
+		p.y = w->r.max.y-Dy(w->tag.all)-Border;
+	r = v->r;
+	r.max.y = p.y;
+	if(r.max.y > v->body.r.min.y){
+		r.max.y -= (r.max.y-v->body.r.min.y)%v->body.font->height;
+		if(v->body.r.min.y == v->body.r.max.y)
+			r.max.y++;
+	}
+	if(!eqrect(v->r, r)){
+		draw(screen, r, textcols[BACK], nil, ZP);
+		winresize(v, r, c->safe);
+	}
+	r.min.y = v->r.max.y;
+	r.max.y = r.min.y+Border;
+	draw(screen, r, display->black, nil, ZP);
+	r.min.y = r.max.y;
+	if(i == c->nw-1)
+		r.max.y = c->r.max.y;
+	else
+		r.max.y = c->w[i+1]->r.min.y-Border;
+	if(!eqrect(w->r, r)){
+		draw(screen, r, textcols[BACK], nil, ZP);
+		winresize(w, r, c->safe);
+	}
+	c->safe = TRUE;
+    	winmousebut(w);
+}
+
+Text*
+colwhich(Column *c, Point p)
+{
+	int i;
+	Window *w;
+
+	if(!ptinrect(p, c->r))
+		return nil;
+	if(ptinrect(p, c->tag.all))
+		return &c->tag;
+	for(i=0; i<c->nw; i++){
+		w = c->w[i];
+		if(ptinrect(p, w->r)){
+			if(ptinrect(p, w->tag.all))
+				return &w->tag;
+			return &w->body;
+		}
+		/* scrollr drops below w->r on low windows */
+		if(ptinrect(p, w->body.scrollr))
+			return &w->body;
+	}
+	return nil;
+}
+
+int
+colclean(Column *c)
+{
+	int i, clean;
+
+	clean = TRUE;
+	for(i=0; i<c->nw; i++)
+		clean &= winclean(c->w[i], TRUE);
+	return clean;
+}
--- /dev/null
+++ b/dat.h
@@ -1,0 +1,636 @@
+enum
+{
+	Qdir,
+	Qacme,
+	Qcons,
+	Qconsctl,
+	Qdraw,
+	Qeditout,
+	Qindex,
+	Qlabel,
+	Qlog,
+	Qnew,
+
+	QWaddr,
+	QWbody,
+	QWctl,
+	QWdata,
+	QWeditout,
+	QWerrors,
+	QWevent,
+	QWrdsel,
+	QWwrsel,
+	QWtag,
+	QWxdata,
+	QMAX,
+};
+
+enum
+{
+	Blockincr =	256,
+	Maxblock = 	8*1024,
+	NRange =		10,
+	Infinity = 		0x7FFFFFFF,	/* huge value for regexp address */
+};
+
+typedef	struct	Block Block;
+typedef	struct	Buffer Buffer;
+typedef	struct	Command Command;
+typedef	struct	Column Column;
+typedef	struct	Dirlist Dirlist;
+typedef	struct	Dirtab Dirtab;
+typedef	struct	Disk Disk;
+typedef	struct	Expand Expand;
+typedef	struct	Fid Fid;
+typedef	struct	File File;
+typedef	struct	Elog Elog;
+typedef	struct	Mntdir Mntdir;
+typedef	struct	Range Range;
+typedef	struct	Rangeset Rangeset;
+typedef	struct	Reffont Reffont;
+typedef	struct	Row Row;
+typedef	struct	Runestr Runestr;
+typedef	struct	Text Text;
+typedef	struct	Timer Timer;
+typedef	struct	Window Window;
+typedef	struct	Xfid Xfid;
+
+struct Runestr
+{
+	Rune	*r;
+	int	nr;
+};
+
+struct Range
+{
+	int	q0;
+	int	q1;
+};
+
+struct Block
+{
+	vlong		addr;	/* disk address in bytes */
+	union
+	{
+		uint	n;		/* number of used runes in block */
+		Block	*next;	/* pointer to next in free list */
+	};
+};
+
+struct Disk
+{
+	int		fd;
+	vlong		addr;	/* length of temp file */
+	Block	*free[Maxblock/Blockincr+1];
+};
+
+Disk*	diskinit(void);
+Block*	disknewblock(Disk*, uint);
+void		diskrelease(Disk*, Block*);
+void		diskread(Disk*, Block*, Rune*, uint);
+void		diskwrite(Disk*, Block**, Rune*, uint);
+
+struct Buffer
+{
+	uint	nc;
+	Rune	*c;			/* cache */
+	uint	cnc;			/* bytes in cache */
+	uint	cmax;		/* size of allocated cache */
+	uint	cq;			/* position of cache */
+	int		cdirty;	/* cache needs to be written */
+	uint	cbi;			/* index of cache Block */
+	Block	**bl;		/* array of blocks */
+	uint	nbl;			/* number of blocks */
+};
+void		bufinsert(Buffer*, uint, Rune*, uint);
+void		bufdelete(Buffer*, uint, uint);
+uint		bufload(Buffer*, uint, int, int*);
+void		bufread(Buffer*, uint, Rune*, uint);
+void		bufclose(Buffer*);
+void		bufreset(Buffer*);
+
+struct Elog
+{
+	short	type;		/* Delete, Insert, Filename */
+	uint		q0;		/* location of change (unused in f) */
+	uint		nd;		/* number of deleted characters */
+	uint		nr;		/* # runes in string or file name */
+	Rune		*r;
+};
+void	elogterm(File*);
+void	elogclose(File*);
+void	eloginsert(File*, int, Rune*, int);
+void	elogdelete(File*, int, int);
+void	elogreplace(File*, int, int, Rune*, int);
+void	elogapply(File*);
+
+struct File
+{
+	Buffer;			/* the data */
+	Buffer	delta;	/* transcript of changes */
+	Buffer	epsilon;	/* inversion of delta for redo */
+	Buffer	*elogbuf;	/* log of pending editor changes */
+	Elog		elog;		/* current pending change */
+	Rune		*name;	/* name of associated file */
+	int		nname;	/* size of name */
+	uvlong	qidpath;	/* of file when read */
+	uint		mtime;	/* of file when read */
+	int		dev;		/* of file when read */
+	int		unread;	/* file has not been read from disk */
+	int		editclean;	/* mark clean after edit command */
+
+	int		seq;		/* if seq==0, File acts like Buffer */
+	int		mod;
+	Text		*curtext;	/* most recently used associated text */
+	Text		**text;	/* list of associated texts */
+	int		ntext;
+	int		dumpid;	/* used in dumping zeroxed windows */
+};
+File*		fileaddtext(File*, Text*);
+void		fileclose(File*);
+void		filedelete(File*, uint, uint);
+void		filedeltext(File*, Text*);
+void		fileinsert(File*, uint, Rune*, uint);
+uint		fileload(File*, uint, int, int*);
+void		filemark(File*);
+void		filereset(File*);
+void		filesetname(File*, Rune*, int);
+void		fileundelete(File*, Buffer*, uint, uint);
+void		fileuninsert(File*, Buffer*, uint, uint);
+void		fileunsetname(File*, Buffer*);
+void		fileundo(File*, int, uint*, uint*);
+uint		fileredoseq(File*);
+
+enum	/* Text.what */
+{
+	Columntag,
+	Rowtag,
+	Tag,
+	Body,
+};
+
+struct Text
+{
+	File		*file;
+	Frame;
+	Reffont	*reffont;
+	uint	org;
+	uint	q0;
+	uint	q1;
+	int	what;
+	int	tabstop;
+	Window	*w;
+	Rectangle scrollr;
+	Rectangle lastsr;
+	Rectangle all;
+	Row		*row;
+	Column	*col;
+
+	uint	eq0;	/* start of typing for ESC */
+	uint	cq0;	/* cache position */
+	int		ncache;	/* storage for insert */
+	int		ncachealloc;
+	Rune	*cache;
+	int	nofill;
+	int	needundo;
+};
+
+uint		textbacknl(Text*, uint, uint);
+uint		textbsinsert(Text*, uint, Rune*, uint, int, int*);
+int		textbswidth(Text*, Rune);
+int		textclickmatch(Text*, int, int, int, uint*);
+void		textclose(Text*);
+void		textcolumnate(Text*, Dirlist**, int);
+void		textcommit(Text*, int);
+void		textconstrain(Text*, uint, uint, uint*, uint*);
+void		textdelete(Text*, uint, uint, int);
+void		textstretchsel(Text*, uint, uint*, uint*, int);
+void		textfill(Text*);
+void		textframescroll(Text*, int);
+void		textinit(Text*, File*, Rectangle, Reffont*, Image**);
+void		textinsert(Text*, uint, Rune*, uint, int);
+uint		textload(Text*, uint, char*, int);
+Rune		textreadc(Text*, uint);
+void		textredraw(Text*, Rectangle, Font*, Image*, int);
+void		textreset(Text*);
+int		textresize(Text*, Rectangle);
+void		textscrdraw(Text*);
+void		textscroll(Text*, int);
+void		textselect(Text*);
+int		textselect2(Text*, uint*, uint*, Text**);
+int		textselect23(Text*, uint*, uint*, Image*, int);
+int		textselect3(Text*, uint*, uint*);
+void		textsetorigin(Text*, uint, int);
+void		textsetselect(Text*, uint, uint);
+void		textshow(Text*, uint, uint, int);
+void		texttype(Text*, Rune);
+
+enum
+{
+	SPACESINDENT	= 0,
+	AUTOINDENT,
+	NINDENT,
+};
+
+struct Window
+{
+		QLock;
+		Ref;
+	Text		tag;
+	Text		body;
+	Rectangle	r;
+	uchar	isdir;
+	uchar	isscratch;
+	uchar	filemenu;
+	uchar	dirty;
+	uchar	indent[NINDENT];
+	uchar	showdel;
+	uint		noredraw;
+	int		id;
+	Range	addr;
+	Range	limit;
+	uchar	nopen[QMAX];
+	uchar	nomark;
+	uchar	noscroll;
+	Range	wrselrange;
+	int		rdselfd;
+	Column	*col;
+	Xfid		*eventx;
+	char		*events;
+	int		nevents;
+	int		owner;
+	int		maxlines;
+	Dirlist	**dlp;
+	int		ndl;
+	int		putseq;
+	int		nincl;
+	Rune		**incl;
+	Reffont	*reffont;
+	QLock	ctllock;
+	uint		ctlfid;
+	char		*dumpstr;
+	char		*dumpdir;
+	int		dumpid;
+	int		utflastqid;
+	int		utflastboff;
+	int		utflastq;
+	int		tagsafe;		/* taglines is correct */
+	int		tagexpand;
+	int		taglines;
+	Rectangle	tagtop;
+};
+
+void	wininit(Window*, Window*, Rectangle);
+void	winlock(Window*, int);
+void	winlock1(Window*, int);
+void	winunlock(Window*);
+void	wintype(Window*, Text*, Rune);
+void	winundo(Window*, int);
+void	winsetname(Window*, Rune*, int);
+void	winsettag(Window*);
+void	winsettag1(Window*);
+void	wincommit(Window*, Text*);
+int	winresize(Window*, Rectangle, int);
+void	winclose(Window*);
+void	windelete(Window*);
+int	winclean(Window*, int);
+void	windirfree(Window*);
+void	winevent(Window*, char*, ...);
+void	winmousebut(Window*);
+void	winaddincl(Window*, Rune*, int);
+void	wincleartag(Window*);
+char	*winctlprint(Window*, char*, int);
+
+struct Column
+{
+	Rectangle r;
+	Text	tag;
+	Row		*row;
+	Window	**w;
+	int		nw;
+	int		safe;
+};
+
+void		colinit(Column*, Rectangle);
+Window*	coladd(Column*, Window*, Window*, int);
+void		colclose(Column*, Window*, int);
+void		colcloseall(Column*);
+void		colresize(Column*, Rectangle);
+Text*	colwhich(Column*, Point);
+void		coldragwin(Column*, Window*, int);
+void		colgrow(Column*, Window*, int);
+int		colclean(Column*);
+void		colsort(Column*);
+void		colmousebut(Column*);
+
+struct Row
+{
+	QLock;
+	Rectangle r;
+	Text	tag;
+	Column	**col;
+	int		ncol;
+
+};
+
+void		rowinit(Row*, Rectangle);
+Column*	rowadd(Row*, Column *c, int);
+void		rowclose(Row*, Column*, int);
+Text*	rowwhich(Row*, Point);
+Column*	rowwhichcol(Row*, Point);
+void		rowresize(Row*, Rectangle);
+Text*	rowtype(Row*, Rune, Point);
+void		rowdragcol(Row*, Column*, int but);
+int		rowclean(Row*);
+void		rowdump(Row*, char*);
+int		rowload(Row*, char*, int);
+void		rowloadfonts(char*);
+
+struct Timer
+{
+	int		dt;
+	int		cancel;
+	Channel	*c;	/* chan(int) */
+	Timer	*next;
+};
+
+struct Command
+{
+	int		pid;
+	Rune		*name;
+	int		nname;
+	char		*text;
+	char		**av;
+	int		iseditcmd;
+	Mntdir	*md;
+	Command	*next;
+};
+
+struct Dirtab
+{
+	char	*name;
+	uchar	type;
+	uint	qid;
+	uint	perm;
+};
+
+struct Mntdir
+{
+	int		id;
+	int		ref;
+	Rune		*dir;
+	int		ndir;
+	Mntdir	*next;
+	int		nincl;
+	Rune		**incl;
+};
+
+struct Fid
+{
+	int		fid;
+	int		busy;
+	int		open;
+	Qid		qid;
+	Window	*w;
+	Dirtab	*dir;
+	Fid		*next;
+	Mntdir	*mntdir;
+	int		nrpart;
+	uchar	rpart[UTFmax];
+	vlong	logoff;	// for putlog
+};
+
+
+struct Xfid
+{
+	void		*arg;	/* args to xfidinit */
+	Fcall;
+	Xfid	*next;
+	Channel	*c;		/* chan(void(*)(Xfid*)) */
+	Fid	*f;
+	uchar	*buf;
+	int	flushed;
+};
+
+void		xfidctl(void *);
+void		xfidflush(Xfid*);
+void		xfidopen(Xfid*);
+void		xfidclose(Xfid*);
+void		xfidread(Xfid*);
+void		xfidwrite(Xfid*);
+void		xfidctlwrite(Xfid*, Window*);
+void		xfideventread(Xfid*, Window*);
+void		xfideventwrite(Xfid*, Window*);
+void		xfidindexread(Xfid*);
+void		xfidutfread(Xfid*, Text*, uint, int);
+int		xfidruneread(Xfid*, Text*, uint, uint);
+void		xfidlogopen(Xfid*);
+void		xfidlogread(Xfid*);
+void		xfidlogflush(Xfid*);
+void		xfidlog(Window*, char*);
+
+struct Reffont
+{
+	Ref;
+	Font		*f;
+
+};
+Reffont	*rfget(int, int, int, char*);
+void		rfclose(Reffont*);
+
+struct Rangeset
+{
+	Range	r[NRange];
+};
+
+struct Dirlist
+{
+	Rune	*r;
+	int		nr;
+	int		wid;
+};
+
+struct Expand
+{
+	uint	q0;
+	uint	q1;
+	Rune	*name;
+	int	nname;
+	char	*bname;
+	int	jump;
+	union{
+		Text	*at;
+		Rune	*ar;
+	};
+	int	(*agetc)(void*, uint);
+	int	a0;
+	int	a1;
+};
+
+enum
+{
+	/* fbufalloc() guarantees room off end of BUFSIZE */
+	BUFSIZE = Maxblock+IOHDRSZ,	/* size from fbufalloc() */
+	RBUFSIZE = BUFSIZE/sizeof(Rune),
+	EVENTSIZE = 256,
+	Scrollwid = 12,	/* width of scroll bar */
+	Scrollgap = 4,	/* gap right of scroll bar */
+	Margin = 4,	/* margin around text */
+	Border = 2,	/* line between rows, cols, windows */
+};
+
+#define	QID(w,q)	((w<<8)|(q))
+#define	WIN(q)	((((ulong)(q).path)>>8) & 0xFFFFFF)
+#define	FILE(q)	((q).path & 0xFF)
+
+enum
+{
+	FALSE,
+	TRUE,
+	XXX,
+};
+
+enum
+{
+	Empty	= 0,
+	Null		= '-',
+	Delete	= 'd',
+	Insert	= 'i',
+	Replace	= 'r',
+	Filename	= 'f',
+};
+
+enum	/* editing */
+{
+	Inactive	= 0,
+	Inserting,
+	Collecting,
+};
+
+uint		globalincref;
+uint		seq;
+uint		maxtab;	/* size of a tab, in units of the '0' character */
+
+Display		*display;
+Image		*screen;
+Font			*font;
+Mouse		*mouse;
+Mousectl		*mousectl;
+Keyboardctl	*keyboardctl;
+Reffont		reffont;
+Image		*modbutton;
+Image		*colbutton;
+Image		*button;
+Image		*but2col;
+Image		*but3col;
+Cursor		boxcursor;
+Row			row;
+int			timerpid;
+Disk			*disk;
+Text			*seltext;
+Text			*argtext;
+Text			*mousetext;	/* global because Text.close needs to clear it */
+Text			*typetext;		/* global because Text.close needs to clear it */
+Text			*barttext;		/* shared between mousetask and keyboardthread */
+int			bartflag;
+Window		*activewin;
+Column		*activecol;
+Buffer		snarfbuf;
+Rectangle		nullrect;
+int			fsyspid;
+char			*user;
+char			*cputype;
+char			*objtype;
+char			*home;
+char			*fontnames[2];
+char			acmeerrorfile[128];
+Image		*tagcols[NCOL];
+Image		*textcols[NCOL];
+int			plumbsendfd;
+int			plumbeditfd;
+char			wdir[];
+int			editing;
+int			messagesize;		/* negotiated in 9P version setup */
+int			globalindent[NINDENT];
+Rune		*delcmd;			/* what command deleted the window. eg, Del, Delete, Delmesg */
+
+Channel	*cplumb;		/* chan(Plumbmsg*) */
+Channel	*cwait;		/* chan(Waitmsg) */
+Channel	*ccommand;	/* chan(Command*) */
+Channel	*ckill;		/* chan(Rune*) */
+Channel	*cxfidalloc;	/* chan(Xfid*) */
+Channel	*cxfidfree;	/* chan(Xfid*) */
+Channel	*cnewwindow;	/* chan(Channel*) */
+Channel	*mouseexit0;	/* chan(int) */
+Channel	*mouseexit1;	/* chan(int) */
+Channel	*cexit;		/* chan(int) */
+Channel	*cerr;		/* chan(char*) */
+Channel	*cedit;		/* chan(int) */
+Channel	*cwarn;		/* chan(void*)[1] (really chan(unit)[1]) */
+
+#define	STACK	8192
+
+/* jgs1 - theme scructs */
+enum {
+	Colrioback,
+
+	/* the following group has to be in order, they are used by libframe */
+	Colback,
+	Colhigh,
+	Colbord,
+	Coltext,
+	Colhtext,
+
+	Coltitle,
+	Colltitle,
+	Colhold,
+	Collhold,
+	Colpalehold,
+	Colpaletext,
+	Colsize,
+
+	/* menuhit */
+	Colmenubar,
+	Colmenuback,
+	Colmenuhigh,
+	Colmenubord,
+	Colmenutext,
+	Colmenuhtext,
+
+	Numcolors
+};
+
+typedef struct Color Color;
+
+struct Color {
+	char *id;
+	union {
+		u32int rgb;
+		char *path;
+	};
+	int flags;
+};
+
+static Color theme[Numcolors] = {
+	[Colrioback]   = {"rioback",   {0x777777}, 0},
+	[Colback]      = {"back",      {0xffffff}, 0},
+	[Colhigh]      = {"high",      {0xcccccc}, 0},
+	[Colbord]      = {"border",    {0x999999}, 0},
+	[Coltext]      = {"text",      {DBlack>>8}, 0},
+	[Colhtext]     = {"htext",     {DBlack>>8}, 0},
+	[Coltitle]     = {"title",     {DGreygreen>>8}, 0},
+	[Colltitle]    = {"ltitle",    {DPalegreygreen>>8}, 0},
+	[Colhold]      = {"hold",      {DMedblue>>8}, 0},
+	[Collhold]     = {"lhold",     {DGreyblue>>8}, 0},
+	[Colpalehold]  = {"palehold",  {DPalegreyblue>>8}, 0},
+	[Colpaletext]  = {"paletext",  {0x666666}, 0},
+	[Colsize]      = {"size",      {DRed>>8}, 0},
+	[Colmenubar]   = {"menubar",   {DDarkgreen>>8}, 1},
+	[Colmenuback]  = {"menuback",  {0xeaffea}, 1},
+	[Colmenuhigh]  = {"menuhigh",  {DDarkgreen>>8}, 1},
+	[Colmenubord]  = {"menubord",  {DMedgreen>>8}, 1},
+	[Colmenutext]  = {"menutext",  {DBlack>>8}, 1},
+	[Colmenuhtext] = {"menuhtext", {0xeaffea}, 1},
+};
+
+Image *col[Numcolors];
\ No newline at end of file
--- /dev/null
+++ b/disk.c
@@ -1,0 +1,142 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <cursor.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <frame.h>
+#include <fcall.h>
+#include <plumb.h>
+#include "dat.h"
+#include "fns.h"
+
+static	Block	*blist;
+
+int
+tempfile(void)
+{
+	char buf[128];
+	int i, fd;
+
+	snprint(buf, sizeof buf, "/tmp/X%d.%.4sacme", getpid(), user);
+	for(i='A'; i<='Z'; i++){
+		buf[5] = i;
+		if(access(buf, AEXIST) == 0)
+			continue;
+		fd = create(buf, ORDWR|ORCLOSE|OCEXEC, 0600);
+		if(fd >= 0)
+			return fd;
+	}
+	return -1;
+}
+
+Disk*
+diskinit()
+{
+	Disk *d;
+
+	d = emalloc(sizeof(Disk));
+	d->fd = tempfile();
+	if(d->fd < 0){
+		fprint(2, "acme: can't create temp file: %r\n");
+		threadexitsall("diskinit");
+	}
+	return d;
+}
+
+static
+uint
+ntosize(uint n, uint *ip)
+{
+	uint size;
+
+	if(n > Maxblock)
+		error("internal error: ntosize");
+	size = n;
+	if(size & (Blockincr-1))
+		size += Blockincr - (size & (Blockincr-1));
+	/* last bucket holds blocks of exactly Maxblock */
+	if(ip)
+		*ip = size/Blockincr;
+	return size * sizeof(Rune);
+}
+
+Block*
+disknewblock(Disk *d, uint n)
+{
+	uint i, j, size;
+	Block *b;
+
+	size = ntosize(n, &i);
+	b = d->free[i];
+	if(b)
+		d->free[i] = b->next;
+	else{
+		/* allocate in chunks to reduce malloc overhead */
+		if(blist == nil){
+			blist = emalloc(100*sizeof(Block));
+			for(j=0; j<100-1; j++)
+				blist[j].next = &blist[j+1];
+		}
+		b = blist;
+		blist = b->next;
+		b->addr = d->addr;
+		if(d->addr+size < d->addr){
+			error("temp file overflow");
+		}
+		d->addr += size;
+	}
+	b->n = n;
+	return b;
+}
+
+void
+diskrelease(Disk *d, Block *b)
+{
+	uint i;
+
+	ntosize(b->n, &i);
+	b->next = d->free[i];
+	d->free[i] = b;
+}
+
+void
+diskwrite(Disk *d, Block **bp, Rune *r, uint n)
+{
+	int size, nsize;
+	Block *b;
+
+	b = *bp;
+	size = ntosize(b->n, nil);
+	nsize = ntosize(n, nil);
+	if(size != nsize){
+		diskrelease(d, b);
+		b = disknewblock(d, n);
+		*bp = b;
+	}
+	if(pwrite(d->fd, r, n*sizeof(Rune), b->addr) != n*sizeof(Rune))
+		error("write error to temp file");
+	b->n = n;
+}
+
+void
+diskread(Disk *d, Block *b, Rune *r, uint n)
+{
+	int tot, nr;
+	char *p;
+
+	if(n > b->n)
+		error("internal error: diskread");
+
+	ntosize(b->n, nil);
+	n *= sizeof(Rune);
+	p = (char*)r;
+	for(tot = 0; tot < n; tot += nr){
+		nr = pread(d->fd, p+tot, n-tot, b->addr+tot);
+		if(nr <= 0)
+			error("read error from temp file");
+	}
+	if(tot != n)
+		error("read error from temp file");
+}
--- /dev/null
+++ b/ecmd.c
@@ -1,0 +1,1366 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <cursor.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <frame.h>
+#include <fcall.h>
+#include <plumb.h>
+#include "dat.h"
+#include "edit.h"
+#include "fns.h"
+
+int	Glooping;
+int	nest;
+char	Enoname[] = "no file name given";
+
+Address	addr;
+File	*menu;
+Rangeset	sel;
+extern	Text*	curtext;
+Rune	*collection;
+int	ncollection;
+
+int	append(File*, Cmd*, long);
+int	pdisplay(File*);
+void	pfilename(File*);
+void	looper(File*, Cmd*, int);
+void	filelooper(Text*, Cmd*, int);
+void	linelooper(File*, Cmd*);
+Address	lineaddr(long, Address, int);
+int	filematch(File*, String*);
+File	*tofile(String*);
+Rune*	cmdname(File *f, String *s, int);
+void	runpipe(Text*, int, Rune*, int, int);
+
+void
+clearcollection(void)
+{
+	free(collection);
+	collection = nil;
+	ncollection = 0;
+}
+
+void
+resetxec(void)
+{
+	Glooping = nest = 0;
+	clearcollection();
+}
+
+void
+mkaddr(Address *a, File *f)
+{
+	a->r.q0 = f->curtext->q0;
+	a->r.q1 = f->curtext->q1;
+	a->f = f;
+}
+
+int
+cmdexec(Text *t, Cmd *cp)
+{
+	int i;
+	Addr *ap;
+	File *f;
+	Window *w;
+	Address dot;
+
+	if(t == nil)
+		w = nil;
+	else
+		w = t->w;
+	if(w==nil && (cp->addr==0 || cp->addr->type!='"') &&
+	    !utfrune("bBnqUXY!", cp->cmdc) &&
+	    !(cp->cmdc=='D' && cp->text))
+		editerror("no current window");
+	i = cmdlookup(cp->cmdc);	/* will be -1 for '{' */
+	f = nil;
+	if(t && t->w){
+		t = &t->w->body;
+		f = t->file;
+		f->curtext = t;
+	}
+	if(i>=0 && cmdtab[i].defaddr != aNo){
+		if((ap=cp->addr)==0 && cp->cmdc!='\n'){
+			cp->addr = ap = newaddr();
+			ap->type = '.';
+			if(cmdtab[i].defaddr == aAll)
+				ap->type = '*';
+		}else if(ap && ap->type=='"' && ap->next==0 && cp->cmdc!='\n'){
+			ap->next = newaddr();
+			ap->next->type = '.';
+			if(cmdtab[i].defaddr == aAll)
+				ap->next->type = '*';
+		}
+		if(cp->addr){	/* may be false for '\n' (only) */
+			static Address none = {0,0,nil};
+			if(f){
+				mkaddr(&dot, f);
+				addr = cmdaddress(ap, dot, 0);
+			}else	/* a " */
+				addr = cmdaddress(ap, none, 0);
+			f = addr.f;
+			t = f->curtext;
+		}
+	}
+	switch(cp->cmdc){
+	case '{':
+		mkaddr(&dot, f);
+		if(cp->addr != nil)
+			dot = cmdaddress(cp->addr, dot, 0);
+		for(cp = cp->cmd; cp; cp = cp->next){
+			if(dot.r.q1 > t->file->nc)
+				editerror("dot extends past end of buffer during { command");
+			t->q0 = dot.r.q0;
+			t->q1 = dot.r.q1;
+			cmdexec(t, cp);
+		}
+		break;
+	default:
+		if(i < 0)
+			editerror("unknown command %c in cmdexec", cp->cmdc);
+		i = (*cmdtab[i].fn)(t, cp);
+		return i;
+	}
+	return 1;
+}
+
+char*
+edittext(Window *w, int q, Rune *r, int nr)
+{
+	File *f;
+
+	switch(editing){
+	case Inactive:
+		return "permission denied";
+	case Inserting:
+		f = w->body.file;
+		eloginsert(f, q, r, nr);
+		return nil;
+	case Collecting:
+		collection = runerealloc(collection, ncollection+nr+1);
+		runemove(collection+ncollection, r, nr);
+		ncollection += nr;
+		collection[ncollection] = '\0';
+		return nil;
+	default:
+		return "unknown state in edittext";
+	}
+}
+
+/* string is known to be NUL-terminated */
+Rune*
+filelist(Text *t, Rune *r, int nr)
+{
+	if(nr == 0)
+		return nil;
+	r = skipbl(r, nr, &nr);
+	clearcollection();
+	if(r[0] != '<'){
+		if((collection = runestrdup(r)) != nil)
+			ncollection += runestrlen(r);
+	}else
+		/* use < command to collect text */
+		runpipe(t, '<', r+1, nr-1, Collecting);
+	return collection;
+}
+
+int
+a_cmd(Text *t, Cmd *cp)
+{
+	return append(t->file, cp, addr.r.q1);
+}
+
+int
+b_cmd(Text*, Cmd *cp)
+{
+	File *f;
+
+	f = tofile(cp->text);
+	if(nest == 0)
+		pfilename(f);
+	curtext = f->curtext;
+	return TRUE;
+}
+
+int
+B_cmd(Text *t, Cmd *cp)
+{
+	Rune *list, *r, *s;
+	int nr;
+
+	list = filelist(t, cp->text->r, cp->text->n);
+	if(list == nil)
+		editerror(Enoname);
+	r = list;
+	nr = runestrlen(r);
+	r = skipbl(r, nr, &nr);
+	if(nr == 0)
+		new(t, t, nil, 0, 0, r, 0);
+	else while(nr > 0){
+		s = findbl(r, nr, &nr);
+		*s = '\0';
+		new(t, t, nil, 0, 0, r, runestrlen(r));
+		if(nr > 0)
+			r = skipbl(s+1, nr-1, &nr);
+	}
+	clearcollection();
+	return TRUE;
+}
+
+int
+c_cmd(Text *t, Cmd *cp)
+{
+	elogreplace(t->file, addr.r.q0, addr.r.q1, cp->text->r, cp->text->n);
+	t->q0 = addr.r.q0;
+	t->q1 = addr.r.q0;
+	return TRUE;
+}
+
+int
+d_cmd(Text *t, Cmd*)
+{
+	if(addr.r.q1 > addr.r.q0)
+		elogdelete(t->file, addr.r.q0, addr.r.q1);
+	t->q0 = addr.r.q0;
+	t->q1 = addr.r.q0;
+	return TRUE;
+}
+
+void
+D1(Text *t)
+{
+	if(t->w->body.file->ntext>1 || winclean(t->w, FALSE))
+		colclose(t->col, t->w, TRUE);
+}
+
+int
+D_cmd(Text *t, Cmd *cp)
+{
+	Rune *list, *r, *s, *n;
+	int nr, nn;
+	Window *w;
+	Runestr dir, rs;
+	char buf[128];
+
+	list = filelist(t, cp->text->r, cp->text->n);
+	if(list == nil){
+		D1(t);
+		return TRUE;
+	}
+	dir = dirname(t, nil, 0);
+	r = list;
+	nr = runestrlen(r);
+	r = skipbl(r, nr, &nr);
+	do{
+		s = findbl(r, nr, &nr);
+		*s = '\0';
+		/* first time through, could be empty string, meaning delete file empty name */
+		nn = runestrlen(r);
+		if(r[0]=='/' || nn==0 || dir.nr==0){
+			rs.r = runestrdup(r);
+			rs.nr = nn;
+		}else{
+			n = runemalloc(dir.nr+1+nn);
+			runemove(n, dir.r, dir.nr);
+			n[dir.nr] = '/';
+			runemove(n+dir.nr+1, r, nn);
+			rs = cleanrname((Runestr){n, dir.nr+1+nn});
+		}
+		w = lookfile(rs.r, rs.nr);
+		if(w == nil){
+			snprint(buf, sizeof buf, "no such file %.*S", rs.nr, rs.r);
+			free(rs.r);
+			editerror(buf);
+		}
+		free(rs.r);
+		D1(&w->body);
+		if(nr > 0)
+			r = skipbl(s+1, nr-1, &nr);
+	}while(nr > 0);
+	clearcollection();
+	free(dir.r);
+	return TRUE;
+}
+
+static int
+readloader(void *v, uint q0, Rune *r, int nr)
+{
+	if(nr > 0)
+		eloginsert(v, q0, r, nr);
+	return 0;
+}
+
+int
+e_cmd(Text *t, Cmd *cp)
+{
+	Rune *name;
+	File *f;
+	int i, isdir, q0, q1, fd, nulls, samename, allreplaced;
+	char *s, tmp[128];
+	Dir *d;
+
+	f = t->file;
+	q0 = addr.r.q0;
+	q1 = addr.r.q1;
+	if(cp->cmdc == 'e'){
+		if(winclean(t->w, TRUE)==FALSE)
+			editerror("");	/* winclean generated message already */
+		q0 = 0;
+		q1 = f->nc;
+	}
+	allreplaced = (q0==0 && q1==f->nc);
+	name = cmdname(f, cp->text, cp->cmdc=='e');
+	if(name == nil)
+		editerror(Enoname);
+	i = runestrlen(name);
+	samename = runeeq(name, i, t->file->name, t->file->nname);
+	s = runetobyte(name, i);
+	free(name);
+	fd = open(s, OREAD);
+	if(fd < 0){
+		snprint(tmp, sizeof tmp, "can't open %s: %r", s);
+		free(s);
+		editerror(tmp);
+	}
+	d = dirfstat(fd);
+	isdir = (d!=nil && (d->qid.type&QTDIR));
+	free(d);
+	if(isdir){
+		close(fd);
+		snprint(tmp, sizeof tmp, "%s is a directory", s);
+		free(s);
+		editerror(tmp);
+	}
+	elogdelete(f, q0, q1);
+	nulls = 0;
+	loadfile(fd, q1, &nulls, readloader, f);
+	free(s);
+	close(fd);
+	if(nulls)
+		warning(nil, "%s: NUL bytes elided\n", s);
+	else if(allreplaced && samename)
+		f->editclean = TRUE;
+	return TRUE;
+}
+
+int
+f_cmd(Text *t, Cmd *cp)
+{
+	Rune *name;
+	String *str;
+	String empty;
+
+	if(cp->text == nil){
+		empty.n = 0;
+		empty.r = L"";
+		str = &empty;
+	}else
+		str = cp->text;
+	name = cmdname(t->file, str, TRUE);
+	free(name);
+	pfilename(t->file);
+	return TRUE;
+}
+
+int
+g_cmd(Text *t, Cmd *cp)
+{
+	if(t->file != addr.f){
+		warning(nil, "internal error: g_cmd f!=addr.f\n");
+		return FALSE;
+	}
+	if(rxcompile(cp->re->r) == FALSE)
+		editerror("bad regexp in g command");
+	if(rxexecute(t, nil, addr.r.q0, addr.r.q1, &sel) ^ cp->cmdc=='v'){
+		t->q0 = addr.r.q0;
+		t->q1 = addr.r.q1;
+		return cmdexec(t, cp->cmd);
+	}
+	return TRUE;
+}
+
+int
+i_cmd(Text *t, Cmd *cp)
+{
+	return append(t->file, cp, addr.r.q0);
+}
+
+void
+copy(File *f, Address addr2)
+{
+	long p;
+	int ni;
+	Rune *buf;
+
+	buf = fbufalloc();
+	for(p=addr.r.q0; p<addr.r.q1; p+=ni){
+		ni = addr.r.q1-p;
+		if(ni > RBUFSIZE)
+			ni = RBUFSIZE;
+		bufread(f, p, buf, ni);
+		eloginsert(addr2.f, addr2.r.q1, buf, ni);
+	}
+	fbuffree(buf);
+}
+
+void
+move(File *f, Address addr2)
+{
+	if(addr.f!=addr2.f || addr.r.q1<=addr2.r.q0){
+		elogdelete(f, addr.r.q0, addr.r.q1);
+		copy(f, addr2);
+	}else if(addr.r.q0 >= addr2.r.q1){
+		copy(f, addr2);
+		elogdelete(f, addr.r.q0, addr.r.q1);
+	}else if(addr.r.q0==addr2.r.q0 && addr.r.q1==addr2.r.q1){
+		;	/* move to self; no-op */
+	}else
+		editerror("move overlaps itself");
+}
+
+int
+m_cmd(Text *t, Cmd *cp)
+{
+	Address dot, addr2;
+
+	mkaddr(&dot, t->file);
+	addr2 = cmdaddress(cp->mtaddr, dot, 0);
+	if(cp->cmdc == 'm')
+		move(t->file, addr2);
+	else
+		copy(t->file, addr2);
+	return TRUE;
+}
+
+int
+p_cmd(Text *t, Cmd*)
+{
+	return pdisplay(t->file);
+}
+
+int
+s_cmd(Text *t, Cmd *cp)
+{
+	int i, j, k, c, m, n, nrp, didsub;
+	long p1, op, delta;
+	String *buf;
+	Rangeset *rp;
+	char *err;
+	Rune *rbuf;
+
+	n = cp->num;
+	op= -1;
+	if(rxcompile(cp->re->r) == FALSE)
+		editerror("bad regexp in s command");
+	nrp = 0;
+	rp = nil;
+	delta = 0;
+	didsub = FALSE;
+	for(p1 = addr.r.q0; p1<=addr.r.q1 && rxexecute(t, nil, p1, addr.r.q1, &sel); ){
+		if(sel.r[0].q0 == sel.r[0].q1){	/* empty match? */
+			if(sel.r[0].q0 == op){
+				p1++;
+				continue;
+			}
+			p1 = sel.r[0].q1+1;
+		}else
+			p1 = sel.r[0].q1;
+		op = sel.r[0].q1;
+		if(--n>0)
+			continue;
+		nrp++;
+		rp = erealloc(rp, nrp*sizeof(Rangeset));
+		rp[nrp-1] = sel;
+	}
+	rbuf = fbufalloc();
+	buf = allocstring(0);
+	for(m=0; m<nrp; m++){
+		buf->n = 0;
+		buf->r[0] = L'\0';
+		sel = rp[m];
+		for(i = 0; i<cp->text->n; i++)
+			if((c = cp->text->r[i])=='\\' && i<cp->text->n-1){
+				c = cp->text->r[++i];
+				if('1'<=c && c<='9') {
+					j = c-'0';
+					if(sel.r[j].q1-sel.r[j].q0>RBUFSIZE){
+						err = "replacement string too long";
+						goto Err;
+					}
+					bufread(t->file, sel.r[j].q0, rbuf, sel.r[j].q1-sel.r[j].q0);
+					for(k=0; k<sel.r[j].q1-sel.r[j].q0; k++)
+						Straddc(buf, rbuf[k]);
+				}else
+				 	Straddc(buf, c);
+			}else if(c!='&')
+				Straddc(buf, c);
+			else{
+				if(sel.r[0].q1-sel.r[0].q0>RBUFSIZE){
+					err = "right hand side too long in substitution";
+					goto Err;
+				}
+				bufread(t->file, sel.r[0].q0, rbuf, sel.r[0].q1-sel.r[0].q0);
+				for(k=0; k<sel.r[0].q1-sel.r[0].q0; k++)
+					Straddc(buf, rbuf[k]);
+			}
+		elogreplace(t->file, sel.r[0].q0, sel.r[0].q1,  buf->r, buf->n);
+		delta -= sel.r[0].q1-sel.r[0].q0;
+		delta += buf->n;
+		didsub = 1;
+		if(!cp->flag)
+			break;
+	}
+	free(rp);
+	freestring(buf);
+	fbuffree(rbuf);
+	if(!didsub && nest==0)
+		editerror("no substitution");
+	t->q0 = addr.r.q0;
+	t->q1 = addr.r.q1;
+	return TRUE;
+
+Err:
+	free(rp);
+	freestring(buf);
+	fbuffree(rbuf);
+	editerror(err);
+	return FALSE;
+}
+
+int
+u_cmd(Text *t, Cmd *cp)
+{
+	int n, oseq, flag;
+
+	n = cp->num;
+	flag = TRUE;
+	if(n < 0){
+		n = -n;
+		flag = FALSE;
+	}
+	oseq = -1;
+	while(n-->0 && t->file->seq!=0 && t->file->seq!=oseq){
+		oseq = t->file->seq;
+		undo(t, nil, nil, flag, 0, nil, 0);
+	}
+	return TRUE;
+}
+
+int
+w_cmd(Text *t, Cmd *cp)
+{
+	Rune *r;
+	File *f;
+
+	f = t->file;
+	if(f->seq == seq)
+		editerror("can't write file with pending modifications");
+	r = cmdname(f, cp->text, FALSE);
+	if(r == nil)
+		editerror("no name specified for 'w' command");
+	putfile(f, addr.r.q0, addr.r.q1, r, runestrlen(r));
+	/* r is freed by putfile */
+	return TRUE;
+}
+
+int
+x_cmd(Text *t, Cmd *cp)
+{
+	if(cp->re)
+		looper(t->file, cp, cp->cmdc=='x');
+	else
+		linelooper(t->file, cp);
+	return TRUE;
+}
+
+int
+X_cmd(Text *t, Cmd *cp)
+{
+	filelooper(t, cp, cp->cmdc=='X');
+	return TRUE;
+}
+
+void
+runpipe(Text *t, int cmd, Rune *cr, int ncr, int state)
+{
+	Rune *r, *s;
+	int n;
+	Runestr dir;
+	Window *w;
+
+	r = skipbl(cr, ncr, &n);
+	if(n == 0)
+		editerror("no command specified for %c", cmd);
+	w = nil;
+	if(state == Inserting){
+		w = t->w;
+		t->q0 = addr.r.q0;
+		t->q1 = addr.r.q1;
+		if(cmd == '<' || cmd=='|')
+			elogdelete(t->file, t->q0, t->q1);
+	}
+	s = runemalloc(n+2);
+	s[0] = cmd;
+	runemove(s+1, r, n);
+	n++;
+	dir.r = nil;
+	dir.nr = 0;
+	if(t != nil)
+		dir = dirname(t, nil, 0);
+	if(dir.nr==1 && dir.r[0]=='.'){	/* sigh */
+		free(dir.r);
+		dir.r = nil;
+		dir.nr = 0;
+	}
+	editing = state;
+	if(t!=nil && t->w!=nil)
+		incref(t->w);	/* run will decref */
+	run(w, runetobyte(s, n), dir.r, dir.nr, TRUE, nil, nil, TRUE);
+	free(s);
+	if(t!=nil && t->w!=nil)
+		winunlock(t->w);
+	qunlock(&row);
+	recvul(cedit);
+	qlock(&row);
+	editing = Inactive;
+	if(t!=nil && t->w!=nil)
+		winlock(t->w, 'M');
+}
+
+int
+pipe_cmd(Text *t, Cmd *cp)
+{
+	runpipe(t, cp->cmdc, cp->text->r, cp->text->n, Inserting);
+	return TRUE;
+}
+
+long
+nlcount(Text *t, long q0, long q1, long *pnr)
+{
+	long nl, start;
+	Rune *buf;
+	int i, nbuf;
+
+	buf = fbufalloc();
+	nbuf = 0;
+	i = nl = 0;
+	start = q0;
+	while(q0 < q1){
+		if(i == nbuf){
+			nbuf = q1-q0;
+			if(nbuf > RBUFSIZE)
+				nbuf = RBUFSIZE;
+			bufread(t->file, q0, buf, nbuf);
+			i = 0;
+		}
+		if(buf[i++] == '\n'){
+			start = q0+1;
+			nl++;
+		}
+		q0++;
+	}
+	fbuffree(buf);
+	if(pnr != nil)
+		*pnr = q0 - start;
+	return nl;
+}
+
+enum {
+	PosnLine = 0,
+	PosnChars = 1,
+	PosnLineChars = 2,
+};
+
+void
+printposn(Text *t, int mode)
+{
+	long l1, l2, r1, r2;
+
+	if (t != nil && t->file != nil && t->file->name != nil)
+		warning(nil, "%.*S:", t->file->nname, t->file->name);
+	switch(mode) {
+	case PosnChars:
+		warning(nil, "#%d", addr.r.q0);
+		if(addr.r.q1 != addr.r.q0)
+			warning(nil, ",#%d", addr.r.q1);
+		warning(nil, "\n");
+		return;
+	default:
+	case PosnLine:
+		l1 = 1+nlcount(t, 0, addr.r.q0, nil);
+		l2 = l1+nlcount(t, addr.r.q0, addr.r.q1, nil);
+		/* check if addr ends with '\n' */
+		if(addr.r.q1>0 && addr.r.q1>addr.r.q0 && textreadc(t, addr.r.q1-1)=='\n')
+			--l2;
+		warning(nil, "%lud", l1);
+		if(l2 != l1)
+			warning(nil, ",%lud", l2);
+		warning(nil, "\n");
+		return;
+	case PosnLineChars:
+		l1 = 1+nlcount(t, 0, addr.r.q0, &r1);
+		l2 = l1+nlcount(t, addr.r.q0, addr.r.q1, &r2);
+		if(l2 == l1)
+			r2 += r1;
+		warning(nil, "%lud+#%lud", l1, r1);
+		if(l2 != l1)
+			warning(nil, ",%lud+#%lud", l2, r2);
+		warning(nil, "\n");
+		return;
+	}
+}
+
+int
+eq_cmd(Text *t, Cmd *cp)
+{
+	int mode;
+
+	switch(cp->text->n){
+	case 0:
+		mode = PosnLine;
+		break;
+	case 1:
+		if(cp->text->r[0] == '#'){
+			mode = PosnChars;
+			break;
+		}
+		if(cp->text->r[0] == '+'){
+			mode = PosnLineChars;
+			break;
+		}
+	default:
+		SET(mode);
+		editerror("newline expected");
+	}
+	printposn(t, mode);
+	return TRUE;
+}
+
+int
+nl_cmd(Text *t, Cmd *cp)
+{
+	Address a;
+	File *f;
+
+	f = t->file;
+	if(cp->addr == 0){
+		/* First put it on newline boundaries */
+		mkaddr(&a, f);
+		addr = lineaddr(0, a, -1);
+		a = lineaddr(0, a, 1);
+		addr.r.q1 = a.r.q1;
+		if(addr.r.q0==t->q0 && addr.r.q1==t->q1){
+			mkaddr(&a, f);
+			addr = lineaddr(1, a, 1);
+		}
+	}
+	textshow(t, addr.r.q0, addr.r.q1, 1);
+	return TRUE;
+}
+
+int
+append(File *f, Cmd *cp, long p)
+{
+	if(cp->text->n > 0)
+		eloginsert(f, p, cp->text->r, cp->text->n);
+	f->curtext->q0 = p;
+	f->curtext->q1 = p;
+	return TRUE;
+}
+
+int
+pdisplay(File *f)
+{
+	long p1, p2;
+	int np;
+	Rune *buf;
+
+	p1 = addr.r.q0;
+	p2 = addr.r.q1;
+	if(p2 > f->nc)
+		p2 = f->nc;
+	buf = fbufalloc();
+	while(p1 < p2){
+		np = p2-p1;
+		if(np>RBUFSIZE-1)
+			np = RBUFSIZE-1;
+		bufread(f, p1, buf, np);
+		buf[np] = L'\0';
+		warning(nil, "%S", buf);
+		p1 += np;
+	}
+	fbuffree(buf);
+	f->curtext->q0 = addr.r.q0;
+	f->curtext->q1 = addr.r.q1;
+	return TRUE;
+}
+
+void
+pfilename(File *f)
+{
+	int dirty;
+	Window *w;
+
+	w = f->curtext->w;
+	/* same check for dirty as in settag, but we know ncache==0 */
+	dirty = !w->isdir && !w->isscratch && f->mod;
+	warning(nil, "%c%c%c %.*S\n", " '"[dirty],
+		'+', " ."[curtext!=nil && curtext->file==f], f->nname, f->name);
+}
+
+void
+loopcmd(File *f, Cmd *cp, Range *rp, long nrp)
+{
+	long i;
+
+	for(i=0; i<nrp; i++){
+		f->curtext->q0 = rp[i].q0;
+		f->curtext->q1 = rp[i].q1;
+		cmdexec(f->curtext, cp);
+	}
+}
+
+void
+looper(File *f, Cmd *cp, int xy)
+{
+	long p, op, nrp;
+	Range r, tr;
+	Range *rp;
+
+	r = addr.r;
+	op= xy? -1 : r.q0;
+	nest++;
+	if(rxcompile(cp->re->r) == FALSE)
+		editerror("bad regexp in %c command", cp->cmdc);
+	nrp = 0;
+	rp = nil;
+	for(p = r.q0; p<=r.q1; ){
+		if(!rxexecute(f->curtext, nil, p, r.q1, &sel)){ /* no match, but y should still run */
+			if(xy || op>r.q1)
+				break;
+			tr.q0 = op, tr.q1 = r.q1;
+			p = r.q1+1;	/* exit next loop */
+		}else{
+			if(sel.r[0].q0==sel.r[0].q1){	/* empty match? */
+				if(sel.r[0].q0==op){
+					p++;
+					continue;
+				}
+				p = sel.r[0].q1+1;
+			}else
+				p = sel.r[0].q1;
+			if(xy)
+				tr = sel.r[0];
+			else
+				tr.q0 = op, tr.q1 = sel.r[0].q0;
+		}
+		op = sel.r[0].q1;
+		nrp++;
+		rp = erealloc(rp, nrp*sizeof(Range));
+		rp[nrp-1] = tr;
+	}
+	loopcmd(f, cp->cmd, rp, nrp);
+	free(rp);
+	--nest;
+}
+
+void
+linelooper(File *f, Cmd *cp)
+{
+	long nrp, p;
+	Range r, linesel;
+	Address a, a3;
+	Range *rp;
+
+	nest++;
+	nrp = 0;
+	rp = nil;
+	r = addr.r;
+	a3.f = f;
+	a3.r.q0 = a3.r.q1 = r.q0;
+	a = lineaddr(0, a3, 1);
+	linesel = a.r;
+	for(p = r.q0; p<r.q1; p = a3.r.q1){
+		a3.r.q0 = a3.r.q1;
+		if(p!=r.q0 || linesel.q1==p){
+			a = lineaddr(1, a3, 1);
+			linesel = a.r;
+		}
+		if(linesel.q0 >= r.q1)
+			break;
+		if(linesel.q1 >= r.q1)
+			linesel.q1 = r.q1;
+		if(linesel.q1 > linesel.q0)
+			if(linesel.q0>=a3.r.q1 && linesel.q1>a3.r.q1){
+				a3.r = linesel;
+				nrp++;
+				rp = erealloc(rp, nrp*sizeof(Range));
+				rp[nrp-1] = linesel;
+				continue;
+			}
+		break;
+	}
+	loopcmd(f, cp->cmd, rp, nrp);
+	free(rp);
+	--nest;
+}
+
+struct Looper
+{
+	Cmd *cp;
+	int	XY;
+	Window	**w;
+	int	nw;
+} loopstruct;	/* only one; X and Y can't nest */
+
+void
+alllooper(Window *w, void *v)
+{
+	Text *t;
+	struct Looper *lp;
+	Cmd *cp;
+
+	lp = v;
+	cp = lp->cp;
+//	if(w->isscratch || w->isdir)
+//		return;
+	t = &w->body;
+	/* only use this window if it's the current window for the file */
+	if(t->file->curtext != t)
+		return;
+//	if(w->nopen[QWevent] > 0)
+//		return;
+	/* no auto-execute on files without names */
+	if(cp->re==nil && t->file->nname==0)
+		return;
+	if(cp->re==nil || filematch(t->file, cp->re)==lp->XY){
+		lp->w = erealloc(lp->w, (lp->nw+1)*sizeof(Window*));
+		lp->w[lp->nw++] = w;
+	}
+}
+
+void
+alllocker(Window *w, void *v)
+{
+	if(v)
+		incref(w);
+	else
+		winclose(w);
+}
+
+void
+filelooper(Text *t, Cmd *cp, int XY)
+{
+	int i;
+	Text *targ;
+
+	if(Glooping++)
+		editerror("can't nest %c command", "YX"[XY]);
+	nest++;
+
+	loopstruct.cp = cp;
+	loopstruct.XY = XY;
+	if(loopstruct.w)	/* error'ed out last time */
+		free(loopstruct.w);
+	loopstruct.w = nil;
+	loopstruct.nw = 0;
+	allwindows(alllooper, &loopstruct);
+	/*
+	 * add a ref to all windows to keep safe windows accessed by X
+	 * that would not otherwise have a ref to hold them up during
+	 * the shenanigans.  note this with globalincref so that any
+	 * newly created windows start with an extra reference.
+	 */
+	allwindows(alllocker, (void*)1);
+	globalincref = 1;
+	/*
+	 * Unlock the window running the X command.
+	 * We'll need to lock and unlock each target window in turn.
+	 */
+	if(t && t->w)
+		winunlock(t->w);
+	for(i=0; i<loopstruct.nw; i++){
+		targ = &loopstruct.w[i]->body;
+		if(targ && targ->w)
+			winlock(targ->w, cp->cmdc);
+		cmdexec(targ, cp->cmd);
+		if(targ && targ->w)
+			winunlock(targ->w);
+	}
+	if(t && t->w)
+		winlock(t->w, cp->cmdc);
+	allwindows(alllocker, (void*)0);
+	globalincref = 0;
+	free(loopstruct.w);
+	loopstruct.w = nil;
+
+	--Glooping;
+	--nest;
+}
+
+void
+nextmatch(File *f, String *r, long p, int sign)
+{
+	if(rxcompile(r->r) == FALSE)
+		editerror("bad regexp in command address");
+	if(sign >= 0){
+		if(!rxexecute(f->curtext, nil, p, 0x7FFFFFFFL, &sel))
+			editerror("no match for regexp");
+		if(sel.r[0].q0==sel.r[0].q1 && sel.r[0].q0==p){
+			if(++p>f->nc)
+				p = 0;
+			if(!rxexecute(f->curtext, nil, p, 0x7FFFFFFFL, &sel))
+				editerror("address");
+		}
+	}else{
+		if(!rxbexecute(f->curtext, p, &sel))
+			editerror("no match for regexp");
+		if(sel.r[0].q0==sel.r[0].q1 && sel.r[0].q1==p){
+			if(--p<0)
+				p = f->nc;
+			if(!rxbexecute(f->curtext, p, &sel))
+				editerror("address");
+		}
+	}
+}
+
+File	*matchfile(String*);
+Address	charaddr(long, Address, int);
+Address	lineaddr(long, Address, int);
+
+Address
+cmdaddress(Addr *ap, Address a, int sign)
+{
+	File *f = a.f;
+	Address a1, a2;
+
+	do{
+		switch(ap->type){
+		case 'l':
+		case '#':
+			a = (*(ap->type=='#'?charaddr:lineaddr))(ap->num, a, sign);
+			break;
+
+		case '.':
+			mkaddr(&a, f);
+			break;
+
+		case '$':
+			a.r.q0 = a.r.q1 = f->nc;
+			break;
+
+		case '\'':
+editerror("can't handle '");
+//			a.r = f->mark;
+			break;
+
+		case '?':
+			sign = -sign;
+			if(sign == 0)
+				sign = -1;
+			/* fall through */
+		case '/':
+			nextmatch(f, ap->re, sign>=0? a.r.q1 : a.r.q0, sign);
+			a.r = sel.r[0];
+			break;
+
+		case '"':
+			f = matchfile(ap->re);
+			mkaddr(&a, f);
+			break;
+
+		case '*':
+			a.r.q0 = 0, a.r.q1 = f->nc;
+			return a;
+
+		case ',':
+		case ';':
+			if(ap->left)
+				a1 = cmdaddress(ap->left, a, 0);
+			else
+				a1.f = a.f, a1.r.q0 = a1.r.q1 = 0;
+			if(ap->type == ';'){
+				f = a1.f;
+				a = a1;
+				f->curtext->q0 = a1.r.q0;
+				f->curtext->q1 = a1.r.q1;
+			}
+			if(ap->next)
+				a2 = cmdaddress(ap->next, a, 0);
+			else
+				a2.f = a.f, a2.r.q0 = a2.r.q1 = f->nc;
+			if(a1.f != a2.f)
+				editerror("addresses in different files");
+			a.f = a1.f, a.r.q0 = a1.r.q0, a.r.q1 = a2.r.q1;
+			if(a.r.q1 < a.r.q0)
+				editerror("addresses out of order");
+			return a;
+
+		case '+':
+		case '-':
+			sign = 1;
+			if(ap->type == '-')
+				sign = -1;
+			if(ap->next==0 || ap->next->type=='+' || ap->next->type=='-')
+				a = lineaddr(1L, a, sign);
+			break;
+		default:
+			error("cmdaddress");
+			return a;
+		}
+	}while(ap = ap->next);	/* assign = */
+	return a;
+}
+
+struct Tofile{
+	File		*f;
+	String	*r;
+};
+
+void
+alltofile(Window *w, void *v)
+{
+	Text *t;
+	struct Tofile *tp;
+
+	tp = v;
+	if(tp->f != nil)
+		return;
+	if(w->isscratch || w->isdir)
+		return;
+	t = &w->body;
+	/* only use this window if it's the current window for the file */
+	if(t->file->curtext != t)
+		return;
+//	if(w->nopen[QWevent] > 0)
+//		return;
+	if(runeeq(tp->r->r, tp->r->n, t->file->name, t->file->nname))
+		tp->f = t->file;
+}
+
+File*
+tofile(String *r)
+{
+	struct Tofile t;
+	String rr;
+
+	rr.r = skipbl(r->r, r->n, &rr.n);
+	t.f = nil;
+	t.r = &rr;
+	allwindows(alltofile, &t);
+	if(t.f == nil)
+		editerror("no such file\"%S\"", rr.r);
+	return t.f;
+}
+
+void
+allmatchfile(Window *w, void *v)
+{
+	struct Tofile *tp;
+	Text *t;
+
+	tp = v;
+	if(w->isscratch || w->isdir)
+		return;
+	t = &w->body;
+	/* only use this window if it's the current window for the file */
+	if(t->file->curtext != t)
+		return;
+//	if(w->nopen[QWevent] > 0)
+//		return;
+	if(filematch(w->body.file, tp->r)){
+		if(tp->f != nil)
+			editerror("too many files match \"%S\"", tp->r->r);
+		tp->f = w->body.file;
+	}
+}
+
+File*
+matchfile(String *r)
+{
+	struct Tofile tf;
+
+	tf.f = nil;
+	tf.r = r;
+	allwindows(allmatchfile, &tf);
+
+	if(tf.f == nil)
+		editerror("no file matches \"%S\"", r->r);
+	return tf.f;
+}
+
+int
+filematch(File *f, String *r)
+{
+	char *buf;
+	Rune *rbuf;
+	Window *w;
+	int match, i, dirty;
+	Rangeset s;
+
+	/* compile expr first so if we get an error, we haven't allocated anything */
+	if(rxcompile(r->r) == FALSE)
+		editerror("bad regexp in file match");
+	buf = fbufalloc();
+	w = f->curtext->w;
+	/* same check for dirty as in settag, but we know ncache==0 */
+	dirty = !w->isdir && !w->isscratch && f->mod;
+	snprint(buf, BUFSIZE, "%c%c%c %.*S\n", " '"[dirty],
+		'+', " ."[curtext!=nil && curtext->file==f], f->nname, f->name);
+	rbuf = bytetorune(buf, &i);
+	fbuffree(buf);
+	match = rxexecute(nil, rbuf, 0, i, &s);
+	free(rbuf);
+	return match;
+}
+
+Address
+charaddr(long l, Address addr, int sign)
+{
+	if(sign == 0)
+		addr.r.q0 = addr.r.q1 = l;
+	else if(sign < 0)
+		addr.r.q1 = addr.r.q0 -= l;
+	else if(sign > 0)
+		addr.r.q0 = addr.r.q1 += l;
+	if(addr.r.q0<0 || addr.r.q1>addr.f->nc)
+		editerror("address out of range");
+	return addr;
+}
+
+Address
+lineaddr(long l, Address addr, int sign)
+{
+	int n;
+	int c;
+	File *f = addr.f;
+	Address a;
+	long p;
+
+	a.f = f;
+	if(sign >= 0){
+		if(l == 0){
+			if(sign==0 || addr.r.q1==0){
+				a.r.q0 = a.r.q1 = 0;
+				return a;
+			}
+			a.r.q0 = addr.r.q1;
+			p = addr.r.q1-1;
+		}else{
+			if(sign==0 || addr.r.q1==0){
+				p = 0;
+				n = 1;
+			}else{
+				p = addr.r.q1-1;
+				n = textreadc(f->curtext, p++)=='\n';
+			}
+			while(n < l){
+				if(p >= f->nc)
+					editerror("address out of range");
+				if(textreadc(f->curtext, p++) == '\n')
+					n++;
+			}
+			a.r.q0 = p;
+		}
+		while(p < f->nc && textreadc(f->curtext, p++)!='\n')
+			;
+		a.r.q1 = p;
+	}else{
+		p = addr.r.q0;
+		if(l == 0)
+			a.r.q1 = addr.r.q0;
+		else{
+			for(n = 0; n<l; ){	/* always runs once */
+				if(p == 0){
+					if(++n != l)
+						editerror("address out of range");
+				}else{
+					c = textreadc(f->curtext, p-1);
+					if(c != '\n' || ++n != l)
+						p--;
+				}
+			}
+			a.r.q1 = p;
+			if(p > 0)
+				p--;
+		}
+		while(p > 0 && textreadc(f->curtext, p-1)!='\n')	/* lines start after a newline */
+			p--;
+		a.r.q0 = p;
+	}
+	return a;
+}
+
+struct Filecheck
+{
+	File	*f;
+	Rune	*r;
+	int nr;
+};
+
+void
+allfilecheck(Window *w, void *v)
+{
+	struct Filecheck *fp;
+	File *f;
+
+	fp = v;
+	f = w->body.file;
+	if(w->body.file == fp->f)
+		return;
+	if(runeeq(fp->r, fp->nr, f->name, f->nname))
+		warning(nil, "warning: duplicate file name \"%.*S\"\n", fp->nr, fp->r);
+}
+
+Rune*
+cmdname(File *f, String *str, int set)
+{
+	Rune *r, *s;
+	int n;
+	struct Filecheck fc;
+	Runestr newname;
+
+	r = nil;
+	n = str->n;
+	s = str->r;
+	if(n == 0){
+		/* no name; use existing */
+		if(f->nname == 0)
+			return nil;
+		r = runemalloc(f->nname+1);
+		runemove(r, f->name, f->nname);
+		return r;
+	}
+	s = skipbl(s, n, &n);
+	if(n == 0)
+		goto Return;
+
+	if(s[0] == '/'){
+		r = runemalloc(n+1);
+		runemove(r, s, n);
+	}else{
+		newname = dirname(f->curtext, runestrdup(s), n);
+		n = newname.nr;
+		r = runemalloc(n+1);	/* NUL terminate */
+		runemove(r, newname.r, n);
+		free(newname.r);
+	}
+	fc.f = f;
+	fc.r = r;
+	fc.nr = n;
+	allwindows(allfilecheck, &fc);
+	if(f->nname == 0)
+		set = TRUE;
+
+    Return:
+	if(set && !runeeq(r, n, f->name, f->nname)){
+		filemark(f);
+		f->mod = TRUE;
+		f->curtext->w->dirty = TRUE;
+		winsetname(f->curtext->w, r, n);
+	}
+	return r;
+}
--- /dev/null
+++ b/edit.c
@@ -1,0 +1,679 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <cursor.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <frame.h>
+#include <fcall.h>
+#include <plumb.h>
+#include "dat.h"
+#include "edit.h"
+#include "fns.h"
+
+static char	linex[]="\n";
+static char	wordx[]=" \t\n";
+struct cmdtab cmdtab[]={
+/*	cmdc	text	regexp	addr	defcmd	defaddr	count	token	 fn	*/
+	'\n',	0,	0,	0,	0,	aDot,	0,	0,	nl_cmd,
+	'a',	1,	0,	0,	0,	aDot,	0,	0,	a_cmd,
+	'b',	0,	0,	0,	0,	aNo,	0,	linex,	b_cmd,
+	'c',	1,	0,	0,	0,	aDot,	0,	0,	c_cmd,
+	'd',	0,	0,	0,	0,	aDot,	0,	0,	d_cmd,
+	'e',	0,	0,	0,	0,	aNo,	0,	wordx,	e_cmd,
+	'f',	0,	0,	0,	0,	aNo,	0,	wordx,	f_cmd,
+	'g',	0,	1,	0,	'p',	aDot,	0,	0,	g_cmd,
+	'i',	1,	0,	0,	0,	aDot,	0,	0,	i_cmd,
+	'm',	0,	0,	1,	0,	aDot,	0,	0,	m_cmd,
+	'p',	0,	0,	0,	0,	aDot,	0,	0,	p_cmd,
+	'r',	0,	0,	0,	0,	aDot,	0,	wordx,	e_cmd,
+	's',	0,	1,	0,	0,	aDot,	1,	0,	s_cmd,
+	't',	0,	0,	1,	0,	aDot,	0,	0,	m_cmd,
+	'u',	0,	0,	0,	0,	aNo,	2,	0,	u_cmd,
+	'v',	0,	1,	0,	'p',	aDot,	0,	0,	g_cmd,
+	'w',	0,	0,	0,	0,	aAll,	0,	wordx,	w_cmd,
+	'x',	0,	1,	0,	'p',	aDot,	0,	0,	x_cmd,
+	'y',	0,	1,	0,	'p',	aDot,	0,	0,	x_cmd,
+	'=',	0,	0,	0,	0,	aDot,	0,	linex,	eq_cmd,
+	'B',	0,	0,	0,	0,	aNo,	0,	linex,	B_cmd,
+	'D',	0,	0,	0,	0,	aNo,	0,	linex,	D_cmd,
+	'X',	0,	1,	0,	'f',	aNo,	0,	0,	X_cmd,
+	'Y',	0,	1,	0,	'f',	aNo,	0,	0,	X_cmd,
+	'<',	0,	0,	0,	0,	aDot,	0,	linex,	pipe_cmd,
+	'|',	0,	0,	0,	0,	aDot,	0,	linex,	pipe_cmd,
+	'>',	0,	0,	0,	0,	aDot,	0,	linex,	pipe_cmd,
+/* deliberately unimplemented:
+	'k',	0,	0,	0,	0,	aDot,	0,	0,	k_cmd,
+	'n',	0,	0,	0,	0,	aNo,	0,	0,	n_cmd,
+	'q',	0,	0,	0,	0,	aNo,	0,	0,	q_cmd,
+	'!',	0,	0,	0,	0,	aNo,	0,	linex,	plan9_cmd,
+ */
+	0,	0,	0,	0,	0,	0,	0,	0,
+};
+
+Cmd	*parsecmd(int);
+Addr	*compoundaddr(void);
+Addr	*simpleaddr(void);
+void	freecmd(void);
+void	okdelim(int);
+
+Rune	*cmdstartp;
+Rune	*cmdendp;
+Rune	*cmdp;
+Channel	*editerrc;
+
+String	*lastpat;
+int	patset;
+
+List	cmdlist;
+List	addrlist;
+List	stringlist;
+Text	*curtext;
+int	editing = Inactive;
+
+String*	newstring(int);
+
+void
+editthread(void*)
+{
+	Cmd *cmdp;
+
+	threadsetname("editthread");
+	while((cmdp=parsecmd(0)) != 0){
+//		ocurfile = curfile;
+//		loaded = curfile && !curfile->unread;
+		if(cmdexec(curtext, cmdp) == 0)
+			break;
+		freecmd();
+	}
+	sendp(editerrc, nil);
+}
+
+void
+allelogterm(Window *w, void*)
+{
+	elogterm(w->body.file);
+}
+
+void
+alleditinit(Window *w, void*)
+{
+	textcommit(&w->tag, TRUE);
+	textcommit(&w->body, TRUE);
+	w->body.file->editclean = FALSE;
+}
+
+void
+allupdate(Window *w, void*)
+{
+	Text *t;
+	int i;
+	File *f;
+
+	t = &w->body;
+	f = t->file;
+	if(f->curtext != t)	/* do curtext only */
+		return;
+	if(f->elog.type == Null)
+		elogterm(f);
+	else if(f->elog.type != Empty){
+		elogapply(f);
+		if(f->editclean){
+			f->mod = FALSE;
+			for(i=0; i<f->ntext; i++)
+				f->text[i]->w->dirty = FALSE;
+		}
+	}
+	textsetselect(t, t->q0, t->q1);
+	textscrdraw(t);
+	winsettag(w);
+}
+
+void
+editerror(char *fmt, ...)
+{
+	va_list arg;
+	char *s;
+
+	va_start(arg, fmt);
+	s = vsmprint(fmt, arg);
+	va_end(arg);
+	freecmd();
+	allwindows(allelogterm, nil);	/* truncate the edit logs */
+	sendp(editerrc, s);
+	threadexits(nil);
+}
+
+void
+editcmd(Text *ct, Rune *r, uint n)
+{
+	char *err;
+
+	if(n == 0)
+		return;
+	if(2*n > RBUFSIZE){
+		warning(nil, "string too long\n");
+		return;
+	}
+
+	allwindows(alleditinit, nil);
+	if(cmdstartp)
+		free(cmdstartp);
+	cmdstartp = runemalloc(n+2);
+	runemove(cmdstartp, r, n);
+	if(r[n] != '\n')
+		cmdstartp[n++] = '\n';
+	cmdstartp[n] = '\0';
+	cmdendp = cmdstartp+n;
+	cmdp = cmdstartp;
+	if(ct->w == nil)
+		curtext = nil;
+	else
+		curtext = &ct->w->body;
+	resetxec();
+	if(editerrc == nil){
+		editerrc = chancreate(sizeof(char*), 0);
+		lastpat = allocstring(0);
+	}
+	threadcreate(editthread, nil, STACK);
+	err = recvp(editerrc);
+	editing = Inactive;
+	if(err != nil){
+		if(err[0] != '\0')
+			warning(nil, "Edit: %s\n", err);
+		free(err);
+	}
+
+	/* update everyone whose edit log has data */
+	allwindows(allupdate, nil);
+}
+
+int
+getch(void)
+{
+	if(*cmdp == *cmdendp)
+		return -1;
+	return *cmdp++;
+}
+
+int
+nextc(void)
+{
+	if(*cmdp == *cmdendp)
+		return -1;
+	return *cmdp;
+}
+
+void
+ungetch(void)
+{
+	if(--cmdp < cmdstartp)
+		error("ungetch");
+}
+
+long
+getnum(int signok)
+{
+	long n;
+	int c, sign;
+
+	n = 0;
+	sign = 1;
+	if(signok>1 && nextc()=='-'){
+		sign = -1;
+		getch();
+	}
+	if((c=nextc())<'0' || '9'<c)	/* no number defaults to 1 */
+		return sign;
+	while('0'<=(c=getch()) && c<='9')
+		n = n*10 + (c-'0');
+	ungetch();
+	return sign*n;
+}
+
+int
+cmdskipbl(void)
+{
+	int c;
+	do
+		c = getch();
+	while(c==' ' || c=='\t');
+	if(c >= 0)
+		ungetch();
+	return c;
+}
+
+/*
+ * Check that list has room for one more element.
+ */
+void
+growlist(List *l)
+{
+	if(l->listptr==0 || l->nalloc==0){
+		l->nalloc = INCR;
+		l->listptr = emalloc(INCR*sizeof(void*));
+		l->nused = 0;
+	}else if(l->nused == l->nalloc){
+		l->listptr = erealloc(l->listptr, (l->nalloc+INCR)*sizeof(void*));
+		memset(l->ptr+l->nalloc, 0, INCR*sizeof(void*));
+		l->nalloc += INCR;
+	}
+}
+
+/*
+ * Remove the ith element from the list
+ */
+void
+dellist(List *l, int i)
+{
+	l->nused--;
+	memmove(&l->ptr[i], &l->ptr[i+1], (l->nused-i)*sizeof(void*));
+}
+
+/*
+ * Add a new element, whose position is i, to the list
+ */
+void
+inslist(List *l, int i, void *v)
+{
+	growlist(l);
+	memmove(&l->ptr[i+1], &l->ptr[i], (l->nused-i)*sizeof(void*));
+	l->ptr[i] = v;
+	l->nused++;
+}
+
+void
+listfree(List *l)
+{
+	free(l->listptr);
+	free(l);
+}
+
+String*
+allocstring(int n)
+{
+	String *s;
+
+	s = emalloc(sizeof(String));
+	s->n = n;
+	s->nalloc = n+10;
+	s->r = emalloc(s->nalloc*sizeof(Rune));
+	s->r[n] = '\0';
+	return s;
+}
+
+void
+freestring(String *s)
+{
+	free(s->r);
+	free(s);
+}
+
+Cmd*
+newcmd(void){
+	Cmd *p;
+
+	p = emalloc(sizeof(Cmd));
+	inslist(&cmdlist, cmdlist.nused, p);
+	return p;
+}
+
+String*
+newstring(int n)
+{
+	String *p;
+
+	p = allocstring(n);
+	inslist(&stringlist, stringlist.nused, p);
+	return p;
+}
+
+Addr*
+newaddr(void)
+{
+	Addr *p;
+
+	p = emalloc(sizeof(Addr));
+	inslist(&addrlist, addrlist.nused, p);
+	return p;
+}
+
+void
+freecmd(void)
+{
+	int i;
+
+	while(cmdlist.nused > 0)
+		free(cmdlist.ucharptr[--cmdlist.nused]);
+	while(addrlist.nused > 0)
+		free(addrlist.ucharptr[--addrlist.nused]);
+	while(stringlist.nused>0){
+		i = --stringlist.nused;
+		freestring(stringlist.stringptr[i]);
+	}
+}
+
+void
+okdelim(int c)
+{
+	if(c=='\\' || ('a'<=c && c<='z')
+	|| ('A'<=c && c<='Z') || ('0'<=c && c<='9'))
+		editerror("bad delimiter %c\n", c);
+}
+
+void
+atnl(void)
+{
+	int c;
+
+	cmdskipbl();
+	c = getch();
+	if(c != '\n')
+		editerror("newline expected (saw %C)", c);
+}
+
+void
+Straddc(String *s, int c)
+{
+	if(s->n+1 >= s->nalloc){
+		s->nalloc += 10;
+		s->r = erealloc(s->r, s->nalloc*sizeof(Rune));
+	}
+	s->r[s->n++] = c;
+	s->r[s->n] = '\0';
+}
+
+void
+getrhs(String *s, int delim, int cmd)
+{
+	int c;
+
+	while((c = getch())>0 && c!=delim && c!='\n'){
+		if(c == '\\'){
+			if((c=getch()) <= 0)
+				error("bad right hand side");
+			if(c == '\n'){
+				ungetch();
+				c='\\';
+			}else if(c == 'n')
+				c='\n';
+			else if(c!=delim && (cmd=='s' || c!='\\'))	/* s does its own */
+				Straddc(s, '\\');
+		}
+		Straddc(s, c);
+	}
+	ungetch();	/* let client read whether delimiter, '\n' or whatever */
+}
+
+String *
+collecttoken(char *end)
+{
+	String *s = newstring(0);
+	int c;
+
+	while((c=nextc())==' ' || c=='\t')
+		Straddc(s, getch()); /* blanks significant for getname() */
+	while((c=getch())>0 && utfrune(end, c)==0)
+		Straddc(s, c);
+	if(c != '\n')
+		atnl();
+	return s;
+}
+
+String *
+collecttext(void)
+{
+	String *s;
+	int begline, i, c, delim;
+
+	s = newstring(0);
+	if(cmdskipbl()=='\n'){
+		getch();
+		i = 0;
+		do{
+			begline = i;
+			while((c = getch())>0 && c!='\n')
+				i++, Straddc(s, c);
+			i++, Straddc(s, '\n');
+			if(c < 0)
+				goto Return;
+		}while(s->r[begline]!='.' || s->r[begline+1]!='\n');
+		s->r[s->n-2] = '\0';
+		s->n -= 2;
+	}else{
+		okdelim(delim = getch());
+		getrhs(s, delim, 'a');
+		if(nextc()==delim)
+			getch();
+		atnl();
+	}
+    Return:
+	return s;
+}
+
+int
+cmdlookup(int c)
+{
+	int i;
+
+	for(i=0; cmdtab[i].cmdc; i++)
+		if(cmdtab[i].cmdc == c)
+			return i;
+	return -1;
+}
+
+Cmd*
+parsecmd(int nest)
+{
+	int i, c;
+	struct cmdtab *ct;
+	Cmd *cp, *ncp;
+	Cmd cmd;
+
+	cmd.next = cmd.cmd = 0;
+	cmd.re = 0;
+	cmd.flag = cmd.num = 0;
+	cmd.addr = compoundaddr();
+	if(cmdskipbl() == -1)
+		return 0;
+	if((c=getch())==-1)
+		return 0;
+	cmd.cmdc = c;
+	if(cmd.cmdc=='c' && nextc()=='d'){	/* sleazy two-character case */
+		getch();		/* the 'd' */
+		cmd.cmdc='c'|0x100;
+	}
+	i = cmdlookup(cmd.cmdc);
+	if(i >= 0){
+		if(cmd.cmdc == '\n')
+			goto Return;	/* let nl_cmd work it all out */
+		ct = &cmdtab[i];
+		if(ct->defaddr==aNo && cmd.addr)
+			editerror("command takes no address");
+		if(ct->count)
+			cmd.num = getnum(ct->count);
+		if(ct->regexp){
+			/* x without pattern -> .*\n, indicated by cmd.re==0 */
+			/* X without pattern is all files */
+			if((ct->cmdc!='x' && ct->cmdc!='X') ||
+			   ((c = nextc())!=' ' && c!='\t' && c!='\n')){
+				cmdskipbl();
+				if((c = getch())=='\n' || c<0)
+					editerror("no address");
+				okdelim(c);
+				cmd.re = getregexp(c);
+				if(ct->cmdc == 's'){
+					cmd.text = newstring(0);
+					getrhs(cmd.text, c, 's');
+					if(nextc() == c){
+						getch();
+						if(nextc() == 'g')
+							cmd.flag = getch();
+					}
+			
+				}
+			}
+		}
+		if(ct->addr && (cmd.mtaddr=simpleaddr())==0)
+			editerror("bad address");
+		if(ct->defcmd){
+			if(cmdskipbl() == '\n'){
+				getch();
+				cmd.cmd = newcmd();
+				cmd.cmd->cmdc = ct->defcmd;
+			}else if((cmd.cmd = parsecmd(nest))==0)
+				error("defcmd");
+		}else if(ct->text)
+			cmd.text = collecttext();
+		else if(ct->token)
+			cmd.text = collecttoken(ct->token);
+		else
+			atnl();
+	}else
+		switch(cmd.cmdc){
+		case '{':
+			cp = 0;
+			do{
+				if(cmdskipbl()=='\n')
+					getch();
+				ncp = parsecmd(nest+1);
+				if(cp)
+					cp->next = ncp;
+				else
+					cmd.cmd = ncp;
+			}while(cp = ncp);
+			break;
+		case '}':
+			atnl();
+			if(nest==0)
+				editerror("right brace with no left brace");
+			return 0;
+		default:
+			editerror("unknown command %c", cmd.cmdc);
+		}
+    Return:
+	cp = newcmd();
+	*cp = cmd;
+	return cp;
+}
+
+String*
+getregexp(int delim)
+{
+	String *buf, *r;
+	int i, c;
+
+	buf = allocstring(0);
+	for(i=0; ; i++){
+		if((c = getch())=='\\'){
+			if(nextc()==delim)
+				c = getch();
+			else if(nextc()=='\\'){
+				Straddc(buf, c);
+				c = getch();
+			}
+		}else if(c==delim || c=='\n')
+			break;
+		if(i >= RBUFSIZE)
+			editerror("regular expression too long");
+		Straddc(buf, c);
+	}
+	if(c!=delim && c)
+		ungetch();
+	if(buf->n > 0){
+		patset = TRUE;
+		freestring(lastpat);
+		lastpat = buf;
+	}else
+		freestring(buf);
+	if(lastpat->n == 0)
+		editerror("no regular expression defined");
+	r = newstring(lastpat->n);
+	runemove(r->r, lastpat->r, lastpat->n);	/* newstring put \0 at end */
+	return r;
+}
+
+Addr *
+simpleaddr(void)
+{
+	Addr addr;
+	Addr *ap, *nap;
+
+	addr.next = 0;
+	addr.left = 0;
+	switch(cmdskipbl()){
+	case '#':
+		addr.type = getch();
+		addr.num = getnum(1);
+		break;
+	case '0': case '1': case '2': case '3': case '4':
+	case '5': case '6': case '7': case '8': case '9': 
+		addr.num = getnum(1);
+		addr.type='l';
+		break;
+	case '/': case '?': case '"':
+		addr.re = getregexp(addr.type = getch());
+		break;
+	case '.':
+	case '$':
+	case '+':
+	case '-':
+	case '\'':
+		addr.type = getch();
+		break;
+	default:
+		return 0;
+	}
+	if(addr.next = simpleaddr())
+		switch(addr.next->type){
+		case '.':
+		case '$':
+		case '\'':
+			if(addr.type!='"')
+		case '"':
+				editerror("bad address syntax");
+			break;
+		case 'l':
+		case '#':
+			if(addr.type=='"')
+				break;
+			/* fall through */
+		case '/':
+		case '?':
+			if(addr.type!='+' && addr.type!='-'){
+				/* insert the missing '+' */
+				nap = newaddr();
+				nap->type='+';
+				nap->next = addr.next;
+				addr.next = nap;
+			}
+			break;
+		case '+':
+		case '-':
+			break;
+		default:
+			error("simpleaddr");
+		}
+	ap = newaddr();
+	*ap = addr;
+	return ap;
+}
+
+Addr *
+compoundaddr(void)
+{
+	Addr addr;
+	Addr *ap, *next;
+
+	addr.left = simpleaddr();
+	if((addr.type = cmdskipbl())!=',' && addr.type!=';')
+		return addr.left;
+	getch();
+	next = addr.next = compoundaddr();
+	if(next && (next->type==',' || next->type==';') && next->left==0)
+		editerror("bad address syntax");
+	ap = newaddr();
+	*ap = addr;
+	return ap;
+}
--- /dev/null
+++ b/edit.h
@@ -1,0 +1,99 @@
+#pragma	varargck	argpos	editerror	1
+
+typedef struct Addr	Addr;
+typedef struct Address	Address;
+typedef struct Cmd	Cmd;
+typedef struct List	List;
+typedef struct String	String;
+
+struct String
+{
+	int	n;		/* excludes NUL */
+	Rune	*r;		/* includes NUL */
+	int	nalloc;
+};
+
+struct Addr
+{
+	char	type;	/* # (char addr), l (line addr), / ? . $ + - , ; */
+	union{
+		String	*re;
+		Addr	*left;		/* left side of , and ; */
+	};
+	ulong	num;
+	Addr	*next;			/* or right side of , and ; */
+};
+
+struct Address
+{
+	Range	r;
+	File	*f;
+};
+
+struct Cmd
+{
+	Addr	*addr;			/* address (range of text) */
+	String	*re;			/* regular expression for e.g. 'x' */
+	union{
+		Cmd	*cmd;		/* target of x, g, {, etc. */
+		String	*text;		/* text of a, c, i; rhs of s */
+		Addr	*mtaddr;		/* address for m, t */
+	};
+	Cmd	*next;			/* pointer to next element in {} */
+	short	num;
+	ushort	flag;			/* whatever */
+	ushort	cmdc;			/* command character; 'x' etc. */
+};
+
+extern struct cmdtab{
+	ushort	cmdc;		/* command character */
+	uchar	text;		/* takes a textual argument? */
+	uchar	regexp;		/* takes a regular expression? */
+	uchar	addr;		/* takes an address (m or t)? */
+	uchar	defcmd;		/* default command; 0==>none */
+	uchar	defaddr;	/* default address */
+	uchar	count;		/* takes a count e.g. s2/// */
+	char	*token;		/* takes text terminated by one of these */
+	int	(*fn)(Text*, Cmd*);	/* function to call with parse tree */
+}cmdtab[];
+
+#define	INCR	25	/* delta when growing list */
+
+struct List
+{
+	int	nalloc;
+	int	nused;
+	union{
+		void	*listptr;
+		void*	*ptr;
+		uchar*	*ucharptr;
+		String*	*stringptr;
+	};
+};
+
+enum Defaddr{	/* default addresses */
+	aNo,
+	aDot,
+	aAll,
+};
+
+int	nl_cmd(Text*, Cmd*), a_cmd(Text*, Cmd*), b_cmd(Text*, Cmd*);
+int	c_cmd(Text*, Cmd*), d_cmd(Text*, Cmd*);
+int	B_cmd(Text*, Cmd*), D_cmd(Text*, Cmd*), e_cmd(Text*, Cmd*);
+int	f_cmd(Text*, Cmd*), g_cmd(Text*, Cmd*), i_cmd(Text*, Cmd*);
+int	k_cmd(Text*, Cmd*), m_cmd(Text*, Cmd*), n_cmd(Text*, Cmd*);
+int	p_cmd(Text*, Cmd*);
+int	s_cmd(Text*, Cmd*), u_cmd(Text*, Cmd*), w_cmd(Text*, Cmd*);
+int	x_cmd(Text*, Cmd*), X_cmd(Text*, Cmd*), pipe_cmd(Text*, Cmd*);
+int	eq_cmd(Text*, Cmd*);
+
+String	*allocstring(int);
+void	freestring(String*);
+String	*getregexp(int);
+Addr	*newaddr(void);
+Address	cmdaddress(Addr*, Address, int);
+int	cmdexec(Text*, Cmd*);
+void	editerror(char*, ...);
+int	cmdlookup(int);
+void	resetxec(void);
+void	Straddc(String*, int);
--- /dev/null
+++ b/elog.c
@@ -1,0 +1,353 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <cursor.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <frame.h>
+#include <fcall.h>
+#include <plumb.h>
+#include "dat.h"
+#include "fns.h"
+#include "edit.h"
+
+static char Wsequence[] = "warning: changes out of sequence\n";
+static int	warned = FALSE;
+
+/*
+ * Log of changes made by editing commands.  Three reasons for this:
+ * 1) We want addresses in commands to apply to old file, not file-in-change.
+ * 2) It's difficult to track changes correctly as things move, e.g. ,x m$
+ * 3) This gives an opportunity to optimize by merging adjacent changes.
+ * It's a little bit like the Undo/Redo log in Files, but Point 3) argues for a
+ * separate implementation.  To do this well, we use Replace as well as
+ * Insert and Delete
+ */
+
+typedef struct Buflog Buflog;
+struct Buflog
+{
+	short	type;		/* Replace, Filename */
+	uint		q0;		/* location of change (unused in f) */
+	uint		nd;		/* # runes to delete */
+	uint		nr;		/* # runes in string or file name */
+};
+
+enum
+{
+	Buflogsize = sizeof(Buflog)/sizeof(Rune),
+};
+
+/*
+ * Minstring shouldn't be very big or we will do lots of I/O for small changes.
+ * Maxstring is RBUFSIZE so we can fbufalloc() once and not realloc elog.r.
+ */
+enum
+{
+	Minstring = 16,		/* distance beneath which we merge changes */
+	Maxstring = RBUFSIZE,	/* maximum length of change we will merge into one */
+};
+
+void
+eloginit(File *f)
+{
+	if(f->elog.type != Empty)
+		return;
+	f->elog.type = Null;
+	if(f->elogbuf == nil)
+		f->elogbuf = emalloc(sizeof(Buffer));
+	if(f->elog.r == nil)
+		f->elog.r = fbufalloc();
+	bufreset(f->elogbuf);
+}
+
+void
+elogclose(File *f)
+{
+	if(f->elogbuf){
+		bufclose(f->elogbuf);
+		free(f->elogbuf);
+		f->elogbuf = nil;
+	}
+}
+
+void
+elogreset(File *f)
+{
+	f->elog.type = Null;
+	f->elog.nd = 0;
+	f->elog.nr = 0;
+}
+
+void
+elogterm(File *f)
+{
+	elogreset(f);
+	if(f->elogbuf)
+		bufreset(f->elogbuf);
+	f->elog.type = Empty;
+	fbuffree(f->elog.r);
+	f->elog.r = nil;
+	warned = FALSE;
+}
+
+void
+elogflush(File *f)
+{
+	Buflog b;
+
+	b.type = f->elog.type;
+	b.q0 = f->elog.q0;
+	b.nd = f->elog.nd;
+	b.nr = f->elog.nr;
+	switch(f->elog.type){
+	default:
+		warning(nil, "unknown elog type 0x%ux\n", f->elog.type);
+		break;
+	case Null:
+		break;
+	case Insert:
+	case Replace:
+		if(f->elog.nr > 0)
+			bufinsert(f->elogbuf, f->elogbuf->nc, f->elog.r, f->elog.nr);
+		/* fall through */
+	case Delete:
+		bufinsert(f->elogbuf, f->elogbuf->nc, (Rune*)&b, Buflogsize);
+		break;
+	}
+	elogreset(f);
+}
+
+void
+elogreplace(File *f, int q0, int q1, Rune *r, int nr)
+{
+	uint gap;
+
+	if(q0==q1 && nr==0)
+		return;
+	eloginit(f);
+	if(f->elog.type!=Null && q0<f->elog.q0){
+		if(warned++ == 0)
+			warning(nil, Wsequence);
+		elogflush(f);
+	}
+	/* try to merge with previous */
+	gap = q0 - (f->elog.q0+f->elog.nd);	/* gap between previous and this */
+	if(f->elog.type==Replace && f->elog.nr+gap+nr<Maxstring){
+		if(gap < Minstring){
+			if(gap > 0){
+				bufread(f, f->elog.q0+f->elog.nd, f->elog.r+f->elog.nr, gap);
+				f->elog.nr += gap;
+			}
+			f->elog.nd += gap + q1-q0;
+			runemove(f->elog.r+f->elog.nr, r, nr);
+			f->elog.nr += nr;
+			return;
+		}
+	}
+	elogflush(f);
+	f->elog.type = Replace;
+	f->elog.q0 = q0;
+	f->elog.nd = q1-q0;
+	f->elog.nr = nr;
+	if(nr > RBUFSIZE)
+		editerror("internal error: replacement string too large(%d)", nr);
+	runemove(f->elog.r, r, nr);
+}
+
+void
+eloginsert(File *f, int q0, Rune *r, int nr)
+{
+	int n;
+
+	if(nr == 0)
+		return;
+	eloginit(f);
+	if(f->elog.type!=Null && q0<f->elog.q0){
+		if(warned++ == 0)
+			warning(nil, Wsequence);
+		elogflush(f);
+	}
+	/* try to merge with previous */
+	if(f->elog.type==Insert && q0==f->elog.q0 && f->elog.nr+nr<Maxstring){
+		runemove(f->elog.r+f->elog.nr, r, nr);
+		f->elog.nr += nr;
+		return;
+	}
+	while(nr > 0){
+		elogflush(f);
+		f->elog.type = Insert;
+		f->elog.q0 = q0;
+		n = nr;
+		if(n > RBUFSIZE)
+			n = RBUFSIZE;
+		f->elog.nr = n;
+		runemove(f->elog.r, r, n);
+		r += n;
+		nr -= n;
+	}
+}
+
+void
+elogdelete(File *f, int q0, int q1)
+{
+	if(q0 == q1)
+		return;
+	eloginit(f);
+	if(f->elog.type!=Null && q0<f->elog.q0+f->elog.nd){
+		if(warned++ == 0)
+			warning(nil, Wsequence);
+		elogflush(f);
+	}
+	/* try to merge with previous */
+	if(f->elog.type==Delete && f->elog.q0+f->elog.nd==q0){
+		f->elog.nd += q1-q0;
+		return;
+	}
+	elogflush(f);
+	f->elog.type = Delete;
+	f->elog.q0 = q0;
+	f->elog.nd = q1-q0;
+}
+
+#define tracelog 0
+void
+elogapply(File *f)
+{
+	Buflog b;
+	Rune *buf;
+	uint i, n, up, mod;
+	uint tq0, tq1;
+	Buffer *log;
+	Text *t;
+	int owner;
+
+	elogflush(f);
+	log = f->elogbuf;
+	t = f->curtext;
+
+	buf = fbufalloc();
+	mod = FALSE;
+
+	owner = 0;
+	if(t->w){
+		owner = t->w->owner;
+		if(owner == 0)
+			t->w->owner = 'E';
+	}
+
+	/*
+	 * The edit commands have already updated the selection in t->q0, t->q1,
+	 * but using coordinates relative to the unmodified buffer.  As we apply the log,
+	 * we have to update the coordinates to be relative to the modified buffer.
+	 * Textinsert and textdelete will do this for us; our only work is to apply the
+	 * convention that an insertion at t->q0==t->q1 is intended to select the 
+	 * inserted text.
+	 */
+
+	/*
+	 * We constrain the addresses in here (with textconstrain()) because
+	 * overlapping changes will generate bogus addresses.   We will warn
+	 * about changes out of sequence but proceed anyway; here we must
+	 * keep things in range.
+	 */
+
+	while(log->nc > 0){
+		up = log->nc-Buflogsize;
+		bufread(log, up, (Rune*)&b, Buflogsize);
+		switch(b.type){
+		default:
+			fprint(2, "elogapply: 0x%ux\n", b.type);
+			abort();
+			break;
+
+		case Replace:
+			if(tracelog)
+				warning(nil, "elog replace %d %d (%d %d)\n",
+					b.q0, b.q0+b.nd, t->q0, t->q1);
+			if(!mod){
+				mod = TRUE;
+				filemark(f);
+			}
+			textconstrain(t, b.q0, b.q0+b.nd, &tq0, &tq1);
+			textdelete(t, tq0, tq1, TRUE);
+			up -= b.nr;
+			for(i=0; i<b.nr; i+=n){
+				n = b.nr - i;
+				if(n > RBUFSIZE)
+					n = RBUFSIZE;
+				bufread(log, up+i, buf, n);
+				textinsert(t, tq0+i, buf, n, TRUE);
+			}
+			if(t->q0 == b.q0 && t->q1 == b.q0)
+				t->q1 += b.nr;
+			break;
+
+		case Delete:
+			if(tracelog)
+				warning(nil, "elog delete %d %d (%d %d)\n",
+					b.q0, b.q0+b.nd, t->q0, t->q1);
+			if(!mod){
+				mod = TRUE;
+				filemark(f);
+			}
+			textconstrain(t, b.q0, b.q0+b.nd, &tq0, &tq1);
+			textdelete(t, tq0, tq1, TRUE);
+			break;
+
+		case Insert:
+			if(tracelog)
+				warning(nil, "elog insert %d %d (%d %d)\n",
+					b.q0, b.q0+b.nr, t->q0, t->q1);
+			if(!mod){
+				mod = TRUE;
+				filemark(f);
+			}
+			textconstrain(t, b.q0, b.q0, &tq0, &tq1);
+			up -= b.nr;
+			for(i=0; i<b.nr; i+=n){
+				n = b.nr - i;
+				if(n > RBUFSIZE)
+					n = RBUFSIZE;
+				bufread(log, up+i, buf, n);
+				textinsert(t, tq0+i, buf, n, TRUE);
+			}
+			if(t->q0 == b.q0 && t->q1 == b.q0)
+				t->q1 += b.nr;
+			break;
+
+/*		case Filename:
+			f->seq = u.seq;
+			fileunsetname(f, epsilon);
+			f->mod = u.mod;
+			up -= u.n;
+			free(f->name);
+			if(u.n == 0)
+				f->name = nil;
+			else
+				f->name = runemalloc(u.n);
+			bufread(delta, up, f->name, u.n);
+			f->nname = u.n;
+			break;
+*/
+		}
+		bufdelete(log, up, log->nc);
+	}
+	fbuffree(buf);
+	elogterm(f);
+
+	/*
+	 * Bad addresses will cause bufload to crash, so double check.
+	 * If changes were out of order, we expect problems so don't complain further.
+	 */
+	if(t->q0 > f->nc || t->q1 > f->nc || t->q0 > t->q1){
+		if(!warned)
+			warning(nil, "elogapply: can't happen %d %d %d\n", t->q0, t->q1, f->nc);
+		t->q1 = min(t->q1, f->nc);
+		t->q0 = min(t->q0, t->q1);
+	}
+
+	if(t->w)
+		t->w->owner = owner;
+}
--- /dev/null
+++ b/exec.c
@@ -1,0 +1,1501 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <cursor.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <frame.h>
+#include <fcall.h>
+#include <plumb.h>
+#include "dat.h"
+#include "fns.h"
+
+Buffer	snarfbuf;
+
+/*
+ * These functions get called as:
+ *
+ *	fn(et, t, argt, flag1, flag1, flag2, s, n);
+ *
+ * Where the arguments are:
+ *
+ *	et: the Text* in which the executing event (click) occurred
+ *	t: the Text* containing the current selection (Edit, Cut, Snarf, Paste)
+ *	argt: the Text* containing the argument for a 2-1 click.
+ *	e->flag1: from Exectab entry
+ * 	e->flag2: from Exectab entry
+ *	s: the command line remainder (e.g., "x" if executing "Dump x")
+ *	n: length of s  (s is *not* NUL-terminated)
+ */
+
+void	del(Text*, Text*, Text*, int, int, Rune*, int);
+void	delcol(Text*, Text*, Text*, int, int, Rune*, int);
+void	dump(Text*, Text*, Text*, int, int, Rune*, int);
+void	edit(Text*, Text*, Text*, int, int, Rune*, int);
+void	exit(Text*, Text*, Text*, int, int, Rune*, int);
+void	fontx(Text*, Text*, Text*, int, int, Rune*, int);
+void	get(Text*, Text*, Text*, int, int, Rune*, int);
+void	id(Text*, Text*, Text*, int, int, Rune*, int);
+void	incl(Text*, Text*, Text*, int, int, Rune*, int);
+void	indent(Text*, Text*, Text*, int, int, Rune*, int);
+void	kill(Text*, Text*, Text*, int, int, Rune*, int);
+void	local(Text*, Text*, Text*, int, int, Rune*, int);
+void	look(Text*, Text*, Text*, int, int, Rune*, int);
+void	newcol(Text*, Text*, Text*, int, int, Rune*, int);
+void	paste(Text*, Text*, Text*, int, int, Rune*, int);
+void	put(Text*, Text*, Text*, int, int, Rune*, int);
+void	putall(Text*, Text*, Text*, int, int, Rune*, int);
+void	sendx(Text*, Text*, Text*, int, int, Rune*, int);
+void	sort(Text*, Text*, Text*, int, int, Rune*, int);
+void	tab(Text*, Text*, Text*, int, int, Rune*, int);
+void	zeroxx(Text*, Text*, Text*, int, int, Rune*, int);
+
+typedef struct Exectab Exectab;
+struct Exectab
+{
+	Rune	*name;
+	void	(*fn)(Text*, Text*, Text*, int, int, Rune*, int);
+	int		mark;
+	int		flag1;
+	int		flag2;
+};
+
+Exectab exectab[] = {
+	{ L"Cut",		cut,		TRUE,	TRUE,	TRUE	},
+	{ L"Del",		del,		FALSE,	FALSE,	XXX		},
+	{ L"Delcol",	delcol,	FALSE,	XXX,		XXX		},
+	{ L"Delete",	del,		FALSE,	TRUE,	XXX		},
+	{ L"Dump",	dump,	FALSE,	TRUE,	XXX		},
+	{ L"Edit",		edit,		FALSE,	XXX,		XXX		},
+	{ L"Exit",		exit,		FALSE,	XXX,		XXX		},
+	{ L"Font",		fontx,	FALSE,	XXX,		XXX		},
+	{ L"Get",		get,		FALSE,	TRUE,	XXX		},
+	{ L"ID",		id,		FALSE,	XXX,		XXX		},
+	{ L"Incl",		incl,		FALSE,	XXX,		XXX		},
+	{ L"Indent",	indent,		FALSE,	AUTOINDENT,		XXX		},
+	{ L"Kill",		kill,		FALSE,	XXX,		XXX		},
+	{ L"Load",		dump,	FALSE,	FALSE,	XXX		},
+	{ L"Local",		local,	FALSE,	XXX,		XXX		},
+	{ L"Look",		look,		FALSE,	XXX,		XXX		},
+	{ L"New",		new,		FALSE,	XXX,		XXX		},
+	{ L"Newcol",	newcol,	FALSE,	XXX,		XXX		},
+	{ L"Paste",		paste,	TRUE,	TRUE,	XXX		},
+	{ L"Put",		put,		FALSE,	XXX,		XXX		},
+	{ L"Putall",		putall,	FALSE,	XXX,		XXX		},
+	{ L"Redo",		undo,	FALSE,	FALSE,	XXX		},
+	{ L"Send",		sendx,	TRUE,	XXX,		XXX		},
+	{ L"Snarf",		cut,		FALSE,	TRUE,	FALSE	},
+	{ L"Sort",		sort,		FALSE,	XXX,		XXX		},
+	{ L"Spaces",	indent,		FALSE,	SPACESINDENT,	XXX		},
+	{ L"Tab",		tab,		FALSE,	XXX,		XXX		},
+	{ L"Undo",		undo,	FALSE,	TRUE,	XXX		},
+	{ L"Zerox",	zeroxx,	FALSE,	XXX,		XXX		},
+	{ nil, 			nil,		0,		0,		0		},
+};
+
+Exectab*
+lookup(Rune *r, int n)
+{
+	Exectab *e;
+	int nr;
+
+	r = skipbl(r, n, &n);
+	if(n == 0)
+		return nil;
+	findbl(r, n, &nr);
+	nr = n-nr;
+	for(e=exectab; e->name; e++)
+		if(runeeq(r, nr, e->name, runestrlen(e->name)) == TRUE)
+			return e;
+	return nil;
+}
+
+int
+isexecc(int c)
+{
+	if(isfilec(c))
+		return 1;
+	return c=='<' || c=='|' || c=='>';
+}
+
+void
+execute(Text *t, uint aq0, uint aq1, int external, Text *argt)
+{
+	uint q0, q1;
+	Rune *r, *s;
+	char *b, *a, *aa;
+	Exectab *e;
+	int c, n, f;
+	Runestr dir;
+
+	q0 = aq0;
+	q1 = aq1;
+	if(q1 == q0){	/* expand to find word (actually file name) */
+		/* if in selection, choose selection */
+		if(t->q1>t->q0 && t->q0<=q0 && q0<=t->q1){
+			q0 = t->q0;
+			q1 = t->q1;
+		}else{
+			while(q1<t->file->nc && isexecc(c=textreadc(t, q1)) && c!=':')
+				q1++;
+			while(q0>0 && isexecc(c=textreadc(t, q0-1)) && c!=':')
+				q0--;
+			if(q1 == q0)
+				return;
+		}
+	}
+	r = runemalloc(q1-q0);
+	bufread(t->file, q0, r, q1-q0);
+	free(delcmd);
+	delcmd = runesmprint("%.*S", q1-q0, r);
+	e = lookup(r, q1-q0);
+	if(!external && t->w!=nil && t->w->nopen[QWevent]>0){
+		f = 0;
+		if(e)
+			f |= 1;
+		if(q0!=aq0 || q1!=aq1){
+			bufread(t->file, aq0, r, aq1-aq0);
+			f |= 2;
+		}
+		aa = getbytearg(argt, TRUE, TRUE, &a);
+		if(a){	
+			if(strlen(a) > EVENTSIZE){	/* too big; too bad */
+				free(r);
+				free(aa);
+				free(a);
+				warning(nil, "`argument string too long\n");
+				return;
+			}
+			f |= 8;
+		}
+		c = 'x';
+		if(t->what == Body)
+			c = 'X';
+		n = aq1-aq0;
+		if(n <= EVENTSIZE)
+			winevent(t->w, "%c%d %d %d %d %.*S\n", c, aq0, aq1, f, n, n, r);
+		else
+			winevent(t->w, "%c%d %d %d 0 \n", c, aq0, aq1, f, n);
+		if(q0!=aq0 || q1!=aq1){
+			n = q1-q0;
+			bufread(t->file, q0, r, n);
+			if(n <= EVENTSIZE)
+				winevent(t->w, "%c%d %d 0 %d %.*S\n", c, q0, q1, n, n, r);
+			else
+				winevent(t->w, "%c%d %d 0 0 \n", c, q0, q1, n);
+		}
+		if(a){
+			winevent(t->w, "%c0 0 0 %d %s\n", c, utflen(a), a);
+			if(aa)
+				winevent(t->w, "%c0 0 0 %d %s\n", c, utflen(aa), aa);
+			else
+				winevent(t->w, "%c0 0 0 0 \n", c);
+		}
+		free(r);
+		free(aa);
+		free(a);
+		return;
+	}
+	if(e){
+		if(e->mark && seltext!=nil)
+		if(seltext->what == Body){
+			seq++;
+			filemark(seltext->w->body.file);
+		}
+		s = skipbl(r, q1-q0, &n);
+		s = findbl(s, n, &n);
+		s = skipbl(s, n, &n);
+		(*e->fn)(t, seltext, argt, e->flag1, e->flag2, s, n);
+		free(r);
+		return;
+	}
+
+	b = runetobyte(r, q1-q0);
+	free(r);
+	dir = dirname(t, nil, 0);
+	if(dir.nr==1 && dir.r[0]=='.'){	/* sigh */
+		free(dir.r);
+		dir.r = nil;
+		dir.nr = 0;
+	}
+	aa = getbytearg(argt, TRUE, TRUE, &a);
+	if(t->w)
+		incref(t->w);
+	run(t->w, b, dir.r, dir.nr, TRUE, aa, a, FALSE);
+}
+
+char*
+printarg(Text *argt, uint q0, uint q1)
+{
+	char *buf;
+
+	if(argt->what!=Body || argt->file->name==nil)
+		return nil;
+	buf = emalloc(argt->file->nname+32);
+	if(q0 == q1)
+		sprint(buf, "%.*S:#%d", argt->file->nname, argt->file->name, q0);
+	else
+		sprint(buf, "%.*S:#%d,#%d", argt->file->nname, argt->file->name, q0, q1);
+	return buf;
+}
+
+char*
+getarg(Text *argt, int doaddr, int dofile, Rune **rp, int *nrp)
+{
+	int n;
+	Expand e;
+	char *a;
+
+	*rp = nil;
+	*nrp = 0;
+	if(argt == nil)
+		return nil;
+	a = nil;
+	textcommit(argt, TRUE);
+	if(expand(argt, argt->q0, argt->q1, &e)){
+		free(e.bname);
+		if(e.nname && dofile){
+			e.name = runerealloc(e.name, e.nname+1);
+			if(doaddr)
+				a = printarg(argt, e.q0, e.q1);
+			*rp = e.name;
+			*nrp = e.nname;
+			return a;
+		}
+		free(e.name);
+	}else{
+		e.q0 = argt->q0;
+		e.q1 = argt->q1;
+	}
+	n = e.q1 - e.q0;
+	*rp = runemalloc(n+1);
+	bufread(argt->file, e.q0, *rp, n);
+	if(doaddr)
+		a = printarg(argt, e.q0, e.q1);
+	*nrp = n;
+	return a;
+}
+
+char*
+getbytearg(Text *argt, int doaddr, int dofile, char **bp)
+{
+	Rune *r;
+	int n;
+	char *aa;
+
+	*bp = nil;
+	aa = getarg(argt, doaddr, dofile, &r, &n);
+	if(r == nil)
+		return nil;
+	*bp = runetobyte(r, n);
+	free(r);
+	return aa;
+}
+
+void
+newcol(Text *et, Text*, Text*, int, int, Rune*, int)
+{
+	Column *c;
+	Window *w;
+
+	c = rowadd(et->row, nil, -1);
+	if(c) {
+		w = coladd(c, nil, nil, -1);
+		winsettag(w);
+		xfidlog(w, "new");
+	}
+}
+
+void
+delcol(Text *et, Text*, Text*, int, int, Rune*, int)
+{
+	int i;
+	Column *c;
+	Window *w;
+
+	c = et->col;
+	if(c==nil || colclean(c)==0)
+		return;
+	for(i=0; i<c->nw; i++){
+		w = c->w[i];
+		if(w->nopen[QWevent]+w->nopen[QWaddr]+w->nopen[QWdata]+w->nopen[QWxdata] > 0){
+			warning(nil, "can't delete column; %.*S is running an external command\n", w->body.file->nname, w->body.file->name);
+			return;
+		}
+	}
+	rowclose(et->col->row, et->col, TRUE);
+}
+
+void
+del(Text *et, Text*, Text *argt, int flag1, int, Rune *arg, int narg)
+{
+	Window *w;
+	char *name, *p;
+	Plumbmsg *pm;
+
+	if(et->col==nil || et->w == nil)
+		return;
+	if(flag1 || et->w->body.file->ntext>1 || winclean(et->w, FALSE)){
+		w = et->w;
+		name = getname(&w->body, argt, arg, narg, TRUE);
+		if(name && plumbsendfd >= 0){
+			pm = emalloc(sizeof(Plumbmsg));
+			pm->src = estrdup("acme");
+			pm->dst = estrdup("close");
+			pm->wdir = estrdup(name);
+			if(p = strrchr(pm->wdir, '/'))
+				*p = '\0';
+			pm->type = estrdup("text");
+			pm->attr = nil;
+			pm->data = estrdup(name);
+			pm->ndata = strlen(pm->data);
+			if(pm->ndata < messagesize-1024)
+				plumbsend(plumbsendfd, pm);
+			else
+				plumbfree(pm);
+		}
+		colclose(et->col, et->w, TRUE);
+	}
+}
+
+void
+sort(Text *et, Text*, Text*, int, int, Rune*, int)
+{
+	if(et->col)
+		colsort(et->col);
+}
+
+uint
+seqof(Window *w, int isundo)
+{
+	/* if it's undo, see who changed with us */
+	if(isundo)
+		return w->body.file->seq;
+	/* if it's redo, see who we'll be sync'ed up with */
+	return fileredoseq(w->body.file);
+}
+
+void
+undo(Text *et, Text*, Text*, int flag1, int, Rune*, int)
+{
+	int i, j;
+	Column *c;
+	Window *w;
+	uint seq;
+
+	if(et==nil || et->w== nil)
+		return;
+	seq = seqof(et->w, flag1);
+	if(seq == 0){
+		/* nothing to undo */
+		return;
+	}
+	/*
+	 * Undo the executing window first. Its display will update. other windows
+	 * in the same file will not call show() and jump to a different location in the file.
+	 * Simultaneous changes to other files will be chaotic, however.
+	 */
+	winundo(et->w, flag1);
+	for(i=0; i<row.ncol; i++){
+		c = row.col[i];
+		for(j=0; j<c->nw; j++){
+			w = c->w[j];
+			if(w == et->w)
+				continue;
+			if(seqof(w, flag1) == seq)
+				winundo(w, flag1);
+		}
+	}
+}
+
+char*
+getname(Text *t, Text *argt, Rune *arg, int narg, int isput)
+{
+	char *s;
+	Rune *r;
+	int i, n, promote;
+	Runestr dir;
+
+	getarg(argt, FALSE, TRUE, &r, &n);
+	promote = FALSE;
+	if(r == nil)
+		promote = TRUE;
+	else if(isput){
+		/* if are doing a Put, want to synthesize name even for non-existent file */
+		/* best guess is that file name doesn't contain a slash */
+		promote = TRUE;
+		for(i=0; i<n; i++)
+			if(r[i] == '/'){
+				promote = FALSE;
+				break;
+			}
+		if(promote){
+			t = argt;
+			arg = r;
+			narg = n;
+		}
+	}
+	if(promote){
+		n = narg;
+		if(n <= 0){
+			s = runetobyte(t->file->name, t->file->nname);
+			return s;
+		}
+		/* prefix with directory name if necessary */
+		dir.r = nil;
+		dir.nr = 0;
+		if(n>0 && arg[0]!='/'){
+			dir = dirname(t, nil, 0);
+			if(dir.nr==1 && dir.r[0]=='.'){	/* sigh */
+				free(dir.r);
+				dir.r = nil;
+				dir.nr = 0;
+			}
+		}
+		if(dir.r){
+			r = runemalloc(dir.nr+n+1);
+			runemove(r, dir.r, dir.nr);
+			free(dir.r);
+			if(dir.nr>0 && r[dir.nr]!='/' && n>0 && arg[0]!='/')
+				r[dir.nr++] = '/';
+			runemove(r+dir.nr, arg, n);
+			n += dir.nr;
+		}else{
+			r = runemalloc(n+1);
+			runemove(r, arg, n);
+		}
+	}
+	s = runetobyte(r, n);
+	free(r);
+	if(strlen(s) == 0){
+		free(s);
+		s = nil;
+	}
+	return s;
+}
+
+void
+zeroxx(Text *et, Text *t, Text*, int, int, Rune*, int)
+{
+	Window *nw;
+	int c, locked;
+
+	locked = FALSE;
+	if(t!=nil && t->w!=nil && t->w!=et->w){
+		locked = TRUE;
+		c = 'M';
+		if(et->w)
+			c = et->w->owner;
+		winlock(t->w, c);
+	}
+	if(t == nil)
+		t = et;
+	if(t==nil || t->w==nil)
+		return;
+	t = &t->w->body;
+	if(t->w->isdir)
+		warning(nil, "%.*S is a directory; Zerox illegal\n", t->file->nname, t->file->name);
+	else{
+		nw = coladd(t->w->col, nil, t->w, -1);
+		/* ugly: fix locks so w->unlock works */
+		winlock1(nw, t->w->owner);
+		xfidlog(nw, "zerox");
+	}
+	if(locked)
+		winunlock(t->w);
+}
+
+typedef struct TextAddr TextAddr;
+struct TextAddr {
+	long lorigin; // line+rune for origin
+	long rorigin;
+	long lq0; // line+rune for q0
+	long rq0;
+	long lq1; // line+rune for q1
+	long rq1;
+};
+
+void
+get(Text *et, Text *t, Text *argt, int flag1, int, Rune *arg, int narg)
+{
+	char *name;
+	Rune *r;
+	int i, n, dirty, samename, isdir;
+	TextAddr *addr, *a;
+	Window *w;
+	Text *u;
+	Dir *d;
+	long q0, q1;
+
+	if(flag1)
+		if(et==nil || et->w==nil)
+			return;
+	if(!et->w->isdir && (et->w->body.file->nc>0 && !winclean(et->w, TRUE)))
+		return;
+	w = et->w;
+	t = &w->body;
+	name = getname(t, argt, arg, narg, FALSE);
+	if(name == nil){
+		warning(nil, "no file name\n");
+		return;
+	}
+	if(t->file->ntext>1){
+		d = dirstat(name);
+		isdir = (d!=nil && (d->qid.type & QTDIR));
+		free(d);
+		if(isdir){
+			warning(nil, "%s is a directory; can't read with multiple windows on it\n", name);
+			return;
+		}
+	}
+	addr = emalloc((t->file->ntext)*sizeof(TextAddr));
+	for(i=0; i<t->file->ntext; i++) {
+		a = &addr[i];
+		u = t->file->text[i];
+		a->lorigin = nlcount(u, 0, u->org, &a->rorigin);
+		a->lq0 = nlcount(u, 0, u->q0, &a->rq0);
+		a->lq1 = nlcount(u, u->q0, u->q1, &a->rq1);
+	}
+	r = bytetorune(name, &n);
+	for(i=0; i<t->file->ntext; i++){
+		u = t->file->text[i];
+		/* second and subsequent calls with zero an already empty buffer, but OK */
+		textreset(u);
+		windirfree(u->w);
+	}
+	samename = runeeq(r, n, t->file->name, t->file->nname);
+	textload(t, 0, name, samename);
+	if(samename){
+		t->file->mod = FALSE;
+		dirty = FALSE;
+	}else{
+		t->file->mod = TRUE;
+		dirty = TRUE;
+	}
+	for(i=0; i<t->file->ntext; i++)
+		t->file->text[i]->w->dirty = dirty;
+	free(name);
+	free(r);
+	winsettag(w);
+	t->file->unread = FALSE;
+	for(i=0; i<t->file->ntext; i++){
+		u = t->file->text[i];
+		textsetselect(&u->w->tag, u->w->tag.file->nc, u->w->tag.file->nc);
+		if(samename) {
+			a = &addr[i];
+			// warning(nil, "%d %d %d %d %d %d\n", a->lorigin, a->rorigin, a->lq0, a->rq0, a->lq1, a->rq1);
+			q0 = nlcounttopos(u, 0, a->lq0, a->rq0);
+			q1 = nlcounttopos(u, q0, a->lq1, a->rq1);
+			textsetselect(u, q0, q1);
+			q0 = nlcounttopos(u, 0, a->lorigin, a->rorigin);
+			textsetorigin(u, q0, FALSE);
+		}
+		textscrdraw(u);
+	}
+	free(addr);
+	xfidlog(w, "get");
+}
+
+void
+putfile(File *f, int q0, int q1, Rune *namer, int nname)
+{
+	uint n, m;
+	Rune *r;
+	char *s, *name, *p;
+	int i, fd, q;
+	Dir *d, *d1;
+	Window *w;
+	Plumbmsg *pm;
+	int isapp;
+
+	w = f->curtext->w;
+	name = runetobyte(namer, nname);
+	d = dirstat(name);
+	if(d!=nil && runeeq(namer, nname, f->name, f->nname)){
+		/* f->mtime+1 because when talking over NFS it's often off by a second */
+		if(f->dev!=d->dev || f->qidpath!=d->qid.path || f->mtime+1<d->mtime){
+			f->dev = d->dev;
+			f->qidpath = d->qid.path;
+			f->mtime = d->mtime;
+			if(f->unread)
+				warning(nil, "%s not written; file already exists\n", name);
+			else
+				warning(nil, "%s modified%s%s since last read\n", name, d->muid[0]?" by ":"", d->muid);
+			goto Rescue1;
+		}
+	}
+	fd = create(name, OWRITE, 0666);
+	if(fd < 0){
+		warning(nil, "can't create file %s: %r\n", name);
+		goto Rescue1;
+	}
+	r = fbufalloc();
+	s = fbufalloc();
+	free(d);
+	d = dirfstat(fd);
+	isapp = (d!=nil && d->length>0 && (d->qid.type&QTAPPEND));
+	if(isapp){
+		warning(nil, "%s not written; file is append only\n", name);
+		goto Rescue2;
+	}
+
+	for(q=q0; q<q1; q+=n){
+		n = q1 - q;
+		if(n > (BUFSIZE-1)/UTFmax)
+			n = (BUFSIZE-1)/UTFmax;
+		bufread(f, q, r, n);
+		m = snprint(s, BUFSIZE, "%.*S", n, r);
+		if(write(fd, s, m) != m){
+			warning(nil, "can't write file %s: %r\n", name);
+			goto Rescue2;
+		}
+	}
+	if(runeeq(namer, nname, f->name, f->nname)){
+		if(q0!=0 || q1!=f->nc){
+			f->mod = TRUE;
+			w->dirty = TRUE;
+			f->unread = TRUE;
+		}else{
+			d1 = dirfstat(fd);
+			if(d1 != nil){
+				free(d);
+				d = d1;
+			}
+			f->qidpath = d->qid.path;
+			f->dev = d->dev;
+			f->mtime = d->mtime;
+			f->mod = FALSE;
+			w->dirty = FALSE;
+			f->unread = FALSE;
+		}
+		for(i=0; i<f->ntext; i++){
+			f->text[i]->w->putseq = f->seq;
+			f->text[i]->w->dirty = w->dirty;
+		}
+	}
+	if(plumbsendfd >= 0){
+		pm = emalloc(sizeof(Plumbmsg));
+		pm->src = estrdup("acme");
+		pm->dst = estrdup("put");
+		pm->wdir = estrdup(name);
+		if(p = strrchr(pm->wdir, '/'))
+			*p = '\0';
+		pm->type = estrdup("text");
+		pm->attr = nil;
+		pm->data = estrdup(name);
+		pm->ndata = strlen(pm->data);
+		if(pm->ndata < messagesize-1024)
+			plumbsend(plumbsendfd, pm);
+		else
+			plumbfree(pm);
+	}
+	fbuffree(s);
+	fbuffree(r);
+	free(d);
+	free(namer);
+	free(name);
+	close(fd);
+	winsettag(w);
+	return;
+
+    Rescue2:
+	fbuffree(s);
+	fbuffree(r);
+	close(fd);
+	/* fall through */
+
+    Rescue1:
+	free(d);
+	free(namer);
+	free(name);
+}
+
+void
+put(Text *et, Text*, Text *argt, int, int, Rune *arg, int narg)
+{
+	int nname;
+	Rune  *namer;
+	Window *w;
+	File *f;
+	char *name;
+
+	if(et==nil || et->w==nil || et->w->isdir)
+		return;
+	w = et->w;
+	f = w->body.file;
+	name = getname(&w->body, argt, arg, narg, TRUE);
+	if(name == nil){
+		warning(nil, "no file name\n");
+		return;
+	}
+	namer = bytetorune(name, &nname);
+	putfile(f, 0, f->nc, namer, nname);
+	xfidlog(w, "put");
+	free(name);
+}
+
+void
+dump(Text *, Text *, Text *argt, int isdump, int, Rune *arg, int narg)
+{
+	char *name;
+
+	if(narg)
+		name = runetobyte(arg, narg);
+	else
+		getbytearg(argt, FALSE, TRUE, &name);
+	if(isdump)
+		rowdump(&row, name);
+	else
+		rowload(&row, name, FALSE);
+	free(name);
+}
+
+void
+cut(Text *et, Text *t, Text*, int dosnarf, int docut, Rune*, int)
+{
+	uint q0, q1, n, locked, c;
+	Rune *r;
+
+	/*
+	 * if not executing a mouse chord (et != t) and snarfing (dosnarf)
+	 * and executed Cut or Snarf in window tag (et->w != nil),
+	 * then use the window body selection or the tag selection
+	 * or do nothing at all.
+	 */
+	if(et!=t && dosnarf && et->w!=nil){
+		if(et->w->body.q1>et->w->body.q0){
+			t = &et->w->body;
+			if(docut)
+				filemark(t->file);	/* seq has been incremented by execute */
+		}else if(et->w->tag.q1>et->w->tag.q0)
+			t = &et->w->tag;
+		else
+			t = nil;
+	}
+	if(t == nil)	/* no selection */
+		return;
+
+	locked = FALSE;
+	if(t->w!=nil && et->w!=t->w){
+		locked = TRUE;
+		c = 'M';
+		if(et->w)
+			c = et->w->owner;
+		winlock(t->w, c);
+	}
+	if(t->q0 == t->q1){
+		if(locked)
+			winunlock(t->w);
+		return;
+	}
+	if(dosnarf){
+		q0 = t->q0;
+		q1 = t->q1;
+		bufdelete(&snarfbuf, 0, snarfbuf.nc);
+		r = fbufalloc();
+		while(q0 < q1){
+			n = q1 - q0;
+			if(n > RBUFSIZE)
+				n = RBUFSIZE;
+			bufread(t->file, q0, r, n);
+			bufinsert(&snarfbuf, snarfbuf.nc, r, n);
+			q0 += n;
+		}
+		fbuffree(r);
+		putsnarf();
+	}
+	if(docut){
+		textdelete(t, t->q0, t->q1, TRUE);
+		textsetselect(t, t->q0, t->q0);
+		if(t->w){
+			textscrdraw(t);
+			winsettag(t->w);
+		}
+	}else if(dosnarf)	/* Snarf command */
+		argtext = t;
+	if(locked)
+		winunlock(t->w);
+}
+
+void
+paste(Text *et, Text *t, Text*, int selectall, int tobody, Rune*, int)
+{
+	int c;
+	uint q, q0, q1, n;
+	Rune *r;
+
+	/* if(tobody), use body of executing window  (Paste or Send command) */
+	if(tobody && et!=nil && et->w!=nil){
+		t = &et->w->body;
+		filemark(t->file);	/* seq has been incremented by execute */
+	}
+	if(t == nil)
+		return;
+
+	getsnarf();
+	if(t==nil || snarfbuf.nc==0)
+		return;
+	if(t->w!=nil && et->w!=t->w){
+		c = 'M';
+		if(et->w)
+			c = et->w->owner;
+		winlock(t->w, c);
+	}
+	cut(t, t, nil, FALSE, TRUE, nil, 0);
+	q = 0;
+	q0 = t->q0;
+	q1 = t->q0+snarfbuf.nc;
+	r = fbufalloc();
+	while(q0 < q1){
+		n = q1 - q0;
+		if(n > RBUFSIZE)
+			n = RBUFSIZE;
+		if(r == nil)
+			r = runemalloc(n);
+		bufread(&snarfbuf, q, r, n);
+		textinsert(t, q0, r, n, TRUE);
+		q += n;
+		q0 += n;
+	}
+	fbuffree(r);
+	if(selectall)
+		textsetselect(t, t->q0, q1);
+	else
+		textsetselect(t, q1, q1);
+	if(t->w){
+		textscrdraw(t);
+		winsettag(t->w);
+	}
+	if(t->w!=nil && et->w!=t->w)
+		winunlock(t->w);
+}
+
+void
+look(Text *et, Text *t, Text *argt, int, int, Rune *arg, int narg)
+{
+	Rune *r;
+	int n;
+
+	if(et && et->w){
+		t = &et->w->body;
+		if(narg > 0){
+			search(t, arg, narg);
+			return;
+		}
+		getarg(argt, FALSE, FALSE, &r, &n);
+		if(r == nil){
+			n = t->q1-t->q0;
+			r = runemalloc(n);
+			bufread(t->file, t->q0, r, n);
+		}
+		search(t, r, n);
+		free(r);
+	}
+}
+
+void
+sendx(Text *et, Text *t, Text*, int, int, Rune*, int)
+{
+	if(et->w==nil)
+		return;
+	t = &et->w->body;
+	if(t->q0 != t->q1)
+		cut(t, t, nil, TRUE, FALSE, nil, 0);
+	textsetselect(t, t->file->nc, t->file->nc);
+	paste(t, t, nil, TRUE, TRUE, nil, 0);
+	if(textreadc(t, t->file->nc-1) != '\n'){
+		textinsert(t, t->file->nc, L"\n", 1, TRUE);
+		textsetselect(t, t->file->nc, t->file->nc);
+	}
+}
+
+void
+edit(Text *et, Text*, Text *argt, int, int, Rune *arg, int narg)
+{
+	Rune *r;
+	int len;
+
+	if(et == nil)
+		return;
+	getarg(argt, FALSE, TRUE, &r, &len);
+	seq++;
+	if(r != nil){
+		editcmd(et, r, len);
+		free(r);
+	}else
+		editcmd(et, arg, narg);
+}
+
+void
+exit(Text*, Text*, Text*, int, int, Rune*, int)
+{
+	if(rowclean(&row)){
+		sendul(cexit, 0);
+		threadexits(nil);
+	}
+}
+
+void
+putall(Text*, Text*, Text*, int, int, Rune*, int)
+{
+	int i, j, e;
+	Window *w;
+	Column *c;
+	char *a;
+
+	for(i=0; i<row.ncol; i++){
+		c = row.col[i];
+		for(j=0; j<c->nw; j++){
+			w = c->w[j];
+			if(w->isscratch || w->isdir || w->body.file->nname==0)
+				continue;
+			if(w->nopen[QWevent] > 0)
+				continue;
+			a = runetobyte(w->body.file->name, w->body.file->nname);
+			e = access(a, 0);
+			if(w->body.file->mod || w->body.ncache)
+				if(e < 0)
+					warning(nil, "no auto-Put of %s: %r\n", a);
+				else{
+					wincommit(w, &w->body);
+					put(&w->body, nil, nil, XXX, XXX, nil, 0);
+				}
+			free(a);
+		}
+	}
+}
+
+
+void
+id(Text *et, Text*, Text*, int, int, Rune*, int)
+{
+	if(et && et->w)
+		warning(nil, "/mnt/acme/%d/\n", et->w->id);
+}
+
+void
+local(Text *et, Text*, Text *argt, int, int, Rune *arg, int narg)
+{
+	char *a, *aa;
+	Runestr dir;
+
+	aa = getbytearg(argt, TRUE, TRUE, &a);
+
+	dir = dirname(et, nil, 0);
+	if(dir.nr==1 && dir.r[0]=='.'){	/* sigh */
+		free(dir.r);
+		dir.r = nil;
+		dir.nr = 0;
+	}
+	run(nil, runetobyte(arg, narg), dir.r, dir.nr, FALSE, aa, a, FALSE);
+}
+
+void
+kill(Text*, Text*, Text *argt, int, int, Rune *arg, int narg)
+{
+	Rune *a, *cmd, *r;
+	int na;
+
+	getarg(argt, FALSE, FALSE, &r, &na);
+	if(r)
+		kill(nil, nil, nil, 0, 0, r, na);
+	/* loop condition: *arg is not a blank */
+	for(;;){
+		a = findbl(arg, narg, &na);
+		if(a == arg)
+			break;
+		cmd = runemalloc(narg-na+1);
+		runemove(cmd, arg, narg-na);
+		sendp(ckill, cmd);
+		arg = skipbl(a, na, &narg);
+	}
+}
+
+void
+fontx(Text *et, Text *t, Text *argt, int, int, Rune *arg, int narg)
+{
+	Rune *a, *r, *flag, *file;
+	int na, nf;
+	char *aa;
+	Reffont *newfont;
+	Dirlist *dp;
+	int i, fix;
+
+	if(et==nil || et->w==nil)
+		return;
+	t = &et->w->body;
+	flag = nil;
+	file = nil;
+	/* loop condition: *arg is not a blank */
+	nf = 0;
+	for(;;){
+		a = findbl(arg, narg, &na);
+		if(a == arg)
+			break;
+		r = runemalloc(narg-na+1);
+		runemove(r, arg, narg-na);
+		if(runeeq(r, narg-na, L"fix", 3) || runeeq(r, narg-na, L"var", 3)){
+			free(flag);
+			flag = r;
+		}else{
+			free(file);
+			file = r;
+			nf = narg-na;
+		}
+		arg = skipbl(a, na, &narg);
+	}
+	getarg(argt, FALSE, TRUE, &r, &na);
+	if(r)
+		if(runeeq(r, na, L"fix", 3) || runeeq(r, na, L"var", 3)){
+			free(flag);
+			flag = r;
+		}else{
+			free(file);
+			file = r;
+			nf = na;
+		}
+	fix = 1;
+	if(flag)
+		fix = runeeq(flag, runestrlen(flag), L"fix", 3);
+	else if(file == nil){
+		newfont = rfget(FALSE, FALSE, FALSE, nil);
+		if(newfont)
+			fix = strcmp(newfont->f->name, t->font->name)==0;
+	}
+	if(file){
+		aa = runetobyte(file, nf);
+		newfont = rfget(fix, flag!=nil, FALSE, aa);
+		free(aa);
+	}else
+		newfont = rfget(fix, FALSE, FALSE, nil);
+	if(newfont){
+		draw(screen, t->w->r, textcols[BACK], nil, ZP);
+		rfclose(t->reffont);
+		t->reffont = newfont;
+		t->font = newfont->f;
+		frinittick(t);
+		if(t->w->isdir){
+			t->all.min.x++;	/* force recolumnation; disgusting! */
+			for(i=0; i<t->w->ndl; i++){
+				dp = t->w->dlp[i];
+				aa = runetobyte(dp->r, dp->nr);
+				dp->wid = stringwidth(newfont->f, aa);
+				free(aa);
+			}
+		}
+		/* avoid shrinking of window due to quantization */
+		colgrow(t->w->col, t->w, -1);
+	}
+	free(file);
+	free(flag);
+}
+
+void
+incl(Text *et, Text*, Text *argt, int, int, Rune *arg, int narg)
+{
+	Rune *a, *r;
+	Window *w;
+	int na, n, len;
+
+	if(et==nil || et->w==nil)
+		return;
+	w = et->w;
+	n = 0;
+	getarg(argt, FALSE, TRUE, &r, &len);
+	if(r){
+		n++;
+		winaddincl(w, r, len);
+	}
+	/* loop condition: *arg is not a blank */
+	for(;;){
+		a = findbl(arg, narg, &na);
+		if(a == arg)
+			break;
+		r = runemalloc(narg-na+1);
+		runemove(r, arg, narg-na);
+		n++;
+		winaddincl(w, r, narg-na);
+		arg = skipbl(a, na, &narg);
+	}
+	if(n==0 && w->nincl){
+		for(n=w->nincl; --n>=0; )
+			warning(nil, "%S ", w->incl[n]);
+		warning(nil, "\n");
+	}
+}
+
+enum {
+	IGlobal = -2,
+	IError = -1,
+};
+
+static int
+indentval(Rune *s, int n, int type)
+{
+	static char *strs[] = {
+		[SPACESINDENT] "Spaces",
+		[AUTOINDENT] "Indent",
+	};
+
+	if(n < 2)
+		return IError;
+	if(runestrncmp(s, L"ON", n) == 0){
+		globalindent[type] = TRUE;
+		warning(nil, "%s ON\n", strs[type]);
+		return IGlobal;
+	}
+	if(runestrncmp(s, L"OFF", n) == 0){
+		globalindent[type] = FALSE;
+		warning(nil, "%s OFF\n", strs[type]);
+		return IGlobal;
+	}
+	if(runestrncmp(s, L"on", n) == 0)
+		return TRUE;
+	if(runestrncmp(s, L"off", n) == 0)
+		return FALSE;
+	return IError;
+}
+
+static void
+fixindent(Window *w, void *v)
+{
+	int t = *(int*)v;
+	w->indent[t] = globalindent[t];
+}
+
+void
+indent(Text *et, Text*, Text *argt, int type, int, Rune *arg, int narg)
+{
+	Rune *a, *r;
+	Window *w;
+	int na, len, ival;
+
+	w = nil;
+	if(et!=nil && et->w!=nil)
+		w = et->w;
+	ival = IError;
+	getarg(argt, FALSE, TRUE, &r, &len);
+	if(r!=nil && len>0)
+		ival = indentval(r, len, type);
+	else{
+		a = findbl(arg, narg, &na);
+		if(a != arg)
+			ival = indentval(arg, narg-na, type);
+	}
+	if(ival == IGlobal)
+		allwindows(fixindent, &type);
+	else if(w != nil && ival >= 0)
+		w->indent[type] = ival;
+}
+
+void
+tab(Text *et, Text*, Text *argt, int, int, Rune *arg, int narg)
+{
+	Rune *a, *r;
+	Window *w;
+	int na, len, tab;
+	char *p;
+
+	if(et==nil || et->w==nil)
+		return;
+	w = et->w;
+	getarg(argt, FALSE, TRUE, &r, &len);
+	tab = 0;
+	if(r!=nil && len>0){
+		p = runetobyte(r, len);
+		if('0'<=p[0] && p[0]<='9')
+			tab = atoi(p);
+		free(p);
+	}else{
+		a = findbl(arg, narg, &na);
+		if(a != arg){
+			p = runetobyte(arg, narg-na);
+			if('0'<=p[0] && p[0]<='9')
+				tab = atoi(p);
+			free(p);
+		}
+	}
+	if(tab > 0){
+		if(w->body.tabstop != tab){
+			w->body.tabstop = tab;
+			winresize(w, w->r, 1);
+		}
+	}else
+		warning(nil, "%.*S: Tab %d\n", w->body.file->nname, w->body.file->name, w->body.tabstop);
+}
+
+void
+runproc(void *argvp)
+{
+	/* args: */
+		Window *win;
+		char *s;
+		Rune *rdir;
+		int ndir;
+		int newns;
+		char *argaddr;
+		char *arg;
+		Command *c;
+		Channel *cpid;
+		int iseditcmd;
+	/* end of args */
+	char *e, *t, *name, *filename, *dir, **av, *news;
+	Rune r, **incl;
+	int ac, w, inarg, i, n, fd, nincl, winid;
+	int pipechar;
+	char buf[512];
+	void **argv;
+
+	argv = argvp;
+	win = argv[0];
+	s = argv[1];
+	rdir = argv[2];
+	ndir = (uintptr)argv[3];
+	newns = (uintptr)argv[4];
+	argaddr = argv[5];
+	arg = argv[6];
+	c = argv[7];
+	cpid = argv[8];
+	iseditcmd = (uintptr)argv[9];
+	free(argv);
+
+	t = s;
+	while(*t==' ' || *t=='\n' || *t=='\t')
+		t++;
+	for(e=t; *e; e++)
+		if(*e==' ' || *e=='\n' || *e=='\t' )
+			break;
+	name = emalloc((e-t)+2);
+	memmove(name, t, e-t);
+	name[e-t] = 0;
+	e = utfrrune(name, '/');
+	if(e)
+		memmove(name, e+1, strlen(e+1)+1);	/* strcpy but overlaps */
+	strcat(name, " ");	/* add blank here for ease in waittask */
+	c->name = bytetorune(name, &c->nname);
+	free(name);
+	pipechar = 0;
+	if(*t=='<' || *t=='|' || *t=='>')
+		pipechar = *t++;
+	c->iseditcmd = iseditcmd;
+	c->text = s;
+	if(rdir != nil){
+		dir = runetobyte(rdir, ndir);
+		chdir(dir);	/* ignore error: probably app. window */
+		free(dir);
+	}
+	if(newns){
+		nincl = 0;
+		incl = nil;
+		if(win){
+			filename = smprint("%.*S", win->body.file->nname, win->body.file->name);
+			nincl = win->nincl;
+			if(nincl > 0){
+				incl = emalloc(nincl*sizeof(Rune*));
+				for(i=0; i<nincl; i++){
+					n = runestrlen(win->incl[i]);
+					incl[i] = runemalloc(n+1);
+					runemove(incl[i], win->incl[i], n);
+				}
+			}
+			winid = win->id;
+		}else{
+			filename = nil;
+			winid = 0;
+			if(activewin)
+				winid = activewin->id;
+		}
+		rfork(RFNAMEG|RFENVG|RFFDG|RFNOTEG);
+		sprint(buf, "%d", winid);
+		putenv("winid", buf);
+
+		if(filename){
+			putenv("%", filename);
+			free(filename);
+		}
+		c->md = fsysmount(rdir, ndir, incl, nincl);
+		if(c->md == nil){
+			fprint(2, "child: can't mount /dev/cons: %r\n");
+			threadexits("mount");
+		}
+		close(0);
+		if(winid>0 && (pipechar=='|' || pipechar=='>')){
+			sprint(buf, "/mnt/acme/%d/rdsel", winid);
+			open(buf, OREAD);
+		}else
+			open("/dev/null", OREAD);
+		close(1);
+		if((winid>0 || iseditcmd) && (pipechar=='|' || pipechar=='<')){
+			if(iseditcmd){
+				if(winid > 0)
+					sprint(buf, "/mnt/acme/%d/editout", winid);
+				else
+					sprint(buf, "/mnt/acme/editout");
+			}else
+				sprint(buf, "/mnt/acme/%d/wrsel", winid);
+			open(buf, OWRITE);
+			close(2);
+			open("/dev/cons", OWRITE);
+		}else{
+			open("/dev/cons", OWRITE);
+			dup(1, 2);
+		}
+	}else{
+		rfork(RFFDG|RFNOTEG);
+		fsysclose();
+		close(0);
+		open("/dev/null", OREAD);
+		close(1);
+		open(acmeerrorfile, OWRITE);
+		dup(1, 2);
+	}
+
+	if(win)
+		winclose(win);
+
+	if(argaddr)
+		putenv("acmeaddr", argaddr);
+	if(strlen(t) > sizeof buf-10)	/* may need to print into stack */
+		goto Hard;
+	inarg = FALSE;
+	for(e=t; *e; e+=w){
+		w = chartorune(&r, e);
+		if(r==' ' || r=='\t')
+			continue;
+		if(r < ' ')
+			goto Hard;
+		if(utfrune("#;&|^$=`'{}()<>[]*?^~`", r))
+			goto Hard;
+		inarg = TRUE;
+	}
+	if(!inarg)
+		goto Fail;
+
+	ac = 0;
+	av = nil;
+	inarg = FALSE;
+	for(e=t; *e; e+=w){
+		w = chartorune(&r, e);
+		if(r==' ' || r=='\t'){
+			inarg = FALSE;
+			*e = 0;
+			continue;
+		}
+		if(!inarg){
+			inarg = TRUE;
+			av = realloc(av, (ac+1)*sizeof(char**));
+			av[ac++] = e;
+		}
+	}
+	av = realloc(av, (ac+2)*sizeof(char**));
+	av[ac++] = arg;
+	av[ac] = nil;
+	c->av = av;
+	procexec(cpid, av[0], av);
+	e = av[0];
+	if(e[0]=='/' || (e[0]=='.' && e[1]=='/'))
+		goto Fail;
+	if(cputype){
+		sprint(buf, "%s/%s", cputype, av[0]);
+		procexec(cpid, buf, av);
+	}
+	sprint(buf, "/bin/%s", av[0]);
+	procexec(cpid, buf, av);
+	goto Fail;
+
+Hard:
+
+	/*
+	 * ugly: set path = (. $cputype /bin)
+	 * should honor $path if unusual.
+	 */
+	if(cputype){
+		n = 0;
+		memmove(buf+n, ".", 2);
+		n += 2;
+		i = strlen(cputype)+1;
+		memmove(buf+n, cputype, i);
+		n += i;
+		memmove(buf+n, "/bin", 5);
+		n += 5;
+		fd = create("/env/path", OWRITE, 0666);
+		write(fd, buf, n);
+		close(fd);
+	}
+
+	if(arg){
+		news = emalloc(strlen(t) + 1 + 1 + strlen(arg) + 1 + 1);
+		if(news){
+			sprint(news, "%s '%s'", t, arg);	/* BUG: what if quote in arg? */
+			free(s);
+			t = news;
+			c->text = news;
+		}
+	}
+	procexecl(cpid, "/bin/rc", "rc", "-c", t, nil);
+
+   Fail:
+	/* procexec hasn't happened, so send a zero */
+	sendul(cpid, 0);
+	threadexits(nil);
+}
+
+void
+runwaittask(void *v)
+{
+	Command *c;
+	Channel *cpid;
+	void **a;
+
+	threadsetname("runwaittask");
+	a = v;
+	c = a[0];
+	cpid = a[1];
+	free(a);
+	do
+		c->pid = recvul(cpid);
+	while(c->pid == ~0);
+	free(c->av);
+	if(c->pid != 0)	/* successful exec */
+		sendp(ccommand, c);
+	else{
+		if(c->iseditcmd)
+			sendul(cedit, 0);
+		free(c->name);
+		free(c->text);
+		free(c);
+	}
+	chanfree(cpid);
+}
+
+void
+run(Window *win, char *s, Rune *rdir, int ndir, int newns, char *argaddr, char *xarg, int iseditcmd)
+{
+	void **arg;
+	Command *c;
+	Channel *cpid;
+
+	if(s == nil)
+		return;
+
+	arg = emalloc(10*sizeof(void*));
+	c = emalloc(sizeof *c);
+	cpid = chancreate(sizeof(ulong), 0);
+	arg[0] = win;
+	arg[1] = s;
+	arg[2] = rdir;
+	arg[3] = (void*)ndir;
+	arg[4] = (void*)newns;
+	arg[5] = argaddr;
+	arg[6] = xarg;
+	arg[7] = c;
+	arg[8] = cpid;
+	arg[9] = (void*)iseditcmd;
+	proccreate(runproc, arg, STACK);
+	/* mustn't block here because must be ready to answer mount() call in run() */
+	arg = emalloc(2*sizeof(void*));
+	arg[0] = c;
+	arg[1] = cpid;
+	threadcreate(runwaittask, arg, STACK);
+}
--- /dev/null
+++ b/file.c
@@ -1,0 +1,310 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <cursor.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <frame.h>
+#include <fcall.h>
+#include <plumb.h>
+#include "dat.h"
+#include "fns.h"
+
+/*
+ * Structure of Undo list:
+ * 	The Undo structure follows any associated data, so the list
+ *	can be read backwards: read the structure, then read whatever
+ *	data is associated (insert string, file name) and precedes it.
+ *	The structure includes the previous value of the modify bit
+ *	and a sequence number; successive Undo structures with the
+ *	same sequence number represent simultaneous changes.
+ */
+
+typedef struct Undo Undo;
+struct Undo
+{
+	short	type;		/* Delete, Insert, Filename */
+	short	mod;	/* modify bit */
+	uint		seq;		/* sequence number */
+	uint		p0;		/* location of change (unused in f) */
+	uint		n;		/* # runes in string or file name */
+};
+
+enum
+{
+	Undosize = sizeof(Undo)/sizeof(Rune),
+};
+
+File*
+fileaddtext(File *f, Text *t)
+{
+	if(f == nil){
+		f = emalloc(sizeof(File));
+		f->unread = TRUE;
+	}
+	f->text = realloc(f->text, (f->ntext+1)*sizeof(Text*));
+	f->text[f->ntext++] = t;
+	f->curtext = t;
+	return f;
+}
+
+void
+filedeltext(File *f, Text *t)
+{
+	int i;
+
+	for(i=0; i<f->ntext; i++)
+		if(f->text[i] == t)
+			goto Found;
+	error("can't find text in filedeltext");
+
+    Found:
+	f->ntext--;
+	if(f->ntext == 0){
+		fileclose(f);
+		return;
+	}
+	memmove(f->text+i, f->text+i+1, (f->ntext-i)*sizeof(Text*));
+	if(f->curtext == t)
+		f->curtext = f->text[0];
+}
+
+void
+fileinsert(File *f, uint p0, Rune *s, uint ns)
+{
+	if(p0 > f->nc)
+		error("internal error: fileinsert");
+	if(f->seq > 0)
+		fileuninsert(f, &f->delta, p0, ns);
+	bufinsert(f, p0, s, ns);
+	if(ns)
+		f->mod = TRUE;
+}
+
+void
+fileuninsert(File *f, Buffer *delta, uint p0, uint ns)
+{
+	Undo u;
+
+	/* undo an insertion by deleting */
+	u.type = Delete;
+	u.mod = f->mod;
+	u.seq = f->seq;
+	u.p0 = p0;
+	u.n = ns;
+	bufinsert(delta, delta->nc, (Rune*)&u, Undosize);
+}
+
+void
+filedelete(File *f, uint p0, uint p1)
+{
+	if(!(p0<=p1 && p0<=f->nc && p1<=f->nc))
+		error("internal error: filedelete");
+	if(f->seq > 0)
+		fileundelete(f, &f->delta, p0, p1);
+	bufdelete(f, p0, p1);
+	if(p1 > p0)
+		f->mod = TRUE;
+}
+
+void
+fileundelete(File *f, Buffer *delta, uint p0, uint p1)
+{
+	Undo u;
+	Rune *buf;
+	uint i, n;
+
+	/* undo a deletion by inserting */
+	u.type = Insert;
+	u.mod = f->mod;
+	u.seq = f->seq;
+	u.p0 = p0;
+	u.n = p1-p0;
+	buf = fbufalloc();
+	for(i=p0; i<p1; i+=n){
+		n = p1 - i;
+		if(n > RBUFSIZE)
+			n = RBUFSIZE;
+		bufread(f, i, buf, n);
+		bufinsert(delta, delta->nc, buf, n);
+	}
+	fbuffree(buf);
+	bufinsert(delta, delta->nc, (Rune*)&u, Undosize);
+
+}
+
+void
+filesetname(File *f, Rune *name, int n)
+{
+	if(f->seq > 0)
+		fileunsetname(f, &f->delta);
+	free(f->name);
+	f->name = runemalloc(n);
+	runemove(f->name, name, n);
+	f->nname = n;
+	f->unread = TRUE;
+}
+
+void
+fileunsetname(File *f, Buffer *delta)
+{
+	Undo u;
+
+	/* undo a file name change by restoring old name */
+	u.type = Filename;
+	u.mod = f->mod;
+	u.seq = f->seq;
+	u.p0 = 0;	/* unused */
+	u.n = f->nname;
+	if(f->nname)
+		bufinsert(delta, delta->nc, f->name, f->nname);
+	bufinsert(delta, delta->nc, (Rune*)&u, Undosize);
+}
+
+uint
+fileload(File *f, uint p0, int fd, int *nulls)
+{
+	if(f->seq > 0)
+		error("undo in file.load unimplemented");
+	return bufload(f, p0, fd, nulls);
+}
+
+/* return sequence number of pending redo */
+uint
+fileredoseq(File *f)
+{
+	Undo u;
+	Buffer *delta;
+
+	delta = &f->epsilon;
+	if(delta->nc == 0)
+		return 0;
+	bufread(delta, delta->nc-Undosize, (Rune*)&u, Undosize);
+	return u.seq;
+}
+
+void
+fileundo(File *f, int isundo, uint *q0p, uint *q1p)
+{
+	Undo u;
+	Rune *buf;
+	uint i, j, n, up;
+	uint stop;
+	Buffer *delta, *epsilon;
+
+	if(isundo){
+		/* undo; reverse delta onto epsilon, seq decreases */
+		delta = &f->delta;
+		epsilon = &f->epsilon;
+		stop = f->seq;
+	}else{
+		/* redo; reverse epsilon onto delta, seq increases */
+		delta = &f->epsilon;
+		epsilon = &f->delta;
+		stop = 0;	/* don't know yet */
+	}
+
+	buf = fbufalloc();
+	while(delta->nc > 0){
+		up = delta->nc-Undosize;
+		bufread(delta, up, (Rune*)&u, Undosize);
+		if(isundo){
+			if(u.seq < stop){
+				f->seq = u.seq;
+				goto Return;
+			}
+		}else{
+			if(stop == 0)
+				stop = u.seq;
+			if(u.seq > stop)
+				goto Return;
+		}
+		switch(u.type){
+		default:
+			fprint(2, "undo: 0x%ux\n", u.type);
+			abort();
+			break;
+
+		case Delete:
+			f->seq = u.seq;
+			fileundelete(f, epsilon, u.p0, u.p0+u.n);
+			f->mod = u.mod;
+			bufdelete(f, u.p0, u.p0+u.n);
+			for(j=0; j<f->ntext; j++)
+				textdelete(f->text[j], u.p0, u.p0+u.n, FALSE);
+			*q0p = u.p0;
+			*q1p = u.p0;
+			break;
+
+		case Insert:
+			f->seq = u.seq;
+			fileuninsert(f, epsilon, u.p0, u.n);
+			f->mod = u.mod;
+			up -= u.n;
+			for(i=0; i<u.n; i+=n){
+				n = u.n - i;
+				if(n > RBUFSIZE)
+					n = RBUFSIZE;
+				bufread(delta, up+i, buf, n);
+				bufinsert(f, u.p0+i, buf, n);
+				for(j=0; j<f->ntext; j++)
+					textinsert(f->text[j], u.p0+i, buf, n, FALSE);
+			}
+			*q0p = u.p0;
+			*q1p = u.p0+u.n;
+			break;
+
+		case Filename:
+			f->seq = u.seq;
+			fileunsetname(f, epsilon);
+			f->mod = u.mod;
+			up -= u.n;
+			free(f->name);
+			if(u.n == 0)
+				f->name = nil;
+			else
+				f->name = runemalloc(u.n);
+			bufread(delta, up, f->name, u.n);
+			f->nname = u.n;
+			break;
+		}
+		bufdelete(delta, up, delta->nc);
+	}
+	if(isundo)
+		f->seq = 0;
+    Return:
+	fbuffree(buf);
+}
+
+void
+filereset(File *f)
+{
+	bufreset(&f->delta);
+	bufreset(&f->epsilon);
+	f->seq = 0;
+}
+
+void
+fileclose(File *f)
+{
+	free(f->name);
+	f->nname = 0;
+	f->name = nil;
+	free(f->text);
+	f->ntext = 0;
+	f->text = nil;
+	bufclose(f);
+	bufclose(&f->delta);
+	bufclose(&f->epsilon);
+	elogclose(f);
+	free(f);
+}
+
+void
+filemark(File *f)
+{
+	if(f->epsilon.nc)
+		bufdelete(&f->epsilon, 0, f->epsilon.nc);
+	f->seq = seq;
+}
--- /dev/null
+++ b/fns.h
@@ -1,0 +1,96 @@
+#pragma	varargck	argpos	warning	2
+
+void	warning(Mntdir*, char*, ...);
+
+#define	fbufalloc()	emalloc(BUFSIZE)
+#define	fbuffree(x)	free(x)
+
+void	plumblook(Plumbmsg*m);
+void	plumbshow(Plumbmsg*m);
+void	putsnarf(void);
+void	getsnarf(void);
+int	tempfile(void);
+void	scrlresize(void);
+Font*	getfont(int, int, char*);
+char*	getarg(Text*, int, int, Rune**, int*);
+char*	getbytearg(Text*, int, int, char**);
+void	new(Text*, Text*, Text*, int, int, Rune*, int);
+void	undo(Text*, Text*, Text*, int, int, Rune*, int);
+char*	getname(Text*, Text*, Rune*, int, int);
+void	scrsleep(uint);
+void	savemouse(Window*);
+int	restoremouse(Window*);
+void	clearmouse(void);
+void	allwindows(void(*)(Window*, void*), void*);
+uint loadfile(int, uint, int*, int(*)(void*, uint, Rune*, int), void*);
+void	movetodel(Window*);
+
+Window*	errorwin(Mntdir*, int);
+Window*	errorwinforwin(Window*);
+Runestr cleanrname(Runestr);
+void	run(Window*, char*, Rune*, int, int, char*, char*, int);
+void fsysclose(void);
+void	setcurtext(Text*, int);
+int	isfilec(Rune);
+void	rxinit(void);
+int rxnull(void);
+Runestr	dirname(Text*, Rune*, int);
+void	error(char*);
+void	cvttorunes(char*, int, Rune*, int*, int*, int*);
+void*	tmalloc(uint);
+void	tfree(void);
+void	killprocs(void);
+void	killtasks(void);
+int	runeeq(Rune*, uint, Rune*, uint);
+int	ALEF_tid(void);
+void	iconinit(void);
+Timer*	timerstart(int);
+void	timerstop(Timer*);
+void	timercancel(Timer*);
+void	timerinit(void);
+void	cut(Text*, Text*, Text*, int, int, Rune*, int);
+void	paste(Text*, Text*, Text*, int, int, Rune*, int);
+void	get(Text*, Text*, Text*, int, int, Rune*, int);
+void	put(Text*, Text*, Text*, int, int, Rune*, int);
+void	putfile(File*, int, int, Rune*, int);
+void	fontx(Text*, Text*, Text*, int, int, Rune*, int);
+int	isspace(Rune);
+int	isalnum(Rune);
+void	execute(Text*, uint, uint, int, Text*);
+int	search(Text*, Rune*, uint);
+void	look3(Text*, uint, uint, int);
+void	editcmd(Text*, Rune*, uint);
+uint	min(uint, uint);
+uint	max(uint, uint);
+Window*	lookfile(Rune*, int);
+Window*	lookid(int, int);
+char*	runetobyte(Rune*, int);
+Rune*	bytetorune(char*, int*);
+void	fsysinit(void);
+Mntdir*	fsysmount(Rune*, int, Rune**, int);
+void		fsysincid(Mntdir*);
+void		fsysdelid(Mntdir*);
+Xfid*		respond(Xfid*, Fcall*, char*);
+int		rxcompile(Rune*);
+int		rgetc(void*, uint);
+int		tgetc(void*, uint);
+int		isaddrc(int);
+int		isregexc(int);
+void *emalloc(uint);
+void *erealloc(void*, uint);
+char	*estrdup(char*);
+Range		address(Mntdir*, Text*, Range, Range, void*, uint, uint, int (*)(void*, uint),  int*, uint*);
+int		rxexecute(Text*, Rune*, uint, uint, Rangeset*);
+int		rxbexecute(Text*, uint, Rangeset*);
+Window*	makenewwindow(Text *t);
+int	expand(Text*, uint, uint, Expand*);
+Rune*	skipbl(Rune*, int, int*);
+Rune*	findbl(Rune*, int, int*);
+char*	edittext(Window*, int, Rune*, int);
+void		flushwarnings(void);
+long	nlcount(Text*, long, long, long*);
+long	nlcounttopos(Text*, long, long, long);
+
+#define	runemalloc(a)		(Rune*)emalloc((a)*sizeof(Rune))
+#define	runerealloc(a, b)	(Rune*)erealloc((a), (b)*sizeof(Rune))
+#define	runemove(a, b, c)	memmove((a), (b), (c)*sizeof(Rune))
--- /dev/null
+++ b/fsys.c
@@ -1,0 +1,749 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <cursor.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <frame.h>
+#include <fcall.h>
+#include <plumb.h>
+#include "dat.h"
+#include "fns.h"
+
+static	int	cfd;
+static	int	sfd;
+
+enum
+{
+	Nhash	= 16,
+	DEBUG	= 0
+};
+
+static	Fid	*fids[Nhash];
+
+Fid	*newfid(int);
+
+static	Xfid*	fsysflush(Xfid*, Fid*);
+static	Xfid*	fsysauth(Xfid*, Fid*);
+static	Xfid*	fsysversion(Xfid*, Fid*);
+static	Xfid*	fsysattach(Xfid*, Fid*);
+static	Xfid*	fsyswalk(Xfid*, Fid*);
+static	Xfid*	fsysopen(Xfid*, Fid*);
+static	Xfid*	fsyscreate(Xfid*, Fid*);
+static	Xfid*	fsysread(Xfid*, Fid*);
+static	Xfid*	fsyswrite(Xfid*, Fid*);
+static	Xfid*	fsysclunk(Xfid*, Fid*);
+static	Xfid*	fsysremove(Xfid*, Fid*);
+static	Xfid*	fsysstat(Xfid*, Fid*);
+static	Xfid*	fsyswstat(Xfid*, Fid*);
+
+Xfid* 	(*fcall[Tmax])(Xfid*, Fid*) =
+{
+	[Tflush]	= fsysflush,
+	[Tversion]	= fsysversion,
+	[Tauth]	= fsysauth,
+	[Tattach]	= fsysattach,
+	[Twalk]	= fsyswalk,
+	[Topen]	= fsysopen,
+	[Tcreate]	= fsyscreate,
+	[Tread]	= fsysread,
+	[Twrite]	= fsyswrite,
+	[Tclunk]	= fsysclunk,
+	[Tremove]= fsysremove,
+	[Tstat]	= fsysstat,
+	[Twstat]	= fsyswstat,
+};
+
+char Eperm[] = "permission denied";
+char Eexist[] = "file does not exist";
+char Enotdir[] = "not a directory";
+
+Dirtab dirtab[]=
+{
+	{ ".",			QTDIR,	Qdir,		0500|DMDIR },
+	{ "acme",		QTDIR,	Qacme,	0500|DMDIR },
+	{ "cons",		QTFILE,	Qcons,	0600 },
+	{ "consctl",	QTFILE,	Qconsctl,	0000 },
+	{ "draw",		QTDIR,	Qdraw,	0000|DMDIR },	/* to suppress graphics progs started in acme */
+	{ "editout",	QTFILE,	Qeditout,	0200 },
+	{ "index",		QTFILE,	Qindex,	0400 },
+	{ "label",		QTFILE,	Qlabel,	0600 },
+	{ "log",		QTFILE,	Qlog,	0400 },
+	{ "new",		QTDIR,	Qnew,	0500|DMDIR },
+	{ nil, }
+};
+
+Dirtab dirtabw[]=
+{
+	{ ".",			QTDIR,		Qdir,			0500|DMDIR },
+	{ "addr",		QTFILE,		QWaddr,		0600 },
+	{ "body",		QTAPPEND,	QWbody,		0600|DMAPPEND },
+	{ "ctl",		QTFILE,		QWctl,		0600 },
+	{ "data",		QTFILE,		QWdata,		0600 },
+	{ "editout",	QTFILE,		QWeditout,	0200 },
+	{ "errors",		QTFILE,		QWerrors,		0200 },
+	{ "event",		QTFILE,		QWevent,		0600 },
+	{ "rdsel",		QTFILE,		QWrdsel,		0400 },
+	{ "wrsel",		QTFILE,		QWwrsel,		0200 },
+	{ "tag",		QTAPPEND,	QWtag,		0600|DMAPPEND },
+	{ "xdata",		QTFILE,		QWxdata,		0600 },
+	{ nil, }
+};
+
+typedef struct Mnt Mnt;
+struct Mnt
+{
+	QLock;
+	int		id;
+	Mntdir	*md;
+};
+
+Mnt	mnt;
+
+Xfid*	respond(Xfid*, Fcall*, char*);
+int		dostat(int, Dirtab*, uchar*, int, uint);
+uint	getclock(void);
+
+char	*user = "Wile E. Coyote";
+int	clockfd;
+static int closing = 0;
+int	messagesize = Maxblock+IOHDRSZ;	/* good start */
+
+void	fsysproc(void *);
+
+void
+fsysinit(void)
+{
+	int p[2];
+
+	if(pipe(p) < 0)
+		error("can't create pipe");
+	cfd = p[0];
+	sfd = p[1];
+	fmtinstall('F', fcallfmt);
+	clockfd = open("/dev/time", OREAD|OCEXEC);
+	user = getuser();
+	proccreate(fsysproc, nil, STACK);
+}
+
+void
+fsysproc(void *)
+{
+	int n;
+	Xfid *x;
+	Fid *f;
+	Fcall t;
+	uchar *buf;
+
+	x = nil;
+	for(;;){
+		buf = emalloc(messagesize+UTFmax);	/* overflow for appending partial rune in xfidwrite */
+		n = read9pmsg(sfd, buf, messagesize);
+		if(n <= 0){
+			if(closing)
+				break;
+			error("i/o error on server channel");
+		}
+		if(x == nil){
+			sendp(cxfidalloc, nil);
+			x = recvp(cxfidalloc);
+		}
+		x->buf = buf;
+		if(convM2S(buf, n, x) != n)
+			error("convert error in convM2S");
+		if(DEBUG)
+			fprint(2, "%F\n", &x->Fcall);
+		if(fcall[x->type] == nil)
+			x = respond(x, &t, "bad fcall type");
+		else{
+			switch(x->type){
+			case Tversion:
+			case Tauth:
+			case Tflush:
+				f = nil;
+				break;
+			case Tattach:
+				f = newfid(x->fid);
+				break;
+			default:
+				f = newfid(x->fid);
+				if(!f->busy){
+					x->f = f;
+					x = respond(x, &t, "fid not in use");
+					continue;
+				}
+				break;
+			}
+			x->f = f;
+			x  = (*fcall[x->type])(x, f);
+		}
+	}
+}
+
+Mntdir*
+fsysaddid(Rune *dir, int ndir, Rune **incl, int nincl)
+{
+	Mntdir *m;
+	int id;
+
+	qlock(&mnt);
+	id = ++mnt.id;
+	m = emalloc(sizeof *m);
+	m->id = id;
+	m->dir =  dir;
+	m->ref = 1;	/* one for Command, one will be incremented in attach */
+	m->ndir = ndir;
+	m->next = mnt.md;
+	m->incl = incl;
+	m->nincl = nincl;
+	mnt.md = m;
+	qunlock(&mnt);
+	return m;
+}
+
+void
+fsysincid(Mntdir *m)
+{
+	qlock(&mnt);
+	m->ref++;
+	qunlock(&mnt);
+}
+
+void
+fsysdelid(Mntdir *idm)
+{
+	Mntdir *m, *prev;
+	int i;
+	char buf[64];
+
+	if(idm == nil)
+		return;
+	qlock(&mnt);
+	if(--idm->ref > 0){
+		qunlock(&mnt);
+		return;
+	}
+	prev = nil;
+	for(m=mnt.md; m; m=m->next){
+		if(m == idm){
+			if(prev)
+				prev->next = m->next;
+			else
+				mnt.md = m->next;
+			for(i=0; i<m->nincl; i++)
+				free(m->incl[i]);
+			free(m->incl);
+			free(m->dir);
+			free(m);
+			qunlock(&mnt);
+			return;
+		}
+		prev = m;
+	}
+	qunlock(&mnt);
+	sprint(buf, "fsysdelid: can't find id %d\n", idm->id);
+	sendp(cerr, estrdup(buf));
+}
+
+/*
+ * Called only in exec.c:/^run(), from a different FD group
+ */
+Mntdir*
+fsysmount(Rune *dir, int ndir, Rune **incl, int nincl)
+{
+	char buf[256];
+	Mntdir *m;
+
+	/* close server side so don't hang if acme is half-exited */
+	close(sfd);
+	m = fsysaddid(dir, ndir, incl, nincl);
+	sprint(buf, "%d", m->id);
+	if(mount(cfd, -1, "/mnt/acme", MREPL, buf) == -1){
+		fsysdelid(m);
+		return nil;
+	}
+	bind("/mnt/acme", "/mnt/wsys", MREPL);
+	if(bind("/mnt/acme", "/dev", MBEFORE) == -1){
+		fsysdelid(m);
+		return nil;
+	}
+	return m;
+}
+
+void
+fsysclose(void)
+{
+	closing = 1;
+	close(cfd);
+	close(sfd);
+}
+
+Xfid*
+respond(Xfid *x, Fcall *t, char *err)
+{
+	int n;
+
+	if(err){
+		t->type = Rerror;
+		t->ename = err;
+	}else
+		t->type = x->type+1;
+	t->fid = x->fid;
+	t->tag = x->tag;
+	if(x->buf == nil)
+		x->buf = emalloc(messagesize);
+	n = convS2M(t, x->buf, messagesize);
+	if(n <= 0)
+		error("convert error in convS2M");
+	if(write(sfd, x->buf, n) != n)
+		error("write error in respond");
+	free(x->buf);
+	x->buf = nil;
+	if(DEBUG)
+		fprint(2, "r: %F\n", t);
+	return x;
+}
+
+static
+Xfid*
+fsysversion(Xfid *x, Fid*)
+{
+	Fcall t;
+
+	if(x->msize < 256)
+		return respond(x, &t, "version: message size too small");
+	messagesize = x->msize;
+	t.msize = messagesize;
+	t.version = "9P2000";
+	if(strncmp(x->version, "9P", 2) != 0)
+		t.version = "unknown";
+	return respond(x, &t, nil);
+}
+
+static
+Xfid*
+fsysauth(Xfid *x, Fid*)
+{
+	Fcall t;
+
+	return respond(x, &t, "acme: authentication not required");
+}
+
+static
+Xfid*
+fsysflush(Xfid *x, Fid*)
+{
+	sendp(x->c, xfidflush);
+	return nil;
+}
+
+static
+Xfid*
+fsysattach(Xfid *x, Fid *f)
+{
+	Fcall t;
+	int id;
+	Mntdir *m;
+
+	if(strcmp(x->uname, user) != 0)
+		return respond(x, &t, Eperm);
+	f->busy = TRUE;
+	f->open = FALSE;
+	f->qid.path = Qdir;
+	f->qid.type = QTDIR;
+	f->qid.vers = 0;
+	f->dir = dirtab;
+	f->nrpart = 0;
+	f->w = nil;
+	t.qid = f->qid;
+	f->mntdir = nil;
+	id = atoi(x->aname);
+	qlock(&mnt);
+	for(m=mnt.md; m; m=m->next)
+		if(m->id == id){
+			f->mntdir = m;
+			m->ref++;
+			break;
+		}
+	if(m == nil)
+		sendp(cerr, estrdup("unknown id in attach"));
+	qunlock(&mnt);
+	return respond(x, &t, nil);
+}
+
+static
+Xfid*
+fsyswalk(Xfid *x, Fid *f)
+{
+	Fcall t;
+	int c, i, j, id;
+	Qid q;
+	uchar type;
+	ulong path;
+	Fid *nf;
+	Dirtab *d, *dir;
+	Window *w;
+	char *err;
+
+	nf = nil;
+	w = nil;
+	if(f->open)
+		return respond(x, &t, "walk of open file");
+	if(x->fid != x->newfid){
+		nf = newfid(x->newfid);
+		if(nf->busy)
+			return respond(x, &t, "newfid already in use");
+		nf->busy = TRUE;
+		nf->open = FALSE;
+		nf->mntdir = f->mntdir;
+		if(f->mntdir)
+			f->mntdir->ref++;
+		nf->dir = f->dir;
+		nf->qid = f->qid;
+		nf->w = f->w;
+		nf->nrpart = 0;	/* not open, so must be zero */
+		if(nf->w)
+			incref(nf->w);
+		f = nf;	/* walk f */
+	}
+
+	t.nwqid = 0;
+	err = nil;
+	dir = nil;
+	id = WIN(f->qid);
+	q = f->qid;
+
+	if(x->nwname > 0){
+		for(i=0; i<x->nwname; i++){
+			if((q.type & QTDIR) == 0){
+				err = Enotdir;
+				break;
+			}
+
+			if(strcmp(x->wname[i], "..") == 0){
+				type = QTDIR;
+				path = Qdir;
+				id = 0;
+				if(w){
+					winclose(w);
+					w = nil;
+				}
+    Accept:
+				if(i == MAXWELEM){
+					err = "name too long";
+					break;
+				}
+				q.type = type;
+				q.vers = 0;
+				q.path = QID(id, path);
+				t.wqid[t.nwqid++] = q;
+				continue;
+			}
+
+			/* is it a numeric name? */
+			for(j=0; (c=x->wname[i][j]); j++)
+				if(c<'0' || '9'<c)
+					goto Regular;
+			/* yes: it's a directory */
+			if(w)	/* name has form 27/23; get out before losing w */
+				break;
+			id = atoi(x->wname[i]);
+			qlock(&row);
+			w = lookid(id, FALSE);
+			if(w == nil){
+				qunlock(&row);
+				break;
+			}
+			incref(w);	/* we'll drop reference at end if there's an error */
+			path = Qdir;
+			type = QTDIR;
+			qunlock(&row);
+			dir = dirtabw;
+			goto Accept;
+	
+    Regular:
+//			if(FILE(f->qid) == Qacme)	/* empty directory */
+//				break;
+			if(strcmp(x->wname[i], "new") == 0){
+				if(w)
+					error("w set in walk to new");
+				sendp(cnewwindow, nil);	/* signal newwindowthread */
+				w = recvp(cnewwindow);	/* receive new window */
+				incref(w);
+				type = QTDIR;
+				path = QID(w->id, Qdir);
+				id = w->id;
+				dir = dirtabw;
+				goto Accept;
+			}
+
+			if(id == 0)
+				d = dirtab;
+			else
+				d = dirtabw;
+			d++;	/* skip '.' */
+			for(; d->name; d++)
+				if(strcmp(x->wname[i], d->name) == 0){
+					path = d->qid;
+					type = d->type;
+					dir = d;
+					goto Accept;
+				}
+
+			break;	/* file not found */
+		}
+
+		if(i==0 && err == nil)
+			err = Eexist;
+	}
+
+	if(err!=nil || t.nwqid<x->nwname){
+		if(nf){
+			nf->busy = FALSE;
+			fsysdelid(nf->mntdir);
+		}
+	}else if(t.nwqid  == x->nwname){
+		if(w){
+			f->w = w;
+			w = nil;	/* don't drop the reference */
+		}
+		if(dir)
+			f->dir = dir;
+		f->qid = q;
+	}
+
+	if(w != nil)
+		winclose(w);
+
+	return respond(x, &t, err);
+}
+
+static
+Xfid*
+fsysopen(Xfid *x, Fid *f)
+{
+	Fcall t;
+	int m;
+
+	/* can't truncate anything, so just disregard */
+	x->mode &= ~(OTRUNC|OCEXEC);
+	/* can't execute or remove anything */
+	if(x->mode==OEXEC || (x->mode&ORCLOSE))
+		goto Deny;
+	switch(x->mode){
+	default:
+		goto Deny;
+	case OREAD:
+		m = 0400;
+		break;
+	case OWRITE:
+		m = 0200;
+		break;
+	case ORDWR:
+		m = 0600;
+		break;
+	}
+	if(((f->dir->perm&~(DMDIR|DMAPPEND))&m) != m)
+		goto Deny;
+
+	sendp(x->c, xfidopen);
+	return nil;
+
+    Deny:
+	return respond(x, &t, Eperm);
+}
+
+static
+Xfid*
+fsyscreate(Xfid *x, Fid*)
+{
+	Fcall t;
+
+	return respond(x, &t, Eperm);
+}
+
+static
+int
+idcmp(void *a, void *b)
+{
+	return *(int*)a - *(int*)b;
+}
+
+static
+Xfid*
+fsysread(Xfid *x, Fid *f)
+{
+	Fcall t;
+	uchar *b;
+	int i, id, n, o, e, j, k, *ids, nids;
+	Dirtab *d, dt;
+	Column *c;
+	uint clock, len;
+	char buf[16];
+
+	if(f->qid.type & QTDIR){
+		if(FILE(f->qid) == Qacme){	/* empty dir */
+			t.data = nil;
+			t.count = 0;
+			respond(x, &t, nil);
+			return x;
+		}
+		o = x->offset;
+		e = x->offset+x->count;
+		clock = getclock();
+		b = emalloc(messagesize);
+		id = WIN(f->qid);
+		n = 0;
+		if(id > 0)
+			d = dirtabw;
+		else
+			d = dirtab;
+		d++;	/* first entry is '.' */
+		for(i=0; d->name!=nil && i<e; i+=len){
+			len = dostat(WIN(x->f->qid), d, b+n, x->count-n, clock);
+			if(len <= BIT16SZ)
+				break;
+			if(i >= o)
+				n += len;
+			d++;
+		}
+		if(id == 0){
+			qlock(&row);
+			nids = 0;
+			ids = nil;
+			for(j=0; j<row.ncol; j++){
+				c = row.col[j];
+				for(k=0; k<c->nw; k++){
+					ids = realloc(ids, (nids+1)*sizeof(int));
+					ids[nids++] = c->w[k]->id;
+				}
+			}
+			qunlock(&row);
+			qsort(ids, nids, sizeof ids[0], idcmp);
+			j = 0;
+			dt.name = buf;
+			for(; j<nids && i<e; i+=len){
+				k = ids[j];
+				sprint(dt.name, "%d", k);
+				dt.qid = QID(k, Qdir);
+				dt.type = QTDIR;
+				dt.perm = DMDIR|0700;
+				len = dostat(k, &dt, b+n, x->count-n, clock);
+				if(len == 0)
+					break;
+				if(i >= o)
+					n += len;
+				j++;
+			}
+			free(ids);
+		}
+		t.data = (char*)b;
+		t.count = n;
+		respond(x, &t, nil);
+		free(b);
+		return x;
+	}
+	sendp(x->c, xfidread);
+	return nil;
+}
+
+static
+Xfid*
+fsyswrite(Xfid *x, Fid*)
+{
+	sendp(x->c, xfidwrite);
+	return nil;
+}
+
+static
+Xfid*
+fsysclunk(Xfid *x, Fid *f)
+{
+	fsysdelid(f->mntdir);
+	sendp(x->c, xfidclose);
+	return nil;
+}
+
+static
+Xfid*
+fsysremove(Xfid *x, Fid*)
+{
+	Fcall t;
+
+	return respond(x, &t, Eperm);
+}
+
+static
+Xfid*
+fsysstat(Xfid *x, Fid *f)
+{
+	Fcall t;
+
+	t.stat = emalloc(messagesize-IOHDRSZ);
+	t.nstat = dostat(WIN(x->f->qid), f->dir, t.stat, messagesize-IOHDRSZ, getclock());
+	x = respond(x, &t, nil);
+	free(t.stat);
+	return x;
+}
+
+static
+Xfid*
+fsyswstat(Xfid *x, Fid*)
+{
+	Fcall t;
+
+	return respond(x, &t, Eperm);
+}
+
+Fid*
+newfid(int fid)
+{
+	Fid *f, *ff, **fh;
+
+	ff = nil;
+	fh = &fids[fid&(Nhash-1)];
+	for(f=*fh; f; f=f->next)
+		if(f->fid == fid)
+			return f;
+		else if(ff==nil && f->busy==FALSE)
+			ff = f;
+	if(ff){
+		ff->fid = fid;
+		return ff;
+	}
+	f = emalloc(sizeof *f);
+	f->fid = fid;
+	f->next = *fh;
+	*fh = f;
+	return f;
+}
+
+uint
+getclock()
+{
+	char buf[32];
+
+	buf[0] = '\0';
+	pread(clockfd, buf, sizeof buf, 0);
+	return atoi(buf);
+}
+
+int
+dostat(int id, Dirtab *dir, uchar *buf, int nbuf, uint clock)
+{
+	Dir d;
+
+	d.qid.path = QID(id, dir->qid);
+	d.qid.vers = 0;
+	d.qid.type = dir->type;
+	d.mode = dir->perm;
+	d.length = 0;	/* would be nice to do better */
+	d.name = dir->name;
+	d.uid = user;
+	d.gid = user;
+	d.muid = user;
+	d.atime = clock;
+	d.mtime = clock;
+	return convD2M(&d, buf, nbuf);
+}
--- /dev/null
+++ b/logf.c
@@ -1,0 +1,202 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <cursor.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <frame.h>
+#include <fcall.h>
+#include <plumb.h>
+#include <libsec.h>
+#include "dat.h"
+#include "fns.h"
+
+// State for global log file.
+typedef struct Log Log;
+struct Log
+{
+	QLock lk;
+	Rendez r;
+
+	vlong start; // msg[0] corresponds to 'start' in the global sequence of events
+
+	// queued events (nev=entries in ev, mev=capacity of p)
+	char **ev;
+	int nev;
+	int mev;
+
+	// open acme/put files that need to read events
+	Fid **f;
+	int nf;
+	int mf;
+
+	// active (blocked) reads waiting for events
+	Xfid **read;
+	int nread;
+	int mread;
+};
+
+static Log eventlog;
+
+void
+xfidlogopen(Xfid *x)
+{
+	qlock(&eventlog.lk);
+	if(eventlog.nf >= eventlog.mf) {
+		eventlog.mf = eventlog.mf*2;
+		if(eventlog.mf == 0)
+			eventlog.mf = 8;
+		eventlog.f = erealloc(eventlog.f, eventlog.mf*sizeof eventlog.f[0]);
+	}
+	eventlog.f[eventlog.nf++] = x->f;
+	x->f->logoff = eventlog.start + eventlog.nev;
+
+	qunlock(&eventlog.lk);
+}
+
+void
+xfidlogclose(Xfid *x)
+{
+	int i;
+
+	qlock(&eventlog.lk);
+	for(i=0; i<eventlog.nf; i++) {
+		if(eventlog.f[i] == x->f) {
+			eventlog.f[i] = eventlog.f[--eventlog.nf];
+			break;
+		}
+	}
+	qunlock(&eventlog.lk);
+}
+
+void
+xfidlogread(Xfid *x)
+{
+	char *p;
+	int i;
+	Fcall fc;
+
+	qlock(&eventlog.lk);
+	if(eventlog.nread >= eventlog.mread) {
+		eventlog.mread = eventlog.mread*2;
+		if(eventlog.mread == 0)
+			eventlog.mread = 8;
+		eventlog.read = erealloc(eventlog.read, eventlog.mread*sizeof eventlog.read[0]);
+	}
+	eventlog.read[eventlog.nread++] = x;
+
+	if(eventlog.r.l == nil)
+		eventlog.r.l = &eventlog.lk;
+	x->flushed = FALSE;
+	while(x->f->logoff >= eventlog.start+eventlog.nev && !x->flushed)
+		rsleep(&eventlog.r);
+
+	for(i=0; i<eventlog.nread; i++) {
+		if(eventlog.read[i] == x) {
+			eventlog.read[i] = eventlog.read[--eventlog.nread];
+			break;
+		}
+	}
+
+	if(x->flushed) {
+		qunlock(&eventlog.lk);
+		return;
+	}
+
+	i = x->f->logoff - eventlog.start;
+	p = estrdup(eventlog.ev[i]);
+	x->f->logoff++;
+	qunlock(&eventlog.lk);
+
+	fc.data = p;
+	fc.count = strlen(p);
+	respond(x, &fc, nil);
+	free(p);
+}
+
+void
+xfidlogflush(Xfid *x)
+{
+	int i;
+	Xfid *rx;
+
+	qlock(&eventlog.lk);
+	for(i=0; i<eventlog.nread; i++) {
+		rx = eventlog.read[i];
+		if(rx->tag == x->oldtag) {
+			rx->flushed = TRUE;
+			rwakeupall(&eventlog.r);
+		}
+	}
+	qunlock(&eventlog.lk);
+}
+
+/*
+ * add a log entry for op on w.
+ * expected calls:
+ *
+ * op == "new" for each new window
+ *	- caller of coladd or makenewwindow responsible for calling
+ *		xfidlog after setting window name
+ *	- exception: zerox
+ *
+ * op == "zerox" for new window created via zerox
+ *	- called from zeroxx
+ *
+ * op == "get" for Get executed on window
+ *	- called from get
+ *
+ * op == "put" for Put executed on window
+ *	- called from put
+ *
+ * op == "del" for deleted window
+ *	- called from winclose
+ *
+ * op == "focus" for window focus change
+ *	- called from mousethread
+ */
+void
+xfidlog(Window *w, char *op)
+{
+	int i, n;
+	vlong min;
+	File *f;
+	char *name;
+
+	qlock(&eventlog.lk);
+	if(eventlog.nev >= eventlog.mev) {
+		// Remove and free any entries that all readers have read.
+		min = eventlog.start + eventlog.nev;
+		for(i=0; i<eventlog.nf; i++) {
+			if(min > eventlog.f[i]->logoff)
+				min = eventlog.f[i]->logoff;
+		}
+		if(min > eventlog.start) {
+			n = min - eventlog.start;
+			for(i=0; i<n; i++)
+				free(eventlog.ev[i]);
+			eventlog.nev -= n;
+			eventlog.start += n;
+			memmove(eventlog.ev, eventlog.ev+n, eventlog.nev*sizeof eventlog.ev[0]);
+		}
+
+		// Otherwise grow.
+		if(eventlog.nev >= eventlog.mev) {
+			eventlog.mev = eventlog.mev*2;
+			if(eventlog.mev == 0)
+				eventlog.mev = 8;
+			eventlog.ev = erealloc(eventlog.ev, eventlog.mev*sizeof eventlog.ev[0]);
+		}
+	}
+	f = w->body.file;
+	name = runetobyte(f->name, f->nname);
+	if(name == nil)
+		name = estrdup("");
+	eventlog.ev[eventlog.nev++] = smprint("%d %s %s\n", w->id, op, name);
+	free(name);
+	if(eventlog.r.l == nil)
+		eventlog.r.l = &eventlog.lk;
+	rwakeupall(&eventlog.r);
+	qunlock(&eventlog.lk);
+}
--- /dev/null
+++ b/look.c
@@ -1,0 +1,737 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <cursor.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <frame.h>
+#include <fcall.h>
+#include <plumb.h>
+#include "dat.h"
+#include "fns.h"
+
+Window*	openfile(Text*, Expand*);
+
+int	nuntitled;
+
+void
+look3(Text *t, uint q0, uint q1, int external)
+{
+	int n, c, f, expanded;
+	Text *ct;
+	Expand e;
+	Rune *r;
+	uint p;
+	Plumbmsg *m;
+	Runestr dir;
+	char buf[32];
+
+	ct = seltext;
+	if(ct == nil)
+		seltext = t;
+	expanded = expand(t, q0, q1, &e);
+	if(!external && t->w!=nil && t->w->nopen[QWevent]>0){
+		/* send alphanumeric expansion to external client */
+		if(expanded == FALSE)
+			return;
+		f = 0;
+		if((e.at!=nil && t->w!=nil) || (e.nname>0 && lookfile(e.name, e.nname)!=nil))
+			f = 1;		/* acme can do it without loading a file */
+		if(q0!=e.q0 || q1!=e.q1)
+			f |= 2;	/* second (post-expand) message follows */
+		if(e.nname)
+			f |= 4;	/* it's a file name */
+		c = 'l';
+		if(t->what == Body)
+			c = 'L';
+		n = q1-q0;
+		if(n <= EVENTSIZE){
+			r = runemalloc(n);
+			bufread(t->file, q0, r, n);
+			winevent(t->w, "%c%d %d %d %d %.*S\n", c, q0, q1, f, n, n, r);
+			free(r);
+		}else
+			winevent(t->w, "%c%d %d %d 0 \n", c, q0, q1, f, n);
+		if(q0==e.q0 && q1==e.q1)
+			return;
+		if(e.nname){
+			n = e.nname;
+			if(e.a1 > e.a0)
+				n += 1+(e.a1-e.a0);
+			r = runemalloc(n);
+			runemove(r, e.name, e.nname);
+			if(e.a1 > e.a0){
+				r[e.nname] = ':';
+				bufread(e.at->file, e.a0, r+e.nname+1, e.a1-e.a0);
+			}
+		}else{
+			n = e.q1 - e.q0;
+			r = runemalloc(n);
+			bufread(t->file, e.q0, r, n);
+		}
+		f &= ~2;
+		if(n <= EVENTSIZE)
+			winevent(t->w, "%c%d %d %d %d %.*S\n", c, e.q0, e.q1, f, n, n, r);
+		else
+			winevent(t->w, "%c%d %d %d 0 \n", c, e.q0, e.q1, f, n);
+		free(r);
+		goto Return;
+	}
+	if(plumbsendfd >= 0){
+		/* send whitespace-delimited word to plumber */
+		m = emalloc(sizeof(Plumbmsg));
+		m->src = estrdup("acme");
+		m->dst = nil;
+		dir = dirname(t, nil, 0);
+		if(dir.nr==1 && dir.r[0]=='.'){	/* sigh */
+			free(dir.r);
+			dir.r = nil;
+			dir.nr = 0;
+		}
+		if(dir.nr == 0)
+			m->wdir = estrdup(wdir);
+		else
+			m->wdir = runetobyte(dir.r, dir.nr);
+		free(dir.r);
+		m->type = estrdup("text");
+		m->attr = nil;
+		buf[0] = '\0';
+		if(q1 == q0){
+			if(t->q1>t->q0 && t->q0<=q0 && q0<=t->q1){
+				q0 = t->q0;
+				q1 = t->q1;
+			}else{
+				p = q0;
+				while(q0>0 && (c=tgetc(t, q0-1))!=' ' && c!='\t' && c!='\n')
+					q0--;
+				while(q1<t->file->nc && (c=tgetc(t, q1))!=' ' && c!='\t' && c!='\n')
+					q1++;
+				if(q1 == q0){
+					plumbfree(m);
+					goto Return;
+				}
+				sprint(buf, "click=%d", p-q0);
+				m->attr = plumbunpackattr(buf);
+			}
+		}
+		r = runemalloc(q1-q0);
+		bufread(t->file, q0, r, q1-q0);
+		m->data = runetobyte(r, q1-q0);
+		m->ndata = strlen(m->data);
+		free(r);
+		if(m->ndata<messagesize-1024 && plumbsend(plumbsendfd, m) >= 0){
+			plumbfree(m);
+			goto Return;
+		}
+		plumbfree(m);
+		/* plumber failed to match; fall through */
+	}
+
+	/* interpret alphanumeric string ourselves */
+	if(expanded == FALSE)
+		return;
+	if(e.name || e.at)
+		openfile(t, &e);
+	else{
+		if(t->w == nil)
+			return;
+		ct = &t->w->body;
+		if(t->w != ct->w)
+			winlock(ct->w, 'M');
+		if(t == ct)
+			textsetselect(ct, e.q1, e.q1);
+		n = e.q1 - e.q0;
+		r = runemalloc(n);
+		bufread(t->file, e.q0, r, n);
+		if(search(ct, r, n) && e.jump)
+			moveto(mousectl, addpt(frptofchar(ct, ct->p0), Pt(4, ct->font->height-4)));
+		if(t->w != ct->w)
+			winunlock(ct->w);
+		free(r);
+	}
+
+   Return:
+	free(e.name);
+	free(e.bname);
+}
+
+int
+plumbgetc(void *a, uint n)
+{
+	Rune *r;
+
+	r = a;
+	if(n>runestrlen(r))
+		return 0;
+	return r[n];
+}
+
+void
+plumblook(Plumbmsg *m)
+{
+	Expand e;
+	char *addr;
+
+	if(m->ndata >= BUFSIZE){
+		warning(nil, "insanely long file name (%d bytes) in plumb message (%.32s...)\n", m->ndata, m->data);
+		return;
+	}
+	e.q0 = 0;
+	e.q1 = 0;
+	if(m->data[0] == '\0')
+		return;
+	e.ar = nil;
+	e.bname = m->data;
+	e.name = bytetorune(e.bname, &e.nname);
+	e.jump = TRUE;
+	e.a0 = 0;
+	e.a1 = 0;
+	addr = plumblookup(m->attr, "addr");
+	if(addr != nil){
+		e.ar = bytetorune(addr, &e.a1);
+		e.agetc = plumbgetc;
+	}
+	openfile(nil, &e);
+	free(e.name);
+	free(e.at);
+}
+
+void
+plumbshow(Plumbmsg *m)
+{
+	Window *w;
+	Rune rb[256], *r;
+	int nb, nr;
+	Runestr rs;
+	char *name, *p, namebuf[16];
+
+	w = makenewwindow(nil);
+	name = plumblookup(m->attr, "filename");
+	if(name == nil){
+		name = namebuf;
+		nuntitled++;
+		snprint(namebuf, sizeof namebuf, "Untitled-%d", nuntitled);
+	}
+	p = nil;
+	if(name[0]!='/' && m->wdir!=nil && m->wdir[0]!='\0'){
+		nb = strlen(m->wdir) + 1 + strlen(name) + 1;
+		p = emalloc(nb);
+		snprint(p, nb, "%s/%s", m->wdir, name);
+		name = p;
+	}
+	cvttorunes(name, strlen(name), rb, &nb, &nr, nil);
+	free(p);
+	rs = cleanrname((Runestr){rb, nr});
+	winsetname(w, rs.r, rs.nr);
+	r = runemalloc(m->ndata);
+	cvttorunes(m->data, m->ndata, r, &nb, &nr, nil);
+	textinsert(&w->body, 0, r, nr, TRUE);
+	free(r);
+	w->body.file->mod = FALSE;
+	w->dirty = FALSE;
+	winsettag(w);
+	textscrdraw(&w->body);
+	textsetselect(&w->tag, w->tag.file->nc, w->tag.file->nc);
+	xfidlog(w, "new");
+}
+
+int
+search(Text *ct, Rune *r, uint n)
+{
+	uint q, nb, maxn;
+	int around;
+	Rune *s, *b, *c;
+
+	if(n==0 || n>ct->file->nc)
+		return FALSE;
+	if(2*n > RBUFSIZE){
+		warning(nil, "string too long\n");
+		return FALSE;
+	}
+	maxn = max(2*n, RBUFSIZE);
+	s = fbufalloc();
+	b = s;
+	nb = 0;
+	b[nb] = 0;
+	around = 0;
+	q = ct->q1;
+	for(;;){
+		if(q >= ct->file->nc){
+			q = 0;
+			around = 1;
+			nb = 0;
+			b[nb] = 0;
+		}
+		if(nb > 0){
+			c = runestrchr(b, r[0]);
+			if(c == nil){
+				q += nb;
+				nb = 0;
+				b[nb] = 0;
+				if(around && q>=ct->q1)
+					break;
+				continue;
+			}
+			q += (c-b);
+			nb -= (c-b);
+			b = c;
+		}
+		/* reload if buffer covers neither string nor rest of file */
+		if(nb<n && nb!=ct->file->nc-q){
+			nb = ct->file->nc-q;
+			if(nb >= maxn)
+				nb = maxn-1;
+			bufread(ct->file, q, s, nb);
+			b = s;
+			b[nb] = '\0';
+		}
+		/* this runeeq is fishy but the null at b[nb] makes it safe */
+		if(runeeq(b, n, r, n)==TRUE){
+			if(ct->w){
+				textshow(ct, q, q+n, 1);
+				winsettag(ct->w);
+			}else{
+				ct->q0 = q;
+				ct->q1 = q+n;
+			}
+			seltext = ct;
+			fbuffree(s);
+			return TRUE;
+		}
+		--nb;
+		b++;
+		q++;
+		if(around && q>=ct->q1)
+			break;
+	}
+	fbuffree(s);
+	return FALSE;
+}
+
+int
+isfilec(Rune r)
+{
+	if(isalnum(r))
+		return TRUE;
+	if(runestrchr(L".-+/:@", r))
+		return TRUE;
+	return FALSE;
+}
+
+/* Runestr wrapper for cleanname */
+Runestr
+cleanrname(Runestr rs)
+{
+	char *s;
+	int nb, nulls;
+
+	s = runetobyte(rs.r, rs.nr);
+	cleanname(s);
+	cvttorunes(s, strlen(s), rs.r, &nb, &rs.nr, &nulls);
+	free(s);
+	return rs;
+}
+
+Runestr
+includefile(Rune *dir, Rune *file, int nfile)
+{
+	int m, n;
+	char *a;
+	Rune *r;
+
+	m = runestrlen(dir);
+	a = emalloc((m+1+nfile)*UTFmax+1);
+	sprint(a, "%S/%.*S", dir, nfile, file);
+	n = access(a, 0);
+	free(a);
+	if(n < 0)
+		return (Runestr){nil, 0};
+	r = runemalloc(m+1+nfile);
+	runemove(r, dir, m);
+	runemove(r+m, L"/", 1);
+	runemove(r+m+1, file, nfile);
+	free(file);
+	return cleanrname((Runestr){r, m+1+nfile});
+}
+
+static	Rune	*objdir;
+
+Runestr
+includename(Text *t, Rune *r, int n)
+{
+	Window *w;
+	char buf[128];
+	Runestr file;
+	int i;
+
+	if(objdir==nil && objtype!=nil){
+		sprint(buf, "/%s/include", objtype);
+		objdir = bytetorune(buf, &i);
+		objdir = runerealloc(objdir, i+1);
+		objdir[i] = '\0';	
+	}
+
+	w = t->w;
+	if(n==0 || r[0]=='/' || w==nil)
+		goto Rescue;
+	if(n>2 && r[0]=='.' && r[1]=='/')
+		goto Rescue;
+	file.r = nil;
+	file.nr = 0;
+	for(i=0; i<w->nincl && file.r==nil; i++)
+		file = includefile(w->incl[i], r, n);
+
+	if(file.r == nil)
+		file = includefile(L"/sys/include", r, n);
+	if(file.r==nil && objdir!=nil)
+		file = includefile(objdir, r, n);
+	if(file.r == nil)
+		goto Rescue;
+	return file;
+
+    Rescue:
+	return (Runestr){r, n};
+}
+
+Runestr
+dirname(Text *t, Rune *r, int n)
+{
+	Rune *b, c;
+	uint m, nt;
+	int slash;
+	Runestr tmp;
+
+	b = nil;
+	if(t==nil || t->w==nil)
+		goto Rescue;
+	nt = t->w->tag.file->nc;
+	if(nt == 0)
+		goto Rescue;
+	if(n>=1 && r[0]=='/')
+		goto Rescue;
+	b = runemalloc(nt+n+1);
+	bufread(t->w->tag.file, 0, b, nt);
+	slash = -1;
+	for(m=0; m<nt; m++){
+		c = b[m];
+		if(c == '/')
+			slash = m;
+		if(c==' ' || c=='\t')
+			break;
+	}
+	if(slash < 0)
+		goto Rescue;
+	runemove(b+slash+1, r, n);
+	free(r);
+	return cleanrname((Runestr){b, slash+1+n});
+
+    Rescue:
+	free(b);
+	tmp = (Runestr){r, n};
+	if(r)
+		return cleanrname(tmp);
+	return tmp;
+}
+
+int
+expandfile(Text *t, uint q0, uint q1, Expand *e)
+{
+	int i, n, nname, colon, eval;
+	uint amin, amax;
+	Rune *r, c;
+	Window *w;
+	Runestr rs;
+
+	amax = q1;
+	if(q1 == q0){
+		colon = -1;
+		while(q1<t->file->nc && isfilec(c=textreadc(t, q1))){
+			if(c == ':'){
+				colon = q1;
+				break;
+			}
+			q1++;
+		}
+		while(q0>0 && (isfilec(c=textreadc(t, q0-1)) || isaddrc(c) || isregexc(c))){
+			q0--;
+			if(colon<0 && c==':')
+				colon = q0;
+		}
+		/*
+		 * if it looks like it might begin file: , consume address chars after :
+		 * otherwise terminate expansion at :
+		 */
+		if(colon >= 0){
+			q1 = colon;
+			if(colon<t->file->nc-1 && isaddrc(textreadc(t, colon+1))){
+				q1 = colon+1;
+				while(q1<t->file->nc && isaddrc(textreadc(t, q1)))
+					q1++;
+			}
+		}
+		if(q1 > q0)
+			if(colon >= 0){	/* stop at white space */
+				for(amax=colon+1; amax<t->file->nc; amax++)
+					if((c=textreadc(t, amax))==' ' || c=='\t' || c=='\n')
+						break;
+			}else
+				amax = t->file->nc;
+	}
+	amin = amax;
+	e->q0 = q0;
+	e->q1 = q1;
+	n = q1-q0;
+	if(n == 0)
+		return FALSE;
+	/* see if it's a file name */
+	r = runemalloc(n);
+	bufread(t->file, q0, r, n);
+	/* first, does it have bad chars? */
+	nname = -1;
+	for(i=0; i<n; i++){
+		c = r[i];
+		if(c==':' && nname<0){
+			if(q0+i+1<t->file->nc && (i==n-1 || isaddrc(textreadc(t, q0+i+1))))
+				amin = q0+i;
+			else
+				goto Isntfile;
+			nname = i;
+		}
+	}
+	if(nname == -1)
+		nname = n;
+	for(i=0; i<nname; i++)
+		if(!isfilec(r[i]))
+			goto Isntfile;
+	/*
+	 * See if it's a file name in <>, and turn that into an include
+	 * file name if so.  Should probably do it for "" too, but that's not
+	 * restrictive enough syntax and checking for a #include earlier on the
+	 * line would be silly.
+	 */
+	if(q0>0 && textreadc(t, q0-1)=='<' && q1<t->file->nc && textreadc(t, q1)=='>'){
+		rs = includename(t, r, nname);
+		r = rs.r;
+		nname = rs.nr;
+	}
+	else if(amin == q0)
+		goto Isfile;
+	else{
+		rs = dirname(t, r, nname);
+		r = rs.r;
+		nname = rs.nr;
+	}
+	e->bname = runetobyte(r, nname);
+	/* if it's already a window name, it's a file */
+	w = lookfile(r, nname);
+	if(w != nil)
+		goto Isfile;
+	/* if it's the name of a file, it's a file */
+	if(access(e->bname, 0) < 0){
+		free(e->bname);
+		e->bname = nil;
+		goto Isntfile;
+	}
+
+  Isfile:
+	e->name = r;
+	e->nname = nname;
+	e->at = t;
+	e->a0 = amin+1;
+	eval = FALSE;
+	address(nil, nil, (Range){-1,-1}, (Range){0, 0}, t, e->a0, amax, tgetc, &eval, (uint*)&e->a1);
+	return TRUE;
+
+   Isntfile:
+	free(r);
+	return FALSE;
+}
+
+int
+expand(Text *t, uint q0, uint q1, Expand *e)
+{
+	memset(e, 0, sizeof *e);
+	e->agetc = tgetc;
+	/* if in selection, choose selection */
+	e->jump = TRUE;
+	if(q1==q0 && t->q1>t->q0 && t->q0<=q0 && q0<=t->q1){
+		q0 = t->q0;
+		q1 = t->q1;
+		if(t->what == Tag)
+			e->jump = FALSE;
+	}
+
+	if(expandfile(t, q0, q1, e))
+		return TRUE;
+
+	if(q0 == q1){
+		while(q1<t->file->nc && isalnum(textreadc(t, q1)))
+			q1++;
+		while(q0>0 && isalnum(textreadc(t, q0-1)))
+			q0--;
+	}
+	e->q0 = q0;
+	e->q1 = q1;
+	return q1 > q0;
+}
+
+Window*
+lookfile(Rune *s, int n)
+{
+	int i, j, k;
+	Window *w;
+	Column *c;
+	Text *t;
+
+	/* avoid terminal slash on directories */
+	if(n>1 && s[n-1] == '/')
+		--n;
+	for(j=0; j<row.ncol; j++){
+		c = row.col[j];
+		for(i=0; i<c->nw; i++){
+			w = c->w[i];
+			t = &w->body;
+			k = t->file->nname;
+			if(k>1 && t->file->name[k-1] == '/')
+				k--;
+			if(runeeq(t->file->name, k, s, n)){
+				w = w->body.file->curtext->w;
+				if(w->col != nil)	/* protect against race deleting w */
+					return w;
+			}
+		}
+	}
+	return nil;
+}
+
+Window*
+lookid(int id, int dump)
+{
+	int i, j;
+	Window *w;
+	Column *c;
+
+	for(j=0; j<row.ncol; j++){
+		c = row.col[j];
+		for(i=0; i<c->nw; i++){
+			w = c->w[i];
+			if(dump && w->dumpid == id)
+				return w;
+			if(!dump && w->id == id)
+				return w;
+		}
+	}
+	return nil;
+}
+
+
+Window*
+openfile(Text *t, Expand *e)
+{
+	Range r;
+	Window *w, *ow;
+	int eval, i, n;
+	Rune *rp;
+	uint dummy;
+
+	if(e->nname == 0){
+		w = t->w;
+		if(w == nil)
+			return nil;
+	}else
+		w = lookfile(e->name, e->nname);
+	if(w){
+		t = &w->body;
+		if(!t->col->safe && t->maxlines==0) /* window is obscured by full-column window */
+			colgrow(t->col, t->col->w[0], 1);
+	}else{
+		ow = nil;
+		if(t)
+			ow = t->w;
+		w = makenewwindow(t);
+		t = &w->body;
+		winsetname(w, e->name, e->nname);
+		textload(t, 0, e->bname, 1);
+		t->file->mod = FALSE;
+		t->w->dirty = FALSE;
+		winsettag(t->w);
+		textsetselect(&t->w->tag, t->w->tag.file->nc, t->w->tag.file->nc);
+		if(ow != nil){
+			for(i=ow->nincl; --i>=0; ){
+				n = runestrlen(ow->incl[i]);
+				rp = runemalloc(n);
+				runemove(rp, ow->incl[i], n);
+				winaddincl(w, rp, n);
+			}
+			for(i=0; i < NINDENT; i++)
+				w->indent[i] = ow->indent[i];
+		}else
+			for(i=0; i < NINDENT; i++)
+				w->indent[i] = globalindent[i];
+		xfidlog(w, "new");
+	}
+	if(e->a1 == e->a0)
+		eval = FALSE;
+	else{
+		eval = TRUE;
+		r = address(nil, t, (Range){-1, -1}, (Range){t->q0, t->q1}, e->at, e->a0, e->a1, e->agetc, &eval, &dummy);
+		if(eval == FALSE)
+			e->jump = FALSE;	/* don't jump if invalid address */
+	}
+	if(eval == FALSE){
+		r.q0 = t->q0;
+		r.q1 = t->q1;
+	}
+	textshow(t, r.q0, r.q1, 1);
+	winsettag(t->w);
+	seltext = t;
+	if(e->jump)
+		moveto(mousectl, addpt(frptofchar(t, t->p0), Pt(4, font->height-4)));
+	return w;
+}
+
+void
+new(Text *et, Text *t, Text *argt, int flag1, int flag2, Rune *arg, int narg)
+{
+	int ndone;
+	Rune *a, *f;
+	int na, nf;
+	Expand e;
+	Runestr rs;
+	Window *w;
+
+	getarg(argt, FALSE, TRUE, &a, &na);
+	if(a){
+		new(et, t, nil, flag1, flag2, a, na);
+		if(narg == 0)
+			return;
+	}
+	/* loop condition: *arg is not a blank */
+	for(ndone=0; ; ndone++){
+		a = findbl(arg, narg, &na);
+		if(a == arg){
+			if(ndone==0 && et->col!=nil) {
+				w = coladd(et->col, nil, nil, -1);
+				winsettag(w);
+				xfidlog(w, "new");
+			}
+			break;
+		}
+		nf = narg-na;
+		f = runemalloc(nf);
+		runemove(f, arg, nf);
+		rs = dirname(et, f, nf);
+		f = rs.r;
+		nf = rs.nr;
+		memset(&e, 0, sizeof e);
+		e.name = f;
+		e.nname = nf;
+		e.bname = runetobyte(f, nf);
+		e.jump = TRUE;
+		openfile(et, &e);
+		free(f);
+		free(e.bname);
+		arg = skipbl(a, na, &narg);
+	}
+}
--- /dev/null
+++ b/mkfile
@@ -1,0 +1,46 @@
+</$objtype/mkfile
+BIN=/$objtype/bin
+
+TARG=acme-themes
+
+OFILES=\
+	acme.$O\
+	addr.$O\
+	buff.$O\
+	cols.$O\
+	disk.$O\
+	ecmd.$O\
+	edit.$O\
+	elog.$O\
+	exec.$O\
+	file.$O\
+	fsys.$O\
+	logf.$O\
+	look.$O\
+	regx.$O\
+	rows.$O\
+	scrl.$O\
+	text.$O\
+	time.$O\
+	util.$O\
+	wind.$O\
+	xfid.$O\
+
+HFILES=dat.h\
+	edit.h\
+	fns.h\
+
+UPDATE=\
+	mkfile\
+	$HFILES\
+	${OFILES:%.$O=%.c}\
+
+</sys/src/cmd/mkone
+
+$O.out:	/$objtype/lib/libframe.a /$objtype/lib/libdraw.a /$objtype/lib/libthread.a
+
+edit.$O ecmd.$O elog.$O:	edit.h
+
+syms:V:
+	$CC -a acme.c > syms
+	for(i in ????.c) $CC -aa $i >> syms
--- /dev/null
+++ b/regx.c
@@ -1,0 +1,839 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <cursor.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <frame.h>
+#include <fcall.h>
+#include <plumb.h>
+#include "dat.h"
+#include "fns.h"
+
+Rangeset	sel;
+Rune		*lastregexp;
+
+/*
+ * Machine Information
+ */
+typedef struct Inst Inst;
+struct Inst
+{
+	uint	type;	/* <= Runemax+1 ==> literal, otherwise action */
+	union {
+		int sid;
+		int subid;
+		int class;
+		Inst *other;
+		Inst *right;
+	};
+	union{
+		Inst *left;
+		Inst *next;
+	};
+};
+
+#define	NPROG	1024
+Inst	program[NPROG];
+Inst	*progp;
+Inst	*startinst;	/* First inst. of program; might not be program[0] */
+Inst	*bstartinst;	/* same for backwards machine */
+Channel	*rechan;	/* chan(Inst*) */
+
+typedef struct Ilist Ilist;
+struct Ilist
+{
+	Inst	*inst;		/* Instruction of the thread */
+	Rangeset se;
+	uint	startp;		/* first char of match */
+};
+
+#define	NLIST	127
+
+Ilist	*tl, *nl;	/* This list, next list */
+Ilist	list[2][NLIST+1];	/* +1 for trailing null */
+static	Rangeset sempty;
+
+/*
+ * Actions and Tokens
+ *
+ *	0x100xx are operators, value == precedence
+ *	0x200xx are tokens, i.e. operands for operators
+ */
+enum {
+	OPERATOR = Runemask+1,	/* Bitmask of all operators */
+	START	= OPERATOR,	/* Start, used for marker on stack */
+	RBRA,			/* Right bracket, ) */
+	LBRA,			/* Left bracket, ( */
+	OR,			/* Alternation, | */
+	CAT,			/* Concatentation, implicit operator */
+	STAR,			/* Closure, * */
+	PLUS,			/* a+ == aa* */
+	QUEST,			/* a? == a|nothing, i.e. 0 or 1 a's */
+
+	ANY	= OPERATOR<<1,	/* Any character but newline, . */
+	NOP,			/* No operation, internal use only */
+	BOL,			/* Beginning of line, ^ */
+	EOL,			/* End of line, $ */
+	CCLASS,			/* Character class, [] */
+	NCCLASS,		/* Negated character class, [^] */
+	END,			/* Terminate: match found */
+
+	ISATOR	= OPERATOR,
+	ISAND	= OPERATOR<<1,
+};
+
+/*
+ * Parser Information
+ */
+typedef struct Node Node;
+struct Node
+{
+	Inst	*first;
+	Inst	*last;
+};
+
+#define	NSTACK	20
+Node	andstack[NSTACK];
+Node	*andp;
+int	atorstack[NSTACK];
+int	*atorp;
+int	lastwasand;	/* Last token was operand */
+int	cursubid;
+int	subidstack[NSTACK];
+int	*subidp;
+int	backwards;
+int	nbra;
+Rune	*exprp;		/* pointer to next character in source expression */
+#define	DCLASS	10	/* allocation increment */
+int	nclass;		/* number active */
+int	Nclass;		/* high water mark */
+Rune	**class;
+int	negateclass;
+
+int	addinst(Ilist *l, Inst *inst, Rangeset *sep);
+void	newmatch(Rangeset*);
+void	bnewmatch(Rangeset*);
+void	pushand(Inst*, Inst*);
+void	pushator(int);
+Node	*popand(int);
+int	popator(void);
+void	startlex(Rune*);
+int	lex(void);
+void	operator(int);
+void	operand(int);
+void	evaluntil(int);
+void	optimize(Inst*);
+void	bldcclass(void);
+
+void
+rxinit(void)
+{
+	rechan = chancreate(sizeof(Inst*), 0);
+	lastregexp = runemalloc(1);
+}
+
+void
+regerror(char *e)
+{
+	lastregexp[0] = 0;
+	warning(nil, "regexp: %s\n", e);
+	sendp(rechan, nil);
+	threadexits(nil);
+}
+
+Inst *
+newinst(int t)
+{
+	if(progp >= &program[NPROG])
+		regerror("expression too long");
+	progp->type = t;
+	progp->left = nil;
+	progp->right = nil;
+	return progp++;
+}
+
+void
+realcompile(void *arg)
+{
+	int token;
+	Rune *s;
+
+	threadsetname("regcomp");
+	s = arg;
+	startlex(s);
+	atorp = atorstack;
+	andp = andstack;
+	subidp = subidstack;
+	cursubid = 0;
+	lastwasand = FALSE;
+	/* Start with a low priority operator to prime parser */
+	pushator(START-1);
+	while((token=lex()) != END){
+		if((token&ISATOR) == OPERATOR)
+			operator(token);
+		else
+			operand(token);
+	}
+	/* Close with a low priority operator */
+	evaluntil(START);
+	/* Force END */
+	operand(END);
+	evaluntil(START);
+	if(nbra)
+		regerror("unmatched `('");
+	--andp;	/* points to first and only operand */
+	sendp(rechan, andp->first);
+	threadexits(nil);
+}
+
+/* r is null terminated */
+int
+rxcompile(Rune *r)
+{
+	int i, nr;
+	Inst *oprogp;
+
+	nr = runestrlen(r)+1;
+	if(runeeq(lastregexp, runestrlen(lastregexp)+1, r, nr)==TRUE)
+		return TRUE;
+	lastregexp[0] = 0;
+	for(i=0; i<nclass; i++)
+		free(class[i]);
+	nclass = 0;
+	progp = program;
+	backwards = FALSE;
+	bstartinst = nil;
+	threadcreate(realcompile, r, STACK);
+	startinst = recvp(rechan);
+	if(startinst == nil)
+		return FALSE;
+	optimize(program);
+	oprogp = progp;
+	backwards = TRUE;
+	threadcreate(realcompile, r, STACK);
+	bstartinst = recvp(rechan);
+	if(bstartinst == nil)
+		return FALSE;
+	optimize(oprogp);
+	lastregexp = runerealloc(lastregexp, nr);
+	runemove(lastregexp, r, nr);
+	return TRUE;
+}
+
+void
+operand(int t)
+{
+	Inst *i;
+	if(lastwasand)
+		operator(CAT);	/* catenate is implicit */
+	i = newinst(t);
+	if(t == CCLASS){
+		if(negateclass)
+			i->type = NCCLASS;	/* UGH */
+		i->class = nclass-1;		/* UGH */
+	}
+	pushand(i, i);
+	lastwasand = TRUE;
+}
+
+void
+operator(int t)
+{
+	if(t==RBRA && --nbra<0)
+		regerror("unmatched `)'");
+	if(t==LBRA){
+		cursubid++;	/* silently ignored */
+		nbra++;
+		if(lastwasand)
+			operator(CAT);
+	}else
+		evaluntil(t);
+	if(t!=RBRA)
+		pushator(t);
+	lastwasand = FALSE;
+	if(t==STAR || t==QUEST || t==PLUS || t==RBRA)
+		lastwasand = TRUE;	/* these look like operands */
+}
+
+void
+pushand(Inst *f, Inst *l)
+{
+	if(andp >= &andstack[NSTACK])
+		error("operand stack overflow");
+	andp->first = f;
+	andp->last = l;
+	andp++;
+}
+
+void
+pushator(int t)
+{
+	if(atorp >= &atorstack[NSTACK])
+		error("operator stack overflow");
+	*atorp++=t;
+	if(cursubid >= NRange)
+		*subidp++= -1;
+	else
+		*subidp++=cursubid;
+}
+
+Node *
+popand(int op)
+{
+	char buf[64];
+
+	if(andp <= &andstack[0])
+		if(op){
+			sprint(buf, "missing operand for %c", op);
+			regerror(buf);
+		}else
+			regerror("malformed regexp");
+	return --andp;
+}
+
+int
+popator()
+{
+	if(atorp <= &atorstack[0])
+		error("operator stack underflow");
+	--subidp;
+	return *--atorp;
+}
+
+void
+evaluntil(int pri)
+{
+	Node *op1, *op2, *t;
+	Inst *inst1, *inst2;
+
+	while(pri==RBRA || atorp[-1]>=pri){
+		switch(popator()){
+		case LBRA:
+			op1 = popand('(');
+			inst2 = newinst(RBRA);
+			inst2->subid = *subidp;
+			op1->last->next = inst2;
+			inst1 = newinst(LBRA);
+			inst1->subid = *subidp;
+			inst1->next = op1->first;
+			pushand(inst1, inst2);
+			return;		/* must have been RBRA */
+		default:
+			error("unknown regexp operator");
+			break;
+		case OR:
+			op2 = popand('|');
+			op1 = popand('|');
+			inst2 = newinst(NOP);
+			op2->last->next = inst2;
+			op1->last->next = inst2;
+			inst1 = newinst(OR);
+			inst1->right = op1->first;
+			inst1->left = op2->first;
+			pushand(inst1, inst2);
+			break;
+		case CAT:
+			op2 = popand(0);
+			op1 = popand(0);
+			if(backwards && op2->first->type!=END){
+				t = op1;
+				op1 = op2;
+				op2 = t;
+			}
+			op1->last->next = op2->first;
+			pushand(op1->first, op2->last);
+			break;
+		case STAR:
+			op2 = popand('*');
+			inst1 = newinst(OR);
+			op2->last->next = inst1;
+			inst1->right = op2->first;
+			pushand(inst1, inst1);
+			break;
+		case PLUS:
+			op2 = popand('+');
+			inst1 = newinst(OR);
+			op2->last->next = inst1;
+			inst1->right = op2->first;
+			pushand(op2->first, inst1);
+			break;
+		case QUEST:
+			op2 = popand('?');
+			inst1 = newinst(OR);
+			inst2 = newinst(NOP);
+			inst1->left = inst2;
+			inst1->right = op2->first;
+			op2->last->next = inst2;
+			pushand(inst1, inst2);
+			break;
+		}
+	}
+}
+
+
+void
+optimize(Inst *start)
+{
+	Inst *inst, *target;
+
+	for(inst=start; inst->type!=END; inst++){
+		target = inst->next;
+		while(target->type == NOP)
+			target = target->next;
+		inst->next = target;
+	}
+}
+
+void
+startlex(Rune *s)
+{
+	exprp = s;
+	nbra = 0;
+}
+
+
+int
+lex(void){
+	int c;
+
+	c = *exprp++;
+	switch(c){
+	case '\\':
+		if(*exprp)
+			if((c= *exprp++)=='n')
+				c='\n';
+		break;
+	case 0:
+		c = END;
+		--exprp;	/* In case we come here again */
+		break;
+	case '*':
+		c = STAR;
+		break;
+	case '?':
+		c = QUEST;
+		break;
+	case '+':
+		c = PLUS;
+		break;
+	case '|':
+		c = OR;
+		break;
+	case '.':
+		c = ANY;
+		break;
+	case '(':
+		c = LBRA;
+		break;
+	case ')':
+		c = RBRA;
+		break;
+	case '^':
+		c = BOL;
+		break;
+	case '$':
+		c = EOL;
+		break;
+	case '[':
+		c = CCLASS;
+		bldcclass();
+		break;
+	}
+	return c;
+}
+
+int
+nextrec(void)
+{
+	if(exprp[0]==0 || (exprp[0]=='\\' && exprp[1]==0))
+		regerror("malformed `[]'");
+	if(exprp[0] == '\\'){
+		exprp++;
+		if(*exprp=='n'){
+			exprp++;
+			return '\n';
+		}
+		return *exprp++|(Runemask+1);
+	}
+	return *exprp++;
+}
+
+void
+bldcclass(void)
+{
+	int c1, c2, n, na;
+	Rune *classp;
+
+	classp = runemalloc(DCLASS);
+	n = 0;
+	na = DCLASS;
+	/* we have already seen the '[' */
+	if(*exprp == '^'){
+		classp[n++] = '\n';	/* don't match newline in negate case */
+		negateclass = TRUE;
+		exprp++;
+	}else
+		negateclass = FALSE;
+	while((c1 = nextrec()) != ']'){
+		if(c1 == '-'){
+    Error:
+			free(classp);
+			regerror("malformed `[]'");
+		}
+		if(n+4 >= na){		/* 3 runes plus NUL */
+			na += DCLASS;
+			classp = runerealloc(classp, na);
+		}
+		if(*exprp == '-'){
+			exprp++;	/* eat '-' */
+			if((c2 = nextrec()) == ']')
+				goto Error;
+			classp[n+0] = Runemax;
+			classp[n+1] = c1 & Runemask;
+			classp[n+2] = c2 & Runemask;
+			n += 3;
+		}else
+			classp[n++] = c1 & Runemask;
+	}
+	classp[n] = 0;
+	if(nclass == Nclass){
+		Nclass += DCLASS;
+		class = realloc(class, Nclass*sizeof(Rune*));
+	}
+	class[nclass++] = classp;
+}
+
+int
+classmatch(int classno, int c, int negate)
+{
+	Rune *p;
+
+	p = class[classno];
+	while(*p){
+		if(*p == Runemax){
+			if(p[1]<=c && c<=p[2])
+				return !negate;
+			p += 3;
+		}else if(*p++ == c)
+			return !negate;
+	}
+	return negate;
+}
+
+/*
+ * Note optimization in addinst:
+ * 	*l must be pending when addinst called; if *l has been looked
+ *		at already, the optimization is a bug.
+ */
+int
+addinst(Ilist *l, Inst *inst, Rangeset *sep)
+{
+	Ilist *p;
+
+	for(p = l; p->inst; p++){
+		if(p->inst==inst){
+			if((sep)->r[0].q0 < p->se.r[0].q0)
+				p->se= *sep;	/* this would be bug */
+			return 0;	/* It's already there */
+		}
+	}
+	p->inst = inst;
+	p->se= *sep;
+	(p+1)->inst = nil;
+	return 1;
+}
+
+int
+rxnull(void)
+{
+	return startinst==nil || bstartinst==nil;
+}
+
+/* either t!=nil or r!=nil, and we match the string in the appropriate place */
+int
+rxexecute(Text *t, Rune *r, uint startp, uint eof, Rangeset *rp)
+{
+	int flag;
+	Inst *inst;
+	Ilist *tlp;
+	uint p;
+	int nnl, ntl;
+	int nc, c;
+	int wrapped;
+	int startchar;
+
+	flag = 0;
+	p = startp;
+	startchar = 0;
+	wrapped = 0;
+	nnl = 0;
+	if(startinst->type<OPERATOR)
+		startchar = startinst->type;
+	list[0][0].inst = list[1][0].inst = nil;
+	sel.r[0].q0 = -1;
+	if(t != nil)
+		nc = t->file->nc;
+	else
+		nc = runestrlen(r);
+	/* Execute machine once for each character */
+	for(;;p++){
+	doloop:
+		if(p>=eof || p>=nc){
+			switch(wrapped++){
+			case 0:		/* let loop run one more click */
+			case 2:
+				break;
+			case 1:		/* expired; wrap to beginning */
+				if(sel.r[0].q0>=0 || eof!=Infinity)
+					goto Return;
+				list[0][0].inst = list[1][0].inst = nil;
+				p = 0;
+				goto doloop;
+			default:
+				goto Return;
+			}
+			c = 0;
+		}else{
+			if(((wrapped && p>=startp) || sel.r[0].q0>0) && nnl==0)
+				break;
+			if(t != nil)
+				c = textreadc(t, p);
+			else
+				c = r[p];
+		}
+		/* fast check for first char */
+		if(startchar && nnl==0 && c!=startchar)
+			continue;
+		tl = list[flag];
+		nl = list[flag^=1];
+		nl->inst = nil;
+		ntl = nnl;
+		nnl = 0;
+		if(sel.r[0].q0<0 && (!wrapped || p<startp || startp==eof)){
+			/* Add first instruction to this list */
+			sempty.r[0].q0 = p;
+			if(addinst(tl, startinst, &sempty))
+			if(++ntl >= NLIST){
+	Overflow:
+				warning(nil, "regexp list overflow\n");
+				sel.r[0].q0 = -1;
+				goto Return;
+			}
+		}
+		/* Execute machine until this list is empty */
+		for(tlp = tl; inst = tlp->inst; tlp++){	/* assignment = */
+	Switchstmt:
+			switch(inst->type){
+			default:	/* regular character */
+				if(inst->type==c){
+	Addinst:
+					if(addinst(nl, inst->next, &tlp->se))
+					if(++nnl >= NLIST)
+						goto Overflow;
+				}
+				break;
+			case LBRA:
+				if(inst->subid>=0)
+					tlp->se.r[inst->subid].q0 = p;
+				inst = inst->next;
+				goto Switchstmt;
+			case RBRA:
+				if(inst->subid>=0)
+					tlp->se.r[inst->subid].q1 = p;
+				inst = inst->next;
+				goto Switchstmt;
+			case ANY:
+				if(c!='\n')
+					goto Addinst;
+				break;
+			case BOL:
+				if(p==0 || (t!=nil && textreadc(t, p-1)=='\n') || (r!=nil && r[p-1]=='\n')){
+	Step:
+					inst = inst->next;
+					goto Switchstmt;
+				}
+				break;
+			case EOL:
+				if(c == '\n')
+					goto Step;
+				break;
+			case CCLASS:
+				if(c>=0 && classmatch(inst->class, c, 0))
+					goto Addinst;
+				break;
+			case NCCLASS:
+				if(c>=0 && classmatch(inst->class, c, 1))
+					goto Addinst;
+				break;
+			case OR:
+				/* evaluate right choice later */
+				if(addinst(tlp, inst->right, &tlp->se))
+				if(++ntl >= NLIST)
+					goto Overflow;
+				/* efficiency: advance and re-evaluate */
+				inst = inst->left;
+				goto Switchstmt;
+			case END:	/* Match! */
+				tlp->se.r[0].q1 = p;
+				newmatch(&tlp->se);
+				break;
+			}
+		}
+	}
+    Return:
+	*rp = sel;
+	return sel.r[0].q0 >= 0;
+}
+
+void
+newmatch(Rangeset *sp)
+{
+	if(sel.r[0].q0<0 || sp->r[0].q0<sel.r[0].q0 ||
+	   (sp->r[0].q0==sel.r[0].q0 && sp->r[0].q1>sel.r[0].q1))
+		sel = *sp;
+}
+
+int
+rxbexecute(Text *t, uint startp, Rangeset *rp)
+{
+	int flag;
+	Inst *inst;
+	Ilist *tlp;
+	int p;
+	int nnl, ntl;
+	int c;
+	int wrapped;
+	int startchar;
+
+	flag = 0;
+	nnl = 0;
+	wrapped = 0;
+	p = startp;
+	startchar = 0;
+	if(bstartinst->type<OPERATOR)
+		startchar = bstartinst->type;
+	list[0][0].inst = list[1][0].inst = nil;
+	sel.r[0].q0= -1;
+	/* Execute machine once for each character, including terminal NUL */
+	for(;;--p){
+	doloop:
+		if(p <= 0){
+			switch(wrapped++){
+			case 0:		/* let loop run one more click */
+			case 2:
+				break;
+			case 1:		/* expired; wrap to end */
+				if(sel.r[0].q0>=0)
+					goto Return;
+				list[0][0].inst = list[1][0].inst = nil;
+				p = t->file->nc;
+				goto doloop;
+			case 3:
+			default:
+				goto Return;
+			}
+			c = 0;
+		}else{
+			if(((wrapped && p<=startp) || sel.r[0].q0>0) && nnl==0)
+				break;
+			c = textreadc(t, p-1);
+		}
+		/* fast check for first char */
+		if(startchar && nnl==0 && c!=startchar)
+			continue;
+		tl = list[flag];
+		nl = list[flag^=1];
+		nl->inst = nil;
+		ntl = nnl;
+		nnl = 0;
+		if(sel.r[0].q0<0 && (!wrapped || p>startp)){
+			/* Add first instruction to this list */
+			/* the minus is so the optimizations in addinst work */
+			sempty.r[0].q0 = -p;
+			if(addinst(tl, bstartinst, &sempty))
+			if(++ntl >= NLIST){
+	Overflow:
+				warning(nil, "regexp list overflow\n");
+				sel.r[0].q0 = -1;
+				goto Return;
+			}
+		}
+		/* Execute machine until this list is empty */
+		for(tlp = tl; inst = tlp->inst; tlp++){	/* assignment = */
+	Switchstmt:
+			switch(inst->type){
+			default:	/* regular character */
+				if(inst->type == c){
+	Addinst:
+					if(addinst(nl, inst->next, &tlp->se))
+					if(++nnl >= NLIST)
+						goto Overflow;
+				}
+				break;
+			case LBRA:
+				if(inst->subid>=0)
+					tlp->se.r[inst->subid].q0 = p;
+				inst = inst->next;
+				goto Switchstmt;
+			case RBRA:
+				if(inst->subid >= 0)
+					tlp->se.r[inst->subid].q1 = p;
+				inst = inst->next;
+				goto Switchstmt;
+			case ANY:
+				if(c != '\n')
+					goto Addinst;
+				break;
+			case BOL:
+				if(c=='\n' || p==0){
+	Step:
+					inst = inst->next;
+					goto Switchstmt;
+				}
+				break;
+			case EOL:
+				if(p<t->file->nc && textreadc(t, p)=='\n')
+					goto Step;
+				break;
+			case CCLASS:
+				if(c>0 && classmatch(inst->class, c, 0))
+					goto Addinst;
+				break;
+			case NCCLASS:
+				if(c>0 && classmatch(inst->class, c, 1))
+					goto Addinst;
+				break;
+			case OR:
+				/* evaluate right choice later */
+				if(addinst(tl, inst->right, &tlp->se))
+				if(++ntl >= NLIST)
+					goto Overflow;
+				/* efficiency: advance and re-evaluate */
+				inst = inst->left;
+				goto Switchstmt;
+			case END:	/* Match! */
+				tlp->se.r[0].q0 = -tlp->se.r[0].q0; /* minus sign */
+				tlp->se.r[0].q1 = p;
+				bnewmatch(&tlp->se);
+				break;
+			}
+		}
+	}
+    Return:
+	*rp = sel;
+	return sel.r[0].q0 >= 0;
+}
+
+void
+bnewmatch(Rangeset *sp)
+{
+        int  i;
+
+        if(sel.r[0].q0<0 || sp->r[0].q0>sel.r[0].q1 || (sp->r[0].q0==sel.r[0].q1 && sp->r[0].q1<sel.r[0].q0))
+                for(i = 0; i<NRange; i++){       /* note the reversal; q0<=q1 */
+                        sel.r[i].q0 = sp->r[i].q1;
+                        sel.r[i].q1 = sp->r[i].q0;
+                }
+}
--- /dev/null
+++ b/rows.c
@@ -1,0 +1,736 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <cursor.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <frame.h>
+#include <fcall.h>
+#include <bio.h>
+#include <plumb.h>
+#include "dat.h"
+#include "fns.h"
+
+void
+rowinit(Row *row, Rectangle r)
+{
+	Rectangle r1;
+	Text *t;
+
+	draw(screen, r, display->white, nil, ZP);
+	row->r = r;
+	row->col = nil;
+	row->ncol = 0;
+	r1 = r;
+	r1.max.y = r1.min.y + font->height;
+	t = &row->tag;
+	textinit(t, fileaddtext(nil, t), r1, rfget(FALSE, FALSE, FALSE, nil), tagcols);
+	t->what = Rowtag;
+	t->row = row;
+	t->w = nil;
+	t->col = nil;
+	r1.min.y = r1.max.y;
+	r1.max.y += Border;
+	draw(screen, r1, display->black, nil, ZP);
+	textinsert(t, 0, L"Newcol Kill Putall Dump Exit ", 29, TRUE);
+	textsetselect(t, t->file->nc, t->file->nc);
+}
+
+Column*
+rowadd(Row *row, Column *c, int x)
+{
+	Rectangle r, r1;
+	Column *d;
+	int i;
+
+	d = nil;
+	r = row->r;
+	r.min.y = row->tag.r.max.y+Border;
+	if(x<r.min.x && row->ncol>0){	/*steal 40% of last column by default */
+		d = row->col[row->ncol-1];
+		x = d->r.min.x + 3*Dx(d->r)/5;
+	}
+	/* look for column we'll land on */
+	for(i=0; i<row->ncol; i++){
+		d = row->col[i];
+		if(x < d->r.max.x)
+			break;
+	}
+	if(row->ncol > 0){
+		if(i < row->ncol)
+			i++;	/* new column will go after d */
+		r = d->r;
+		if(Dx(r) < 100)
+			return nil;
+		draw(screen, r, display->white, nil, ZP);
+		r1 = r;
+		r1.max.x = min(x, r.max.x-50);
+		if(Dx(r1) < 50)
+			r1.max.x = r1.min.x+50;
+		colresize(d, r1);
+		r1.min.x = r1.max.x;
+		r1.max.x = r1.min.x+Border;
+		draw(screen, r1, display->black, nil, ZP);
+		r.min.x = r1.max.x;
+	}
+	if(c == nil){
+		c = emalloc(sizeof(Column));
+		colinit(c, r);
+		incref(&reffont);
+	}else
+		colresize(c, r);
+	c->row = row;
+	c->tag.row = row;
+	row->col = realloc(row->col, (row->ncol+1)*sizeof(Column*));
+	memmove(row->col+i+1, row->col+i, (row->ncol-i)*sizeof(Column*));
+	row->col[i] = c;
+	row->ncol++;
+	clearmouse();
+	return c;
+}
+
+void
+rowresize(Row *row, Rectangle r)
+{
+	int i, dx, odx;
+	Rectangle r1, r2;
+	Column *c;
+
+	dx = Dx(r);
+	odx = Dx(row->r);
+	row->r = r;
+	r1 = r;
+	r1.max.y = r1.min.y + font->height;
+	textresize(&row->tag, r1);
+	r1.min.y = r1.max.y;
+	r1.max.y += Border;
+	draw(screen, r1, display->black, nil, ZP);
+	r.min.y = r1.max.y;
+	r1 = r;
+	r1.max.x = r1.min.x;
+	for(i=0; i<row->ncol; i++){
+		c = row->col[i];
+		r1.min.x = r1.max.x;
+		if(i == row->ncol-1)
+			r1.max.x = r.max.x;
+		else
+			r1.max.x = r1.min.x+Dx(c->r)*dx/odx;
+		if(i > 0){
+			r2 = r1;
+			r2.max.x = r2.min.x+Border;
+			draw(screen, r2, display->black, nil, ZP);
+			r1.min.x = r2.max.x;
+		}
+		colresize(c, r1);
+	}
+}
+
+void
+rowdragcol(Row *row, Column *c, int)
+{
+	Rectangle r;
+	int i, b, x;
+	Point p, op;
+	Column *d;
+
+	clearmouse();
+	setcursor(mousectl, &boxcursor);
+	b = mouse->buttons;
+	op = mouse->xy;
+	while(mouse->buttons == b)
+		readmouse(mousectl);
+	setcursor(mousectl, nil);
+	if(mouse->buttons){
+		while(mouse->buttons)
+			readmouse(mousectl);
+		return;
+	}
+
+	for(i=0; i<row->ncol; i++)
+		if(row->col[i] == c)
+			goto Found;
+	error("can't find column");
+
+  Found:
+	p = mouse->xy;
+	if((abs(p.x-op.x)<5 && abs(p.y-op.y)<5))
+		return;
+	if((i>0 && p.x<row->col[i-1]->r.min.x) || (i<row->ncol-1 && p.x>c->r.max.x)){
+		/* shuffle */
+		x = c->r.min.x;
+		rowclose(row, c, FALSE);
+		if(rowadd(row, c, p.x) == nil)	/* whoops! */
+		if(rowadd(row, c, x) == nil)		/* WHOOPS! */
+		if(rowadd(row, c, -1)==nil){		/* shit! */
+			rowclose(row, c, TRUE);
+			return;
+		}
+		colmousebut(c);
+		return;
+	}
+	if(i == 0)
+		return;
+	d = row->col[i-1];
+	if(p.x < d->r.min.x+80+Scrollwid)
+		p.x = d->r.min.x+80+Scrollwid;
+	if(p.x > c->r.max.x-80-Scrollwid)
+		p.x = c->r.max.x-80-Scrollwid;
+	r = d->r;
+	r.max.x = c->r.max.x;
+	draw(screen, r, display->white, nil, ZP);
+	r.max.x = p.x;
+	colresize(d, r);
+	r = c->r;
+	r.min.x = p.x;
+	r.max.x = r.min.x;
+	r.max.x += Border;
+	draw(screen, r, display->black, nil, ZP);
+	r.min.x = r.max.x;
+	r.max.x = c->r.max.x;
+	colresize(c, r);
+	colmousebut(c);
+}
+
+void
+rowclose(Row *row, Column *c, int dofree)
+{
+	Rectangle r;
+	int i;
+
+	for(i=0; i<row->ncol; i++)
+		if(row->col[i] == c)
+			goto Found;
+	error("can't find column");
+  Found:
+	r = c->r;
+	if(dofree)
+		colcloseall(c);
+	memmove(row->col+i, row->col+i+1, (row->ncol-i)*sizeof(Column*));
+	row->ncol--;
+	row->col = realloc(row->col, row->ncol*sizeof(Column*));
+	if(row->ncol == 0){
+		draw(screen, r, display->white, nil, ZP);
+		return;
+	}
+	if(i == row->ncol){		/* extend last column right */
+		c = row->col[i-1];
+		r.min.x = c->r.min.x;
+		r.max.x = row->r.max.x;
+	}else{			/* extend next window left */
+		c = row->col[i];
+		r.max.x = c->r.max.x;
+	}
+	draw(screen, r, display->white, nil, ZP);
+	colresize(c, r);
+}
+
+Column*
+rowwhichcol(Row *row, Point p)
+{
+	int i;
+	Column *c;
+
+	for(i=0; i<row->ncol; i++){
+		c = row->col[i];
+		if(ptinrect(p, c->r))
+			return c;
+	}
+	return nil;
+}
+
+Text*
+rowwhich(Row *row, Point p)
+{
+	Column *c;
+
+	if(ptinrect(p, row->tag.all))
+		return &row->tag;
+	c = rowwhichcol(row, p);
+	if(c)
+		return colwhich(c, p);
+	return nil;
+}
+
+Text*
+rowtype(Row *row, Rune r, Point p)
+{
+	Window *w;
+	Text *t;
+
+	clearmouse();
+	qlock(row);
+	if(bartflag)
+		t = barttext;
+	else
+		t = rowwhich(row, p);
+	if(t!=nil && !(t->what==Tag && ptinrect(p, t->scrollr))){
+		w = t->w;
+		if(w == nil)
+			texttype(t, r);
+		else{
+			winlock(w, 'K');
+			wintype(w, t, r);
+			winunlock(w);
+		}
+	}
+	qunlock(row);
+	return t;
+}
+
+int
+rowclean(Row *row)
+{
+	int clean;
+	int i;
+
+	clean = TRUE;
+	for(i=0; i<row->ncol; i++)
+		clean &= colclean(row->col[i]);
+	return clean;
+}
+
+void
+rowdump(Row *row, char *file)
+{
+	int i, j, fd, m, n, start, dumped;
+	uint q0, q1;
+	Biobuf *b;
+	char *buf, *a, *fontname;
+	Rune *r;
+	Column *c;
+	Window *w, *w1;
+	Text *t;
+
+	if(row->ncol == 0)
+		return;
+	buf = fbufalloc();
+	if(file == nil){
+		if(home == nil){
+			warning(nil, "can't find file for dump: $home not defined\n");
+			goto Rescue;
+		}
+		sprint(buf, "%s/acme.dump", home);
+		file = buf;
+	}
+	fd = create(file, OWRITE, 0600);
+	if(fd < 0){
+		warning(nil, "can't open %s: %r\n", file);
+		goto Rescue;
+	}
+	b = emalloc(sizeof(Biobuf));
+	Binit(b, fd, OWRITE);
+	r = fbufalloc();
+	Bprint(b, "%s\n", wdir);
+	Bprint(b, "%s\n", fontnames[0]);
+	Bprint(b, "%s\n", fontnames[1]);
+	for(i=0; i<row->ncol; i++){
+		c = row->col[i];
+		Bprint(b, "%11d", 100*(c->r.min.x-row->r.min.x)/Dx(row->r));
+		if(i == row->ncol-1)
+			Bputc(b, '\n');
+		else
+			Bputc(b, ' ');
+	}
+	for(i=0; i<row->ncol; i++){
+		c = row->col[i];
+		for(j=0; j<c->nw; j++)
+			c->w[j]->body.file->dumpid = 0;
+	}
+	for(i=0; i<row->ncol; i++){
+		c = row->col[i];
+		for(j=0; j<c->nw; j++){
+			w = c->w[j];
+			wincommit(w, &w->tag);
+			t = &w->body;
+			/* windows owned by others get special treatment */
+			if(w->nopen[QWevent] > 0)
+				if(w->dumpstr == nil)
+					continue;
+			/* zeroxes of external windows are tossed */
+			if(t->file->ntext > 1)
+				for(n=0; n<t->file->ntext; n++){
+					w1 = t->file->text[n]->w;
+					if(w == w1)
+						continue;
+					if(w1->nopen[QWevent])
+						goto Continue2;
+				}
+			fontname = "";
+			if(t->reffont->f != font)
+				fontname = t->reffont->f->name;
+			if(t->file->nname)
+				a = runetobyte(t->file->name, t->file->nname);
+			else
+				a = emalloc(1);
+			if(t->file->dumpid){
+				dumped = FALSE;
+				Bprint(b, "x%11d %11d %11d %11d %11d %s\n", i, t->file->dumpid,
+					w->body.q0, w->body.q1,
+					100*(w->r.min.y-c->r.min.y)/Dy(c->r),
+					fontname);
+			}else if(w->dumpstr){
+				dumped = FALSE;
+				Bprint(b, "e%11d %11d %11d %11d %11d %s\n", i, t->file->dumpid,
+					0, 0,
+					100*(w->r.min.y-c->r.min.y)/Dy(c->r),
+					fontname);
+			}else if((w->dirty==FALSE && access(a, 0)==0) || w->isdir){
+				dumped = FALSE;
+				t->file->dumpid = w->id;
+				Bprint(b, "f%11d %11d %11d %11d %11d %s\n", i, w->id,
+					w->body.q0, w->body.q1,
+					100*(w->r.min.y-c->r.min.y)/Dy(c->r),
+					fontname);
+			}else{
+				dumped = TRUE;
+				t->file->dumpid = w->id;
+				Bprint(b, "F%11d %11d %11d %11d %11d %11d %s\n", i, j,
+					w->body.q0, w->body.q1,
+					100*(w->r.min.y-c->r.min.y)/Dy(c->r),
+					w->body.file->nc, fontname);
+			}
+			free(a);
+			winctlprint(w, buf, 0);
+			Bwrite(b, buf, strlen(buf));
+			m = min(RBUFSIZE, w->tag.file->nc);
+			bufread(w->tag.file, 0, r, m);
+			n = 0;
+			while(n<m) {
+				start = n;
+				while(n<m && r[n]!='\n')
+					n++;
+				Bprint(b, "%.*S", n-start, r+start);
+				if(n<m) {
+					Bputc(b, 0xff);	// \n in tag becomes 0xff byte (invalid UTF)
+					n++;
+				}
+			}
+			Bprint(b, "\n");
+			if(dumped){
+				q0 = 0;
+				q1 = t->file->nc;
+				while(q0 < q1){
+					n = q1 - q0;
+					if(n > BUFSIZE/UTFmax)
+						n = BUFSIZE/UTFmax;
+					bufread(t->file, q0, r, n);
+					Bprint(b, "%.*S", n, r);
+					q0 += n;
+				}
+			}
+			if(w->dumpstr){
+				if(w->dumpdir)
+					Bprint(b, "%s\n%s\n", w->dumpdir, w->dumpstr);
+				else
+					Bprint(b, "\n%s\n", w->dumpstr);
+			}
+    Continue2:;
+		}
+	}
+	Bterm(b);
+	close(fd);
+	free(b);
+	fbuffree(r);
+
+   Rescue:
+	fbuffree(buf);
+}
+
+static
+char*
+rdline(Biobuf *b, int *linep)
+{
+	char *l;
+
+	l = Brdline(b, '\n');
+	if(l)
+		(*linep)++;
+	return l;
+}
+
+/*
+ * Get font names from load file so we don't load fonts we won't use
+ */
+void
+rowloadfonts(char *file)
+{
+	int i;
+	Biobuf *b;
+	char *l;
+
+	b = Bopen(file, OREAD);
+	if(b == nil)
+		return;
+	/* current directory */
+	l = Brdline(b, '\n');
+	if(l == nil)
+		goto Return;
+	/* global fonts */
+	for(i=0; i<2; i++){
+		l = Brdline(b, '\n');
+		if(l == nil)
+			goto Return;
+		l[Blinelen(b)-1] = 0;
+		if(*l && strcmp(l, fontnames[i])!=0){
+			free(fontnames[i]);
+			fontnames[i] = estrdup(l);
+		}
+	}
+    Return:
+	Bterm(b);
+}
+
+int
+rowload(Row *row, char *file, int initing)
+{
+	int i, j, line, percent, y, nr, nfontr, n, ns, ndumped, dumpid, x, fd;
+	Biobuf *b, *bout;
+	char *buf, *l, *t, *fontname;
+	Rune *r, rune, *fontr;
+	Column *c, *c1, *c2;
+	uint q0, q1;
+	Rectangle r1, r2;
+	Window *w;
+
+	buf = fbufalloc();
+	if(file == nil){
+		if(home == nil){
+			warning(nil, "can't find file for load: $home not defined\n");
+			goto Rescue1;
+		}
+		sprint(buf, "%s/acme.dump", home);
+		file = buf;
+	}
+	b = Bopen(file, OREAD);
+	if(b == nil){
+		warning(nil, "can't open load file %s: %r\n", file);
+		goto Rescue1;
+	}
+	/* current directory */
+	line = 0;
+	l = rdline(b, &line);
+	if(l == nil)
+		goto Rescue2;
+	l[Blinelen(b)-1] = 0;
+	if(chdir(l) < 0){
+		warning(nil, "can't chdir %s\n", l);
+		goto Rescue2;
+	}
+	/* global fonts */
+	for(i=0; i<2; i++){
+		l = rdline(b, &line);
+		if(l == nil)
+			goto Rescue2;
+		l[Blinelen(b)-1] = 0;
+		if(*l && strcmp(l, fontnames[i])!=0)
+			rfget(i, TRUE, i==0 && initing, l);
+	}
+	if(initing && row->ncol==0)
+		rowinit(row, screen->clipr);
+	l = rdline(b, &line);
+	if(l == nil)
+		goto Rescue2;
+	j = Blinelen(b)/12;
+	if(j<=0 || j>10)
+		goto Rescue2;
+	for(i=0; i<j; i++){
+		percent = atoi(l+i*12);
+		if(percent<0 || percent>=100)
+			goto Rescue2;
+		x = row->r.min.x+percent*Dx(row->r)/100;
+		if(i < row->ncol){
+			if(i == 0)
+				continue;
+			c1 = row->col[i-1];
+			c2 = row->col[i];
+			r1 = c1->r;
+			r2 = c2->r;
+			r1.max.x = x;
+			r2.min.x = x+Border;
+			if(Dx(r1) < 50 || Dx(r2) < 50)
+				continue;
+			draw(screen, Rpt(r1.min, r2.max), display->white, nil, ZP);
+			colresize(c1, r1);
+			colresize(c2, r2);
+			r2.min.x = x;
+			r2.max.x = x+Border;
+			draw(screen, r2, display->black, nil, ZP);
+		}
+		if(i >= row->ncol)
+			rowadd(row, nil, x);
+	}
+	for(;;){
+		l = rdline(b, &line);
+		if(l == nil)
+			break;
+		dumpid = 0;
+		switch(l[0]){
+		case 'e':
+			if(Blinelen(b) < 1+5*12+1)
+				goto Rescue2;
+			l = rdline(b, &line);	/* ctl line; ignored */
+			if(l == nil)
+				goto Rescue2;
+			l = rdline(b, &line);	/* directory */
+			if(l == nil)
+				goto Rescue2;
+			l[Blinelen(b)-1] = 0;
+			if(*l == '\0'){
+				if(home == nil)
+					r = bytetorune("./", &nr);
+				else{
+					t = emalloc(strlen(home)+1+1);
+					sprint(t, "%s/", home);
+					r = bytetorune(t, &nr);
+					free(t);
+				}
+			}else
+				r = bytetorune(l, &nr);
+			l = rdline(b, &line);	/* command */
+			if(l == nil)
+				goto Rescue2;
+			t = emalloc(Blinelen(b)+1);
+			memmove(t, l, Blinelen(b));
+			run(nil, t, r, nr, TRUE, nil, nil, FALSE);
+			/* r is freed in run() */
+			continue;
+		case 'f':
+			if(Blinelen(b) < 1+5*12+1)
+				goto Rescue2;
+			fontname = l+1+5*12;
+			ndumped = -1;
+			break;
+		case 'F':
+			if(Blinelen(b) < 1+6*12+1)
+				goto Rescue2;
+			fontname = l+1+6*12;
+			ndumped = atoi(l+1+5*12+1);
+			break;
+		case 'x':
+			if(Blinelen(b) < 1+5*12+1)
+				goto Rescue2;
+			fontname = l+1+5*12;
+			ndumped = -1;
+			dumpid = atoi(l+1+1*12);
+			break;
+		default:
+			goto Rescue2;
+		}
+		l[Blinelen(b)-1] = 0;
+		fontr = nil;
+		nfontr = 0;
+		if(*fontname)
+			fontr = bytetorune(fontname, &nfontr);
+		i = atoi(l+1+0*12);
+		j = atoi(l+1+1*12);
+		q0 = atoi(l+1+2*12);
+		q1 = atoi(l+1+3*12);
+		percent = atoi(l+1+4*12);
+		if(i<0 || i>10)
+			goto Rescue2;
+		if(i > row->ncol)
+			i = row->ncol;
+		c = row->col[i];
+		y = c->r.min.y+(percent*Dy(c->r))/100;
+		if(y<c->r.min.y || y>=c->r.max.y)
+			y = -1;
+		if(dumpid == 0)
+			w = coladd(c, nil, nil, y);
+		else
+			w = coladd(c, nil, lookid(dumpid, TRUE), y);
+		if(w == nil)
+			continue;
+		w->dumpid = j;
+		l = rdline(b, &line);
+		if(l == nil)
+			goto Rescue2;
+		l[Blinelen(b)-1] = 0;
+		/* convert 0xff in multiline tag back to \n */
+		for(i=0; l[i]!=0; i++)
+			if((uchar)l[i] == 0xff)
+				l[i] = '\n';
+		r = bytetorune(l+5*12, &nr);
+		ns = -1;
+		for(n=0; n<nr; n++){
+			if(r[n] == '/')
+				ns = n;
+			if(r[n] == ' ')
+				break;
+		}
+		if(dumpid == 0)
+			winsetname(w, r, n);
+		for(; n<nr; n++)
+			if(r[n] == '|')
+				break;
+		wincleartag(w);
+		textinsert(&w->tag, w->tag.file->nc, r+n+1, nr-(n+1), TRUE);
+		if(ndumped >= 0){
+			/* simplest thing is to put it in a file and load that */
+			sprint(buf, "/tmp/d%d.%.4sacme", getpid(), user);
+			fd = create(buf, OWRITE|ORCLOSE, 0600);
+			if(fd < 0){
+				free(r);
+				warning(nil, "can't create temp file: %r\n");
+				goto Rescue2;
+			}
+			bout = emalloc(sizeof(Biobuf));
+			Binit(bout, fd, OWRITE);
+			for(n=0; n<ndumped; n++){
+				rune = Bgetrune(b);
+				if(rune == '\n')
+					line++;
+				if(rune == (Rune)Beof){
+					free(r);
+					Bterm(bout);
+					free(bout);
+					close(fd);
+					goto Rescue2;
+				}
+				Bputrune(bout, rune);
+			}
+			Bterm(bout);
+			free(bout);
+			textload(&w->body, 0, buf, 1);
+			close(fd);
+			w->body.file->mod = TRUE;
+			for(n=0; n<w->body.file->ntext; n++)
+				w->body.file->text[n]->w->dirty = TRUE;
+			winsettag(w);
+		}else if(dumpid==0 && r[ns+1]!='+' && r[ns+1]!='-')
+			get(&w->body, nil, nil, FALSE, XXX, nil, 0);
+		if(fontr){
+			fontx(&w->body, nil, nil, 0, 0, fontr, nfontr);
+			free(fontr);
+		}
+		free(r);
+		if(q0>w->body.file->nc || q1>w->body.file->nc || q0>q1)
+			q0 = q1 = 0;
+		textshow(&w->body, q0, q1, 1);
+		w->maxlines = min(w->body.nlines, max(w->maxlines, w->body.maxlines));
+		xfidlog(w, "new");
+	}
+	Bterm(b);
+	fbuffree(buf);
+	return TRUE;
+
+Rescue2:
+	warning(nil, "bad load file %s:%d\n", file, line);
+	Bterm(b);
+Rescue1:
+	fbuffree(buf);
+	return FALSE;
+}
+
+void
+allwindows(void (*f)(Window*, void*), void *arg)
+{
+	int i, j;
+	Column *c;
+
+	for(i=0; i<row.ncol; i++){
+		c = row.col[i];
+		for(j=0; j<c->nw; j++)
+			(*f)(c->w[j], arg);
+	}
+}
--- /dev/null
+++ b/scrl.c
@@ -1,0 +1,153 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <cursor.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <frame.h>
+#include <fcall.h>
+#include <plumb.h>
+#include "dat.h"
+#include "fns.h"
+
+static Image *scrtmp;
+
+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
+scrlresize(void)
+{
+	freeimage(scrtmp);
+	scrtmp = allocimage(display, Rect(0, 0, 32, screen->r.max.y), screen->chan, 0, DNofill);
+	if(scrtmp == nil)
+		error("scroll alloc");
+}
+
+void
+textscrdraw(Text *t)
+{
+	Rectangle r, r1, r2;
+	Image *b;
+
+	if(t->w==nil || t!=&t->w->body)
+		return;
+	if(scrtmp == nil)
+		scrlresize();
+	r = t->scrollr;
+	b = scrtmp;
+	r1 = r;
+	r1.min.x = 0;
+	r1.max.x = Dx(r);
+	r2 = scrpos(r1, t->org, t->org+t->nchars, t->file->nc);
+	if(!eqrect(r2, t->lastsr)){
+		t->lastsr = r2;
+		draw(b, r1, t->cols[BORD], nil, ZP);
+		draw(b, r2, t->cols[BACK], nil, ZP);
+		r2.min.x = r2.max.x-1;
+		draw(b, r2, t->cols[BORD], nil, ZP);
+		draw(t->b, r, b, nil, Pt(0, r1.min.y));
+/*flushimage(display, 1);/*BUG?*/
+	}
+}
+
+void
+scrsleep(uint dt)
+{
+	Timer	*timer;
+	static Alt alts[3];
+
+	timer = timerstart(dt);
+	alts[0].c = timer->c;
+	alts[0].v = nil;
+	alts[0].op = CHANRCV;
+	alts[1].c = mousectl->c;
+	alts[1].v = &mousectl->Mouse;
+	alts[1].op = CHANRCV;
+	alts[2].op = CHANEND;
+	for(;;)
+		switch(alt(alts)){
+		case 0:
+			timerstop(timer);
+			return;
+		case 1:
+			timercancel(timer);
+			return;
+		}
+}
+
+void
+textscroll(Text *t, int but)
+{
+	uint p0, oldp0;
+	Rectangle s;
+	int y, my, h, first;
+
+	s = insetrect(t->scrollr, 1);
+	h = s.max.y-s.min.y;
+	oldp0 = ~0;
+	first = TRUE;
+	do{
+		flushimage(display, 1);
+		my = mouse->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;
+			p0 = (vlong)t->file->nc*(y-s.min.y)/h;
+			if(p0 >= t->q1)
+				p0 = textbacknl(t, p0, 2);
+			if(oldp0 != p0)
+				textsetorigin(t, p0, FALSE);
+			oldp0 = p0;
+			readmouse(mousectl);
+			continue;
+		}
+		if(but == 1)
+			p0 = textbacknl(t, t->org, (my-s.min.y)/t->font->height);
+		else
+			p0 = t->org+frcharofpt(t, Pt(s.max.x, my));
+		if(oldp0 != p0)
+			textsetorigin(t, p0, TRUE);
+		oldp0 = p0;
+		/* debounce */
+		if(first){
+			flushimage(display, 1);
+			sleep(200);
+			nbrecv(mousectl->c, &mousectl->Mouse);
+			first = FALSE;
+		}
+		scrsleep(80);
+	}while(mouse->buttons & (1<<(but-1)));
+	while(mouse->buttons)
+		readmouse(mousectl);
+}
--- /dev/null
+++ b/text.c
@@ -1,0 +1,1461 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <cursor.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <frame.h>
+#include <fcall.h>
+#include <plumb.h>
+#include <complete.h>
+#include "dat.h"
+#include "fns.h"
+
+Image	*tagcols[NCOL];
+Image	*textcols[NCOL];
+
+enum{
+	TABDIR = 3	/* width of tabs in directory windows */
+};
+
+void
+textinit(Text *t, File *f, Rectangle r, Reffont *rf, Image *cols[NCOL])
+{
+	t->file = f;
+	t->all = r;
+	t->scrollr = r;
+	t->scrollr.max.x = r.min.x+Scrollwid;
+	t->lastsr = nullrect;
+	r.min.x += Scrollwid+Scrollgap;
+	t->eq0 = ~0;
+	t->ncache = 0;
+	t->reffont = rf;
+	t->tabstop = maxtab;
+	memmove(t->Frame.cols, cols, sizeof t->Frame.cols);
+	textredraw(t, r, rf->f, screen, -1);
+}
+
+void
+textredraw(Text *t, Rectangle r, Font *f, Image *b, int odx)
+{
+	int maxt;
+	Rectangle rr;
+
+	frinit(t, r, f, b, t->Frame.cols);
+	rr = t->r;
+	rr.min.x -= Scrollwid+Scrollgap;	/* back fill to scroll bar */
+	draw(t->b, rr, t->cols[BACK], nil, ZP);
+	/* use no wider than 3-space tabs in a directory */
+	maxt = maxtab;
+	if(t->what == Body){
+		if(t->w->isdir)
+			maxt = min(TABDIR, maxtab);
+		else
+			maxt = t->tabstop;
+	}
+	t->maxtab = maxt*stringwidth(f, "0");
+	if(t->what==Body && t->w->isdir && odx!=Dx(t->all)){
+		if(t->maxlines > 0){
+			textreset(t);
+			textcolumnate(t, t->w->dlp,  t->w->ndl);
+			textshow(t, 0, 0, 1);
+		}
+	}else{
+		textfill(t);
+		textsetselect(t, t->q0, t->q1);
+	}
+}
+
+int
+textresize(Text *t, Rectangle r)
+{
+	int odx;
+
+	if(Dy(r) > 0)
+		r.max.y -= Dy(r)%t->font->height;
+	else
+		r.max.y = r.min.y;
+	odx = Dx(t->all);
+	t->all = r;
+	t->scrollr = r;
+	t->scrollr.max.x = r.min.x+Scrollwid;
+	t->lastsr = nullrect;
+	r.min.x += Scrollwid+Scrollgap;
+	frclear(t, 0);
+	textredraw(t, r, t->font, t->b, odx);
+	return r.max.y;
+}
+
+void
+textclose(Text *t)
+{
+	free(t->cache);
+	frclear(t, 1);
+	filedeltext(t->file, t);
+	t->file = nil;
+	rfclose(t->reffont);
+	if(argtext == t)
+		argtext = nil;
+	if(typetext == t)
+		typetext = nil;
+	if(seltext == t)
+		seltext = nil;
+	if(mousetext == t)
+		mousetext = nil;
+	if(barttext == t)
+		barttext = nil;
+}
+
+int
+dircmp(void *a, void *b)
+{
+	Dirlist *da, *db;
+	int i, n;
+
+	da = *(Dirlist**)a;
+	db = *(Dirlist**)b;
+	n = min(da->nr, db->nr);
+	i = memcmp(da->r, db->r, n*sizeof(Rune));
+	if(i)
+		return i;
+	return da->nr - db->nr;
+}
+
+void
+textcolumnate(Text *t, Dirlist **dlp, int ndl)
+{
+	int i, j, w, colw, mint, maxt, ncol, nrow;
+	Dirlist *dl;
+	uint q1;
+
+	if(t->file->ntext > 1)
+		return;
+	mint = stringwidth(t->font, "0");
+	/* go for narrower tabs if set more than 3 wide */
+	t->maxtab = min(maxtab, TABDIR)*mint;
+	maxt = t->maxtab;
+	colw = 0;
+	for(i=0; i<ndl; i++){
+		dl = dlp[i];
+		w = dl->wid;
+		if(maxt-w%maxt < mint || w%maxt==0)
+			w += mint;
+		if(w % maxt)
+			w += maxt-(w%maxt);
+		if(w > colw)
+			colw = w;
+	}
+	if(colw == 0)
+		ncol = 1;
+	else
+		ncol = max(1, Dx(t->r)/colw);
+	nrow = (ndl+ncol-1)/ncol;
+
+	q1 = 0;
+	for(i=0; i<nrow; i++){
+		for(j=i; j<ndl; j+=nrow){
+			dl = dlp[j];
+			fileinsert(t->file, q1, dl->r, dl->nr);
+			q1 += dl->nr;
+			if(j+nrow >= ndl)
+				break;
+			w = dl->wid;
+			if(maxt-w%maxt < mint){
+				fileinsert(t->file, q1, L"\t", 1);
+				q1++;
+				w += mint;
+			}
+			do{
+				fileinsert(t->file, q1, L"\t", 1);
+				q1++;
+				w += maxt-(w%maxt);
+			}while(w < colw);
+		}
+		fileinsert(t->file, q1, L"\n", 1);
+		q1++;
+	}
+}
+
+uint
+textload(Text *t, uint q0, char *file, int setqid)
+{
+	Rune *rp;
+	Dirlist *dl, **dlp;
+	int fd, i, j, n, ndl, nulls;
+	uint q, q1;
+	Dir *d, *dbuf;
+	char *tmp;
+	Text *u;
+
+	if(t->ncache!=0 || t->file->nc || t->w==nil || t!=&t->w->body)
+		error("text.load");
+	if(t->w->isdir && t->file->nname==0){
+		warning(nil, "empty directory name\n");
+		return 0;
+	}
+	fd = open(file, OREAD);
+	if(fd < 0){
+		warning(nil, "can't open %s: %r\n", file);
+		return 0;
+	}
+	d = dirfstat(fd);
+	if(d == nil){
+		warning(nil, "can't fstat %s: %r\n", file);
+		goto Rescue;
+	}
+	nulls = FALSE;
+	if(d->qid.type & QTDIR){
+		/* this is checked in get() but it's possible the file changed underfoot */
+		if(t->file->ntext > 1){
+			warning(nil, "%s is a directory; can't read with multiple windows on it\n", file);
+			goto Rescue;
+		}
+		t->w->isdir = TRUE;
+		t->w->filemenu = FALSE;
+		if(t->file->nname > 0 && t->file->name[t->file->nname-1] != '/'){
+			rp = runemalloc(t->file->nname+1);
+			runemove(rp, t->file->name, t->file->nname);
+			rp[t->file->nname] = '/';
+			winsetname(t->w, rp, t->file->nname+1);
+			free(rp);
+		}
+		dlp = nil;
+		ndl = 0;
+		dbuf = nil;
+		while((n=dirread(fd, &dbuf)) > 0){
+			for(i=0; i<n; i++){
+				dl = emalloc(sizeof(Dirlist));
+				j = strlen(dbuf[i].name);
+				tmp = emalloc(j+1+1);
+				memmove(tmp, dbuf[i].name, j);
+				if(dbuf[i].qid.type & QTDIR)
+					tmp[j++] = '/';
+				tmp[j] = '\0';
+				dl->r = bytetorune(tmp, &dl->nr);
+				dl->wid = stringwidth(t->font, tmp);
+				free(tmp);
+				ndl++;
+				dlp = realloc(dlp, ndl*sizeof(Dirlist*));
+				dlp[ndl-1] = dl;
+			}
+			free(dbuf);
+		}
+		qsort(dlp, ndl, sizeof(Dirlist*), dircmp);
+		t->w->dlp = dlp;
+		t->w->ndl = ndl;
+		textcolumnate(t, dlp, ndl);
+		q1 = t->file->nc;
+	}else{
+		t->w->isdir = FALSE;
+		t->w->filemenu = TRUE;
+		q1 = q0 + fileload(t->file, q0, fd, &nulls);
+	}
+	if(setqid){
+		t->file->dev = d->dev;
+		t->file->mtime = d->mtime;
+		t->file->qidpath = d->qid.path;
+	}
+	close(fd);
+	rp = fbufalloc();
+	for(q=q0; q<q1; q+=n){
+		n = q1-q;
+		if(n > RBUFSIZE)
+			n = RBUFSIZE;
+		bufread(t->file, q, rp, n);
+		if(q < t->org)
+			t->org += n;
+		else if(q <= t->org+t->nchars)
+			frinsert(t, rp, rp+n, q-t->org);
+		if(t->lastlinefull)
+			break;
+	}
+	fbuffree(rp);
+	for(i=0; i<t->file->ntext; i++){
+		u = t->file->text[i];
+		if(u != t){
+			if(u->org > u->file->nc)	/* will be 0 because of reset(), but safety first */
+				u->org = 0;
+			textresize(u, u->all);
+			textbacknl(u, u->org, 0);	/* go to beginning of line */
+		}
+		textsetselect(u, q0, q0);
+	}
+	if(nulls)
+		warning(nil, "%s: NUL bytes elided\n", file);
+	free(d);
+	return q1-q0;
+
+    Rescue:
+	close(fd);
+	return 0;
+}
+
+uint
+textbsinsert(Text *t, uint q0, Rune *r, uint n, int tofile, int *nrp)
+{
+	Rune *bp, *tp, *up;
+	int i, initial;
+
+	if(t->what == Tag){	/* can't happen but safety first: mustn't backspace over file name */
+    Err:
+		textinsert(t, q0, r, n, tofile);
+		*nrp = n;
+		return q0;
+	}
+	bp = r;
+	for(i=0; i<n; i++)
+		if(*bp++ == '\b'){
+			--bp;
+			initial = 0;
+			tp = runemalloc(n);
+			runemove(tp, r, i);
+			up = tp+i;
+			for(; i<n; i++){
+				*up = *bp++;
+				if(*up == '\b')
+					if(up == tp)
+						initial++;
+					else
+						--up;
+				else
+					up++;
+			}
+			if(initial){
+				if(initial > q0)
+					initial = q0;
+				q0 -= initial;
+				textdelete(t, q0, q0+initial, tofile);
+			}
+			n = up-tp;
+			textinsert(t, q0, tp, n, tofile);
+			free(tp);
+			*nrp = n;
+			return q0;
+		}
+	goto Err;
+}
+
+void
+textinsert(Text *t, uint q0, Rune *r, uint n, int tofile)
+{
+	int c, i;
+	Text *u;
+
+	if(tofile && t->ncache != 0)
+		error("text.insert");
+	if(n == 0)
+		return;
+	if(tofile){
+		fileinsert(t->file, q0, r, n);
+		if(t->what == Body){
+			t->w->dirty = TRUE;
+			t->w->utflastqid = -1;
+		}
+		if(t->file->ntext > 1)
+			for(i=0; i<t->file->ntext; i++){
+				u = t->file->text[i];
+				if(u != t){
+					u->w->dirty = TRUE;	/* always a body */
+					textinsert(u, q0, r, n, FALSE);
+					textsetselect(u, u->q0, u->q1);
+					textscrdraw(u);
+				}
+			}
+					
+	}
+	if(q0 < t->q1)
+		t->q1 += n;
+	if(q0 < t->q0)
+		t->q0 += n;
+	if(q0 < t->org)
+		t->org += n;
+	else if(q0 <= t->org+t->nchars)
+		frinsert(t, r, r+n, q0-t->org);
+	if(t->w){
+		c = 'i';
+		if(t->what == Body)
+			c = 'I';
+		if(n <= EVENTSIZE)
+			winevent(t->w, "%c%d %d 0 %d %.*S\n", c, q0, q0+n, n, n, r);
+		else
+			winevent(t->w, "%c%d %d 0 0 \n", c, q0, q0+n, n);
+	}
+}
+
+void
+typecommit(Text *t)
+{
+	if(t->w != nil)
+		wincommit(t->w, t);
+	else
+		textcommit(t, TRUE);
+}
+
+void
+textfill(Text *t)
+{
+	Rune *rp;
+	int i, n, m, nl;
+
+	if(t->lastlinefull || t->nofill)
+		return;
+	if(t->ncache > 0)
+		typecommit(t);
+	rp = fbufalloc();
+	do{
+		n = t->file->nc-(t->org+t->nchars);
+		if(n == 0)
+			break;
+		if(n > 2000)	/* educated guess at reasonable amount */
+			n = 2000;
+		bufread(t->file, t->org+t->nchars, rp, n);
+		/*
+		 * it's expensive to frinsert more than we need, so
+		 * count newlines.
+		 */
+		nl = t->maxlines-t->nlines;
+		m = 0;
+		for(i=0; i<n; ){
+			if(rp[i++] == '\n'){
+				m++;
+				if(m >= nl)
+					break;
+			}
+		}
+		frinsert(t, rp, rp+i, t->nchars);
+	}while(t->lastlinefull == FALSE);
+	fbuffree(rp);
+}
+
+void
+textdelete(Text *t, uint q0, uint q1, int tofile)
+{
+	uint n, p0, p1;
+	int i, c;
+	Text *u;
+
+	if(tofile && t->ncache != 0)
+		error("text.delete");
+	n = q1-q0;
+	if(n == 0)
+		return;
+	if(tofile){
+		filedelete(t->file, q0, q1);
+		if(t->what == Body){
+			t->w->dirty = TRUE;
+			t->w->utflastqid = -1;
+		}
+		if(t->file->ntext > 1)
+			for(i=0; i<t->file->ntext; i++){
+				u = t->file->text[i];
+				if(u != t){
+					u->w->dirty = TRUE;	/* always a body */
+					textdelete(u, q0, q1, FALSE);
+					textsetselect(u, u->q0, u->q1);
+					textscrdraw(u);
+				}
+			}
+	}
+	if(q0 < t->q0)
+		t->q0 -= min(n, t->q0-q0);
+	if(q0 < t->q1)
+		t->q1 -= min(n, t->q1-q0);
+	if(q1 <= t->org)
+		t->org -= n;
+	else if(q0 < t->org+t->nchars){
+		p1 = q1 - t->org;
+		if(p1 > t->nchars)
+			p1 = t->nchars;
+		if(q0 < t->org){
+			t->org = q0;
+			p0 = 0;
+		}else
+			p0 = q0 - t->org;
+		frdelete(t, p0, p1);
+		textfill(t);
+	}
+	if(t->w){
+		c = 'd';
+		if(t->what == Body)
+			c = 'D';
+		winevent(t->w, "%c%d %d 0 0 \n", c, q0, q1);
+	}
+}
+
+void
+textconstrain(Text *t, uint q0, uint q1, uint *p0, uint *p1)
+{
+	*p0 = min(q0, t->file->nc);
+	*p1 = min(q1, t->file->nc);
+}
+
+Rune
+textreadc(Text *t, uint q)
+{
+	Rune r;
+
+	if(t->cq0<=q && q<t->cq0+t->ncache)
+		r = t->cache[q-t->cq0];
+	else
+		bufread(t->file, q, &r, 1);
+	return r;
+}
+
+static int
+spacesindentbswidth(Text *t)
+{
+	uint q, col;
+	Rune r;
+
+	col = textbswidth(t, 0x15);
+	q = t->q0;
+	while(q > 0){
+		r = textreadc(t, q-1);
+		if(r != ' ')
+			break;
+		q--;
+		if(--col % t->tabstop == 0)
+			break;
+	}
+	if(t->q0 == q)
+		return 1;
+	return t->q0-q;
+}
+
+int
+textbswidth(Text *t, Rune c)
+{
+	uint q, eq;
+	Rune r;
+	int skipping;
+
+	/* there is known to be at least one character to erase */
+	if(c == 0x08){	/* ^H: erase character */
+		if(t->what == Body && t->w->indent[SPACESINDENT])
+			return spacesindentbswidth(t);
+		return 1;
+	}
+	q = t->q0;
+	skipping = TRUE;
+	while(q > 0){
+		r = textreadc(t, q-1);
+		if(r == '\n'){		/* eat at most one more character */
+			if(q == t->q0)	/* eat the newline */
+				--q;
+			break; 
+		}
+		if(c == 0x17){
+			eq = isalnum(r);
+			if(eq && skipping)	/* found one; stop skipping */
+				skipping = FALSE;
+			else if(!eq && !skipping)
+				break;
+		}
+		--q;
+	}
+	return t->q0-q;
+}
+
+int
+textfilewidth(Text *t, uint q0, int oneelement)
+{
+	uint q;
+	Rune r;
+
+	q = q0;
+	while(q > 0){
+		r = textreadc(t, q-1);
+		if(r <= ' ')
+			break;
+		if(oneelement && r=='/')
+			break;
+		--q;
+	}
+	return q0-q;
+}
+
+Rune*
+textcomplete(Text *t)
+{
+	int i, nstr, npath;
+	uint q;
+	Rune tmp[200];
+	Rune *str, *path;
+	Rune *rp;
+	Completion *c;
+	char *s, *dirs;
+	Runestr dir;
+
+	/* control-f: filename completion; works back to white space or / */
+	if(t->q0<t->file->nc && textreadc(t, t->q0)>' ')	/* must be at end of word */
+		return nil;
+	nstr = textfilewidth(t, t->q0, TRUE);
+	str = runemalloc(nstr);
+	npath = textfilewidth(t, t->q0-nstr, FALSE);
+	path = runemalloc(npath);
+
+	c = nil;
+	rp = nil;
+	dirs = nil;
+
+	q = t->q0-nstr;
+	for(i=0; i<nstr; i++)
+		str[i] = textreadc(t, q++);
+	q = t->q0-nstr-npath;
+	for(i=0; i<npath; i++)
+		path[i] = textreadc(t, q++);
+	/* is path rooted? if not, we need to make it relative to window path */
+	if(npath>0 && path[0]=='/')
+		dir = (Runestr){path, npath};
+	else{
+		dir = dirname(t, nil, 0);
+		if(dir.nr + 1 + npath > nelem(tmp)){
+			free(dir.r);
+			goto Return;
+		}
+		if(dir.nr == 0){
+			dir.nr = 1;
+			dir.r = runestrdup(L".");
+		}
+		runemove(tmp, dir.r, dir.nr);
+		tmp[dir.nr] = '/';
+		runemove(tmp+dir.nr+1, path, npath);
+		free(dir.r);
+		dir.r = tmp;
+		dir.nr += 1+npath;
+		dir = cleanrname(dir);
+	}
+
+	s = smprint("%.*S", nstr, str);
+	dirs = smprint("%.*S", dir.nr, dir.r);
+	c = complete(dirs, s);
+	free(s);
+	if(c == nil){
+		warning(nil, "error attempting completion: %r\n");
+		goto Return;
+	}
+
+	if(!c->advance){
+		warning(nil, "%.*S%s%.*S*%s\n",
+			dir.nr, dir.r,
+			dir.nr>0 && dir.r[dir.nr-1]!='/' ? "/" : "",
+			nstr, str,
+			c->nmatch? "" : ": no matches in:");
+		for(i=0; i<c->nfile; i++)
+			warning(nil, " %s\n", c->filename[i]);
+	}
+
+	if(c->advance)
+		rp = runesmprint("%s", c->string);
+	else
+		rp = nil;
+  Return:
+	freecompletion(c);
+	free(dirs);
+	free(str);
+	free(path);
+	return rp;
+}
+
+void
+texttype(Text *t, Rune r)
+{
+	uint q0, q1;
+	int nnb, nb, n, i;
+	int nr;
+	Rune rr;
+	Rune *rp;
+	Text *u;
+
+	nr = 1;
+	rp = &r;
+	switch(r){
+	case Kleft:
+		typecommit(t);
+		if(t->q0 > 0)
+			textshow(t, t->q0-1, t->q0-1, TRUE);
+		return;
+	case Kright:
+		typecommit(t);
+		if(t->q1 < t->file->nc)
+			textshow(t, t->q1+1, t->q1+1, TRUE);
+		return;
+	case Kdown:
+		n = t->maxlines/3;
+		goto case_Down;
+	case Kscrollonedown:
+		n = mousescrollsize(t->maxlines);
+		if(n <= 0)
+			n = 1;
+		goto case_Down;
+	case Kpgdown:
+		n = 2*t->maxlines/3;
+	case_Down:
+		q0 = t->org+frcharofpt(t, Pt(t->r.min.x, t->r.min.y+n*t->font->height));
+		if(t->what == Body)
+			textsetorigin(t, q0, TRUE);
+		return;
+	case Kup:
+		n = t->maxlines/3;
+		goto case_Up;
+	case Kscrolloneup:
+		n = mousescrollsize(t->maxlines);
+		goto case_Up;
+	case Kpgup:
+		n = 2*t->maxlines/3;
+	case_Up:
+		q0 = textbacknl(t, t->org, n);
+		if(t->what == Body)
+			textsetorigin(t, q0, TRUE);
+		return;
+	case Khome:
+		typecommit(t);
+		textshow(t, 0, 0, FALSE);
+		return;
+	case Kend:
+		typecommit(t);
+		textshow(t, t->file->nc, t->file->nc, FALSE);
+		return;
+	case 0x01:	/* ^A: beginning of line */
+		typecommit(t);
+		/* go to where ^U would erase, if not already at BOL */
+		nnb = 0;
+		if(t->q0>0 && textreadc(t, t->q0-1)!='\n')
+			nnb = textbswidth(t, 0x15);
+		textshow(t, t->q0-nnb, t->q0-nnb, TRUE);
+		return;
+	case 0x05:	/* ^E: end of line */
+		typecommit(t);
+		q0 = t->q0;
+		while(q0<t->file->nc && textreadc(t, q0)!='\n')
+			q0++;
+		textshow(t, q0, q0, TRUE);
+		return;
+	}
+	if(t->what == Body){
+		seq++;
+		filemark(t->file);
+	}
+	if(t->q1 > t->q0){
+		if(t->ncache != 0)
+			error("text.type");
+		cut(t, t, nil, TRUE, TRUE, nil, 0);
+		t->eq0 = ~0;
+	}
+	textshow(t, t->q0, t->q0, 1);
+	switch(r){
+	case 0x06:
+	case Kins:
+		rp = textcomplete(t);
+		if(rp == nil)
+			return;
+		nr = runestrlen(rp);
+		break;	/* fall through to normal insertion case */
+	case 0x1B:
+		if(t->eq0 != ~0)
+			textsetselect(t, t->eq0, t->q0);
+		if(t->ncache > 0)
+			typecommit(t);
+		return;
+	case 0x08:	/* ^H: erase character */
+	case 0x15:	/* ^U: erase line */
+	case 0x17:	/* ^W: erase word */
+		if(t->q0 == 0)	/* nothing to erase */
+			return;
+		nnb = textbswidth(t, r);
+		q1 = t->q0;
+		q0 = q1-nnb;
+		/* if selection is at beginning of window, avoid deleting invisible text */
+		if(q0 < t->org){
+			q0 = t->org;
+			nnb = q1-q0;
+		}
+		if(nnb <= 0)
+			return;
+		for(i=0; i<t->file->ntext; i++){
+			u = t->file->text[i];
+			u->nofill = TRUE;
+			nb = nnb;
+			n = u->ncache;
+			if(n > 0){
+				if(q1 != u->cq0+n)
+					error("text.type backspace");
+				if(n > nb)
+					n = nb;
+				u->ncache -= n;
+				textdelete(u, q1-n, q1, FALSE);
+				nb -= n;
+			}
+			if(u->eq0==q1 || u->eq0==~0)
+				u->eq0 = q0;
+			if(nb && u==t)
+				textdelete(u, q0, q0+nb, TRUE);
+			if(u != t)
+				textsetselect(u, u->q0, u->q1);
+			else
+				textsetselect(t, q0, q0);
+			u->nofill = FALSE;
+		}
+		for(i=0; i<t->file->ntext; i++)
+			textfill(t->file->text[i]);
+		return;
+	case '\t':
+		if(t->what == Body && t->w->indent[SPACESINDENT]){
+			nnb = textbswidth(t, 0x15);
+			if(nnb == 1 && textreadc(t, t->q0-1) == '\n')
+				nnb = 0;
+			nnb = t->tabstop - nnb % t->tabstop;
+			rp = runemalloc(nnb);
+			for(nr = 0; nr < nnb; nr++)
+				rp[nr] = ' ';
+		}
+		break;
+	case '\n':
+		if(t->what == Body && t->w->indent[AUTOINDENT]){
+			/* find beginning of previous line using backspace code */
+			nnb = textbswidth(t, 0x15); /* ^U case */
+			rp = runemalloc(nnb + 1);
+			nr = 0;
+			rp[nr++] = r;
+			for(i=0; i<nnb; i++){
+				rr = textreadc(t, t->q0-nnb+i);
+				if(rr != ' ' && rr != '\t')
+					break;
+				rp[nr++] = rr;
+			}
+		}
+		break; /* fall through to normal code */
+	}
+	/* otherwise ordinary character; just insert, typically in caches of all texts */
+	for(i=0; i<t->file->ntext; i++){
+		u = t->file->text[i];
+		if(u->eq0 == ~0)
+			u->eq0 = t->q0;
+		if(u->ncache == 0)
+			u->cq0 = t->q0;
+		else if(t->q0 != u->cq0+u->ncache)
+			error("text.type cq1");
+		textinsert(u, t->q0, rp, nr, FALSE);
+		if(u != t)
+			textsetselect(u, u->q0, u->q1);
+		if(u->ncache+nr > u->ncachealloc){
+			u->ncachealloc += 10 + nr;
+			u->cache = runerealloc(u->cache, u->ncachealloc);
+		}
+		runemove(u->cache+u->ncache, rp, nr);
+		u->ncache += nr;
+	}
+	if(rp != &r)
+		free(rp);
+	textsetselect(t, t->q0+nr, t->q0+nr);
+	if(r=='\n' && t->w!=nil)
+		wincommit(t->w, t);
+}
+
+void
+textcommit(Text *t, int tofile)
+{
+	if(t->ncache == 0)
+		return;
+	if(tofile)
+		fileinsert(t->file, t->cq0, t->cache, t->ncache);
+	if(t->what == Body){
+		t->w->dirty = TRUE;
+		t->w->utflastqid = -1;
+	}
+	t->ncache = 0;
+}
+
+static	Text	*clicktext;
+static	uint	clickmsec;
+static	int	clickcount;
+static	Point	clickpt;
+static	Text	*selecttext;
+static	uint	selectq;
+
+/*
+ * called from frame library
+ */
+void
+framescroll(Frame *f, int dl)
+{
+	if(f != &selecttext->Frame)
+		error("frameselect not right frame");
+	textframescroll(selecttext, dl);
+}
+
+void
+textframescroll(Text *t, int dl)
+{
+	uint q0;
+
+	if(dl == 0){
+		scrsleep(100);
+		return;
+	}
+	if(dl < 0){
+		q0 = textbacknl(t, t->org, -dl);
+		if(selectq > t->org+t->p0)
+			textsetselect(t, t->org+t->p0, selectq);
+		else
+			textsetselect(t, selectq, t->org+t->p0);
+	}else{
+		if(t->org+t->nchars == t->file->nc)
+			return;
+		q0 = t->org+frcharofpt(t, Pt(t->r.min.x, t->r.min.y+dl*t->font->height));
+		if(selectq > t->org+t->p1)
+			textsetselect(t, t->org+t->p1, selectq);
+		else
+			textsetselect(t, selectq, t->org+t->p1);
+	}
+	textsetorigin(t, q0, TRUE);
+	flushimage(display, 1);
+}
+
+
+void
+textselect(Text *t)
+{
+	uint q0, q1;
+	int b, x, y, dx, dy;
+	int state;
+
+	selecttext = t;
+	/*
+	 * To have double-clicking and chording, we double-click
+	 * immediately if it might make sense.
+	 */
+	b = mouse->buttons;
+	q0 = t->q0;
+	q1 = t->q1;
+	dx = abs(clickpt.x - mouse->xy.x);
+	dy = abs(clickpt.y - mouse->xy.y);
+	clickpt = mouse->xy;
+	selectq = t->org+frcharofpt(t, mouse->xy);
+	clickcount++;
+	if(mouse->msec-clickmsec >= 500 || selecttext != t || clickcount > 3 || dx > 3 || dy > 3)
+		clickcount = 0;
+	if(clickcount >= 1 && selecttext==t && mouse->msec-clickmsec < 500){
+		textstretchsel(t, selectq, &q0, &q1, clickcount);
+		textsetselect(t, q0, q1);
+		flushimage(display, 1);
+		x = mouse->xy.x;
+		y = mouse->xy.y;
+		/* stay here until something interesting happens */
+		while(1){
+			readmouse(mousectl);
+			dx = abs(mouse->xy.x - x);
+			dy = abs(mouse->xy.y - y);
+			if(mouse->buttons != b || dx >= 3 || dy >= 3)
+				break;
+			clickcount++;
+			clickmsec = mouse->msec;
+		}
+		mouse->xy.x = x;	/* in case we're calling frselect */
+		mouse->xy.y = y;
+		q0 = t->q0;	/* may have changed */
+		q1 = t->q1;
+		selectq = t->org+frcharofpt(t, mouse->xy);;
+	}
+	if(mouse->buttons == b && clickcount == 0){
+		t->Frame.scroll = framescroll;
+		frselect(t, mousectl);
+		/* horrible botch: while asleep, may have lost selection altogether */
+		if(selectq > t->file->nc)
+			selectq = t->org + t->p0;
+		t->Frame.scroll = nil;
+		if(selectq < t->org)
+			q0 = selectq;
+		else
+			q0 = t->org + t->p0;
+		if(selectq > t->org+t->nchars)
+			q1 = selectq;
+		else
+			q1 = t->org+t->p1;
+	}
+	if(q0 == q1){
+		if(q0==t->q0 && mouse->msec-clickmsec<500)
+			textstretchsel(t, selectq, &q0, &q1, clickcount);
+		else
+			clicktext = t;
+		clickmsec = mouse->msec;
+	}else
+		clicktext = nil;
+	textsetselect(t, q0, q1);
+	flushimage(display, 1);
+	state = 0;	/* undo when possible; +1 for cut, -1 for paste */
+	while(mouse->buttons){
+		mouse->msec = 0;
+		b = mouse->buttons;
+		if((b&1) && (b&6)){
+			if(state==0 && t->what==Body){
+				seq++;
+				filemark(t->w->body.file);
+			}
+			if(b & 2){
+				if(state==-1 && t->what==Body){
+					winundo(t->w, TRUE);
+					textsetselect(t, q0, t->q0);
+					state = 0;
+				}else if(state != 1){
+					cut(t, t, nil, TRUE, TRUE, nil, 0);
+					state = 1;
+				}
+			}else{
+				if(state==1 && t->what==Body){
+					winundo(t->w, TRUE);
+					textsetselect(t, q0, t->q1);
+					state = 0;
+				}else if(state != -1){
+					paste(t, t, nil, TRUE, FALSE, nil, 0);
+					state = -1;
+				}
+			}
+			textscrdraw(t);
+			clearmouse();
+		}
+		flushimage(display, 1);
+		while(mouse->buttons == b)
+			readmouse(mousectl);
+		if(mouse->msec-clickmsec >= 500)
+			clicktext = nil;
+	}
+}
+
+void
+textshow(Text *t, uint q0, uint q1, int doselect)
+{
+	int qe;
+	int nl;
+	uint q;
+
+	if(t->what != Body){
+		if(doselect)
+			textsetselect(t, q0, q1);
+		return;
+	}
+	if(t->w!=nil && t->maxlines==0)
+		colgrow(t->col, t->w, 1);
+	if(doselect)
+		textsetselect(t, q0, q1);
+	qe = t->org+t->nchars;
+	if(t->org<=q0 && (q0<qe || (q0==qe && qe==t->file->nc+t->ncache)))
+		textscrdraw(t);
+	else{
+		if(t->w->nopen[QWevent] > 0)
+			nl = 3*t->maxlines/4;
+		else
+			nl = t->maxlines/4;
+		q = textbacknl(t, q0, nl);
+		/* avoid going backwards if trying to go forwards - long lines! */
+		if(!(q0>t->org && q<t->org))
+			textsetorigin(t, q, TRUE);
+		while(q0 > t->org+t->nchars)
+			textsetorigin(t, t->org+1, FALSE);
+	}
+}
+
+static
+int
+region(int a, int b)
+{
+	if(a < b)
+		return -1;
+	if(a == b)
+		return 0;
+	return 1;
+}
+
+void
+selrestore(Frame *f, Point pt0, uint p0, uint p1)
+{
+	if(p1<=f->p0 || p0>=f->p1){
+		/* no overlap */
+		frdrawsel0(f, pt0, p0, p1, f->cols[BACK], f->cols[TEXT]);
+		return;
+	}
+	if(p0>=f->p0 && p1<=f->p1){
+		/* entirely inside */
+		frdrawsel0(f, pt0, p0, p1, f->cols[HIGH], f->cols[HTEXT]);
+		return;
+	}
+
+	/* they now are known to overlap */
+
+	/* before selection */
+	if(p0 < f->p0){
+		frdrawsel0(f, pt0, p0, f->p0, f->cols[BACK], f->cols[TEXT]);
+		p0 = f->p0;
+		pt0 = frptofchar(f, p0);
+	}
+	/* after selection */
+	if(p1 > f->p1){
+		frdrawsel0(f, frptofchar(f, f->p1), f->p1, p1, f->cols[BACK], f->cols[TEXT]);
+		p1 = f->p1;
+	}
+	/* inside selection */
+	frdrawsel0(f, pt0, p0, p1, f->cols[HIGH], f->cols[HTEXT]);
+}
+
+void
+textsetselect(Text *t, uint q0, uint q1)
+{
+	int p0, p1;
+
+	/* t->p0 and t->p1 are always right; t->q0 and t->q1 may be off */
+	t->q0 = q0;
+	t->q1 = q1;
+	/* compute desired p0,p1 from q0,q1 */
+	p0 = q0-t->org;
+	p1 = q1-t->org;
+	if(p0 < 0)
+		p0 = 0;
+	if(p1 < 0)
+		p1 = 0;
+	if(p0 > t->nchars)
+		p0 = t->nchars;
+	if(p1 > t->nchars)
+		p1 = t->nchars;
+	if(p0==t->p0 && p1==t->p1)
+		return;
+	/* screen disagrees with desired selection */
+	if(t->p1<=p0 || p1<=t->p0 || p0==p1 || t->p1==t->p0){
+		/* no overlap or too easy to bother trying */
+		frdrawsel(t, frptofchar(t, t->p0), t->p0, t->p1, 0);
+		frdrawsel(t, frptofchar(t, p0), p0, p1, 1);
+		goto Return;
+	}
+	/* overlap; avoid unnecessary painting */
+	if(p0 < t->p0){
+		/* extend selection backwards */
+		frdrawsel(t, frptofchar(t, p0), p0, t->p0, 1);
+	}else if(p0 > t->p0){
+		/* trim first part of selection */
+		frdrawsel(t, frptofchar(t, t->p0), t->p0, p0, 0);
+	}
+	if(p1 > t->p1){
+		/* extend selection forwards */
+		frdrawsel(t, frptofchar(t, t->p1), t->p1, p1, 1);
+	}else if(p1 < t->p1){
+		/* trim last part of selection */
+		frdrawsel(t, frptofchar(t, p1), p1, t->p1, 0);
+	}
+
+    Return:
+	t->p0 = p0;
+	t->p1 = p1;
+}
+
+/*
+ * Release the button in less than DELAY ms and it's considered a null selection
+ * if the mouse hardly moved, regardless of whether it crossed a char boundary.
+ */
+enum {
+	DELAY = 2,
+	MINMOVE = 4,
+};
+
+uint
+xselect(Frame *f, Mousectl *mc, Image *col, uint *p1p)	/* when called, button is down */
+{
+	uint p0, p1, q, tmp;
+	ulong msec;
+	Point mp, pt0, pt1, qt;
+	int reg, b;
+
+	mp = mc->xy;
+	b = mc->buttons;
+	msec = mc->msec;
+
+	/* remove tick */
+	if(f->p0 == f->p1)
+		frtick(f, frptofchar(f, f->p0), 0);
+	p0 = p1 = frcharofpt(f, mp);
+	pt0 = frptofchar(f, p0);
+	pt1 = frptofchar(f, p1);
+	reg = 0;
+	frtick(f, pt0, 1);
+	do{
+		q = frcharofpt(f, mc->xy);
+		if(p1 != q){
+			if(p0 == p1)
+				frtick(f, pt0, 0);
+			if(reg != region(q, p0)){	/* crossed starting point; reset */
+				if(reg > 0)
+					selrestore(f, pt0, p0, p1);
+				else if(reg < 0)
+					selrestore(f, pt1, p1, p0);
+				p1 = p0;
+				pt1 = pt0;
+				reg = region(q, p0);
+				if(reg == 0)
+					frdrawsel0(f, pt0, p0, p1, col, display->white);
+			}
+			qt = frptofchar(f, q);
+			if(reg > 0){
+				if(q > p1)
+					frdrawsel0(f, pt1, p1, q, col, display->white);
+
+				else if(q < p1)
+					selrestore(f, qt, q, p1);
+			}else if(reg < 0){
+				if(q > p1)
+					selrestore(f, pt1, p1, q);
+				else
+					frdrawsel0(f, qt, q, p1, col, display->white);
+			}
+			p1 = q;
+			pt1 = qt;
+		}
+		if(p0 == p1)
+			frtick(f, pt0, 1);
+		flushimage(f->display, 1);
+		readmouse(mc);
+	}while(mc->buttons == b);
+	if(mc->msec-msec < DELAY && p0!=p1
+	&& abs(mp.x-mc->xy.x)<MINMOVE
+	&& abs(mp.y-mc->xy.y)<MINMOVE) {
+		if(reg > 0)
+			selrestore(f, pt0, p0, p1);
+		else if(reg < 0)
+			selrestore(f, pt1, p1, p0);
+		p1 = p0;
+	}
+	if(p1 < p0){
+		tmp = p0;
+		p0 = p1;
+		p1 = tmp;
+	}
+	pt0 = frptofchar(f, p0);
+	if(p0 == p1)
+		frtick(f, pt0, 0);
+	selrestore(f, pt0, p0, p1);
+	/* restore tick */
+	if(f->p0 == f->p1)
+		frtick(f, frptofchar(f, f->p0), 1);
+	flushimage(f->display, 1);
+	*p1p = p1;
+	return p0;
+}
+
+int
+textselect23(Text *t, uint *q0, uint *q1, Image *high, int mask)
+{
+	uint p0, p1;
+	int buts;
+	
+	p0 = xselect(t, mousectl, high, &p1);
+	buts = mousectl->buttons;
+	if((buts & mask) == 0){
+		*q0 = p0+t->org;
+		*q1 = p1+t->org;
+	}
+
+	while(mousectl->buttons)
+		readmouse(mousectl);
+	return buts;
+}
+
+int
+textselect2(Text *t, uint *q0, uint *q1, Text **tp)
+{
+	int buts;
+
+	*tp = nil;
+	buts = textselect23(t, q0, q1, but2col, 4);
+	if(buts & 4)
+		return 0;
+	if(buts & 1){	/* pick up argument */
+		*tp = argtext;
+		return 1;
+	}
+	return 1;
+}
+
+int
+textselect3(Text *t, uint *q0, uint *q1)
+{
+	int h;
+
+	h = (textselect23(t, q0, q1, but3col, 1|2) == 0);
+	return h;
+}
+
+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
+};
+
+int
+inmode(Rune r, int mode)
+{
+	return (mode == 1) ? isalnum(r) : r && !isspace(r);
+}
+
+void
+textstretchsel(Text *t, uint mp, uint *q0, uint *q1, int mode)
+{
+	int c, i;
+	Rune *r, *l, *p;
+	uint q;
+
+	*q0 = mp;
+	*q1 = mp;
+	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 = textreadc(t, q-1);
+		p = runestrchr(l, c);
+		if(p != nil){
+			if(textclickmatch(t, c, r[p-l], 1, &q))
+				*q1 = q-(c!='\n');
+			return;
+		}
+		/* try matching character to right, looking left */
+		if(q == t->file->nc)
+			c = '\n';
+		else
+			c = textreadc(t, q);
+		p = runestrchr(r, c);
+		if(p != nil){
+			if(textclickmatch(t, c, l[p-r], -1, &q)){
+				*q1 = *q0+(*q0<t->file->nc && c=='\n');
+				*q0 = q;
+				if(c!='\n' || q!=0 || textreadc(t, 0)=='\n')
+					(*q0)++;
+			}
+			return;
+		}
+	}
+	/* try filling out word to right */
+	while(*q1<t->file->nc && inmode(textreadc(t, *q1), mode))
+		(*q1)++;
+	/* try filling out word to left */
+	while(*q0>0 && inmode(textreadc(t, *q0-1), mode))
+		(*q0)--;
+}
+
+int
+textclickmatch(Text *t, int cl, int cr, int dir, uint *q)
+{
+	Rune c;
+	int nest;
+
+	nest = 1;
+	for(;;){
+		if(dir > 0){
+			if(*q == t->file->nc)
+				break;
+			c = textreadc(t, *q);
+			(*q)++;
+		}else{
+			if(*q == 0)
+				break;
+			(*q)--;
+			c = textreadc(t, *q);
+		}
+		if(c == cr){
+			if(--nest==0)
+				return 1;
+		}else if(c == cl)
+			nest++;
+	}
+	return cl=='\n' && nest==1;
+}
+
+uint
+textbacknl(Text *t, uint p, uint n)
+{
+	int i, j;
+
+	/* look for start of this line if n==0 */
+	if(n==0 && p>0 && textreadc(t, 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(textreadc(t, p-1)=='\n')
+				break;
+	}
+	return p;
+}
+
+void
+textsetorigin(Text *t, uint org, int exact)
+{
+	int i, a, fixup;
+	Rune *r;
+	uint n;
+
+	if(org>0 && !exact && textreadc(t, org-1) != '\n'){
+		/* org is an estimate of the char posn; find a newline */
+		/* don't try harder than 256 chars */
+		for(i=0; i<256 && org<t->file->nc; i++){
+			if(textreadc(t, org) == '\n'){
+				org++;
+				break;
+			}
+			org++;
+		}
+	}
+	a = org-t->org;
+	fixup = 0;
+	if(a>=0 && a<t->nchars){
+		frdelete(t, 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<t->nchars){
+		n = t->org - org;
+		r = runemalloc(n);
+		bufread(t->file, org, r, n);
+		frinsert(t, r, r+n, 0);
+		free(r);
+	}else
+		frdelete(t, 0, t->nchars);
+	t->org = org;
+	textfill(t);
+	textscrdraw(t);
+	textsetselect(t, t->q0, t->q1);
+	if(fixup && t->p1 > t->p0)
+		frdrawsel(t, frptofchar(t, t->p1-1), t->p1-1, t->p1, 1);
+}
+
+void
+textreset(Text *t)
+{
+	t->file->seq = 0;
+	t->eq0 = ~0;
+	/* do t->delete(0, t->nc, TRUE) without building backup stuff */
+	textsetselect(t, t->org, t->org);
+	frdelete(t, 0, t->nchars);
+	t->org = 0;
+	t->q0 = 0;
+	t->q1 = 0;
+	filereset(t->file);
+	bufreset(t->file);
+}
--- /dev/null
+++ b/time.c
@@ -1,0 +1,120 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <cursor.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <frame.h>
+#include <fcall.h>
+#include <plumb.h>
+#include "dat.h"
+#include "fns.h"
+
+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;
+
+	threadsetname("timerproc");
+	rfork(RFFDG);
+	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 = FALSE;
+			if(x->cancel){
+				timerstop(x);
+				del = TRUE;
+			}else if(x->dt <= 0){
+				/*
+				 * avoid possible deadlock if client is
+				 * now sending on ctimer
+				 */
+				if(nbsendul(x->c, 0) > 0)
+					del = TRUE;
+			}
+			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)
+					error("timer realloc failed");
+			}
+			t[nt++] = x;
+			old = msec();
+		}
+		if(nbrecv(ctimer, &x) > 0)
+			goto gotit;
+	}
+}
+
+void
+timerinit(void)
+{
+	ctimer = chancreate(sizeof(Timer*), 100);
+	proccreate(timerproc, nil, STACK);
+}
+
+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,485 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <cursor.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <frame.h>
+#include <fcall.h>
+#include <plumb.h>
+#include "dat.h"
+#include "fns.h"
+
+static	Point		prevmouse;
+static	Window	*mousew;
+
+void
+cvttorunes(char *p, int n, Rune *r, int *nb, int *nr, int *nulls)
+{
+	uchar *q;
+	Rune *s;
+	int j, w;
+
+	/*
+	 * Always guaranteed that n bytes may be interpreted
+	 * without worrying about partial runes.  This may mean
+	 * reading up to UTFmax-1 more bytes than n; the caller
+	 * knows this.  If n is a firm limit, the caller should
+	 * set p[n] = 0.
+	 */
+	q = (uchar*)p;
+	s = r;
+	for(j=0; j<n; j+=w){
+		if(*q < Runeself){
+			w = 1;
+			*s = *q++;
+		}else{
+			w = chartorune(s, (char*)q);
+			q += w;
+		}
+		if(*s)
+			s++;
+		else if(nulls)
+			*nulls = TRUE;
+	}
+	*nb = (char*)q-p;
+	*nr = s-r;
+}
+
+void
+error(char *s)
+{
+	fprint(2, "acme: %s: %r\n", s);
+	remove(acmeerrorfile);
+	abort();
+}
+
+Window*
+errorwin1(Rune *dir, int ndir, Rune **incl, int nincl)
+{
+	Window *w;
+	Rune *r;
+	int i, n;
+
+	r = runemalloc(ndir+8);
+	if(n = ndir){	/* assign = */
+		runemove(r, dir, ndir);
+		r[n++] = L'/';
+	}
+	runemove(r+n, L"+Errors", 7);
+	n += 7;
+	w = lookfile(r, n);
+	if(w == nil){
+		if(row.ncol == 0)
+			if(rowadd(&row, nil, -1) == nil)
+				error("can't create column to make error window");
+		w = coladd(row.col[row.ncol-1], nil, nil, -1);
+		w->filemenu = FALSE;
+		winsetname(w, r, n);
+		xfidlog(w, "new");
+	}
+	free(r);
+	for(i=nincl; --i>=0; ){
+		n = runestrlen(incl[i]);
+		r = runemalloc(n);
+		runemove(r, incl[i], n);
+		winaddincl(w, r, n);
+	}
+	for(i=0; i<NINDENT; i++)
+		w->indent[i] = globalindent[i];
+	return w;
+}
+
+/* make new window, if necessary; return with it locked */
+Window*
+errorwin(Mntdir *md, int owner)
+{
+	Window *w;
+
+	for(;;){
+		if(md == nil)
+			w = errorwin1(nil, 0, nil, 0);
+		else
+			w = errorwin1(md->dir, md->ndir, md->incl, md->nincl);
+		winlock(w, owner);
+		if(w->col != nil)
+			break;
+		/* window was deleted too fast */
+		winunlock(w);
+	}
+	return w;
+}
+
+/*
+ * Incoming window should be locked. 
+ * It will be unlocked and returned window
+ * will be locked in its place.
+ */
+Window*
+errorwinforwin(Window *w)
+{
+	int i, n, nincl, owner;
+	Rune **incl;
+	Runestr dir;
+	Text *t;
+
+	t = &w->body;
+	dir = dirname(t, nil, 0);
+	if(dir.nr==1 && dir.r[0]=='.'){	/* sigh */
+		free(dir.r);
+		dir.r = nil;
+		dir.nr = 0;
+	}
+	incl = nil;
+	nincl = w->nincl;
+	if(nincl > 0){
+		incl = emalloc(nincl*sizeof(Rune*));
+		for(i=0; i<nincl; i++){
+			n = runestrlen(w->incl[i]);
+			incl[i] = runemalloc(n+1);
+			runemove(incl[i], w->incl[i], n);
+		}
+	}
+	owner = w->owner;
+	winunlock(w);
+	for(;;){
+		w = errorwin1(dir.r, dir.nr, incl, nincl);
+		winlock(w, owner);
+		if(w->col != nil)
+			break;
+		/* window deleted too fast */
+		winunlock(w);
+	}
+	return w;
+}
+
+typedef struct Warning Warning;
+
+struct Warning{
+	Mntdir *md;
+	Buffer buf;
+	Warning *next;
+};
+
+static Warning *warnings;
+
+static
+void
+addwarningtext(Mntdir *md, Rune *r, int nr)
+{
+	Warning *warn;
+	
+	for(warn = warnings; warn; warn=warn->next){
+		if(warn->md == md){
+			bufinsert(&warn->buf, warn->buf.nc, r, nr);
+			return;
+		}
+	}
+	warn = emalloc(sizeof(Warning));
+	warn->next = warnings;
+	warn->md = md;
+	if(md)
+		fsysincid(md);
+	warnings = warn;
+	bufinsert(&warn->buf, 0, r, nr);
+	nbsendp(cwarn, 0);
+}
+
+/* called while row is locked */
+void
+flushwarnings(void)
+{
+	Warning *warn, *next;
+	Window *w;
+	Text *t;
+	int owner, nr, q0, n;
+	Rune *r;
+
+	for(warn=warnings; warn; warn=next) {
+		w = errorwin(warn->md, 'E');
+		t = &w->body;
+		owner = w->owner;
+		if(owner == 0)
+			w->owner = 'E';
+		wincommit(w, t);
+		/*
+		 * Most commands don't generate much output. For instance,
+		 * Edit ,>cat goes through /dev/cons and is already in blocks
+		 * because of the i/o system, but a few can.  Edit ,p will
+		 * put the entire result into a single hunk.  So it's worth doing
+		 * this in blocks (and putting the text in a buffer in the first
+		 * place), to avoid a big memory footprint.
+		 */
+		r = fbufalloc();
+		q0 = t->file->nc;
+		for(n = 0; n < warn->buf.nc; n += nr){
+			nr = warn->buf.nc - n;
+			if(nr > RBUFSIZE)
+				nr = RBUFSIZE;
+			bufread(&warn->buf, n, r, nr);
+			textbsinsert(t, t->file->nc, r, nr, TRUE, &nr);
+		}
+		textshow(t, q0, t->file->nc, 1);
+		free(r);
+		winsettag(t->w);
+		textscrdraw(t);
+		w->owner = owner;
+		w->dirty = FALSE;
+		winunlock(w);
+		bufclose(&warn->buf);
+		next = warn->next;
+		if(warn->md)
+			fsysdelid(warn->md);
+		free(warn);
+	}
+	warnings = nil;
+}
+
+void
+warning(Mntdir *md, char *s, ...)
+{
+	Rune *r;
+	va_list arg;
+
+	va_start(arg, s);
+	r = runevsmprint(s, arg);
+	va_end(arg);
+	if(r == nil)
+		error("runevsmprint failed");
+	addwarningtext(md, r, runestrlen(r));
+	free(r);
+}
+
+int
+runeeq(Rune *s1, uint n1, Rune *s2, uint n2)
+{
+	if(n1 != n2)
+		return FALSE;
+	return memcmp(s1, s2, n1*sizeof(Rune)) == 0;
+}
+
+uint
+min(uint a, uint b)
+{
+	if(a < b)
+		return a;
+	return b;
+}
+
+uint
+max(uint a, uint b)
+{
+	if(a > b)
+		return a;
+	return b;
+}
+
+char*
+runetobyte(Rune *r, int n)
+{
+	char *s;
+
+	if(r == nil)
+		return nil;
+	s = emalloc(n*UTFmax+1);
+	setmalloctag(s, getcallerpc(&r));
+	snprint(s, n*UTFmax+1, "%.*S", n, r);
+	return s;
+}
+
+Rune*
+bytetorune(char *s, int *ip)
+{
+	Rune *r;
+	int nb, nr;
+
+	nb = strlen(s);
+	r = runemalloc(nb+1);
+	cvttorunes(s, nb, r, &nb, &nr, nil);
+	r[nr] = '\0';
+	*ip = nr;
+	return r;
+}
+
+int
+isspace(Rune c)
+{
+	return c == 0 || c == ' ' || c == '\t' ||
+		c == '\n' || c == '\r' || c == '\v';
+}
+
+int
+isalnum(Rune c)
+{
+	/*
+	 * Hard to get absolutely right.  Use what we know about ASCII
+	 * and assume anything above the Latin control characters is
+	 * potentially an alphanumeric.
+	 *
+	 * Treat 0xA0 (non-breaking space) as a special alphanumeric
+	 * character [sape]
+	 */
+	if(c <= ' ')
+		return FALSE;
+	if(0x7F<=c && c<0xA0)
+		return FALSE;
+	if(utfrune("!\"#$%&'()*+,-./:;<=>?@[\\]^`{|}~", c))
+		return FALSE;
+	return TRUE;
+}
+
+int
+rgetc(void *v, uint n)
+{
+	return ((Rune*)v)[n];
+}
+
+int
+tgetc(void *a, uint n)
+{
+	Text *t;
+
+	t = a;
+	if(n >= t->file->nc)
+		return 0;
+	return textreadc(t, n);
+}
+
+Rune*
+skipbl(Rune *r, int n, int *np)
+{
+	while(n>0 && (*r==' ' || *r=='\t' || *r=='\n')){
+		--n;
+		r++;
+	}
+	*np = n;
+	return r;
+}
+
+Rune*
+findbl(Rune *r, int n, int *np)
+{
+	while(n>0 && *r!=' ' && *r!='\t' && *r!='\n'){
+		--n;
+		r++;
+	}
+	*np = n;
+	return r;
+}
+
+void
+savemouse(Window *w)
+{
+	prevmouse = mouse->xy;
+	mousew = w;
+}
+
+int
+restoremouse(Window *w)
+{
+	int did;
+
+	did = 0;
+	if(mousew!=nil && mousew==w) {
+		moveto(mousectl, prevmouse);
+		did = 1;
+	}
+	mousew = nil;
+	return did;
+}
+
+void
+clearmouse()
+{
+	mousew = nil;
+}
+
+char*
+estrdup(char *s)
+{
+	char *t;
+
+	t = strdup(s);
+	if(t == nil)
+		error("strdup failed");
+	setmalloctag(t, getcallerpc(&s));
+	return t;
+}
+
+void*
+emalloc(uint n)
+{
+	void *p;
+
+	p = malloc(n);
+	if(p == nil)
+		error("malloc failed");
+	setmalloctag(p, getcallerpc(&n));
+	memset(p, 0, n);
+	return p;
+}
+
+void*
+erealloc(void *p, uint n)
+{
+	p = realloc(p, n);
+	if(p == nil)
+		error("realloc failed");
+	setmalloctag(p, getcallerpc(&n));
+	return p;
+}
+
+/*
+ * Heuristic city.
+ */
+Window*
+makenewwindow(Text *t)
+{
+	Column *c;
+	Window *w, *bigw, *emptyw;
+	Text *emptyb;
+	int i, y, el;
+
+	if(activecol)
+		c = activecol;
+	else if(seltext && seltext->col)
+		c = seltext->col;
+	else if(t && t->col)
+		c = t->col;
+	else{
+		if(row.ncol==0 && rowadd(&row, nil, -1)==nil)
+			error("can't make column");
+		c = row.col[row.ncol-1];
+	}
+	activecol = c;
+	if(t==nil || t->w==nil || c->nw==0)
+		return coladd(c, nil, nil, -1);
+
+	/* find biggest window and biggest blank spot */
+	emptyw = c->w[0];
+	bigw = emptyw;
+	for(i=1; i<c->nw; i++){
+		w = c->w[i];
+		/* use >= to choose one near bottom of screen */
+		if(w->body.maxlines >= bigw->body.maxlines)
+			bigw = w;
+		if(w->body.maxlines-w->body.nlines >= emptyw->body.maxlines-emptyw->body.nlines)
+			emptyw = w;
+	}
+	emptyb = &emptyw->body;
+	el = emptyb->maxlines-emptyb->nlines;
+	/* if empty space is big, use it */
+	if(el>15 || (el>3 && el>(bigw->body.maxlines-1)/2))
+		y = emptyb->r.min.y+emptyb->nlines*font->height;
+	else{
+		/* if this window is in column and isn't much smaller, split it */
+		if(t->col==c && Dy(t->w->r)>2*Dy(bigw->r)/3)
+			bigw = t->w;
+		y = (bigw->r.min.y + bigw->r.max.y)/2;
+	}
+	w = coladd(c, nil, nil, y);
+	if(w->body.maxlines < 2)
+		colgrow(w->col, w, 1);
+	return w;
+}
--- /dev/null
+++ b/wind.c
@@ -1,0 +1,697 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <cursor.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <frame.h>
+#include <fcall.h>
+#include <plumb.h>
+#include "dat.h"
+#include "fns.h"
+
+int	winid;
+
+void
+wininit(Window *w, Window *clone, Rectangle r)
+{
+	Rectangle r1, br;
+	File *f;
+	Reffont *rf;
+	Rune *rp;
+	int nc, i;
+
+	w->tag.w = w;
+	w->taglines = 1;
+	w->tagexpand = TRUE;
+	w->body.w = w;
+	w->id = ++winid;
+	incref(w);
+	if(globalincref)
+		incref(w);
+	w->ctlfid = ~0;
+	w->utflastqid = -1;
+	r1 = r;
+	r1.max.y = r1.min.y + font->height;
+	w->tagtop = r;
+	w->tagtop.max.y = r.min.y + font->height;
+	incref(&reffont);
+	f = fileaddtext(nil, &w->tag);
+	textinit(&w->tag, f, r1, &reffont, tagcols);
+	w->tag.what = Tag;
+	/* tag is a copy of the contents, not a tracked image */
+	if(clone){
+		textdelete(&w->tag, 0, w->tag.file->nc, TRUE);
+		nc = clone->tag.file->nc;
+		rp = runemalloc(nc);
+		bufread(clone->tag.file, 0, rp, nc);
+		textinsert(&w->tag, 0, rp, nc, TRUE);
+		free(rp);
+		filereset(w->tag.file);
+		textsetselect(&w->tag, nc, nc);
+	}
+	r1 = r;
+	r1.min.y += w->taglines*font->height + 1;
+	if(r1.max.y < r1.min.y)
+		r1.max.y = r1.min.y;
+	f = nil;
+	if(clone){
+		f = clone->body.file;
+		w->body.org = clone->body.org;
+		w->isscratch = clone->isscratch;
+		rf = rfget(FALSE, FALSE, FALSE, clone->body.reffont->f->name);
+	}else
+		rf = rfget(FALSE, FALSE, FALSE, nil);
+	f = fileaddtext(f, &w->body);
+	w->body.what = Body;
+	textinit(&w->body, f, r1, rf, textcols);
+	r1.min.y -= 1;
+	r1.max.y = r1.min.y+1;
+	draw(screen, r1, tagcols[BORD], nil, ZP);
+	textscrdraw(&w->body);
+	w->r = r;
+	w->r.max.y = w->body.r.max.y;
+	br.min = w->tag.scrollr.min;
+	br.max.x = br.min.x + Dx(button->r);
+	br.max.y = br.min.y + Dy(button->r);
+	draw(screen, br, button, nil, button->r.min);
+	w->filemenu = TRUE;
+	w->maxlines = w->body.maxlines;
+	for(i=0; i<NINDENT; i++)
+		w->indent[i] = globalindent[i];
+	if(clone){
+		w->dirty = clone->dirty;
+		for(i=0; i<NINDENT; i++)
+			w->indent[i] = clone->indent[i];
+		textsetselect(&w->body, clone->body.q0, clone->body.q1);
+		winsettag(w);
+	}
+}
+
+int
+tagrunepos(Window *w, Rune *s)
+{
+	int n;
+	Rune *r, *rr;
+
+	if(s == nil)
+		return -1;
+
+	n = w->tag.file->nc;
+	r = runemalloc(n+1);
+	bufread(w->tag.file, 0, r, n);
+	r[n] = L'\0';
+
+	rr = runestrstr(r, s);
+	if(rr == nil || rr == r)
+		return -1;
+	return rr - r;
+}
+
+void
+movetodel(Window *w)
+{
+	int n;
+	
+	n = tagrunepos(w, delcmd);
+	free(delcmd);
+	delcmd = nil;
+	if(n < 0)
+		return;
+	moveto(mousectl, addpt(frptofchar(&w->tag, n), Pt(4, w->tag.font->height-4)));
+}
+
+/*
+ * Compute number of tag lines required
+ * to display entire tag text.
+ */
+int
+wintaglines(Window *w, Rectangle r)
+{
+	int n;
+	Rune rune;
+	Point p;
+
+	if(!w->tagexpand && !w->showdel)
+		return 1;
+	w->showdel = FALSE;
+	w->noredraw = 1;
+	textresize(&w->tag, r);
+	w->noredraw = 0;
+	w->tagsafe = FALSE;
+	
+	if(!w->tagexpand) {
+		/* use just as many lines as needed to show the Del */
+		n = tagrunepos(w, delcmd);
+		if(n < 0)
+			return 1;
+		p = subpt(frptofchar(&w->tag, n), w->tag.r.min);
+		return 1 + p.y / w->tag.font->height;
+	}
+		
+	/* can't use more than we have */
+	if(w->tag.nlines >= w->tag.maxlines)
+		return w->tag.maxlines;
+
+	/* if tag ends with \n, include empty line at end for typing */
+	n = w->tag.nlines;
+	if(w->tag.file->nc > 0){
+		bufread(w->tag.file, w->tag.file->nc-1, &rune, 1);
+		if(rune == '\n')
+			n++;
+	}
+	if(n == 0)
+		n = 1;
+	return n;
+}
+
+int
+winresize(Window *w, Rectangle r, int safe)
+{
+	int oy, mouseintag, mouseinbody;
+	Point p;
+	Rectangle r1;
+	int y;
+	Image *b;
+	Rectangle br;
+
+	mouseintag = ptinrect(mouse->xy, w->tag.all);
+	mouseinbody = ptinrect(mouse->xy, w->body.all);
+
+	w->tagtop = r;
+	w->tagtop.max.y = r.min.y+font->height;
+
+	r1 = r;
+	r1.max.y = r1.min.y + font->height;
+	r1.max.y = min(r.max.y, r1.min.y + w->taglines*font->height);
+
+	if(!safe || !w->tagsafe || !eqrect(w->tag.all, r1)){
+		w->taglines = wintaglines(w, r);
+		r1.max.y = min(r.max.y, r1.min.y + w->taglines*font->height);
+	}
+	if(Dy(r1) < font->height)
+		r1.max.y = r1.min.y+font->height;
+	y = r1.max.y;
+	if(!safe || !eqrect(w->tag.r, r1)){
+		textresize(&w->tag, r1);
+		y = w->tag.r.max.y;
+		b = button;
+		if(w->body.file->mod && !w->isdir && !w->isscratch)
+			b = modbutton;
+		br.min = w->tag.scrollr.min;
+		br.max.x = br.min.x + Dx(b->r);
+		br.max.y = br.min.y + Dy(b->r);
+		draw(screen, br, b, nil, b->r.min);
+
+		w->tagsafe = TRUE;
+
+		/* If mouse is in tag, pull up as tag closes. */
+		if(mouseintag && !ptinrect(mouse->xy, w->tag.all)){
+			p = mouse->xy;
+			p.y = w->tag.all.max.y-3;
+			moveto(mousectl, p);
+		}
+
+		/* If mouse is in body, push down as tag expands. */
+		if(mouseinbody && ptinrect(mouse->xy, w->tag.all)){
+			p = mouse->xy;
+			p.y = w->tag.all.max.y+3;
+			moveto(mousectl, p);
+		}
+
+	}
+	if(!safe || !eqrect(w->body.r, r1)){
+		oy = y;
+		if(y+1+w->body.font->height <= r.max.y){	/* room for one line */
+  			r1.min.y = y;
+			r1.max.y = y+1;
+			draw(screen, r1, tagcols[BORD], nil, ZP);
+			y++;
+			r1.min.y = min(y, r.max.y);
+			r1.max.y = r.max.y;
+		}else{
+			r1.min.y = y;
+			r1.max.y = y;
+		}
+		y = textresize(&w->body, r1);
+		w->r = r;
+		w->r.max.y = y;
+		textscrdraw(&w->body);
+		w->body.all.min.y = oy;
+	}
+	w->maxlines = min(w->body.nlines, max(w->maxlines, w->body.maxlines));
+	return w->r.max.y;
+}
+
+void
+winlock1(Window *w, int owner)
+{
+	incref(w);
+	qlock(w);
+	w->owner = owner;
+}
+
+void
+winlock(Window *w, int owner)
+{
+	int i;
+	File *f;
+
+	f = w->body.file;
+	for(i=0; i<f->ntext; i++)
+		winlock1(f->text[i]->w, owner);
+}
+
+void
+winunlock(Window *w)
+{
+	int i;
+	File *f;
+
+	/*
+	 * subtle: loop runs backwards to avoid tripping over
+	 * winclose indirectly editing f->text and freeing f
+	 * on the last iteration of the loop.
+	 */
+	f = w->body.file;
+	for(i=f->ntext-1; i>=0; i--){
+		w = f->text[i]->w;
+		w->owner = 0;
+		qunlock(w);
+		winclose(w);
+	}
+}
+
+void
+winmousebut(Window *w)
+{
+	moveto(mousectl, addpt(w->tag.scrollr.min, divpt(Pt(Dx(w->tag.scrollr), font->height), 2)));
+}
+
+void
+windirfree(Window *w)
+{
+	int i;
+	Dirlist *dl;
+
+	if(w->isdir){
+		for(i=0; i<w->ndl; i++){
+			dl = w->dlp[i];
+			free(dl->r);
+			free(dl);
+		}
+		free(w->dlp);
+	}
+	w->dlp = nil;
+	w->ndl = 0;
+}
+
+void
+winclose(Window *w)
+{
+	int i;
+
+	if(decref(w) == 0){
+		xfidlog(w, "del");
+		windirfree(w);
+		textclose(&w->tag);
+		textclose(&w->body);
+		if(activewin == w)
+			activewin = nil;
+		for(i=0; i<w->nincl; i++)
+			free(w->incl[i]);
+		free(w->incl);
+		free(w->events);
+		free(w);
+	}
+}
+
+void
+windelete(Window *w)
+{
+	Xfid *x;
+
+	x = w->eventx;
+	if(x){
+		w->nevents = 0;
+		free(w->events);
+		w->events = nil;
+		w->eventx = nil;
+		sendp(x->c, nil);	/* wake him up */
+	}
+}
+
+void
+winundo(Window *w, int isundo)
+{
+	Text *body;
+	int i;
+	File *f;
+	Window *v;
+
+	w->utflastqid = -1;
+	body = &w->body;
+	fileundo(body->file, isundo, &body->q0, &body->q1);
+	textshow(body, body->q0, body->q1, 1);
+	f = body->file;
+	for(i=0; i<f->ntext; i++){
+		v = f->text[i]->w;
+		v->dirty = (f->seq != v->putseq);
+		if(v != w){
+			v->body.q0 = v->body.p0+v->body.org;
+			v->body.q1 = v->body.p1+v->body.org;
+		}
+	}
+	winsettag(w);
+}
+
+void
+winsetname(Window *w, Rune *name, int n)
+{
+	Text *t;
+	Window *v;
+	int i;
+
+	t = &w->body;
+	if(runeeq(t->file->name, t->file->nname, name, n) == TRUE)
+		return;
+	w->isscratch = FALSE;
+	if(n>=6 && runeeq(L"/guide", 6, name+(n-6), 6))
+		w->isscratch = TRUE;
+	else if(n>=7 && runeeq(L"+Errors", 7, name+(n-7), 7))
+		w->isscratch = TRUE;
+	filesetname(t->file, name, n);
+	for(i=0; i<t->file->ntext; i++){
+		v = t->file->text[i]->w;
+		winsettag(v);
+		v->isscratch = w->isscratch;
+	}
+}
+
+void
+wintype(Window *w, Text *t, Rune r)
+{
+	int i;
+
+	texttype(t, r);
+	if(t->what == Tag)
+		w->tagsafe = FALSE;
+	if(t->what == Body)
+		for(i=0; i<t->file->ntext; i++)
+			textscrdraw(t->file->text[i]);
+	winsettag(w);
+}
+
+void
+wincleartag(Window *w)
+{
+	int i, n;
+	Rune *r;
+
+	/* w must be committed */
+	n = w->tag.file->nc;
+	r = runemalloc(n);
+	bufread(w->tag.file, 0, r, n);
+	for(i=0; i<n; i++)
+		if(r[i]==' ' || r[i]=='\t')
+			break;
+	for(; i<n; i++)
+		if(r[i] == '|')
+			break;
+	if(i == n)
+		return;
+	i++;
+	textdelete(&w->tag, i, n, TRUE);
+	free(r);
+	w->tag.file->mod = FALSE;
+	if(w->tag.q0 > i)
+		w->tag.q0 = i;
+	if(w->tag.q1 > i)
+		w->tag.q1 = i;
+	textsetselect(&w->tag, w->tag.q0, w->tag.q1);
+}
+
+void
+winsettag1(Window *w)
+{
+	int i, j, k, n, bar, dirty;
+	Rune *new, *old, *r;
+	Image *b;
+	uint q0, q1;
+	Rectangle br;
+
+	/* there are races that get us here with stuff in the tag cache, so we take extra care to sync it */
+	if(w->tag.ncache!=0 || w->tag.file->mod)
+		wincommit(w, &w->tag);	/* check file name; also guarantees we can modify tag contents */
+	old = runemalloc(w->tag.file->nc+1);
+	bufread(w->tag.file, 0, old, w->tag.file->nc);
+	old[w->tag.file->nc] = '\0';
+	for(i=0; i<w->tag.file->nc; i++)
+		if(old[i]==' ' || old[i]=='\t')
+			break;
+	if(runeeq(old, i, w->body.file->name, w->body.file->nname) == FALSE){
+		textdelete(&w->tag, 0, i, TRUE);
+		textinsert(&w->tag, 0, w->body.file->name, w->body.file->nname, TRUE);
+		free(old);
+		old = runemalloc(w->tag.file->nc+1);
+		bufread(w->tag.file, 0, old, w->tag.file->nc);
+		old[w->tag.file->nc] = '\0';
+		w->tagsafe = FALSE;
+	}
+	new = runemalloc(w->body.file->nname+100);
+	i = 0;
+	runemove(new+i, w->body.file->name, w->body.file->nname);
+	i += w->body.file->nname;
+	runemove(new+i, L" Del Snarf", 10);
+	i += 10;
+	if(w->filemenu){
+		if(w->body.file->delta.nc>0 || w->body.ncache){
+			runemove(new+i, L" Undo", 5);
+			i += 5;
+		}
+		if(w->body.file->epsilon.nc > 0){
+			runemove(new+i, L" Redo", 5);
+			i += 5;
+		}
+		dirty = w->body.file->nname && (w->body.ncache || w->body.file->seq!=w->putseq);
+		if(!w->isdir && dirty){
+			runemove(new+i, L" Put", 4);
+			i += 4;
+		}
+	}
+	if(w->isdir){
+		runemove(new+i, L" Get", 4);
+		i += 4;
+	}
+	runemove(new+i, L" |", 2);
+	i += 2;
+	r = runestrchr(old, '|');
+	if(r)
+		k = r-old+1;
+	else{
+		k = w->tag.file->nc;
+		if(w->body.file->seq == 0){
+			runemove(new+i, L" Look ", 6);
+			i += 6;
+		}
+	}
+
+	new[i] = 0;
+	/* replace tag if the new one is different */
+	if(runeeq(new, i, old, k) == FALSE){
+		n = k;
+		if(n > i)
+			n = i;
+		for(j=0; j<n; j++)
+			if(old[j] != new[j])
+				break;
+		q0 = w->tag.q0;
+		q1 = w->tag.q1;
+		textdelete(&w->tag, j, k, TRUE);
+		textinsert(&w->tag, j, new+j, i-j, TRUE);
+		/* try to preserve user selection */
+		r = runestrchr(old, '|');
+		if(r){
+			bar = r-old;
+			if(q0 > bar){
+				bar = (runestrchr(new, '|')-new)-bar;
+				w->tag.q0 = q0+bar;
+				w->tag.q1 = q1+bar;
+			}
+		}
+		w->tagsafe = FALSE;
+	}
+	free(old);
+	free(new);
+	w->tag.file->mod = FALSE;
+	n = w->tag.file->nc+w->tag.ncache;
+	if(w->tag.q0 > n)
+		w->tag.q0 = n;
+	if(w->tag.q1 > n)
+		w->tag.q1 = n;
+	textsetselect(&w->tag, w->tag.q0, w->tag.q1);
+	b = button;
+	if(!w->isdir && !w->isscratch && (w->body.file->mod || w->body.ncache))
+		b = modbutton;
+	br.min = w->tag.scrollr.min;
+	br.max.x = br.min.x + Dx(b->r);
+	br.max.y = br.min.y + Dy(b->r);
+	draw(screen, br, b, nil, b->r.min);
+	if(w->tagsafe == FALSE)
+		winresize(w, w->r, TRUE);
+}
+
+void
+winsettag(Window *w)
+{
+	int i;
+	File *f;
+	Window *v;
+
+	f = w->body.file;
+	for(i=0; i<f->ntext; i++){
+		v = f->text[i]->w;
+		if(v->col->safe || v->body.maxlines>0)
+			winsettag1(v);
+	}
+}
+
+void
+wincommit(Window *w, Text *t)
+{
+	Rune *r;
+	int i;
+	File *f;
+
+	textcommit(t, TRUE);
+	f = t->file;
+	if(f->ntext > 1)
+		for(i=0; i<f->ntext; i++)
+			textcommit(f->text[i], FALSE);	/* no-op for t */
+	if(t->what == Body)
+		return;
+	r = runemalloc(w->tag.file->nc);
+	bufread(w->tag.file, 0, r, w->tag.file->nc);
+	for(i=0; i<w->tag.file->nc; i++)
+		if(r[i]==' ' || r[i]=='\t')
+			break;
+	if(runeeq(r, i, w->body.file->name, w->body.file->nname) == FALSE){
+		seq++;
+		filemark(w->body.file);
+		w->body.file->mod = TRUE;
+		w->dirty = TRUE;
+		winsetname(w, r, i);
+		winsettag(w);
+	}
+	free(r);
+}
+
+void
+winaddincl(Window *w, Rune *r, int n)
+{
+	char *a;
+	Dir *d;
+	Runestr rs;
+
+	a = runetobyte(r, n);
+	d = dirstat(a);
+	if(d == nil){
+		if(a[0] == '/')
+			goto Rescue;
+		rs = dirname(&w->body, r, n);
+		r = rs.r;
+		n = rs.nr;
+		free(a);
+		a = runetobyte(r, n);
+		d = dirstat(a);
+		if(d == nil)
+			goto Rescue;
+		r = runerealloc(r, n+1);
+		r[n] = 0;
+	}
+	if((d->qid.type&QTDIR) == 0){
+		free(d);
+		warning(nil, "%s: not a directory\n", a);
+		free(r);
+		free(a);
+		return;
+	}
+	free(a);
+	free(d);
+	w->nincl++;
+	w->incl = realloc(w->incl, w->nincl*sizeof(Rune*));
+	memmove(w->incl+1, w->incl, (w->nincl-1)*sizeof(Rune*));
+	w->incl[0] = runemalloc(n+1);
+	runemove(w->incl[0], r, n);
+	free(r);
+	return;
+
+Rescue:
+	warning(nil, "%s: %r\n", a);
+	free(r);
+	free(a);
+	return;
+}
+
+int
+winclean(Window *w, int conservative)
+{
+	if(w->isscratch || w->isdir)	/* don't whine if it's a guide file, error window, etc. */
+		return TRUE;
+	if(!conservative && w->nopen[QWevent]>0)
+		return TRUE;
+	if(w->dirty){
+		if(w->body.file->nname)
+			warning(nil, "%.*S modified\n", w->body.file->nname, w->body.file->name);
+		else{
+			if(w->body.file->nc < 100)	/* don't whine if it's too small */
+				return TRUE;
+			warning(nil, "unnamed file modified\n");
+		}
+		w->dirty = FALSE;
+		return FALSE;
+	}
+	return TRUE;
+}
+
+char*
+winctlprint(Window *w, char *buf, int fonts)
+{
+	sprint(buf, "%11d %11d %11d %11d %11d ", w->id, w->tag.file->nc,
+		w->body.file->nc, w->isdir, w->dirty);
+	if(fonts)
+		return smprint("%s%11d %q %11d " , buf, Dx(w->body.r), 
+			w->body.reffont->f->name, w->body.maxtab);
+	return buf;
+}
+
+void
+winevent(Window *w, char *fmt, ...)
+{
+	int n;
+	char *b;
+	Xfid *x;
+	va_list arg;
+
+	if(w->nopen[QWevent] == 0)
+		return;
+	if(w->owner == 0)
+		error("no window owner");
+	va_start(arg, fmt);
+	b = vsmprint(fmt, arg);
+	va_end(arg);
+	if(b == nil)
+		error("vsmprint failed");
+	n = strlen(b);
+	w->events = erealloc(w->events, w->nevents+1+n);
+	w->events[w->nevents++] = w->owner;
+	memmove(w->events+w->nevents, b, n);
+	free(b);
+	w->nevents += n;
+	x = w->eventx;
+	if(x){
+		w->eventx = nil;
+		sendp(x->c, nil);
+	}
+}
--- /dev/null
+++ b/xfid.c
@@ -1,0 +1,1082 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <cursor.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <frame.h>
+#include <fcall.h>
+#include <plumb.h>
+#include "dat.h"
+#include "fns.h"
+
+enum
+{
+	Ctlsize	= 5*12
+};
+
+char	Edel[]		= "deleted window";
+char	Ebadctl[]		= "ill-formed control message";
+char	Ebadaddr[]	= "bad address syntax";
+char	Eaddr[]		= "address out of range";
+char	Einuse[]		= "already in use";
+char	Ebadevent[]	= "bad event syntax";
+extern char Eperm[];
+
+static
+void
+clampaddr(Window *w)
+{
+	if(w->addr.q0 < 0)
+		w->addr.q0 = 0;
+	if(w->addr.q1 < 0)
+		w->addr.q1 = 0;
+	if(w->addr.q0 > w->body.file->nc)
+		w->addr.q0 = w->body.file->nc;
+	if(w->addr.q1 > w->body.file->nc)
+		w->addr.q1 = w->body.file->nc;
+}
+
+void
+xfidctl(void *arg)
+{
+	Xfid *x;
+	void (*f)(Xfid*);
+
+	threadsetname("xfidctlthread");
+	x = arg;
+	for(;;){
+		f = recvp(x->c);
+		(*f)(x);
+		flushimage(display, 1);
+		sendp(cxfidfree, x);
+	}
+}
+
+void
+xfidflush(Xfid *x)
+{
+	Fcall fc;
+	int i, j;
+	Window *w;
+	Column *c;
+	Xfid *wx;
+
+	xfidlogflush(x);
+
+	/* search windows for matching tag */
+	qlock(&row);
+	for(j=0; j<row.ncol; j++){
+		c = row.col[j];
+		for(i=0; i<c->nw; i++){
+			w = c->w[i];
+			winlock(w, 'E');
+			wx = w->eventx;
+			if(wx!=nil && wx->tag==x->oldtag){
+				w->eventx = nil;
+				wx->flushed = TRUE;
+				sendp(wx->c, nil);
+				winunlock(w);
+				goto out;
+			}
+			winunlock(w);
+		}
+	}
+out:
+	qunlock(&row);
+	respond(x, &fc, nil);
+}
+
+void
+xfidopen(Xfid *x)
+{
+	Fcall fc;
+	Window *w;
+	Text *t;
+	char *s;
+	Rune *r;
+	int m, n, q, q0, q1;
+
+	w = x->f->w;
+	t = &w->body;
+	q = FILE(x->f->qid);
+	if(w){
+		winlock(w, 'E');
+		switch(q){
+		case QWaddr:
+			if(w->nopen[q]++ == 0){
+				w->addr = (Range){0,0};
+				w->limit = (Range){-1,-1};
+			}
+			break;
+		case QWdata:
+		case QWxdata:
+			w->nopen[q]++;
+			break;
+		case QWevent:
+			if(w->nopen[q]++ == 0){
+				if(!w->isdir && w->col!=nil){
+					w->filemenu = FALSE;
+					winsettag(w);
+				}
+			}
+			break;
+		case QWrdsel:
+			/*
+			 * Use a temporary file.
+			 * A pipe would be the obvious, but we can't afford the
+			 * broken pipe notification.  Using the code to read QWbody
+			 * is n², which should probably also be fixed.  Even then,
+			 * though, we'd need to squirrel away the data in case it's
+			 * modified during the operation, e.g. by |sort
+			 */
+			if(w->rdselfd >= 0){
+				winunlock(w);
+				respond(x, &fc, Einuse);
+				return;
+			}
+			w->rdselfd = tempfile();
+			if(w->rdselfd < 0){
+				winunlock(w);
+				respond(x, &fc, "can't create temp file");
+				return;
+			}
+			w->nopen[q]++;
+			q0 = t->q0;
+			q1 = t->q1;
+			r = fbufalloc();
+			s = fbufalloc();
+			while(q0 < q1){
+				n = q1 - q0;
+				if(n > (BUFSIZE-1)/UTFmax)
+					n = (BUFSIZE-1)/UTFmax;
+				bufread(t->file, q0, r, n);
+				m = snprint(s, BUFSIZE, "%.*S", n, r);
+				if(write(w->rdselfd, s, m) != m){
+					warning(nil, "can't write temp file for pipe command %r\n");
+					break;
+				}
+				q0 += n;
+			}
+			fbuffree(s);
+			fbuffree(r);
+			break;
+		case QWwrsel:
+			w->nopen[q]++;
+			seq++;
+			filemark(t->file);
+			cut(t, t, nil, FALSE, TRUE, nil, 0);
+			w->wrselrange = (Range){t->q1, t->q1};
+			w->nomark = TRUE;
+			break;
+		case QWeditout:
+			if(editing == FALSE){
+				winunlock(w);
+				respond(x, &fc, Eperm);
+				return;
+			}
+			w->wrselrange = (Range){t->q1, t->q1};
+			break;
+		}
+		winunlock(w);
+	}
+	else{
+		switch(q){
+		case Qlog:
+			xfidlogopen(x);
+			break;
+		}
+	}
+	fc.qid = x->f->qid;
+	fc.iounit = messagesize-IOHDRSZ;
+	x->f->open = TRUE;
+	respond(x, &fc, nil);
+}
+
+void
+xfidclose(Xfid *x)
+{
+	Fcall fc;
+	Window *w;
+	int q;
+	Text *t;
+
+	w = x->f->w;
+	x->f->busy = FALSE;
+	if(x->f->open == FALSE){
+		if(w != nil)
+			winclose(w);
+		respond(x, &fc, nil);
+		return;
+	}
+
+	x->f->open = FALSE;
+	if(w){
+		winlock(w, 'E');
+		q = FILE(x->f->qid);
+		switch(q){
+		case QWctl:
+			if(w->ctlfid!=~0 && w->ctlfid==x->f->fid){
+				w->ctlfid = ~0;
+				qunlock(&w->ctllock);
+			}
+			break;
+		case QWdata:
+		case QWxdata:
+			w->nomark = FALSE;
+			/* fall through */
+		case QWaddr:
+		case QWevent:	/* BUG: do we need to shut down Xfid? */
+			if(--w->nopen[q] == 0){
+				if(q == QWdata || q == QWxdata)
+					w->nomark = FALSE;
+				if(q==QWevent && !w->isdir && w->col!=nil){
+					w->filemenu = TRUE;
+					winsettag(w);
+				}
+				if(q == QWevent){
+					free(w->dumpstr);
+					free(w->dumpdir);
+					w->dumpstr = nil;
+					w->dumpdir = nil;
+				}
+			}
+			break;
+		case QWrdsel:
+			close(w->rdselfd);
+			w->rdselfd = -1;
+			break;
+		case QWwrsel:
+			w->nomark = FALSE;
+			t = &w->body;
+			/* before: only did this if !w->noscroll, but that didn't seem right in practice */
+			textshow(t, min(w->wrselrange.q0, t->file->nc),
+				min(w->wrselrange.q1, t->file->nc), 1);
+			textscrdraw(t);
+			break;
+		}
+		winunlock(w);
+		winclose(w);
+	}
+	respond(x, &fc, nil);
+}
+
+void
+xfidread(Xfid *x)
+{
+	Fcall fc;
+	int n, q;
+	uint off;
+	char *b;
+	char buf[256];
+	Window *w;
+
+	q = FILE(x->f->qid);
+	w = x->f->w;
+	if(w == nil){
+		fc.count = 0;
+		switch(q){
+		case Qcons:
+		case Qlabel:
+			break;
+		case Qindex:
+			xfidindexread(x);
+			return;
+		case Qlog:
+			xfidlogread(x);
+			return;
+		default:
+			warning(nil, "unknown qid %d\n", q);
+			break;
+		}
+		respond(x, &fc, nil);
+		return;
+	}
+	winlock(w, 'F');
+	if(w->col == nil){
+		winunlock(w);
+		respond(x, &fc, Edel);
+		return;
+	}
+	off = x->offset;
+	switch(q){
+	case QWaddr:
+		textcommit(&w->body, TRUE);
+		clampaddr(w);
+		sprint(buf, "%11d %11d ", w->addr.q0, w->addr.q1);
+		goto Readbuf;
+
+	case QWbody:
+		xfidutfread(x, &w->body, w->body.file->nc, QWbody);
+		break;
+
+	case QWctl:
+		b = winctlprint(w, buf, 1);
+		goto Readb;
+
+	Readbuf:
+		b = buf;
+	Readb:
+		n = strlen(b);
+		if(off > n)
+			off = n;
+		if(off+x->count > n)
+			x->count = n-off;
+		fc.count = x->count;
+		fc.data = b+off;
+		respond(x, &fc, nil);
+		if(b != buf)
+			free(b);
+		break;
+
+	case QWevent:
+		xfideventread(x, w);
+		break;
+
+	case QWdata:
+		/* BUG: what should happen if q1 > q0? */
+		if(w->addr.q0 > w->body.file->nc){
+			respond(x, &fc, Eaddr);
+			break;
+		}
+		w->addr.q0 += xfidruneread(x, &w->body, w->addr.q0, w->body.file->nc);
+		w->addr.q1 = w->addr.q0;
+		break;
+
+	case QWxdata:
+		/* BUG: what should happen if q1 > q0? */
+		if(w->addr.q0 > w->body.file->nc){
+			respond(x, &fc, Eaddr);
+			break;
+		}
+		w->addr.q0 += xfidruneread(x, &w->body, w->addr.q0, w->addr.q1);
+		break;
+
+	case QWtag:
+		xfidutfread(x, &w->tag, w->tag.file->nc, QWtag);
+		break;
+
+	case QWrdsel:
+		seek(w->rdselfd, off, 0);
+		n = x->count;
+		if(n > BUFSIZE)
+			n = BUFSIZE;
+		b = fbufalloc();
+		n = read(w->rdselfd, b, n);
+		if(n < 0){
+			respond(x, &fc, "I/O error in temp file");
+			break;
+		}
+		fc.count = n;
+		fc.data = b;
+		respond(x, &fc, nil);
+		fbuffree(b);
+		break;
+
+	default:
+		sprint(buf, "unknown qid %d in read", q);
+		respond(x, &fc, nil);
+	}
+	winunlock(w);
+}
+
+static Rune*
+fullrunewrite(Xfid *x, int *inr)
+{
+	int q, cnt, c, nb, nr;
+	Rune *r;
+
+	q = x->f->nrpart;
+	cnt = x->count;
+	if(q > 0){
+		memmove(x->data+q, x->data, cnt);	/* there's room; see fsysproc */
+		memmove(x->data, x->f->rpart, q);
+		cnt += q;
+		x->f->nrpart = 0;
+	}
+	r = runemalloc(cnt);
+	cvttorunes(x->data, cnt-UTFmax, r, &nb, &nr, nil);
+	/* approach end of buffer */
+	while(fullrune(x->data+nb, cnt-nb)){
+		c = nb;
+		nb += chartorune(&r[nr], x->data+c);
+		if(r[nr])
+			nr++;
+	}
+	if(nb < cnt){
+		memmove(x->f->rpart, x->data+nb, cnt-nb);
+		x->f->nrpart = cnt-nb;
+	}
+	*inr = nr;
+	return r;
+}
+
+void
+xfidwrite(Xfid *x)
+{
+	Fcall fc;
+	int c, qid, nb, nr, eval;
+	char buf[64], *err;
+	Window *w;
+	Rune *r;
+	Range a;
+	Text *t;
+	uint q0, tq0, tq1;
+
+	qid = FILE(x->f->qid);
+	w = x->f->w;
+	if(w){
+		c = 'F';
+		if(qid==QWtag || qid==QWbody)
+			c = 'E';
+		winlock(w, c);
+		if(w->col == nil){
+			winunlock(w);
+			respond(x, &fc, Edel);
+			return;
+		}
+	}
+	x->data[x->count] = 0;
+	switch(qid){
+	case Qcons:
+		w = errorwin(x->f->mntdir, 'X');
+		t=&w->body;
+		goto BodyTag;
+
+	case Qlabel:
+		fc.count = x->count;
+		respond(x, &fc, nil);
+		break;
+
+	case QWaddr:
+		x->data[x->count] = 0;
+		r = bytetorune(x->data, &nr);
+		t = &w->body;
+		wincommit(w, t);
+		eval = TRUE;
+		a = address(x->f->mntdir, t, w->limit, w->addr, r, 0, nr, rgetc, &eval, (uint*)&nb);
+		free(r);
+		if(nb < nr){
+			respond(x, &fc, Ebadaddr);
+			break;
+		}
+		if(!eval){
+			respond(x, &fc, Eaddr);
+			break;
+		}
+		w->addr = a;
+		fc.count = x->count;
+		respond(x, &fc, nil);
+		break;
+
+	case Qeditout:
+	case QWeditout:
+		r = fullrunewrite(x, &nr);
+		if(w)
+			err = edittext(w, w->wrselrange.q1, r, nr);
+		else
+			err = edittext(nil, 0, r, nr);
+		free(r);
+		if(err != nil){
+			respond(x, &fc, err);
+			break;
+		}
+		fc.count = x->count;
+		respond(x, &fc, nil);
+		break;
+
+	case QWerrors:
+		w = errorwinforwin(w);
+		t = &w->body;
+		goto BodyTag;
+
+	case QWbody:
+	case QWwrsel:
+		t = &w->body;
+		goto BodyTag;
+
+	case QWctl:
+		xfidctlwrite(x, w);
+		break;
+
+	case QWdata:
+		a = w->addr;
+		t = &w->body;
+		wincommit(w, t);
+		if(a.q0>t->file->nc || a.q1>t->file->nc){
+			respond(x, &fc, Eaddr);
+			break;
+		}
+		r = runemalloc(x->count);
+		cvttorunes(x->data, x->count, r, &nb, &nr, nil);
+		if(w->nomark == FALSE){
+			seq++;
+			filemark(t->file);
+		}
+		q0 = a.q0;
+		if(a.q1 > q0){
+			textdelete(t, q0, a.q1, TRUE);
+			w->addr.q1 = q0;
+		}
+		tq0 = t->q0;
+		tq1 = t->q1;
+		textinsert(t, q0, r, nr, TRUE);
+		if(tq0 >= q0)
+			tq0 += nr;
+		if(tq1 >= q0)
+			tq1 += nr;
+		textsetselect(t, tq0, tq1);
+		if(!t->w->noscroll)
+			textshow(t, q0, q0+nr, 0);
+		textscrdraw(t);
+		winsettag(w);
+		free(r);
+		w->addr.q0 += nr;
+		w->addr.q1 = w->addr.q0;
+		fc.count = x->count;
+		respond(x, &fc, nil);
+		break;
+
+	case QWevent:
+		xfideventwrite(x, w);
+		break;
+
+	case QWtag:
+		t = &w->tag;
+		goto BodyTag;
+
+	BodyTag:
+		r = fullrunewrite(x, &nr);
+		if(nr > 0){
+			wincommit(w, t);
+			if(qid == QWwrsel){
+				q0 = w->wrselrange.q1;
+				if(q0 > t->file->nc)
+					q0 = t->file->nc;
+			}else
+				q0 = t->file->nc;
+			if(qid == QWtag)
+				textinsert(t, q0, r, nr, TRUE);
+			else{
+				if(w->nomark == FALSE){
+					seq++;
+					filemark(t->file);
+				}
+				q0 = textbsinsert(t, q0, r, nr, TRUE, &nr);
+				textsetselect(t, t->q0, t->q1);	/* insert could leave it somewhere else */
+				if(qid!=QWwrsel && !t->w->noscroll)
+					textshow(t, q0+nr, q0+nr, 1);
+				textscrdraw(t);
+			}
+			winsettag(w);
+			if(qid == QWwrsel)
+				w->wrselrange.q1 += nr;
+			free(r);
+		}
+		fc.count = x->count;
+		respond(x, &fc, nil);
+		break;
+
+	default:
+		sprint(buf, "unknown qid %d in write", qid);
+		respond(x, &fc, buf);
+		break;
+	}
+	if(w)
+		winunlock(w);
+}
+
+void
+xfidctlwrite(Xfid *x, Window *w)
+{
+	Fcall fc;
+	int i, m, n, nb, nr, nulls;
+	Rune *r;
+	char *err, *p, *pp, *q, *e;
+	int scrdraw, settag;
+	Text *t;
+
+	err = nil;
+	e = x->data+x->count;
+	scrdraw = FALSE;
+	settag = FALSE;
+	r = emalloc(x->count*UTFmax+1);
+	x->data[x->count] = 0;
+	textcommit(&w->tag, TRUE);
+	for(n=0; n<x->count; n+=m){
+		p = x->data+n;
+		if(strncmp(p, "lock", 4) == 0){	/* make window exclusive use */
+			qlock(&w->ctllock);
+			w->ctlfid = x->f->fid;
+			m = 4;
+		}else
+		if(strncmp(p, "unlock", 6) == 0){	/* release exclusive use */
+			w->ctlfid = ~0;
+			qunlock(&w->ctllock);
+			m = 6;
+		}else
+		if(strncmp(p, "clean", 5) == 0){	/* mark window 'clean', seq=0 */
+			t = &w->body;
+			t->eq0 = ~0;
+			filereset(t->file);
+			t->file->mod = FALSE;
+			w->dirty = FALSE;
+			settag = TRUE;
+			m = 5;
+		}else
+		if(strncmp(p, "dirty", 5) == 0){	/* mark window 'dirty' */
+			t = &w->body;
+			/* doesn't change sequence number, so "Put" won't appear.  it shouldn't. */
+			t->file->mod = TRUE;
+			w->dirty = TRUE;
+			settag = TRUE;
+			m = 5;
+		}else
+		if(strncmp(p, "show", 4) == 0){	/* show dot */
+			t = &w->body;
+			textshow(t, t->q0, t->q1, 1);
+			m = 4;
+		}else
+		if(strncmp(p, "name ", 5) == 0){	/* set file name */
+			pp = p+5;
+			m = 5;
+			q = memchr(pp, '\n', e-pp);
+			if(q==nil || q==pp){
+				err = Ebadctl;
+				break;
+			}
+			*q = 0;
+			nulls = FALSE;
+			cvttorunes(pp, q-pp, r, &nb, &nr, &nulls);
+			if(nulls){
+				err = "nulls in file name";
+				break;
+			}
+			for(i=0; i<nr; i++)
+				if(r[i] <= ' '){
+					err = "bad character in file name";
+					goto out;
+				}
+out:
+			seq++;
+			filemark(w->body.file);
+			winsetname(w, r, nr);
+			m += (q+1) - pp;
+		}else
+		if(strncmp(p, "dump ", 5) == 0){	/* set dump string */
+			pp = p+5;
+			m = 5;
+			q = memchr(pp, '\n', e-pp);
+			if(q==nil || q==pp){
+				err = Ebadctl;
+				break;
+			}
+			*q = 0;
+			nulls = FALSE;
+			cvttorunes(pp, q-pp, r, &nb, &nr, &nulls);
+			if(nulls){
+				err = "nulls in dump string";
+				break;
+			}
+			w->dumpstr = runetobyte(r, nr);
+			m += (q+1) - pp;
+		}else
+		if(strncmp(p, "dumpdir ", 8) == 0){	/* set dump directory */
+			pp = p+8;
+			m = 8;
+			q = memchr(pp, '\n', e-pp);
+			if(q==nil || q==pp){
+				err = Ebadctl;
+				break;
+			}
+			*q = 0;
+			nulls = FALSE;
+			cvttorunes(pp, q-pp, r, &nb, &nr, &nulls);
+			if(nulls){
+				err = "nulls in dump directory string";
+				break;
+			}
+			w->dumpdir = runetobyte(r, nr);
+			m += (q+1) - pp;
+		}else
+		if(strncmp(p, "delete", 6) == 0){	/* delete for sure */
+			colclose(w->col, w, TRUE);
+			m = 6;
+		}else
+		if(strncmp(p, "del", 3) == 0){	/* delete, but check dirty */
+			if(!winclean(w, TRUE)){
+				err = "file dirty";
+				break;
+			}
+			colclose(w->col, w, TRUE);
+			m = 3;
+		}else
+		if(strncmp(p, "get", 3) == 0){	/* get file */
+			get(&w->body, nil, nil, FALSE, XXX, nil, 0);
+			m = 3;
+		}else
+		if(strncmp(p, "put", 3) == 0){	/* put file */
+			put(&w->body, nil, nil, XXX, XXX, nil, 0);
+			m = 3;
+		}else
+		if(strncmp(p, "dot=addr", 8) == 0){	/* set dot */
+			textcommit(&w->body, TRUE);
+			clampaddr(w);
+			w->body.q0 = w->addr.q0;
+			w->body.q1 = w->addr.q1;
+			textsetselect(&w->body, w->body.q0, w->body.q1);
+			settag = TRUE;
+			m = 8;
+		}else
+		if(strncmp(p, "addr=dot", 8) == 0){	/* set addr */
+			w->addr.q0 = w->body.q0;
+			w->addr.q1 = w->body.q1;
+			m = 8;
+		}else
+		if(strncmp(p, "limit=addr", 10) == 0){	/* set limit */
+			textcommit(&w->body, TRUE);
+			clampaddr(w);
+			w->limit.q0 = w->addr.q0;
+			w->limit.q1 = w->addr.q1;
+			m = 10;
+		}else
+		if(strncmp(p, "nomark", 6) == 0){	/* turn off automatic marking */
+			w->nomark = TRUE;
+			m = 6;
+		}else
+		if(strncmp(p, "mark", 4) == 0){	/* mark file */
+			seq++;
+			filemark(w->body.file);
+			settag = TRUE;
+			m = 4;
+		}else
+		if(strncmp(p, "nomenu", 6) == 0){	/* turn off automatic menu */
+			w->filemenu = FALSE;
+			m = 6;
+		}else
+		if(strncmp(p, "menu", 4) == 0){	/* enable automatic menu */
+			w->filemenu = TRUE;
+			m = 4;
+		}else
+		if(strncmp(p, "noscroll", 8) == 0){	/* turn off automatic scrolling */
+			w->noscroll = TRUE;
+			m = 8;
+		}else
+		if(strncmp(p, "cleartag", 8) == 0){	/* wipe tag right of bar */
+			wincleartag(w);
+			settag = TRUE;
+			m = 8;
+		}else
+		if(strncmp(p, "scroll", 6) == 0){	/* turn on automatic scrolling (writes to body only) */
+			w->noscroll = FALSE;
+			m = 6;
+		}else
+		if(strncmp(p, "scratch", 7) == 0){ /* mark as a scratch file */
+			w->isscratch = TRUE;
+			m = 7;
+		}else{
+			err = Ebadctl;
+			break;
+		}
+		while(p[m] == '\n')
+			m++;
+	}
+
+	free(r);
+	if(err)
+		n = 0;
+	fc.count = n;
+	respond(x, &fc, err);
+	if(settag)
+		winsettag(w);
+	if(scrdraw)
+		textscrdraw(&w->body);
+}
+
+void
+xfideventwrite(Xfid *x, Window *w)
+{
+	Fcall fc;
+	int m, n;
+	Rune *r;
+	char *err, *p, *q;
+	Text *t;
+	int c;
+	uint q0, q1;
+
+	err = nil;
+ 	r = emalloc(x->count*UTFmax+1);
+	for(n=0; n<x->count; n+=m){
+		p = x->data+n;
+		w->owner = *p++;	/* disgusting */
+		c = *p++;
+		while(*p == ' ')
+			p++;
+		q0 = strtoul(p, &q, 10);
+		if(q == p)
+			goto Rescue;
+		p = q;
+		while(*p == ' ')
+			p++;
+		q1 = strtoul(p, &q, 10);
+		if(q == p)
+			goto Rescue;
+		p = q;
+		while(*p == ' ')
+			p++;
+		if(*p++ != '\n')
+			goto Rescue;
+		m = p-(x->data+n);
+		if('a'<=c && c<='z')
+			t = &w->tag;
+		else if('A'<=c && c<='Z')
+			t = &w->body;
+		else
+			goto Rescue;
+		if(q0>t->file->nc || q1>t->file->nc || q0>q1)
+			goto Rescue;
+
+		qlock(&row);	/* just like mousethread */
+		switch(c){
+		case 'x':
+		case 'X':
+			execute(t, q0, q1, TRUE, nil);
+			break;
+		case 'l':
+		case 'L':
+			look3(t, q0, q1, TRUE);
+			break;
+		default:
+			qunlock(&row);
+			goto Rescue;
+		}
+		qunlock(&row);
+
+	}
+
+    Out:
+	free(r);
+	if(err)
+		n = 0;
+	fc.count = n;
+	respond(x, &fc, err);
+	return;
+
+    Rescue:
+	err = Ebadevent;
+	goto Out;
+}
+
+void
+xfidutfread(Xfid *x, Text *t, uint q1, int qid)
+{
+	Fcall fc;
+	Window *w;
+	Rune *r;
+	char *b, *b1;
+	uint q, off, boff;
+	int m, n, nr, nb;
+
+	w = t->w;
+	wincommit(w, t);
+	off = x->offset;
+	r = fbufalloc();
+	b = fbufalloc();
+	b1 = emalloc(x->count);
+	n = 0;
+	if(qid==w->utflastqid && off>=w->utflastboff && w->utflastq<=q1){
+		boff = w->utflastboff;
+		q = w->utflastq;
+	}else{
+		/* BUG: stupid code: scan from beginning */
+		boff = 0;
+		q = 0;
+	}
+	w->utflastqid = qid;
+	while(q<q1 && n<x->count){
+		/*
+		 * Updating here avoids partial rune problem: we're always on a
+		 * char boundary. The cost is we will usually do one more read
+		 * than we really need, but that's better than being n^2.
+		 */
+		w->utflastboff = boff;
+		w->utflastq = q;
+		nr = q1-q;
+		if(nr > (BUFSIZE-1)/UTFmax)
+			nr = (BUFSIZE-1)/UTFmax;
+		bufread(t->file, q, r, nr);
+		nb = snprint(b, BUFSIZE, "%.*S", nr, r);
+		if(boff >= off){
+			m = nb;
+			if(boff+m > off+x->count)
+				m = off+x->count - boff;
+			memmove(b1+n, b, m);
+			n += m;
+		}else if(boff+nb > off){
+			if(n != 0)
+				error("bad count in utfrune");
+			m = nb - (off-boff);
+			if(m > x->count)
+				m = x->count;
+			memmove(b1, b+(off-boff), m);
+			n += m;
+		}
+		boff += nb;
+		q += nr;
+	}
+	fbuffree(r);
+	fbuffree(b);
+	fc.count = n;
+	fc.data = b1;
+	respond(x, &fc, nil);
+	free(b1);
+}
+
+int
+xfidruneread(Xfid *x, Text *t, uint q0, uint q1)
+{
+	Fcall fc;
+	Window *w;
+	Rune *r, junk;
+	char *b, *b1;
+	uint q, boff;
+	int i, rw, m, n, nr, nb;
+
+	w = t->w;
+	wincommit(w, t);
+	r = fbufalloc();
+	b = fbufalloc();
+	b1 = emalloc(x->count);
+	n = 0;
+	q = q0;
+	boff = 0;
+	while(q<q1 && n<x->count){
+		nr = q1-q;
+		if(nr > (BUFSIZE-1)/UTFmax)
+			nr = (BUFSIZE-1)/UTFmax;
+		bufread(t->file, q, r, nr);
+		nb = snprint(b, BUFSIZE, "%.*S", nr, r);
+		m = nb;
+		if(boff+m > x->count){
+			i = x->count - boff;
+			/* copy whole runes only */
+			m = 0;
+			nr = 0;
+			while(m < i){
+				rw = chartorune(&junk, b+m);
+				if(m+rw > i)
+					break;
+				m += rw;
+				nr++;
+			}
+			if(m == 0)
+				break;
+		}
+		memmove(b1+n, b, m);
+		n += m;
+		boff += nb;
+		q += nr;
+	}
+	fbuffree(r);
+	fbuffree(b);
+	fc.count = n;
+	fc.data = b1;
+	respond(x, &fc, nil);
+	free(b1);
+	return q-q0;
+}
+
+void
+xfideventread(Xfid *x, Window *w)
+{
+	Fcall fc;
+	char *b;
+	int i, n;
+
+	i = 0;
+	x->flushed = FALSE;
+	while(w->nevents == 0){
+		if(i){
+			if(!x->flushed)
+				respond(x, &fc, "window shut down");
+			return;
+		}
+		w->eventx = x;
+		winunlock(w);
+		recvp(x->c);
+		winlock(w, 'F');
+		i++;
+	}
+
+	n = w->nevents;
+	if(n > x->count)
+		n = x->count;
+	fc.count = n;
+	fc.data = w->events;
+	respond(x, &fc, nil);
+	b = w->events;
+	w->events = estrdup(w->events+n);
+	free(b);
+	w->nevents -= n;
+}
+
+void
+xfidindexread(Xfid *x)
+{
+	Fcall fc;
+	int i, j, m, n, nmax, isbuf, cnt, off;
+	Window *w;
+	char *b;
+	Rune *r;
+	Column *c;
+
+	qlock(&row);
+	nmax = 0;
+	for(j=0; j<row.ncol; j++){
+		c = row.col[j];
+		for(i=0; i<c->nw; i++){
+			w = c->w[i];
+			nmax += Ctlsize + w->tag.file->nc*UTFmax + 1;
+		}
+	}
+	nmax++;
+	isbuf = (nmax<=RBUFSIZE);
+	if(isbuf)
+		b = (char*)x->buf;
+	else
+		b = emalloc(nmax);
+	r = fbufalloc();
+	n = 0;
+	for(j=0; j<row.ncol; j++){
+		c = row.col[j];
+		for(i=0; i<c->nw; i++){
+			w = c->w[i];
+			/* only show the currently active window of a set */
+			if(w->body.file->curtext != &w->body)
+				continue;
+			winctlprint(w, b+n, 0);
+			n += Ctlsize;
+			m = min(RBUFSIZE, w->tag.file->nc);
+			bufread(w->tag.file, 0, r, m);
+			m = n + snprint(b+n, nmax-n-1, "%.*S", m, r);
+			while(n<m && b[n]!='\n')
+				n++;
+			b[n++] = '\n';
+		}
+	}
+	qunlock(&row);
+	off = x->offset;
+	cnt = x->count;
+	if(off > n)
+		off = n;
+	if(off+cnt > n)
+		cnt = n-off;
+	fc.count = cnt;
+	memmove(r, b+off, cnt);
+	fc.data = (char*)r;
+	if(!isbuf)
+		free(b);
+	respond(x, &fc, nil);
+	fbuffree(r);
+}