shithub: purgatorio

Download patch

ref: a600cca5ff367ca7e7c64a14a8e87ba376780f6d
parent: d990c25d5795b16c181e875bf2f55aa06c2f75f9
author: henesy <devnull@localhost>
date: Sun Nov 4 12:16:24 EST 2018

init 3

diff: cannot open b/appl/acme/acme/acid/src//null: file does not exist: 'b/appl/acme/acme/acid/src//null' diff: cannot open b/appl/acme/acme/acid//null: file does not exist: 'b/appl/acme/acme/acid//null' diff: cannot open b/appl/acme/acme/bin/src//null: file does not exist: 'b/appl/acme/acme/bin/src//null' diff: cannot open b/appl/acme/acme/bin//null: file does not exist: 'b/appl/acme/acme/bin//null' diff: cannot open b/appl/acme/acme/edit/src//null: file does not exist: 'b/appl/acme/acme/edit/src//null' diff: cannot open b/appl/acme/acme/edit//null: file does not exist: 'b/appl/acme/acme/edit//null' diff: cannot open b/appl/acme/acme/mail/src//null: file does not exist: 'b/appl/acme/acme/mail/src//null' diff: cannot open b/appl/acme/acme/mail//null: file does not exist: 'b/appl/acme/acme/mail//null' diff: cannot open b/appl/acme/acme//null: file does not exist: 'b/appl/acme/acme//null' diff: cannot open b/appl/acme//null: file does not exist: 'b/appl/acme//null' diff: cannot open b/appl/alphabet/abc//null: file does not exist: 'b/appl/alphabet/abc//null' diff: cannot open b/appl/alphabet/auxi//null: file does not exist: 'b/appl/alphabet/auxi//null' diff: cannot open b/appl/alphabet/fs//null: file does not exist: 'b/appl/alphabet/fs//null' diff: cannot open b/appl/alphabet/grid//null: file does not exist: 'b/appl/alphabet/grid//null' diff: cannot open b/appl/alphabet/main//null: file does not exist: 'b/appl/alphabet/main//null' diff: cannot open b/appl/alphabet/typesets//null: file does not exist: 'b/appl/alphabet/typesets//null' diff: cannot open b/appl/alphabet//null: file does not exist: 'b/appl/alphabet//null' diff: cannot open b/appl/charon//null: file does not exist: 'b/appl/charon//null' diff: cannot open b/appl/cmd/asm//null: file does not exist: 'b/appl/cmd/asm//null' diff: cannot open b/appl/cmd/auth/factotum/proto//null: file does not exist: 'b/appl/cmd/auth/factotum/proto//null' diff: cannot open b/appl/cmd/auth/factotum//null: file does not exist: 'b/appl/cmd/auth/factotum//null' diff: cannot open b/appl/cmd/auth//null: file does not exist: 'b/appl/cmd/auth//null' diff: cannot open b/appl/cmd/auxi//null: file does not exist: 'b/appl/cmd/auxi//null' diff: cannot open b/appl/cmd/avr//null: file does not exist: 'b/appl/cmd/avr//null' diff: cannot open b/appl/cmd/dbm//null: file does not exist: 'b/appl/cmd/dbm//null' diff: cannot open b/appl/cmd/fs//null: file does not exist: 'b/appl/cmd/fs//null' diff: cannot open b/appl/cmd/install//null: file does not exist: 'b/appl/cmd/install//null' diff: cannot open b/appl/cmd/ip/nppp//null: file does not exist: 'b/appl/cmd/ip/nppp//null' diff: cannot open b/appl/cmd/ip/ppp//null: file does not exist: 'b/appl/cmd/ip/ppp//null' diff: cannot open b/appl/cmd/ip//null: file does not exist: 'b/appl/cmd/ip//null' diff: cannot open b/appl/cmd/lego//null: file does not exist: 'b/appl/cmd/lego//null' diff: cannot open b/appl/cmd/limbo//null: file does not exist: 'b/appl/cmd/limbo//null' diff: cannot open b/appl/cmd/mash//null: file does not exist: 'b/appl/cmd/mash//null' diff: cannot open b/appl/cmd/mk//null: file does not exist: 'b/appl/cmd/mk//null' diff: cannot open b/appl/cmd/mpc//null: file does not exist: 'b/appl/cmd/mpc//null' diff: cannot open b/appl/cmd/ndb//null: file does not exist: 'b/appl/cmd/ndb//null' diff: cannot open b/appl/cmd/palm//null: file does not exist: 'b/appl/cmd/palm//null' diff: cannot open b/appl/cmd/sh/doc//null: file does not exist: 'b/appl/cmd/sh/doc//null' diff: cannot open b/appl/cmd/sh//null: file does not exist: 'b/appl/cmd/sh//null' diff: cannot open b/appl/cmd/spki//null: file does not exist: 'b/appl/cmd/spki//null' diff: cannot open b/appl/cmd/usb//null: file does not exist: 'b/appl/cmd/usb//null' diff: cannot open b/appl/cmd//null: file does not exist: 'b/appl/cmd//null' diff: cannot open b/appl/collab/clients//null: file does not exist: 'b/appl/collab/clients//null' diff: cannot open b/appl/collab/lib//null: file does not exist: 'b/appl/collab/lib//null' diff: cannot open b/appl/collab/servers//null: file does not exist: 'b/appl/collab/servers//null' diff: cannot open b/appl/collab//null: file does not exist: 'b/appl/collab//null' diff: cannot open b/appl/demo/camera//null: file does not exist: 'b/appl/demo/camera//null' diff: cannot open b/appl/demo/chat//null: file does not exist: 'b/appl/demo/chat//null' diff: cannot open b/appl/demo/cpupool//null: file does not exist: 'b/appl/demo/cpupool//null' diff: cannot open b/appl/demo/lego//null: file does not exist: 'b/appl/demo/lego//null' diff: cannot open b/appl/demo/ns//null: file does not exist: 'b/appl/demo/ns//null' diff: cannot open b/appl/demo/odbc//null: file does not exist: 'b/appl/demo/odbc//null' diff: cannot open b/appl/demo/spree//null: file does not exist: 'b/appl/demo/spree//null' diff: cannot open b/appl/demo/whiteboard//null: file does not exist: 'b/appl/demo/whiteboard//null' diff: cannot open b/appl/demo//null: file does not exist: 'b/appl/demo//null' diff: cannot open b/appl/ebook/dtd//null: file does not exist: 'b/appl/ebook/dtd//null' diff: cannot open b/appl/ebook//null: file does not exist: 'b/appl/ebook//null' diff: cannot open b/appl/examples/minitel//null: file does not exist: 'b/appl/examples/minitel//null' diff: cannot open b/appl/examples//null: file does not exist: 'b/appl/examples//null' diff: cannot open b/appl/grid/demo//null: file does not exist: 'b/appl/grid/demo//null' diff: cannot open b/appl/grid/lib//null: file does not exist: 'b/appl/grid/lib//null' diff: cannot open b/appl/grid//null: file does not exist: 'b/appl/grid//null' diff: cannot open b/appl/lib/convcs//null: file does not exist: 'b/appl/lib/convcs//null' diff: cannot open b/appl/lib/crypt//null: file does not exist: 'b/appl/lib/crypt//null' diff: cannot open b/appl/lib/ecmascript//null: file does not exist: 'b/appl/lib/ecmascript//null' diff: cannot open b/appl/lib/encoding//null: file does not exist: 'b/appl/lib/encoding//null' diff: cannot open b/appl/lib/ida//null: file does not exist: 'b/appl/lib/ida//null' diff: cannot open b/appl/lib/print//null: file does not exist: 'b/appl/lib/print//null' diff: cannot open b/appl/lib/spki//null: file does not exist: 'b/appl/lib/spki//null' diff: cannot open b/appl/lib/strokes//null: file does not exist: 'b/appl/lib/strokes//null' diff: cannot open b/appl/lib/styxconv//null: file does not exist: 'b/appl/lib/styxconv//null' diff: cannot open b/appl/lib/usb//null: file does not exist: 'b/appl/lib/usb//null' diff: cannot open b/appl/lib/w3c//null: file does not exist: 'b/appl/lib/w3c//null' diff: cannot open b/appl/lib//null: file does not exist: 'b/appl/lib//null' diff: cannot open b/appl/math//null: file does not exist: 'b/appl/math//null' diff: cannot open b/appl/spree/clients//null: file does not exist: 'b/appl/spree/clients//null' diff: cannot open b/appl/spree/engines//null: file does not exist: 'b/appl/spree/engines//null' diff: cannot open b/appl/spree/lib//null: file does not exist: 'b/appl/spree/lib//null' diff: cannot open b/appl/spree/other//null: file does not exist: 'b/appl/spree/other//null' diff: cannot open b/appl/spree//null: file does not exist: 'b/appl/spree//null' diff: cannot open b/appl/svc/httpd//null: file does not exist: 'b/appl/svc/httpd//null' diff: cannot open b/appl/svc/webget//null: file does not exist: 'b/appl/svc/webget//null' diff: cannot open b/appl/svc//null: file does not exist: 'b/appl/svc//null' diff: cannot open b/appl/tiny//null: file does not exist: 'b/appl/tiny//null' diff: cannot open b/appl/wm/brutus//null: file does not exist: 'b/appl/wm/brutus//null' diff: cannot open b/appl/wm/drawmux//null: file does not exist: 'b/appl/wm/drawmux//null' diff: cannot open b/appl/wm/ftree//null: file does not exist: 'b/appl/wm/ftree//null' diff: cannot open b/appl/wm/mpeg//null: file does not exist: 'b/appl/wm/mpeg//null' diff: cannot open b/appl/wm//null: file does not exist: 'b/appl/wm//null' diff: cannot open b/appl//null: file does not exist: 'b/appl//null'
--- /dev/null
+++ b/appl/NOTICE
@@ -1,0 +1,25 @@
+This copyright NOTICE applies to all files in this directory and
+subdirectories, unless another copyright notice appears in a given
+file or subdirectory.  If you take substantial code from this software to use in
+other programs, you must somehow include with it an appropriate
+copyright notice that includes the copyright notice and the other
+notices below.  It is fine (and often tidier) to do that in a separate
+file such as NOTICE, LICENCE or COPYING.
+
+Copyright © 1995-1999 Lucent Technologies Inc.
+Portions Copyright © 1997-2000 Vita Nuova Limited
+Portions Copyright © 2000-2010 Vita Nuova Holdings Limited
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
--- /dev/null
+++ b/appl/acme/acme.b
@@ -1,0 +1,1125 @@
+implement Acme;
+
+include "common.m";
+
+sys : Sys;
+bufio : Bufio;
+workdir : Workdir;
+drawm : Draw;
+styx : Styx;
+acme : Acme;
+gui : Gui;
+graph : Graph;
+dat : Dat;
+framem : Framem;
+utils : Utils;
+regx : Regx;
+scrl : Scroll;
+textm : Textm;
+filem : Filem;
+windowm : Windowm;
+rowm : Rowm;
+columnm : Columnm;
+bufferm : Bufferm;
+diskm : Diskm;
+exec : Exec;
+look : Look;
+timerm : Timerm;
+fsys : Fsys;
+xfidm : Xfidm;
+plumbmsg : Plumbmsg;
+editm: Edit;
+editlog: Editlog;
+editcmd: Editcmd;
+styxaux: Styxaux;
+
+sprint : import sys;
+BACK, HIGH, BORD, TEXT, HTEXT, NCOL : import Framem;
+Point, Rect, Font, Image, Display, Pointer: import drawm;
+TRUE, FALSE, maxtab : import dat;
+Ref, Reffont, Command, Timer, Lock, Cursor : import dat;
+row, reffont, activecol, mouse, typetext, mousetext, barttext, argtext, seltext, button, modbutton, colbutton, arrowcursor, boxcursor, plumbed : import dat;
+Xfid : import xfidm;
+cmouse, ckeyboard, cwait, ccommand, ckill, cxfidalloc, cxfidfree, cerr, cplumb, cedit : import dat;
+font, bflush, balloc, draw : import graph;
+Arg, PNPROC, PNGROUP : import utils;
+arginit, argopt, argf, error, warning, postnote : import utils;
+yellow, green, red, blue, black, white, mainwin, display : import gui;
+Disk : import diskm;
+Row : import rowm;
+Column : import columnm;
+Window : import windowm;
+Text, Tag, Body, Columntag : import textm;
+Buffer : import bufferm;
+snarfbuf : import exec;
+Msg : import plumbmsg;
+
+tfd : ref Sys->FD;
+lasttime : int;
+
+init(ctxt : ref Draw->Context, argl : list of string)
+{
+	acmectxt = ctxt;
+
+	sys = load Sys Sys->PATH;
+	sys->pctl(Sys->NEWPGRP, nil);
+
+	{
+		# tfd = sys->create("./time", Sys->OWRITE, 8r600);
+		# lasttime = sys->millisec();
+		bufio = load Bufio Bufio->PATH;
+		workdir = load Workdir Workdir->PATH;
+		drawm = load Draw Draw->PATH;
+	
+		styx = load Styx Styx->PATH;
+	
+		acme = load Acme SELF;
+	
+		gui = load Gui path(Gui->PATH);
+		graph = load Graph path(Graph->PATH);
+		dat = load Dat path(Dat->PATH);
+		framem = load Framem path(Framem->PATH);
+		utils = load Utils path(Utils->PATH);
+		regx = load Regx path(Regx->PATH);
+		scrl = load Scroll path(Scroll->PATH);
+		textm = load Textm path(Textm->PATH);
+		filem = load Filem path(Filem->PATH);
+		windowm = load Windowm path(Windowm->PATH);
+		rowm = load Rowm path(Rowm->PATH);
+		columnm = load Columnm path(Columnm->PATH);
+		bufferm = load Bufferm path(Bufferm->PATH);
+		diskm = load Diskm path(Diskm->PATH);
+		exec = load Exec path(Exec->PATH);
+		look = load Look path(Look->PATH);
+		timerm = load Timerm path(Timerm->PATH);
+		fsys = load Fsys path(Fsys->PATH);
+		xfidm = load Xfidm path(Xfidm->PATH);
+		plumbmsg = load Plumbmsg Plumbmsg->PATH;
+		editm = load Edit path(Edit->PATH);
+		editlog = load Editlog path(Editlog->PATH);
+		editcmd = load Editcmd path(Editcmd->PATH);
+		styxaux = load Styxaux path(Styxaux->PATH);
+		
+		mods := ref Dat->Mods(sys, bufio, drawm, styx, styxaux,
+						acme, gui, graph, dat, framem,
+						utils, regx, scrl,
+						textm, filem, windowm, rowm, columnm,
+						bufferm, diskm, exec, look, timerm,
+						fsys, xfidm, plumbmsg, editm, editlog, editcmd);
+	
+		styx->init();
+		styxaux->init();
+	
+		utils->init(mods);
+		gui->init(mods);
+		graph->init(mods);
+		dat->init(mods);
+		framem->init(mods);
+		regx->init(mods);
+		scrl->init(mods);
+		textm->init(mods);
+		filem->init(mods);
+		windowm->init(mods);
+		rowm->init(mods);
+		columnm->init(mods);
+		bufferm->init(mods);
+		diskm->init(mods);
+		exec->init(mods);
+		look->init(mods);
+		timerm->init(mods);
+		fsys->init(mods);
+		xfidm->init(mods);
+		editm->init(mods);
+		editlog->init(mods);
+		editcmd->init(mods);
+	
+		utils->debuginit();
+	
+		if (plumbmsg->init(1, "edit", Dat->PLUMBSIZE) >= 0)
+			plumbed = 1;
+	
+		main(argl);
+	
+	}
+#	exception{
+#		* =>
+#			sys->fprint(sys->fildes(2), "acme: fatal: %s\n", utils->getexc());
+#			sys->print("acme: fatal: %s\n", utils->getexc());
+#			shutdown("error");
+#	}
+}
+
+timing(s : string)
+{
+	thistime := sys->millisec();
+	sys->fprint(tfd, "%s	%d\n", s, thistime-lasttime);
+	lasttime = thistime;
+}
+
+path(p : string) : string
+{
+	if (RELEASECOPY)
+		return p;
+	else {
+		# inlined strrchr since not loaded yet
+		 for (n := len p - 1; n >= 0; n--)
+			if (p[n] == '/')
+				break;
+		 if (n >= 0)
+			p = p[n+1:];
+		 return "/usr/jrf/acme/" + p;
+	}
+}
+
+waitpid0, waitpid1 : int;
+mainpid : int;
+
+fontcache : array of ref Reffont;
+nfontcache : int;
+reffonts : array of ref Reffont;
+deffontnames := array[2] of {
+	"/fonts/lucidasans/euro.8.font",
+	"/fonts/lucm/unicode.9.font",
+};
+
+command : ref Command;
+
+WPERCOL : con 8;
+
+NSnarf : con 32;
+snarfrune : ref Dat->Astring;
+
+main(argl : list of string)
+{
+	i, ac : int;
+	loadfile : string;
+	p : int;
+	c : ref Column;
+	arg : ref Arg;
+	ncol : int;
+
+	ncol = -1;
+
+	mainpid = sys->pctl(0, nil);
+	loadfile = nil;
+	fontnames = array[2] of string;
+	fontnames[0:] = deffontnames[0:2];
+	f := utils->getenv("acme-font");
+	if (f != nil)
+		fontnames[0] = f;
+	f = utils->getenv("acme-Font");
+	if (f != nil)
+		fontnames[1] = f;
+	arg = arginit(argl);
+	while(ac = argopt(arg)) case(ac){
+	'b' =>
+		dat->bartflag = TRUE;
+	'c' =>
+		ncol = int argf(arg);
+	'f' =>
+		fontnames[0] = argf(arg);
+	'F' =>
+		fontnames[1] = argf(arg);
+	'l' =>
+		loadfile = argf(arg);
+	}
+
+	dat->home = utils->getenv("home");
+	if (dat->home == nil)
+		dat->home = utils->gethome(utils->getuser());
+	ts := utils->getenv("tabstop");
+	if (ts != nil)
+		maxtab = int ts;
+	if (maxtab <= 0)
+		maxtab = 4;
+	snarfrune = utils->stralloc(NSnarf);
+	sys->pctl(Sys->FORKNS|Sys->FORKENV, nil);
+	utils->setenv("font", fontnames[0]);
+	sys->bind("/acme/dis", "/dis", Sys->MBEFORE);
+	wdir = workdir->init();
+	if (wdir == nil)
+		wdir = ".";
+	workdir = nil;
+
+	graph->binit();
+	font = Font.open(display, fontnames[0]);
+	if(font == nil){
+		fontnames[0] = deffontnames[0];
+		font = Font.open(display, fontnames[0]);
+		if (font == nil) {
+			warning(nil, sprint("can't open font file %s: %r\n", fontnames[0]));
+			return;
+		}
+	}
+	reffont = ref Reffont;
+	reffont.r = Ref.init();
+	reffont.f = font;
+	reffonts = array[2] of ref Reffont;
+	reffonts[0] = reffont;
+	reffont.r.inc();	# one to hold up 'font' variable 
+	reffont.r.inc();	# one to hold up reffonts[0] 
+	fontcache = array[1] of ref Reffont;
+	nfontcache = 1;
+	fontcache[0] = reffont;
+
+	iconinit();
+	usercolinit();
+	timerm->timerinit();
+	regx->rxinit();
+
+	cwait = chan of string;
+	ccommand = chan of ref Command;
+	ckill = chan of string;
+	cxfidalloc = chan of ref Xfid;
+	cxfidfree = chan of ref Xfid;
+	cerr = chan of string;
+	cplumb = chan of ref Msg;
+	cedit = chan of int;
+
+	gui->spawnprocs();
+	# spawn keyboardproc();
+	# spawn mouseproc();
+	sync := chan of int;
+	spawn waitproc(sys->pctl(0, nil), sync);
+	<- sync;
+	spawn plumbproc();
+
+	fsys->fsysinit();
+	dat->disk = (dat->disk).init();
+	row = rowm->newrow();
+	if(loadfile != nil) {
+		row.qlock.lock();	# tasks->procs now 
+		row.loadx(loadfile, TRUE);
+		row.qlock.unlock();
+	}
+	else{
+		row.init(mainwin.clipr);
+		if(ncol < 0){
+			if(arg.av == nil)
+				ncol = 2;
+			else{
+				ncol = (len arg.av+(WPERCOL-1))/WPERCOL;
+				if(ncol < 2)
+					ncol = 2;
+			}
+		}
+		if(ncol == 0)
+			ncol = 2;
+		for(i=0; i<ncol; i++){
+			c = row.add(nil, -1);
+			if(c==nil && i==0)
+				error("initializing columns");
+		}
+		c = row.col[row.ncol-1];
+		if(arg.av == nil)
+			readfile(c, wdir);
+		else
+			i = 0;
+			for( ; arg.av != nil; arg.av = tl arg.av){
+				filen := hd arg.av;
+				p = utils->strrchr(filen, '/');
+				if((p>=0 && filen[p:] == "/guide") || i/WPERCOL>=row.ncol)
+					readfile(c, filen);
+				else
+					readfile(row.col[i/WPERCOL], filen);
+				i++;
+			}
+	}
+	bflush();
+
+	spawn keyboardtask();
+	spawn mousetask();
+	spawn waittask();
+	spawn xfidalloctask();
+
+	# notify(shutdown);
+	# waitc := chan of int;
+	# <-waitc;
+	# killprocs();
+	exit;
+}
+
+readfile(c : ref Column, s : string)
+{
+	w : ref Window;
+	r : string;
+	nr : int;
+
+	w = c.add(nil, nil, -1);
+	(r, nr) = look->cleanname(s, len s);
+	w.setname(r, nr);
+	w.body.loadx(0, s, 1);
+	w.body.file.mod = FALSE;
+	w.dirty = FALSE;
+	w.settag();
+	scrl->scrdraw(w.body);
+	w.tag.setselect(w.tag.file.buf.nc, w.tag.file.buf.nc);
+}
+
+oknotes := array[6] of {
+	"delete",
+	"hangup",
+	"kill",
+	"exit",
+	"error",
+	nil
+};
+
+dumping : int;
+
+shutdown(msg : string)
+{
+	i : int;
+
+	# notify(nil);
+	if(!dumping && msg != "kill" && msg != "exit" && (1 || sys->pctl(0, nil)==mainpid) && row != nil){
+		dumping = TRUE;
+		row.dump(nil);
+	}
+	for(i=0; oknotes[i] != nil; i++)
+		if(utils->strncmp(oknotes[i], msg, len oknotes[i]) == 0) {
+			killprocs();
+			exit;
+		}
+	# killprocs();
+	sys->fprint(sys->fildes(2), "acme: %s\n", msg);
+	sys->print("acme: %s\n", msg);
+	# exit;
+}
+
+acmeexit(err: string)
+{
+	if(err != nil)
+		shutdown(err);
+	graph->cursorswitch(nil);
+	if (plumbed)
+		plumbmsg->shutdown();
+	killprocs();
+	gui->killwins();
+	exit;
+}
+
+killprocs()
+{
+	c : ref Command;
+	kill := "kill";
+	thispid := sys->pctl(0, nil);
+	fsys->fsysclose();
+
+	postnote(PNPROC, thispid, mousepid, kill);
+	postnote(PNPROC, thispid, keyboardpid, kill);
+	postnote(PNPROC, thispid, timerpid, kill);
+	postnote(PNPROC, thispid, waitpid0, kill);
+	postnote(PNPROC, thispid, waitpid1, kill);
+	postnote(PNPROC, thispid, fsyspid, kill);
+	postnote(PNPROC, thispid, mainpid, kill);
+	postnote(PNPROC, thispid, keytid, kill);
+	postnote(PNPROC, thispid, mousetid, kill);
+	postnote(PNPROC, thispid, waittid, kill);
+	postnote(PNPROC, thispid, xfidalloctid, kill);
+	# postnote(PNPROC, thispid, lockpid, kill);
+	postnote(PNPROC, thispid, plumbpid, kill);
+
+	# draw(mainwin, mainwin.r, white, nil, mainwin.r.min);
+
+	for(c=command; c != nil; c=c.next)
+		postnote(PNGROUP, thispid, c.pid, "kill");
+
+	xfidm->xfidkill();
+}
+
+keytid : int;
+mousetid : int;
+waittid : int;
+xfidalloctid : int;
+
+keyboardtask()
+{
+	r : int;
+	timer : ref Timer;
+	null : ref Timer;
+	t : ref Text;
+
+	{
+		keytid = sys->pctl(0, nil);
+		null = ref Timer;
+		null.c = chan of int;
+		timer = null;
+		typetext = nil;
+		for(;;){
+			alt{
+			<-(timer.c) =>
+				timerm->timerstop(timer);
+				t = typetext;
+				if(t!=nil && t.what==Tag && !t.w.qlock.locked()){
+					t.w.lock('K');
+					t.w.commit(t);
+					t.w.unlock();
+					bflush();
+				}
+				timer = null;
+			r = <-ckeyboard =>
+				gotkey := 1;
+				while (gotkey) {
+					typetext = row.typex(r, mouse.xy);
+					t = typetext;
+					if(t!=nil && t.col!=nil)
+						activecol = t.col;
+					if(t!=nil && t.w!=nil)
+						t.w.body.file.curtext = t.w.body;
+					if(timer != null)
+						spawn timerm->timerwaittask(timer);
+					if(t!=nil && t.what==Tag)
+						timer = timerm->timerstart(500);
+					else
+						timer = null;
+					alt {
+						r = <- ckeyboard =>
+							gotkey = 1;	# do this case again
+						* =>
+							gotkey = 0;
+					}
+					bflush();
+				}
+			}
+		}
+	}
+	exception{
+		* =>
+			shutdown(utils->getexc());
+			raise;
+			# acmeexit(nil);
+	}
+}
+
+mousetask()
+{
+	t, argt : ref Text;
+	but, ok : int;
+	q0, q1 : int;
+	w : ref Window;
+	m : ref Msg;
+
+	{
+		mousetid = sys->pctl(0, nil);
+		sync := chan of int;
+		spawn waitproc(mousetid, sync);
+		<- sync;
+		for(;;){
+			alt{
+			*mouse = *<-cmouse =>
+				row.qlock.lock();
+				if (mouse.buttons & M_QUIT) {
+					if (row.clean(TRUE))
+						acmeexit(nil);
+					# shutdown("kill");
+					row.qlock.unlock();
+					break;
+				}
+				if (mouse.buttons & M_HELP) {
+					warning(nil, "no help provided (yet)");
+					bflush();
+					row.qlock.unlock();
+					break;
+				}
+				if(mouse.buttons & M_RESIZE){
+					draw(mainwin, mainwin.r, white, nil, mainwin.r.min);
+					scrl->scrresize();
+					row.reshape(mainwin.clipr);
+					bflush();
+					row.qlock.unlock();
+					break;
+				}
+				t = row.which(mouse.xy);
+				if(t!=mousetext && mousetext!=nil && mousetext.w!=nil){
+					mousetext.w.lock('M');
+					mousetext.eq0 = ~0;
+					mousetext.w.commit(mousetext);
+					mousetext.w.unlock();
+				}
+				mousetext = t;
+				if(t == nil) {
+					bflush();
+					row.qlock.unlock();
+					break;
+				}
+				w = t.w;
+				if(t==nil || mouse.buttons==0) {
+					bflush();
+					row.qlock.unlock();
+					break;
+				}
+				if(w != nil)
+					w.body.file.curtext = w.body;
+				but = 0;
+				if(mouse.buttons == 1)
+					but = 1;
+				else if(mouse.buttons == 2)
+					but = 2;
+				else if(mouse.buttons == 4)
+					but = 3;
+				barttext = t;
+				if(t.what==Body && mouse.xy.in(t.scrollr)){
+					if(but){
+						w.lock('M');
+						t.eq0 = ~0;
+						scrl->scroll(t, but);
+						t.w.unlock();
+					}
+					bflush();
+					row.qlock.unlock();
+					break;
+				}
+				if(w != nil && (mouse.buttons &(8|16))){
+					if(mouse.buttons & 8)
+						but = Dat->Kscrollup;
+					else
+						but = Dat->Kscrolldown;
+					w.lock('M');
+					t.eq0 = ~0;
+					t.typex(but, 0);
+					w.unlock();
+					bflush();
+					row.qlock.unlock();
+					break;
+				}
+				if(mouse.xy.in(t.scrollr)){
+					if(but){
+						if(t.what == Columntag)
+							row.dragcol(t.col);
+						else if(t.what == Tag){
+							t.col.dragwin(t.w, but);
+							if(t.w != nil)
+								barttext = t.w.body;
+						}
+						if(t.col != nil)
+							activecol = t.col;
+					}
+					bflush();
+					row.qlock.unlock();
+					break;
+				}
+				if(mouse.buttons){
+					if(w != nil)
+						w.lock('M');
+					t.eq0 = ~0;
+					if(w != nil)
+						w.commit(t);
+					else
+						t.commit(TRUE);
+					if(mouse.buttons & 1){
+						t.select(0);
+						if(w != nil)
+							w.settag();
+						argtext = t;
+						seltext = t;
+						if(t.col != nil)
+							activecol = t.col;	# button 1 only 
+						if(t.w != nil && t == t.w.body)
+							dat->activewin = t.w;
+					}else if(mouse.buttons & 2){
+						(ok, argt, q0, q1) = t.select2(q0, q1);
+						if(ok)
+							exec->execute(t, q0, q1, FALSE, argt);
+					}else if(mouse.buttons & 4){
+						(ok, q0, q1) = t.select3(q0, q1);
+						if(ok)
+							look->look3(t, q0, q1, FALSE);
+					}
+					if(w != nil)
+						w.unlock();
+					bflush();
+					row.qlock.unlock();
+					break;
+				}
+			m = <- cplumb =>
+				if (m.kind == "text") {
+					attrs := plumbmsg->string2attrs(m.attr);
+					(found, act) := plumbmsg->lookup(attrs, "action");
+					if (!found || act == nil || act == "showfile")
+						look->plumblook(m);
+					else if (act == "showdata")
+						look->plumbshow(m);
+				}
+				bflush();
+			}
+		}
+	}
+	exception{
+		* =>
+			shutdown(utils->getexc());
+			raise;
+			# acmeexit(nil);
+	}
+}
+
+# list of processes that have exited but we have not heard of yet
+Pid : adt {
+	pid : int;
+	msg : string;
+	next : cyclic ref Pid;
+};
+
+waittask()
+{
+	status : string;
+	c, lc : ref Command;
+	pid : int;
+	found : int;
+	cmd : string;
+	err : string;
+	t : ref Text;
+	pids : ref Pid;
+
+	waittid = sys->pctl(0, nil);
+	command = nil;
+	for(;;){
+		alt{
+		err = <-cerr =>
+			row.qlock.lock();
+			warning(nil, err);
+			err = nil;
+			bflush();
+			row.qlock.unlock();
+			break;
+		cmd = <-ckill =>
+			found = FALSE;
+			for(c=command; c != nil; c=c.next){
+				# -1 for blank 
+				if(c.name[0:len c.name - 1] == cmd){
+					if(postnote(PNGROUP, waittid, c.pid, "kill") < 0)
+						warning(nil, sprint("kill %s: %r\n", cmd));
+					found = TRUE;
+				}
+			}
+			if(!found)
+				warning(nil, sprint("Kill: no process %s\n", cmd));
+			cmd = nil;
+			break;
+		status = <-cwait =>
+			pid = int status;
+			lc = nil;
+			for(c=command; c != nil; c=c.next){
+				if(c.pid == pid){
+					if(lc != nil)
+						lc.next = c.next;
+					else
+						command = c.next;
+					break;
+				}
+				lc = c;
+			}
+			row.qlock.lock();
+			t = row.tag;
+			t.commit(TRUE);
+			if(c == nil){
+				# warning(nil, sprint("unknown child pid %d\n", pid));
+				p := ref Pid;
+				p.pid = pid;
+				p.msg = status;
+				p.next = pids;
+				pids = p;
+			}
+			else{
+				if(look->search(t, c.name, len c.name)){
+					t.delete(t.q0, t.q1, TRUE);
+					t.setselect(0, 0);
+				}
+				if(status[len status - 1] != ':')
+					warning(c.md, sprint("%s\n", status));
+				bflush();
+			}
+			row.qlock.unlock();
+			if(c != nil){
+				if(c.iseditcmd)
+					cedit <- = 0;
+				fsys->fsysdelid(c.md);
+				c = nil;
+			}
+			break;
+		c = <-ccommand =>
+			lastp : ref Pid = nil;
+			for(p := pids; p != nil; p = p.next){
+				if(p.pid == c.pid){
+					status = p.msg;
+					if(status[len status - 1] != ':')
+						warning(c.md, sprint("%s\n", status));
+					if(lastp == nil)
+						pids = p.next;
+					else
+						lastp.next = p.next;
+					if(c.iseditcmd)
+						cedit <- = 0;
+					fsys->fsysdelid(c.md);
+					c = nil;
+					break;
+				}
+				lastp = p;
+			}	
+			c.next = command;
+			command = c;
+			row.qlock.lock();
+			t = row.tag;
+			t.commit(TRUE);
+			t.insert(0, c.name, len c.name, TRUE, 0);
+			t.setselect(0, 0);
+			bflush();
+			row.qlock.unlock();
+			break;
+		}
+	}
+}
+
+xfidalloctask()
+{
+	xfree, x : ref Xfid;
+
+	xfidalloctid = sys->pctl(0, nil);
+	xfree = nil;
+	for(;;){
+		alt{
+		<-cxfidalloc =>
+			x = xfree;
+			if(x != nil)
+				xfree = x.next;
+			else{
+				x = xfidm->newxfid();
+				x.c = chan of int;
+				spawn x.ctl();
+			}
+			cxfidalloc <-= x;
+			break;
+		x = <-cxfidfree =>
+			x.next = xfree;
+			xfree = x;
+			break;
+		}
+	}
+}
+
+frgetmouse()
+{
+	bflush();
+	*mouse = *<-cmouse;
+}
+
+waitproc(pid : int, sync: chan of int)
+{
+	fd : ref Sys->FD;
+	n : int;
+
+	if (waitpid0 == 0)
+		waitpid0 = sys->pctl(0, nil);
+	else
+		waitpid1 = sys->pctl(0, nil);
+	sys->pctl(Sys->FORKFD, nil);
+	# w := sprint("/prog/%d/wait", pid);
+	w := sprint("#p/%d/wait", pid);
+	fd = sys->open(w, Sys->OREAD);
+	if (fd == nil)
+		error("fd == nil in waitproc");
+	sync <-= 0;
+	buf := array[Sys->WAITLEN] of byte;
+	status := "";
+	for(;;){
+		if ((n = sys->read(fd, buf, len buf))<0)
+			error("bad read in waitproc");
+		status = string buf[0:n];
+		cwait <-= status;
+	}
+}
+
+get(fix : int, save : int, setfont : int, name : string) : ref Reffont
+{
+	r : ref Reffont;
+	f : ref Font;
+	i : int;
+
+	r = nil;
+	if(name == nil){
+		name = fontnames[fix];
+		r = reffonts[fix];
+	}
+	if(r == nil){
+		for(i=0; i<nfontcache; i++)
+			if(name ==  fontcache[i].f.name){
+				r = fontcache[i];
+				break;
+			}
+		if (i >= nfontcache) {
+			f = Font.open(display, name);
+			if(f == nil){
+				warning(nil, sprint("can't open font file %s: %r\n", name));
+				return nil;
+			}
+			r = ref Reffont;
+			r.r = Ref.init();
+			r.f = f;
+			ofc := fontcache;
+			fontcache = array[nfontcache+1] of ref Reffont;
+			fontcache[0:] = ofc[0:nfontcache];
+			ofc = nil;
+			fontcache[nfontcache++] = r;
+		}
+	}
+	if(save){
+		r.r.inc();
+		if(reffonts[fix] != nil)
+			reffonts[fix].close();
+		reffonts[fix] = r;
+		fontnames[fix] = name;
+	}
+	if(setfont){
+		reffont.f = r.f;
+		r.r.inc();
+		reffonts[0].close();
+		font = r.f;
+		reffonts[0] = r;
+		r.r.inc();
+		iconinit();
+	}
+	r.r.inc();
+	return r;
+}
+
+close(r : ref Reffont)
+{
+	i : int;
+
+	if(r.r.dec() == 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{
+			fontcache[i:] = fontcache[i+1:nfontcache];
+			nfontcache--;
+		}
+		r.f = nil;
+		r = nil;
+	}
+}
+
+arrowbits := array[64] of {
+	 byte 16rFF, byte 16rE0, byte 16rFF, byte 16rE0,
+	 byte 16rFF, byte 16rC0, byte 16rFF, byte 16r00,
+	 byte 16rFF, byte 16r00, byte 16rFF, byte 16r80,
+	 byte 16rFF, byte 16rC0, byte 16rFF, byte 16rE0,
+	 byte 16rE7, byte 16rF0, byte 16rE3, byte 16rF8,
+	 byte 16rC1, byte 16rFC, byte 16r00, byte 16rFE,
+	 byte 16r00, byte 16r7F, byte 16r00, byte 16r3E,
+	 byte 16r00, byte 16r1C, byte 16r00, byte 16r08,
+
+	 byte 16r00, byte 16r00, byte 16r7F, byte 16rC0,
+	 byte 16r7F, byte 16r00, byte 16r7C, byte 16r00,
+	 byte 16r7E, byte 16r00, byte 16r7F, byte 16r00,
+	 byte 16r6F, byte 16r80, byte 16r67, byte 16rC0,
+	 byte 16r43, byte 16rE0, byte 16r41, byte 16rF0,
+	 byte 16r00, byte 16rF8, byte 16r00, byte 16r7C,
+	 byte 16r00, byte 16r3E, byte 16r00, byte 16r1C,
+	 byte 16r00, byte 16r08, byte 16r00, byte 16r00,
+};
+
+# outer boundary of width 1 is white
+# next  boundary of width 3 is black
+# next  boundary of width 1 is white
+# inner boundary of width 4 is transparent
+boxbits := array[64] of {
+	 byte 16rFF, byte 16rFF, byte 16rFF, byte 16rFF, 
+	 byte 16rFF, byte 16rFF, byte 16rFF, byte 16rFF,
+	 byte 16rFF, byte 16rFF, byte 16rF8, byte 16r1F,
+	 byte 16rF8, byte 16r1F, byte 16rF8, byte 16r1F,
+	 byte 16rF8, byte 16r1F, byte 16rF8, byte 16r1F,
+	 byte 16rF8, byte 16r1F, byte 16rFF, byte 16rFF,
+	 byte 16rFF, byte 16rFF, byte 16rFF, byte 16rFF,
+	 byte 16rFF, byte 16rFF, byte 16rFF, byte 16rFF,
+
+
+	 byte 16r00, byte 16r00, byte 16r7F, byte 16rFE,
+	 byte 16r7F, byte 16rFE, byte 16r7F, byte 16rFE,
+	 byte 16r70, byte 16r0E, byte 16r70, byte 16r0E,
+	 byte 16r70, byte 16r0E, byte 16r70, byte 16r0E,
+	 byte 16r70, byte 16r0E, byte 16r70, byte 16r0E,
+	 byte 16r70, byte 16r0E, byte 16r70, byte 16r0E,
+	 byte 16r7F, byte 16rFE, byte 16r7F, byte 16rFE,
+	 byte 16r7F, byte 16rFE, byte 16r00, byte 16r00,
+};
+
+iconinit()
+{
+	r : Rect;
+
+	# Blue
+	tagcols = array[NCOL] of ref Draw->Image;
+	tagcols[BACK] = display.colormix(Draw->Palebluegreen, Draw->White);
+	tagcols[HIGH] = display.color(Draw->Palegreygreen);
+	tagcols[BORD] = display.color(Draw->Purpleblue);
+	tagcols[TEXT] = black;
+	tagcols[HTEXT] = black;
+
+	# Yellow
+	textcols = array[NCOL] of ref Draw->Image;
+	textcols[BACK] = display.colormix(Draw->Paleyellow, Draw->White);
+	textcols[HIGH] = display.color(Draw->Darkyellow);
+	textcols[BORD] = display.color(Draw->Yellowgreen); 
+	textcols[TEXT] = black;
+	textcols[HTEXT] = black;
+
+	if(button != nil)
+		button = modbutton = colbutton = nil;
+
+	r = ((0, 0), (Dat->Scrollwid+2, font.height+1));
+	button = balloc(r, mainwin.chans, Draw->White);
+	draw(button, r, tagcols[BACK], nil, r.min);
+	r.max.x -= 2;
+	draw(button, r, tagcols[BORD], nil, (0, 0));   
+	r = r.inset(2);
+	draw(button, r, tagcols[BACK], nil, (0, 0));
+
+	r = button.r;
+	modbutton = balloc(r, mainwin.chans, Draw->White);
+	draw(modbutton, r, tagcols[BACK], nil, r.min);
+	r.max.x -= 2;
+	draw(modbutton, r, tagcols[BORD], nil, (0, 0));
+	r = r.inset(2);
+	draw(modbutton, r, display.rgb(16r00, 16r00, 16r99), nil, (0, 0));	# was DMedblue
+
+	r = button.r;
+	colbutton = balloc(r, mainwin.chans, Draw->White);
+	draw(colbutton, r, tagcols[BACK], nil, r.min);
+	r.max.x -= 2;
+	draw(colbutton, r, tagcols[BORD], nil, (0, 0));
+
+#	arrowcursor = ref Cursor((-1, -1), (16, 32), arrowbits);
+	boxcursor = ref Cursor((-7, -7), (16, 32), boxbits);
+
+	but2col = display.rgb(16raa, 16r00, 16r00);
+	but3col = display.rgb(16r00, 16r66, 16r00);
+	but2colt = white;
+	but3colt = white;
+
+	graph->cursorswitch(arrowcursor);
+}
+
+colrec : adt {
+	name : string;
+	image : ref Image;
+};
+
+coltab : array of colrec;
+
+cinit() 
+{
+	coltab = array[6] of colrec;
+	coltab[0].name = "yellow"; coltab[0].image = yellow;
+	coltab[1].name = "green"; coltab[1].image = green;
+	coltab[2].name = "red"; coltab[2].image = red;
+	coltab[3].name = "blue"; coltab[3].image = blue;
+	coltab[4].name = "black"; coltab[4].image = black;
+	coltab[5].name = "white"; coltab[5].image = white;
+}
+
+col(s : string, n : int) : int
+{
+	return ((s[n]-'0') << 4) | (s[n+1]-'0');
+}
+
+rgb(s : string, n : int) : (int, int, int)
+{
+	return (col(s, n), col(s, n+2), col(s, n+4));
+}
+
+cenv(s : string, t : string, but : int, i : ref Image) : ref Image
+{
+	c := utils->getenv("acme-" + s + "-" + t + "-" + string but);
+	if (c == nil)
+		c = utils->getenv("acme-" + s + "-" + string but);
+	if (c == nil && but != 0)
+		c = utils->getenv("acme-" + s);
+	if (c != nil) {
+		if (c[0] == '#' && len c >= 7) {
+			(r1, g1, b1) := rgb(c, 1);
+			if (len c >= 15 && c[7] == '/' && c[8] == '#') {
+				(r2, g2, b2) := rgb(c, 9);
+				return display.colormix((r1<<24)|(g1<<16)|(b1<<8)|16rFF, (r2<<24)|(g2<<16)|(b2<<8)|16rFF);
+			}
+			return display.color((r1<<24)|(g1<<16)|(b1<<8)|16rFF);
+		}
+		for (j := 0; j < len c; j++)
+			if (c[j] >= 'A' && c[j] <= 'Z')
+				c[j] += 'a'-'A';
+		for (j = 0; j < len coltab; j++)
+			if (c == coltab[j].name)
+				return coltab[j].image;
+	}
+	return i;
+}
+
+usercolinit()
+{
+	cinit();
+	textcols[TEXT] = cenv("fg", "text", 0, textcols[TEXT]);
+	textcols[BACK] = cenv("bg", "text", 0, textcols[BACK]);
+	textcols[HTEXT] = cenv("fg", "text", 1, textcols[HTEXT]);
+	textcols[HIGH] = cenv("bg", "text", 1, textcols[HIGH]);
+	but2colt= cenv("fg", "text", 2, but2colt);
+	but2col = cenv("bg", "text", 2, but2col);
+	but3colt = cenv("fg", "text", 3, but3colt);
+	but3col = cenv("bg", "text", 3, but3col);
+	tagcols[TEXT] = cenv("fg", "tag", 0, tagcols[TEXT]);
+	tagcols[BACK] = cenv("bg", "tag", 0, tagcols[BACK]);
+	tagcols[HTEXT] = cenv("fg", "tag", 1, tagcols[HTEXT]);
+	tagcols[HIGH] = cenv("bg", "tag", 1, tagcols[HIGH]);
+}
+
+getsnarf()
+{
+	# return;
+	fd := sys->open("/chan/snarf", sys->OREAD);
+	if(fd == nil)
+		return;
+	snarfbuf.reset();
+	snarfbuf.loadx(0, fd);
+}
+
+putsnarf()
+{
+	n : int;
+
+	# return;
+	if(snarfbuf.nc == 0)
+		return;
+	fd := sys->open("/chan/snarf", sys->OWRITE);
+	if(fd == nil)
+		return;
+  	for(i:=0; i<snarfbuf.nc; i+=n){
+		n = snarfbuf.nc-i;
+		if(n >= NSnarf)
+			n = NSnarf;
+		snarfbuf.read(i, snarfrune, 0, n);
+		sys->fprint(fd, "%s", snarfrune.s[0:n]);
+	}
+}
+
+plumbpid : int;
+
+plumbproc()
+{
+	plumbpid = sys->pctl(0, nil);
+	for(;;){
+		msg := Msg.recv();
+		if(msg == nil){
+			sys->print("Acme: can't read /chan/plumb.edit: %r\n");
+			plumbpid = 0;
+			plumbed = 0;
+			return;
+		}
+		if(msg.kind != "text"){
+			sys->print("Acme: can't interpret '%s' kind of message\n", msg.kind);
+			continue;
+		}
+# sys->print("msg %s\n", string msg.data);
+		cplumb <-= msg;
+	}
+}
--- /dev/null
+++ b/appl/acme/acme.m
@@ -1,0 +1,32 @@
+Acme : module {
+	PATH : con "/dis/acme.dis";
+
+	RELEASECOPY : con 1;
+
+	M_LBUT : con 1;
+	M_MBUT : con 2;
+	M_RBUT : con 4;
+	M_TBS : con 8;
+	M_PLUMB : con 16;
+	M_QUIT : con 32;
+	M_HELP : con 64;
+	M_RESIZE : con 128;
+	M_DOUBLE : con 256;
+
+	textcols, tagcols : array of ref Draw->Image;
+	but2col, but3col, but2colt, but3colt : ref Draw->Image;
+
+	acmectxt : ref Draw->Context;
+	keyboardpid, mousepid, timerpid, fsyspid : int;
+	fontnames : array of string;
+	wdir : string;
+
+	init : fn(ctxt : ref Draw->Context, argv : list of string);
+	timing : fn(s : string);
+	frgetmouse : fn();
+	get : fn(p, q, r : int, b : string) : ref Dat->Reffont;
+	close : fn(r : ref Dat->Reffont);
+	acmeexit : fn(err : string);
+	getsnarf : fn(); 
+	putsnarf : fn();
+};
--- /dev/null
+++ b/appl/acme/acme/acid/guide
@@ -1,0 +1,2 @@
+Acid pid
+Acid -l alef -l symsfile pid
--- /dev/null
+++ b/appl/acme/acme/acid/mkfile
@@ -1,0 +1,24 @@
+<../../../../mkconfig
+
+BIN=$ROOT/acme/acid
+
+DIRS=\
+	src\
+
+TARG=\
+	guide\
+	readme\
+
+BINTARG=${TARG:%=$BIN/%}
+
+all:V:		$TARG
+
+install:V:	$BINTARG
+
+$BIN/guide : guide
+	rm -f $BIN/guide && cp guide $BIN/guide
+
+$BIN/readme : readme
+	rm -f $BIN/readme && cp readme $BIN/readme
+
+<$ROOT/mkfiles/mksubdirs
--- /dev/null
+++ b/appl/acme/acme/acid/readme
@@ -1,0 +1,12 @@
+Capital A Acid is a rudimentary acme interface to the debugger acid.
+It uses a win to provide an interactive window for acid.  In that window,
+a couple of extra acme-specific features are enabled:
+
+w(command)
+	runs the command and places its output in a new window.
+	e.g. w(lstk()) places the stack trace in a distinct window.
+
+Also, in any such window, text executed with button 2 is
+presented as input to acid in the main Acid window.  Thus, for
+example, one may evaluate variables presented in a stack trace
+by `executing' it with button 2.
--- /dev/null
+++ b/appl/acme/acme/acid/src/Acid.b
@@ -1,0 +1,31 @@
+implement Acid;
+
+include "sys.m";
+include "draw.m";
+include "sh.m";
+
+Acid : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+init(ctxt : ref Draw->Context, argl : list of string)
+{
+	sys := load Sys Sys->PATH;
+	stderr := sys->fildes(2);
+	if (len argl < 2) {
+		sys->fprint(stderr, "usage : Acid pid\n");
+		return;
+	}
+	cmd := "/acme/dis/win";
+	file := cmd + ".dis";
+	c := load Command file;
+	if(c == nil) {
+		sys->fprint(stderr, "%s: %r\n", cmd);
+		return;
+	}
+	argl = "-l" :: argl;
+	argl = "acid" :: argl;
+	argl = "/acme/dis/Acid0" :: argl;
+	argl = cmd :: argl;
+	c->init(ctxt, argl);
+}
--- /dev/null
+++ b/appl/acme/acme/acid/src/Acid0.b
@@ -1,0 +1,86 @@
+implement Acidb;
+
+include "sys.m";
+include "draw.m";
+include "bufio.m";
+
+sys : Sys;
+bufio : Bufio;
+
+FD : import sys;
+Iobuf : import bufio;
+
+Acidb : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+init(nil : ref Draw->Context, nil : list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	# TBS main(argl);
+}
+
+False : con 0;
+True : con 1;
+
+EVENTSIZE : con 256;
+
+Event : adt {
+	c1, c2, q0, q1, flag, nb, nr : int;
+	b : array of byte;
+	r : array of int;
+	# TBS byte	b[EVENTSIZE*UTFmax+1];
+	# TBS Rune	r[EVENTSIZE+1];
+};
+
+Win : adt {
+		winid : int;
+		addr : int;
+		body : ref Iobuf;
+		ctl : int;
+		data : int;
+		event : int;
+		buf : array of byte;
+		# TBS byte	buf[512];
+		bufp : int;
+		nbuf : int;
+
+		wnew : fn(w : ref Win);
+		wwritebody : fn(w : ref Win, s : array of byte, n : int);
+		wread : fn(w : ref Win, m : int, n : int, s : array of byte);
+		wclean : fn(w : ref Win);
+		wname : fn(w : ref Win, s : array of byte);
+		wdormant : fn(w : ref Win);
+		wevent : fn(w : ref Win, e : ref Event);
+		wtagwrite : fn(w : ref Win, s : array of byte, n : int);
+		wwriteevent : fn(w : ref Win, e : ref Event);
+		wslave : fn(w : ref Win, c : chan of Event);
+		wreplace : fn(w : ref Win, s : array of byte, b : array of byte, n : int);
+		wselect : fn(w : ref Win, s : array of byte);
+		wdel : fn(w : ref Win, n : int) : int;
+		wreadall : fn(w : ref Win) : (int, array of byte);
+
+		ctlwrite : fn(w : ref Win, s : array of byte);
+		getec : fn(w : ref Win) : int;
+		geten : fn(w : ref Win) : int;
+		geter : fn(w : ref Win, s : array of byte, r : array of int) : int;
+		openfile : fn(w : ref Win, b : array of byte) : int;
+		openbody : fn(w : ref Win, n : int);
+};
+
+Awin : adt {
+	w : Win;
+
+	slave : fn(w : ref Awin, s : array of byte, c : chan of int);
+	new : fn(w : ref Awin, s : array of byte);
+	command : fn(w : ref Awin, s : array of byte) : int;
+	send : fn(w : ref Awin, m : int, s : array of byte, n : int);
+};
+
+srvfd : ref FD;
+stdin : ref FD;
+srvenv : array of byte;
+# TBS byte	srvenv[64];
+
+srvc : chan of array of byte;
--- /dev/null
+++ b/appl/acme/acme/acid/src/mkfile
@@ -1,0 +1,20 @@
+<../../../../../mkconfig
+
+TARG=\
+	Acid.dis\
+	Acid0.dis\
+#	acid.dis\
+#	awin.dis\
+#	util.dis\
+#	win.dis\
+
+MODULES=\
+
+SYSMODULES=\
+	sh.m\
+	sys.m\
+	draw.m\
+
+DISBIN=$ROOT/acme/acid
+
+<$ROOT/mkfiles/mkdis
--- /dev/null
+++ b/appl/acme/acme/bin/guide
@@ -1,0 +1,5 @@
+win
+new command ...
+aspell file
+adiff file1 file2
+adict -d oed
--- /dev/null
+++ b/appl/acme/acme/bin/mkfile
@@ -1,0 +1,24 @@
+<../../../../mkconfig
+
+BIN=$ROOT/acme/bin
+
+DIRS=\
+	src\
+
+TARG=\
+	guide\
+	readme\
+
+BINTARG=${TARG:%=$BIN/%}
+
+all:V:		$TARG
+
+install:V:	$BINTARG
+
+$BIN/guide : guide
+	rm -f $BIN/guide && cp guide $BIN/guide
+
+$BIN/readme : readme
+	rm -f $BIN/readme && cp readme $BIN/readme
+
+<$ROOT/mkfiles/mksubdirs
--- /dev/null
+++ b/appl/acme/acme/bin/readme
@@ -1,0 +1,25 @@
+This directory and its subdirectory $cputype are always mounted at
+the end of /bin for programs run from acme.  They hold a collection
+of small acme-specific applications:
+
+win [command]
+	Create an acme window to serve as a terminal, analogous
+	to xterm.  By default, it runs the shell, rc, but it works with
+	any interactive program, e.g. hoc.  Within the window,
+	commands executed with button 2 are 'executed' by sending
+	their text to the standard input of the command, appending
+	a newline if necessary.
+new command
+	Run the non-interactive command, placing its standard and
+	diagnostic output in a new window.
+aspell file
+	Run spell on the file, labeling the output with addresses so
+	misspelled words can be found in context using button 3.
+adiff file1 file2
+	Run diff on the files, labeling the output with addresses so
+	changes can be found in context using button 3.
+adict
+	Interactive version of dict(1).  Button 3 looks up words and
+	may be applied to any word in any adict window.
+	When a word has multiple definitions, indicate the number
+	(as in acme Mail) to disambiguate.
--- /dev/null
+++ b/appl/acme/acme/bin/src/adiff.b
@@ -1,0 +1,155 @@
+implement Adiff;
+
+include "sys.m";
+include "draw.m";
+include "sh.m";
+include "workdir.m";
+include "bufio.m";
+
+Adiff : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+sys : Sys;
+
+Context : import Draw;
+OREAD, OWRITE, QTDIR, FD, FORKFD, open, read, write, sprint, fprint, stat, fildes, dup, pctl : import sys;
+
+init(ctxt : ref Context, argl : list of string)
+{
+	sys = load Sys Sys->PATH;
+	workdir := load Workdir Workdir->PATH;
+	stderr := fildes(2);
+	if (len argl != 3) {
+		fprint(stderr, "usage: adiff file1 file2\n");
+		return;
+	}
+	ncfd := open("/chan/new/ctl", OREAD);
+	if (ncfd == nil) {
+		fprint(stderr, "cannot open ctl file\n");
+		return;
+	}
+	b := array[128] of byte;
+	n := read(ncfd, b, len b);
+	id := string int string b[0:n];
+	f1 := hd tl argl;
+	f2 := hd tl tl argl;
+	(ok1, d1) := stat(f1);
+	if (ok1 < 0) {
+		fprint(stderr, "cannot stat %s\n", f1);
+		return;
+	}
+	(ok2, d2) := stat(f2);
+	if (ok2 < 0) {
+		fprint(stderr, "cannot stat %s\n", f2);
+		return;
+	}
+	if (d1.qid.qtype & QTDIR)
+		f1 = f1 + "/" + basename(f2);
+	else if (d2.qid.qtype & QTDIR)
+		f2 = f2 + "/" + basename(f1);
+	buf := "/chan/" + id + "/ctl";
+	icfd := open(buf, OWRITE);
+	if (icfd == nil) {
+		fprint(stderr, "cannot open control file\n");
+		return;
+	}
+	buf = "name " + workdir->init() + "/-diff-" + f1 + "\n";
+	b = array of byte buf;
+	write(icfd, b, len b);
+
+	fds := array[2] of ref FD;
+	if (sys->pipe(fds) < 0) {
+		fprint(stderr, "can't pipe\n");
+		return;
+	}
+	buf = "/chan/" + id + "/body";
+	bfd := open(buf, OWRITE);
+	if (bfd == nil) {
+		fprint(stderr, "cannot open body file\n");
+		return;
+	}
+	spawn diff(fds[1], f1, f2, ctxt);
+	fds[1] = nil;
+	awk(fds[0], bfd, f1, f2);
+	b = array of byte "clean\n";
+	write(icfd, b, len b);
+}
+
+strchr(s : string, c : int) : int
+{
+	for (i := 0; i < len s; i++)
+		if (s[i] == c)
+			return i;
+	return -1;
+}
+	
+strchrs(s, pat : string) : int
+{
+	for (i := 0; i < len s; i++)
+		if (strchr(pat, s[i]) >= 0)
+			return i;
+	return -1;
+}
+
+awk(ifd, ofd : ref FD, f1, f2 : string)
+{
+	bufio := load Bufio Bufio->PATH;
+	Iobuf : import bufio;
+	b := bufio->fopen(ifd, OREAD);
+	while ((s := b.gets('\n')) != nil) {
+		if (s[0] >= '1' && s[0] <= '9') {
+			if ((n := strchrs(s, "acd")) >= 0)
+				s = f1 + ":" + s[0:n] + " " + s[n:n+1] + " " + f2 + ":" + s[n+1:];
+		}
+		fprint(ofd, "%s", s);
+	}
+}
+
+diff(ofd : ref FD, f1, f2 : string, ctxt : ref Context)
+{
+	args : list of string;
+
+	pctl(FORKFD, nil);
+	fd := open("/dev/null", OREAD);
+	dup(fd.fd, 0);
+	fd = nil;
+	dup(ofd.fd, 1);
+	dup(1, 2);
+	ofd = nil;
+	args = nil;
+	args = f2 :: args;
+	args = f1 :: args;
+	args = "diff" :: args;
+	exec("diff", args, ctxt);
+	exit;
+}
+
+exec(cmd : string, argl : list of string, ctxt : ref Context)
+{
+	file := cmd;
+	if(len file<4 || file[len file-4:]!=".dis")
+		file += ".dis";
+	c := load Command file;
+	if(c == nil) {
+		err := sprint("%r");
+		if(file[0]!='/' && file[0:2]!="./"){
+			c = load Command "/dis/"+file;
+			if(c == nil)
+				err = sprint("%r");
+		}
+		if(c == nil){
+			fprint(fildes(2), "%s: %s\n", cmd, err);
+			return;
+		}
+	}
+	c->init(ctxt, argl);
+}
+
+basename(s : string) : string
+{
+	for (i := len s -1; i >= 0; --i)
+		if (s[i] == '/')
+			return s[i+1:];
+	return s;
+}
--- /dev/null
+++ b/appl/acme/acme/bin/src/agrep.b
@@ -1,0 +1,46 @@
+implement Agrep;
+
+include "sys.m";
+include "draw.m";
+include "sh.m";
+
+Agrep : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+init(ctxt : ref Draw->Context, argl : list of string)
+{
+	sys := load Sys Sys->PATH;
+	stderr := sys->fildes(2);
+	cmd := "grep";
+	file := cmd + ".dis";
+	c := load Command file;
+	if(c == nil) {
+		err := sys->sprint("%r");
+		if(file[0]!='/' && file[0:2]!="./"){
+			c = load Command "/dis/"+file;
+			if(c == nil)
+				err = sys->sprint("%r");
+		}
+		if(c == nil){
+			sys->fprint(stderr, "%s: %s\n", cmd, err);
+			return;
+		}
+	}
+	argl = tl argl;
+	argl = rev(argl);
+	argl = "/dev/null" :: argl;
+	argl = rev(argl);
+	argl = "-n" :: argl;
+	argl = cmd :: argl;
+	c->init(ctxt, argl);
+}
+
+rev(a : list of string) : list of string
+{
+	b : list of string;
+
+	for ( ; a != nil; a = tl a)
+		b = hd a :: b;
+	return b;
+}
--- /dev/null
+++ b/appl/acme/acme/bin/src/awd.b
@@ -1,0 +1,45 @@
+implement Awd;
+
+include "sys.m";
+include "draw.m";
+include "workdir.m";
+
+Awd : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+sys : Sys;
+workdir : Workdir;
+
+FD, OWRITE, open, write : import sys;
+
+init(nil : ref Draw->Context, argl : list of string)
+{
+	n : int;
+	fd : ref FD;
+	buf, dir, str : string;
+	ab : array of byte;
+
+	sys = load Sys Sys->PATH;
+	workdir = load Workdir Workdir->PATH;
+	fd = open("/dev/acme/ctl", OWRITE);
+	if(fd == nil)
+		exit;
+	dir = workdir->init();
+	buf = "name " + dir;
+	n = len buf;
+	if(n>0 && buf[n-1] !='/')
+		buf[n++] = '/';
+	buf[n++] = '-';
+	if(tl argl != nil)
+		str = hd tl argl;
+	else
+		str = "rc";
+	buf += str + "\n";
+	ab = array of byte buf;
+	write(fd, ab, len ab);
+	buf = "dumpdir " + dir + "\n";
+	ab = array of byte buf;
+	write(fd, ab, len ab);
+	exit;
+}
--- /dev/null
+++ b/appl/acme/acme/bin/src/cd.b
@@ -1,0 +1,70 @@
+implement Cd;
+
+include "sys.m";
+include "draw.m";
+include "workdir.m";
+
+Cd : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+sys : Sys;
+workdir : Workdir;
+
+FD, OREAD, OWRITE, open, read, write, chdir, fildes, fprint : import sys;
+
+init(nil : ref Draw->Context, argl : list of string)
+{
+	n : int;
+	fd, stderr : ref FD;
+	buf, dir, str : string;
+	ab : array of byte;
+
+	sys = load Sys Sys->PATH;
+	stderr = fildes(2);
+	argl = tl argl;
+	if (argl == nil)
+		argl = "/usr/" + user() :: nil;
+	if (tl argl != nil) {
+		fprint(stderr, "Usage: cd [directory]\n");
+		exit;
+	}
+	if (chdir(hd argl) < 0) {
+		fprint(stderr, "cd: %s: %r\n", hd argl);
+		exit;
+	}
+
+	workdir = load Workdir Workdir->PATH;
+	fd = open("/dev/acme/ctl", OWRITE);
+	if(fd == nil)
+		exit;
+	dir = workdir->init();
+	buf = "name " + dir;
+	n = len buf;
+	if(n>0 && buf[n-1] !='/')
+		buf[n++] = '/';
+	buf[n++] = '-';
+	if(tl argl != nil)
+		str = hd tl argl;
+	else
+		str = "sh";
+	buf += str + "\n";
+	ab = array of byte buf;
+	write(fd, ab, len ab);
+	buf = "dumpdir " + dir + "\n";
+	ab = array of byte buf;
+	write(fd, ab, len ab);
+	exit;
+}
+
+user(): string
+{
+	fd := open("/dev/user", OREAD);
+	if(fd == nil)
+		return "inferno";
+	buf := array[Sys->NAMEMAX] of byte;
+	n := read(fd, buf, len buf);
+	if(n <= 0)
+		return "inferno";
+	return string buf[0:n];
+}
--- /dev/null
+++ b/appl/acme/acme/bin/src/mkfile
@@ -1,0 +1,22 @@
+<../../../../../mkconfig
+
+TARG=\
+	win.dis\
+	winm.dis\
+	adiff.dis\
+	agrep.dis\
+	new.dis\
+	spout.dis\
+	awd.dis\
+	cd.dis\
+
+MODULES=\
+	
+SYSMODULES=\
+	sh.m\
+	sys.m\
+	draw.m\
+
+DISBIN=$ROOT/acme/dis
+
+<$ROOT/mkfiles/mkdis
--- /dev/null
+++ b/appl/acme/acme/bin/src/new.b
@@ -1,0 +1,70 @@
+implement New;
+
+include "sys.m";
+include "draw.m";
+include "sh.m";
+include "workdir.m";
+
+New : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+sys : Sys;
+
+init(ctxt : ref Draw->Context, argl : list of string)
+{
+	sys = load Sys Sys->PATH;
+	workdir := load Workdir Workdir->PATH;
+	if (len argl <= 1)
+		return;
+	ncfd := sys->open("/mnt/acme/new/ctl", Sys->OREAD);
+	if (ncfd == nil)
+		return;
+	b := array[128] of byte;
+	n := sys->read(ncfd, b, len b);
+	id := string int string b[0:n];
+	buf := "/mnt/acme/" + id + "/ctl";
+	icfd := sys->open(buf, Sys->OWRITE);
+	if (icfd == nil)
+		return;
+	base := hd tl argl;
+	for (i := len base - 1; i >= 0; --i)
+		if (base[i] == '/') {
+			base = base[i+1:];
+			break;
+	}
+	buf = "name " + workdir->init() + "/-" + base + "\n";
+	b = array of byte buf;
+	sys->write(icfd, b, len b);
+	buf = "/mnt/acme/" + id + "/body";
+	bfd := sys->open(buf, Sys->OWRITE);
+	if (bfd == nil)
+		return;
+	sys->dup(bfd.fd, 1);
+	sys->dup(1, 2);
+	spawn exec(hd tl argl, tl argl, ctxt);
+	b = array of byte "clean\n";
+	sys->write(icfd, b, len b);
+}
+
+exec(cmd : string, argl : list of string, ctxt : ref Draw->Context)
+{
+	file := cmd;
+	if(len file<4 || file[len file-4:]!=".dis")
+		file += ".dis";
+	c := load Command file;
+	if(c == nil) {
+		err := sys->sprint("%r");
+		if(file[0]!='/' && file[0:2]!="./"){
+			c = load Command "/dis/"+file;
+			if(c == nil)
+				err = sys->sprint("%r");
+		}
+		if(c == nil){
+			sys->fprint(sys->fildes(2), "%s: %s\n", cmd, err);
+			return;
+		}
+	}
+	c->init(ctxt, argl);
+	exit;
+}
--- /dev/null
+++ b/appl/acme/acme/bin/src/spout.b
@@ -1,0 +1,143 @@
+implement Spout;
+
+include "sys.m";
+include "draw.m";
+include "bufio.m";
+
+Spout : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+sys : Sys;
+bufio : Bufio;
+
+OREAD, OWRITE, ORDWR, FORKNS, FORKFD, NEWPGRP, MREPL, FD, UTFmax, pctl, open, read, write, fprint, sprint, fildes, bind, dup, byte2char, utfbytes : import sys;
+Iobuf : import bufio;
+
+stdin, stdout, stderr : ref FD;
+
+init(nil : ref Draw->Context, argl : list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	stdin = fildes(0);
+	stdout = fildes(1);
+	stderr = fildes(2);
+	main(argl);	
+}
+
+bout : ref Iobuf;
+
+main(argv : list of string)
+{
+	fd : ref FD;
+
+	bout = bufio->fopen(stdout, OWRITE);
+	if(len argv == 1)
+		spout(stdin, "");
+	else
+		for(argv = tl argv; argv != nil; argv = tl argv){
+			fd = open(hd argv, OREAD);
+			if(fd == nil){
+				fprint(stderr, "spell: can't open %s: %r\n", hd argv);
+				continue;
+			}
+			spout(fd, hd argv);
+			fd = nil;
+		}
+	exit;
+}
+
+alpha(c : int) : int
+{
+	return ('a'<=(c) && (c)<='z') || ('A'<=(c) && (c)<='Z');
+}
+
+b : ref Iobuf;
+
+spout(fd : ref FD, name : string)
+{
+	s, buf : string;
+	t, w : int;
+	inword, wordchar : int;
+	n, wn, c, m : int;
+
+	b = bufio->fopen(fd, OREAD);
+	n = 0;
+	wn = 0;
+	while((s = b.gets('\n')) != nil){
+		if(s[len s-1] != '\n')
+			s[len s] = '\n';
+		if(s[0] == '.') {
+			for(c=0; c<3 && c < len s && s[c]>' '; c++)
+				n++;
+			s = s[c:];
+		}
+		inword = 0;
+		w = 0;
+		t = 0;
+		do{
+			c = s[t];
+			wordchar = 0;
+			if(alpha(c))
+				wordchar = 1;
+			if(inword && !wordchar){
+				if(c=='\'' && alpha(s[t+1])) {
+					n++;
+					t++;
+					continue;
+				}
+				m = t-w;
+				if(m > 1){
+					buf = s[w:w+m];
+					bout.puts(sprint("%s:#%d,#%d:%s\n", name, wn, n, buf));
+				}
+				inword = 0;
+			}else if(!inword && wordchar){
+				wn = n;
+				w = t;
+				inword = 1;
+			}
+			if(c=='\\' && (alpha(s[t+1]) || s[t+1]=='(')){
+				case(s[t+1]){
+				'(' =>
+					m = 4;
+					break;
+				'f' =>
+					if(s[t+2] == '(')
+						m = 5;
+					else
+						m = 3;
+					break;
+				's' =>
+					if(s[t+2] == '+' || s[t+2]=='-'){
+						if(s[t+3] == '(')
+							m = 6;
+						else
+							m = 4;
+					}else{
+						if(s[t+2] == '(')
+							m = 5;
+						else if(s[t+2]=='1' || s[t+2]=='2' || s[t+2]=='3')
+							m = 4;
+						else
+							m = 3;
+					}
+					break;
+				* =>
+					m = 2;
+				}
+				while(m-- > 0){
+					if(s[t] == '\n')
+						break;
+					n++;
+					t++;
+				}
+				continue;
+			}
+			n++;
+			t ++;
+		}while(c != '\n');
+	}
+	bout.flush();
+}
--- /dev/null
+++ b/appl/acme/acme/bin/src/win.b
@@ -1,0 +1,770 @@
+implement Win;
+
+include "sys.m";
+include "draw.m";
+include "workdir.m";
+include "sh.m";
+include "env.m";
+
+sys : Sys;
+workdir : Workdir;
+env: Env;
+
+OREAD, OWRITE, ORDWR, FORKNS, FORKENV, FORKFD, NEWPGRP, MREPL, FD, UTFmax, pctl, open, read, write, fprint, sprint, fildes, bind, dup, byte2char, utfbytes : import sys;
+
+Win : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+Runeself : con 16r80;
+
+PNPROC, PNGROUP : con iota;
+
+stdout, stderr : ref FD;
+
+drawctxt : ref Draw->Context;
+finish : chan of int;
+
+Lock : adt {
+		c : chan of int;
+
+		init : fn() : ref Lock;
+		lock : fn(l : self ref Lock);
+		unlock : fn(l : self ref Lock);
+};
+
+init(ctxt : ref Draw->Context, argl : list of string)
+{
+	sys = load Sys Sys->PATH;
+	workdir = load Workdir Workdir->PATH;
+	env = load Env Env->PATH;
+	drawctxt = ctxt;
+	stdout = fildes(1);
+	stderr = fildes(2);
+	debuginit();
+	finish = chan[1] of int;
+	spawn main(argl);
+	<-finish;
+}
+
+Lock.init() : ref Lock
+{
+	return ref Lock(chan[1] of int);
+}
+
+Lock.lock(l : self ref Lock)
+{
+	l.c <-= 1;
+}
+
+Lock.unlock(l : self ref Lock)
+{
+	<-l.c;
+}
+
+dlock : ref Lock;
+debfd : ref Sys->FD;
+
+debuginit()
+{
+	# debfd = sys->create("./debugwin", Sys->OWRITE, 8r600);
+	# dlock = Lock.init();
+}
+
+debugpr(nil : string)
+{
+	# fprint(debfd, "%s", s);
+}
+
+debug(nil : string)
+{
+	# dlock.lock();
+	# fprint(debfd, "%s", s);	
+	# dlock.unlock();
+}
+
+exec(cmd : string, argl : list of string)
+{
+	file := cmd;
+	if(len file<4 || file[len file-4:]!=".dis")
+		file += ".dis";
+
+	c := load Command file;
+	if(c == nil) {
+		err := sprint("%r");
+		if(file[0]!='/' && file[0:2]!="./"){
+			c = load Command "/dis/"+file;
+			if(c == nil)
+				err = sprint("%r");
+		}
+		if(c == nil){
+			fprint(stderr, "%s: %s\n", cmd, err);
+			return;
+		}
+	}
+	c->init(drawctxt, argl);
+}
+
+postnote(t : int, pid : int, note : string) : int
+{
+	# fd := open("/prog/" + string pid + "/ctl", OWRITE);
+	fd := open("#p/" + string pid + "/ctl", OWRITE);
+	if (fd == nil)
+		return -1;
+	if (t == PNGROUP)
+		note += "grp";
+	fprint(fd, "%s", note);
+
+	fd = nil;
+	return 0;
+}
+
+sysname(): string
+{
+	fd := sys->open("#c/sysname", sys->OREAD);
+	if(fd == nil)
+		return nil;
+	buf := array[128] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0) 
+		return nil;
+	return string buf[0:n];
+}
+
+EVENTSIZE : con	256;
+
+Event : adt {
+	c1 : int;
+	c2 : int;
+	q0 : int;
+	q1 : int;
+	flag : int;
+	nb : int;
+	nr : int;
+	b : array of byte;
+	r : array of int;
+};
+
+blank : ref Event;
+
+pid : int;
+# pgrpfd : ref FD;
+parentpid : int;
+
+typing : array of byte;
+ntypeb : int;
+ntyper : int;
+ntypebreak : int;
+
+Q : adt {
+	l : ref Lock;
+	p : int;
+	k : int;
+};
+
+q : Q;
+
+newevent(n : int) : ref Event
+{
+	e := ref Event;
+	e.b = array[n*UTFmax+1] of byte;
+	e.r = array[n+1] of int;
+	return e;
+}
+
+main(argv : list of string)
+{
+	program : list of string;
+	fd, ctlfd, eventfd, addrfd, datafd : ref FD;
+	id : int;
+	c : chan of int;
+	name : string;
+
+	sys->pctl(Sys->NEWPGRP, nil);
+	q.l = Lock.init();
+	blank = newevent(2);
+	blank.c1 = 'M';
+	blank.c2 = 'X';
+	blank.q0 = blank.q1 = blank.flag = 0;
+	blank.nb = blank.nr = 1;
+	blank.b[0] = byte ' ';
+	blank.b[1] = byte 0;
+	blank.r[0] = ' ';
+	blank.r[1] = 0;
+	pctl(FORKNS|NEWPGRP, nil);
+	parentpid = pctl(0, nil);
+	program = nil;
+	if(tl argv != nil)
+		program = tl argv;
+	name = nil;
+	if(program == nil){
+		# program = "-i" :: program;
+		program = "sh" :: program;
+		name = sysname();
+	}
+	if(name == nil){
+		prog := hd program;
+		for (n := len prog - 1; n >= 0; n--)
+			if (prog[n] == '/')
+				break;
+		if(n >= 0)
+			name = prog[n+1:];
+		else
+			name = prog;
+		argl := tl argv;
+		if (argl != nil) {
+			for(argl = tl argl; argl != nil && len(name)+1+len(hd argl)<16; argl = tl argl)
+				name += "_" + hd argl;
+		}
+	}
+	if(bind("#|", "/dev/acme", MREPL) < 0)
+		error("pipe");
+	ctlfd = open("/chan/new/ctl", ORDWR);
+	buf := array[12] of byte;
+	if(ctlfd==nil || read(ctlfd, buf, 12)!=12)
+		error("ctl");
+	id = int string buf;
+	buf = nil;
+	env->setenv("acmewin", string id);
+	b := sprint("/chan/%d/tag", id);
+	fd = open(b, OWRITE);
+	write(fd, array of byte " Send Delete", 12);
+	fd = nil;
+	b = sprint("/chan/%d/event", id);
+	eventfd = open(b, ORDWR);
+	b = sprint("/chan/%d/addr", id);
+	addrfd = open(b, ORDWR);
+	b = sprint("/chan/%d/data", id);
+	datafd = open(b, ORDWR); # OCEXEC
+	if(eventfd==nil || addrfd==nil || datafd==nil)
+		error("data files");
+	c = chan of int;
+	spawn run(program, id, c);
+	pid = <-c;
+	# b = sprint("/prog/%d/notepg", pid);
+	# pgrpfd = open(b, OWRITE); # OCEXEC
+	# if(pgrpfd == nil)
+	#	fprint(stdout, "warning: win can't open notepg: %r\n");
+	c <-= 1;
+	fd = open("/dev/acme/data", ORDWR);
+	if(fd == nil)
+		error("/dev/acme/data");
+	wd  := workdir->init();
+	# b = sprint("name %s/-%s\n0\n", wd, name);
+	b = sprint("name %s/-%s\n", wd, name);
+	ab := array of byte b;
+	write(ctlfd, ab, len ab);
+	b = sprint("dumpdir %s/\n", wd);
+	ab = array of byte b;
+	write(ctlfd, ab, len ab);
+	b = sprint("dump %s\n", onestring(argv));
+	ab = array of byte b;
+	write(ctlfd, ab, len ab);
+	ab = nil;
+	spawn stdinx(fd, ctlfd, eventfd, addrfd, datafd);
+	stdoutx(fd, addrfd, datafd);
+}
+
+run(argv : list of string, id : int, c : chan of int)
+{
+	fd0, fd1 : ref FD;
+
+	pctl(FORKENV|FORKFD|NEWPGRP, nil);	# had RFMEM
+	c <-= pctl(0, nil);
+	<-c;
+	pctl(FORKNS, nil);
+	if(bind("/dev/acme/data1", "/dev/cons", MREPL) < 0){
+		fprint(stderr, "can't bind /dev/cons: %r\n");
+		exit;
+	}
+	fd0 = open("/dev/cons", OREAD);
+	fd1 = open("/dev/cons", OWRITE);
+	if(fd0==nil || fd1==nil){
+		fprint(stderr, "can't open /dev/cons: %r\n");
+		exit;
+	}
+	dup(fd0.fd, 0);
+	dup(fd1.fd, 1);
+	dup(fd1.fd, 2);
+	fd0 = fd1 = nil;
+	b := sprint("/chan/%d", id);
+	if(bind(b, "/dev/acme", MREPL) < 0)
+		error("bind /dev/acme");
+	if(bind(sprint("/chan/%d/consctl", id), "/dev/consctl", MREPL) < 0)
+	 	error("bind /dev/consctl");
+	exec(hd argv, argv);
+	exit;
+}
+
+killing : int = 0;
+
+error(s : string)
+{
+	if(s != nil)
+		fprint(stderr, "win: %s: %r\n", s);
+	if (killing)
+		return;
+	killing = 1;
+	s = "kill";
+	if(pid)
+		postnote(PNGROUP, pid, s);
+		# write(pgrpfd, array of byte "hangup", 6);
+	postnote(PNGROUP, parentpid, s);
+	finish <-= 1;
+	exit;
+}
+
+buff := array[8192] of byte;
+bufp : int;
+nbuf : int;
+
+onestring(argv : list of string) : string
+{
+	s : string;
+
+	if(argv == nil)
+		return "";
+	for( ; argv != nil; argv = tl argv){
+		s += hd argv;
+		if (tl argv != nil)
+			s += " ";
+	}
+	return s;
+}
+
+getec(efd : ref FD) : int
+{
+	if(nbuf == 0){
+		nbuf = read(efd, buff, len buff);
+		if(nbuf <= 0)
+			error(nil);
+		bufp = 0;
+	}
+	--nbuf;
+	return int buff[bufp++];
+}
+
+geten(efd : ref FD) : int
+{
+	n, c : int;
+
+	n = 0;
+	while('0'<=(c=getec(efd)) && c<='9')
+		n = n*10+(c-'0');
+	if(c != ' ')
+		error("event number syntax");
+	return n;
+}
+
+geter(efd : ref FD, buf : array of byte) : (int, int)
+{
+	r, m, n, ok : int;
+
+	r = getec(efd);
+	buf[0] = byte r;
+	n = 1;
+	if(r < Runeself)
+		return (r, n);
+	for (;;) {
+		(r, m, ok) = byte2char(buf[0:n], 0);
+		if (m > 0)
+			return (r, n);
+		buf[n++] = byte getec(efd);
+	}
+	return (0, 0);
+}
+
+gete(efd : ref FD, e : ref Event)
+{
+	i, nb : int;
+
+	e.c1 = getec(efd);
+	e.c2 = getec(efd);
+	e.q0 = geten(efd);
+	e.q1 = geten(efd);
+	e.flag = geten(efd);
+	e.nr = geten(efd);
+	if(e.nr > EVENTSIZE)
+		error("event string too long");
+	e.nb = 0;
+	for(i=0; i<e.nr; i++){
+		(e.r[i], nb) = geter(efd, e.b[e.nb:]);
+		e.nb += nb;
+	}
+	e.r[e.nr] = 0;
+	e.b[e.nb] = byte 0;
+	if(getec(efd) != '\n')
+		error("event syntax 2");
+}
+
+nrunes(s : array of byte, nb : int) : int
+{
+	i, n, r, b, ok : int;
+
+	n = 0;
+	for(i=0; i<nb; n++) {
+		(r, b, ok) = byte2char(s, i);
+		if (b == 0)
+			error("not full string in nrunes()");
+		i += b;
+	}
+	return n;
+}
+
+stdinx(fd0 : ref FD, cfd : ref FD, efd : ref FD, afd : ref FD, dfd : ref FD)
+{
+	e, e2, e3, e4 : ref Event;
+
+	e = newevent(EVENTSIZE);
+	e2 = newevent(EVENTSIZE);
+	e3 = newevent(EVENTSIZE);
+	e4 = newevent(EVENTSIZE);
+	for(;;){
+		gete(efd, e);
+		q.l.lock();
+		case(e.c1){
+		'E' =>	# write to body; can't affect us 
+			break;
+		'F' =>	# generated by our actions; ignore 
+			break;
+		'K' or 'M' =>
+			case(e.c2){
+			'R' =>
+				addtype(' ', ntyper, e.b, e.nb, e.nr);
+				sendtype(fd0, 1);
+				break;
+			'I' =>
+				if(e.q0 < q.p)
+					q.p += e.q1-e.q0;
+				else if(e.q0 <= q.p+ntyper)
+					typex(e, fd0, afd, dfd);
+				break;
+			'D' =>
+				q.p -= delete(e);
+				break;
+			'x' or 'X' =>
+				if(e.flag & 2)
+					gete(efd, e2);
+				if(e.flag & 8){
+					gete(efd, e3);
+					gete(efd, e4);
+				}
+				if(e.flag&1 || (e.c2=='x' && e.nr==0 && e2.nr==0)){
+					# send it straight back 
+					fprint(efd, "%c%c%d %d\n", e.c1, e.c2, e.q0, e.q1);
+					break;
+				}
+				if(e.q0==e.q1 && (e.flag&2)){
+					e2.flag = e.flag;
+					*e = *e2;
+				}
+				if(e.flag & 8){
+					if(e.q1 != e.q0){
+						send(e, fd0, cfd, afd, dfd, 0);
+						send(blank, fd0, cfd, afd, dfd, 0);
+					}
+					send(e3, fd0, cfd, afd, dfd, 1);
+				}else	 if(e.q1 != e.q0)
+					send(e, fd0, cfd, afd, dfd, 1);
+				break;
+			'l' or 'L' =>
+				# just send it back 
+				if(e.flag & 2)
+					gete(efd, e2);
+				fprint(efd, "%c%c%d %d\n", e.c1, e.c2, e.q0, e.q1);
+				break;
+			'd' or 'i' =>
+				break;
+			* =>
+				fprint(stdout, "unknown message %c%c\n", e.c1, e.c2);
+				break;
+			}
+		* =>
+			fprint(stdout, "unknown message %c%c\n", e.c1, e.c2);
+			break;
+		}
+		q.l.unlock();
+	}
+}
+
+stdoutx(fd1 : ref FD, afd : ref FD, dfd : ref FD)
+{
+	n, m, w, npart : int;
+	s, t : int;
+	buf, hold, x : array of byte;
+	r, ok : int;
+
+	buf = array[8192+UTFmax+1] of byte;
+	hold = array[UTFmax] of byte;
+	npart = 0;
+	for(;;){
+		n = read(fd1, buf[npart:], 8192);
+		if(n < 0)
+			error(nil);
+		if(n == 0)
+			continue;
+
+		# squash NULs 
+		for (s = 0; s < n; s++)
+			if (buf[npart+s] == byte 0)
+				break;
+		if(s < n){
+			for(t=s; s<n; s++)
+				if(buf[npart+t] == buf[npart+s])	# assign = 
+					t++;
+			n = t;
+		}
+
+		n += npart;
+
+		# hold on to final partial rune 
+		npart = 0;
+		while(n>0 && (int buf[n-1]&16rC0)){
+			--n;
+			npart++;
+			if((int buf[n]&16rC0)!=16r80){
+				if(utfbytes(buf[n:], npart) > 0){
+					(r, w, ok) = byte2char(buf, n);
+					n += w;
+					npart -= w;
+				}
+				break;
+			}
+		}
+		if(n > 0){
+			hold[0:] = buf[n:n+npart];
+			buf[n] = byte 0;
+			q.l.lock();
+			str := sprint("#%d", q.p);
+			x = array of byte str;
+			m = len x;
+			if(write(afd, x, m) != m)
+				error("stdout writing address");
+			x = nil;
+			if(write(dfd, buf, n) != n)
+				error("stdout writing body");
+			q.p += nrunes(buf, n);
+			q.l.unlock();
+			buf[0:] = hold[0:npart];
+		}
+	}
+}
+
+delete(e : ref Event) : int
+{
+	q0, q1 : int;
+	deltap : int;
+
+	q0 = e.q0;
+	q1 = e.q1;
+	if(q1 <= q.p)
+		return e.q1-e.q0;
+	if(q0 >= q.p+ntyper)
+		return 0;
+	deltap = 0;
+	if(q0 < q.p){
+		deltap = q.p-q0;
+		q0 = 0;
+	}else
+		q0 -= q.p;
+	if(q1 > q.p+ntyper)
+		q1 = ntyper;
+	else
+		q1 -= q.p;
+	deltype(q0, q1);
+	return deltap;
+}
+
+addtype(c : int, p0 : int, b : array of byte, nb : int, nr : int)
+{
+	i, w : int;
+	r, ok : int;
+	p : int;
+	b0 : int;
+
+	for(i=0; i<nb; i+=w){
+		(r, w, ok) = byte2char(b, i);
+		if(r==16r7F && c=='K'){
+			if (pid)
+				postnote(PNGROUP, pid, "kill");
+				# write(pgrpfd, array of byte "interrupt", 9);
+			# toss all typing 
+			q.p += ntyper+nr;
+			ntypebreak = 0;
+			ntypeb = 0;
+			ntyper = 0;
+			# buglet:  more than one delete ignored 
+			return;
+		}
+		if(r=='\n' || r==16r04)
+			ntypebreak++;
+	}
+	ot := typing;
+	typing = array[ntypeb+nb] of byte;
+	if(typing == nil)
+		error("realloc");
+	if (ot != nil)
+		typing[0:] = ot[0:ntypeb];
+	ot = nil;
+	if(p0 == ntyper)
+		typing[ntypeb:] = b[0:nb];
+	else{
+		b0 = 0;
+		for(p=0; p<p0 && b0<ntypeb; p++){
+			(r, w, ok) = byte2char(typing[b0:], i);
+			b0 += w;
+		}
+		if(p != p0)
+			error("typing: findrune");
+		typing[b0+nb:] = typing[b0:ntypeb];
+		typing[b0:] = b[0:nb];
+	}
+	ntypeb += nb;
+	ntyper += nr;
+}
+
+sendtype(fd0 : ref FD, raw : int)
+{
+	while(ntypebreak){
+		brkc := 0;
+		i := 0;
+		while(i<ntypeb){
+			if(typing[i]==byte '\n' || typing[i]==byte 16r04){
+				n := i + (typing[i] == byte '\n');
+				i++;
+				if(write(fd0, typing, n) != n)
+					error("sending to program");
+				nr := nrunes(typing, i);
+				if (!raw)
+					q.p += nr;
+				ntyper -= nr;
+				ntypeb -= i;
+				typing[0:] = typing[i:i+ntypeb];
+				i = 0;
+				ntypebreak--;
+				brkc = 1;
+			}else
+				i++;
+		}
+		if (!brkc) {
+			fprint(stdout, "no breakchar\n");
+			ntypebreak = 0;
+		}
+	}
+}
+
+deltype(p0 : int, p1 : int)
+{
+	w : int;
+	p, b0, b1 : int;
+	r, ok : int;
+
+	# advance to p0 
+	b0 = 0;
+	for(p=0; p<p0 && b0<ntypeb; p++){
+		(r, w, ok) = byte2char(typing, b0);
+		b0 += w;
+	}
+	if(p != p0)
+		error("deltype 1");
+	# advance to p1 
+	b1 = b0;
+	for(; p<p1 && b1<ntypeb; p++){
+		(r, w, ok) = byte2char(typing, b1);
+		b1 += w;
+		if(r=='\n' || r==16r04)
+			ntypebreak--;
+	}
+	if(p != p1)
+		error("deltype 2");
+	typing[b0:] = typing[b1:ntypeb];
+	ntypeb -= b1-b0;
+	ntyper -= p1-p0;
+}
+
+typex(e : ref Event, fd0 : ref FD, afd : ref FD, dfd : ref FD)
+{
+	m, n, nr : int;
+	buf : array of byte;
+
+	if(e.nr > 0)
+		addtype(e.c1, e.q0-q.p, e.b, e.nb, e.nr);
+	else{
+		buf = array[128] of byte;
+		m = e.q0;
+		while(m < e.q1){
+			str := sprint("#%d", m);
+			b := array of byte str;
+			n = len b;
+			write(afd, b, n);
+			b = nil;
+			n = read(dfd, buf, len buf);
+			nr = nrunes(buf, n);
+			while(m+nr > e.q1){
+				do; while(n>0 && (int buf[--n]&16rC0)==16r80);
+				--nr;
+			}
+			if(n == 0)
+				break;
+			addtype(e.c1, m-q.p, buf, n, nr);
+			m += nr;
+		}
+	}
+	buf = nil;
+	sendtype(fd0, 0);
+}
+
+send(e : ref Event, fd0 : ref FD, cfd : ref FD, afd : ref FD, dfd : ref FD, donl : int)
+{
+	l, m, n, nr, lastc, end : int;
+	abuf, buf : array of byte;
+
+	buf = array[128] of byte;
+	end = q.p+ntyper;
+	str := sprint("#%d", end);
+	abuf = array of byte str;
+	l = len abuf;
+	write(afd, abuf, l);
+	abuf = nil;
+	if(e.nr > 0){
+		write(dfd, e.b, e.nb);
+		addtype(e.c1, ntyper, e.b, e.nb, e.nr);
+		lastc = e.r[e.nr-1];
+	}else{
+		m = e.q0;
+		lastc = 0;
+		while(m < e.q1){
+			str = sprint("#%d", m);
+			abuf = array of byte str;
+			n = len abuf;
+			write(afd, abuf, n);
+			abuf = nil;
+			n = read(dfd, buf, len buf);
+			nr = nrunes(buf, n);
+			while(m+nr > e.q1){
+				do; while(n>0 && (int buf[--n]&16rC0)==16r80);
+				--nr;
+			}
+			if(n == 0)
+				break;
+			str = sprint("#%d", end);
+			abuf = array of byte str;
+			l = len abuf;
+			write(afd, abuf, l);
+			abuf = nil;
+			write(dfd, buf, n);
+			addtype(e.c1, ntyper, buf, n, nr);
+			lastc = int buf[n-1];
+			m += nr;
+			end += nr;
+		}
+	}
+	if(donl && lastc!='\n'){
+		write(dfd, array of byte "\n", 1);
+		addtype(e.c1, ntyper, array of byte "\n", 1, 1);
+	}
+	write(cfd, array of byte "dot=addr", 8);
+	sendtype(fd0, 0);
+	buf = nil;
+}
+
--- /dev/null
+++ b/appl/acme/acme/bin/src/winm.b
@@ -1,0 +1,791 @@
+implement Win;
+
+include "sys.m";
+include "draw.m";
+include "workdir.m";
+include "sh.m";
+
+sys : Sys;
+workdir : Workdir;
+
+OREAD, OWRITE, ORDWR, FORKNS, FORKENV, FORKFD, NEWPGRP, MREPL, FD, UTFmax, pctl, open, read, write, fprint, sprint, fildes, bind, dup, byte2char, utfbytes : import sys;
+
+Win : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+Runeself : con 16r80;
+
+PNPROC, PNGROUP : con iota;
+
+stdout, stderr : ref FD;
+
+drawctxt : ref Draw->Context;
+
+Lock : adt {
+		cnt : int;
+		chann : chan of int;
+
+		init : fn() : ref Lock;
+		lock : fn(l : self ref Lock);
+		unlock : fn(l : self ref Lock);
+};
+
+lc, uc : chan of ref Lock;
+lockpid : int;
+
+init(ctxt : ref Draw->Context, argl : list of string)
+{
+	sys = load Sys Sys->PATH;
+	workdir = load Workdir Workdir->PATH;
+	drawctxt = ctxt;
+	stdout = fildes(1);
+	stderr = fildes(2);
+	lc = chan of ref Lock;
+	uc = chan of ref Lock;
+	spawn lockmgr();
+	debuginit();
+	main(argl);
+}
+
+lockmgr()
+{
+	l : ref Lock;
+
+	lockpid = pctl(0, nil);
+	for (;;) {
+		alt {
+			l = <- lc =>
+				if (l.cnt++ == 0)
+					l.chann <-= 1;
+			l = <- uc =>
+				if (--l.cnt > 0)
+					l.chann <-= 1;
+		}
+	}
+}
+
+Lock.init() : ref Lock
+{
+	return ref Lock(0, chan of int);
+}
+
+Lock.lock(l : self ref Lock)
+{
+	lc <-= l;
+	<- l.chann;
+}
+
+Lock.unlock(l : self ref Lock)
+{
+	uc <-= l;
+}
+
+dlock : ref Lock;
+debfd : ref Sys->FD;
+
+debuginit()
+{
+	# debfd = sys->create("./debugwin", Sys->OWRITE, 8r600);
+	# dlock = Lock.init();
+}
+
+debugpr(nil : string)
+{
+	# fprint(debfd, "%s", s);
+}
+
+debug(nil : string)
+{
+	# dlock.lock();
+	# fprint(debfd, "%s", s);	
+	# dlock.unlock();
+}
+
+exec(cmd : string, argl : list of string)
+{
+	file := cmd;
+	if(len file<4 || file[len file-4:]!=".dis")
+		file += ".dis";
+
+	c := load Command file;
+	if(c == nil) {
+		err := sprint("%r");
+		if(file[0]!='/' && file[0:2]!="./"){
+			c = load Command "/dis/"+file;
+			if(c == nil)
+				err = sprint("%r");
+		}
+		if(c == nil){
+			fprint(stderr, "%s: %s\n", cmd, err);
+			return;
+		}
+	}
+	c->init(drawctxt, argl);
+}
+
+postnote(t : int, pid : int, note : string) : int
+{
+	# fd := open("/prog/" + string pid + "/ctl", OWRITE);
+	fd := open("#p/" + string pid + "/ctl", OWRITE);
+	if (fd == nil)
+		return -1;
+	if (t == PNGROUP)
+		note += "grp";
+	fprint(fd, "%s", note);
+
+	fd = nil;
+	return 0;
+}
+
+sysname(): string
+{
+	fd := sys->open("#c/sysname", sys->OREAD);
+	if(fd == nil)
+		return nil;
+	buf := array[128] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0) 
+		return nil;
+	return string buf[0:n];
+}
+
+EVENTSIZE : con	256;
+
+Event : adt {
+	c1 : int;
+	c2 : int;
+	q0 : int;
+	q1 : int;
+	flag : int;
+	nb : int;
+	nr : int;
+	b : array of byte;
+	r : array of int;
+};
+
+blank : ref Event;
+
+pid : int;
+# pgrpfd : ref FD;
+parentpid : int;
+
+typing : array of byte;
+ntypeb : int;
+ntyper : int;
+ntypebreak : int;
+
+Q : adt {
+	l : ref Lock;
+	p : int;
+	k : int;
+};
+
+q : Q;
+
+newevent(n : int) : ref Event
+{
+	e := ref Event;
+	e.b = array[n*UTFmax+1] of byte;
+	e.r = array[n+1] of int;
+	return e;
+}
+
+main(argv : list of string)
+{
+	spawn main1(argv);
+	exit;
+}
+
+main1(argv : list of string)
+{
+	program : list of string;
+	fd, ctlfd, eventfd, addrfd, datafd : ref FD;
+	id : int;
+	c : chan of int;
+	name : string;
+
+	q.l = Lock.init();
+	blank = newevent(2);
+	blank.c1 = 'M';
+	blank.c2 = 'X';
+	blank.q0 = blank.q1 = blank.flag = 0;
+	blank.nb = blank.nr = 1;
+	blank.b[0] = byte ' ';
+	blank.b[1] = byte 0;
+	blank.r[0] = ' ';
+	blank.r[1] = 0;
+	pctl(FORKNS|NEWPGRP, nil);
+	parentpid = pctl(0, nil);
+	program = nil;
+	if(tl argv != nil)
+		program = tl argv;
+	name = nil;
+	if(program == nil){
+		# program = "-i" :: program;
+		program = "sh" :: program;
+		name = sysname();
+	}
+	if(name == nil){
+		prog := hd program;
+		for (n := len prog - 1; n >= 0; n--)
+			if (prog[n] == '/')
+				break;
+		if(n >= 0)
+			name = prog[n+1:];
+		else
+			name = prog;
+		argl := tl argv;
+		if (argl != nil) {
+			for(argl = tl argl; argl != nil && len(name)+1+len(hd argl)<16; argl = tl argl)
+				name += "_" + hd argl;
+		}
+	}
+	if(bind("#|", "/dev/acme", MREPL) < 0)
+		error("pipe");
+	ctlfd = open("/chan/new/ctl", ORDWR);
+	buf := array[12] of byte;
+	if(ctlfd==nil || read(ctlfd, buf, 12)!=12)
+		error("ctl");
+	id = int string buf;
+	buf = nil;
+	b := sprint("/chan/%d/tag", id);
+	fd = open(b, OWRITE);
+	write(fd, array of byte " Send Delete", 12);
+	fd = nil;
+	b = sprint("/chan/%d/event", id);
+	eventfd = open(b, ORDWR);
+	b = sprint("/chan/%d/addr", id);
+	addrfd = open(b, ORDWR);
+	b = sprint("/chan/%d/data", id);
+	datafd = open(b, ORDWR); # OCEXEC
+	if(eventfd==nil || addrfd==nil || datafd==nil)
+		error("data files");
+	c = chan of int;
+	spawn run(program, id, c);
+	pid = <-c;
+	# b = sprint("/prog/%d/notepg", pid);
+	# pgrpfd = open(b, OWRITE); # OCEXEC
+	# if(pgrpfd == nil)
+	#	fprint(stdout, "warning: win can't open notepg: %r\n");
+	c <-= 1;
+	fd = open("/dev/acme/data", ORDWR);
+	if(fd == nil)
+		error("/dev/acme/data");
+	wd  := workdir->init();
+	# b = sprint("name %s/-%s\n0\n", wd, name);
+	b = sprint("name %s/-%s\n", wd, name);
+	ab := array of byte b;
+	write(ctlfd, ab, len ab);
+	b = sprint("dumpdir %s/\n", wd);
+	ab = array of byte b;
+	write(ctlfd, ab, len ab);
+	b = sprint("dump %s\n", onestring(argv));
+	ab = array of byte b;
+	write(ctlfd, ab, len ab);
+	ab = nil;
+	spawn stdinx(fd, ctlfd, eventfd, addrfd, datafd);
+	stdoutx(fd, addrfd, datafd);
+}
+
+run(argv : list of string, id : int, c : chan of int)
+{
+	fd0, fd1 : ref FD;
+
+	pctl(FORKENV|FORKFD|NEWPGRP, nil);	# had RFMEM
+	c <-= pctl(0, nil);
+	<-c;
+	pctl(FORKNS, nil);
+	if(bind("/dev/acme/data1", "/dev/cons", MREPL) < 0){
+		fprint(stderr, "can't bind /dev/cons: %r\n");
+		exit;
+	}
+	fd0 = open("/dev/cons", OREAD);
+	fd1 = open("/dev/cons", OWRITE);
+	if(fd0==nil || fd1==nil){
+		fprint(stderr, "can't open /dev/cons: %r\n");
+		exit;
+	}
+	dup(fd0.fd, 0);
+	dup(fd1.fd, 1);
+	dup(fd1.fd, 2);
+	fd0 = fd1 = nil;
+	b := sprint("/chan/%d", id);
+	if(bind(b, "/dev/acme", MREPL) < 0)
+		error("bind /dev/acme");
+	if(bind(sprint("/chan/%d/consctl", id), "/dev/consctl", MREPL) < 0)
+	 	error("bind /dev/consctl");
+	exec(hd argv, argv);
+	exit;
+}
+
+killing : int = 0;
+
+error(s : string)
+{
+	if(s != nil)
+		fprint(stderr, "win: %s: %r\n", s);
+	if (killing)
+		return;
+	killing = 1;
+	s = "kill";
+	if(pid)
+		postnote(PNGROUP, pid, s);
+		# write(pgrpfd, array of byte "hangup", 6);
+	postnote(PNPROC, lockpid, s);
+	postnote(PNGROUP, parentpid, s);
+	exit;
+}
+
+buff := array[8192] of byte;
+bufp : int;
+nbuf : int;
+
+onestring(argv : list of string) : string
+{
+	s : string;
+
+	if(argv == nil)
+		return "";
+	for( ; argv != nil; argv = tl argv){
+		s += hd argv;
+		if (tl argv != nil)
+			s += " ";
+	}
+	return s;
+}
+
+getec(efd : ref FD) : int
+{
+	if(nbuf == 0){
+		nbuf = read(efd, buff, len buff);
+		if(nbuf <= 0)
+			error(nil);
+		bufp = 0;
+	}
+	--nbuf;
+	return int buff[bufp++];
+}
+
+geten(efd : ref FD) : int
+{
+	n, c : int;
+
+	n = 0;
+	while('0'<=(c=getec(efd)) && c<='9')
+		n = n*10+(c-'0');
+	if(c != ' ')
+		error("event number syntax");
+	return n;
+}
+
+geter(efd : ref FD, buf : array of byte) : (int, int)
+{
+	r, m, n, ok : int;
+
+	r = getec(efd);
+	buf[0] = byte r;
+	n = 1;
+	if(r < Runeself)
+		return (r, n);
+	for (;;) {
+		(r, m, ok) = byte2char(buf[0:n], 0);
+		if (m > 0)
+			return (r, n);
+		buf[n++] = byte getec(efd);
+	}
+	return (0, 0);
+}
+
+gete(efd : ref FD, e : ref Event)
+{
+	i, nb : int;
+
+	e.c1 = getec(efd);
+	e.c2 = getec(efd);
+	e.q0 = geten(efd);
+	e.q1 = geten(efd);
+	e.flag = geten(efd);
+	e.nr = geten(efd);
+	if(e.nr > EVENTSIZE)
+		error("event string too long");
+	e.nb = 0;
+	for(i=0; i<e.nr; i++){
+		(e.r[i], nb) = geter(efd, e.b[e.nb:]);
+		e.nb += nb;
+	}
+	e.r[e.nr] = 0;
+	e.b[e.nb] = byte 0;
+	if(getec(efd) != '\n')
+		error("event syntax 2");
+}
+
+nrunes(s : array of byte, nb : int) : int
+{
+	i, n, r, b, ok : int;
+
+	n = 0;
+	for(i=0; i<nb; n++) {
+		(r, b, ok) = byte2char(s, i);
+		if (b == 0)
+			error("not full string in nrunes()");
+		i += b;
+	}
+	return n;
+}
+
+stdinx(fd0 : ref FD, cfd : ref FD, efd : ref FD, afd : ref FD, dfd : ref FD)
+{
+	e, e2, e3, e4 : ref Event;
+
+	e = newevent(EVENTSIZE);
+	e2 = newevent(EVENTSIZE);
+	e3 = newevent(EVENTSIZE);
+	e4 = newevent(EVENTSIZE);
+	for(;;){
+		gete(efd, e);
+		q.l.lock();
+		case(e.c1){
+		'E' =>	# write to body; can't affect us 
+			break;
+		'F' =>	# generated by our actions; ignore 
+			break;
+		'K' or 'M' =>
+			case(e.c2){
+			'R' =>
+				addtype(' ', ntyper, e.b, e.nb, e.nr);
+				sendtype(fd0, 1);
+				break;
+			'I' =>
+				if(e.q0 < q.p)
+					q.p += e.q1-e.q0;
+				else if(e.q0 <= q.p+ntyper)
+					typex(e, fd0, afd, dfd);
+				break;
+			'D' =>
+				q.p -= delete(e);
+				break;
+			'x' or 'X' =>
+				if(e.flag & 2)
+					gete(efd, e2);
+				if(e.flag & 8){
+					gete(efd, e3);
+					gete(efd, e4);
+				}
+				if(e.flag&1 || (e.c2=='x' && e.nr==0 && e2.nr==0)){
+					# send it straight back 
+					fprint(efd, "%c%c%d %d\n", e.c1, e.c2, e.q0, e.q1);
+					break;
+				}
+				if(e.q0==e.q1 && (e.flag&2)){
+					e2.flag = e.flag;
+					*e = *e2;
+				}
+				if(e.flag & 8){
+					if(e.q1 != e.q0){
+						send(e, fd0, cfd, afd, dfd, 0);
+						send(blank, fd0, cfd, afd, dfd, 0);
+					}
+					send(e3, fd0, cfd, afd, dfd, 1);
+				}else	 if(e.q1 != e.q0)
+					send(e, fd0, cfd, afd, dfd, 1);
+				break;
+			'l' or 'L' =>
+				# just send it back 
+				if(e.flag & 2)
+					gete(efd, e2);
+				fprint(efd, "%c%c%d %d\n", e.c1, e.c2, e.q0, e.q1);
+				break;
+			'd' or 'i' =>
+				break;
+			* =>
+				fprint(stdout, "unknown message %c%c\n", e.c1, e.c2);
+				break;
+			}
+		* =>
+			fprint(stdout, "unknown message %c%c\n", e.c1, e.c2);
+			break;
+		}
+		q.l.unlock();
+	}
+}
+
+stdoutx(fd1 : ref FD, afd : ref FD, dfd : ref FD)
+{
+	n, m, w, npart : int;
+	s, t : int;
+	buf, hold, x : array of byte;
+	r, ok : int;
+
+	buf = array[8192+UTFmax+1] of byte;
+	hold = array[UTFmax] of byte;
+	npart = 0;
+	for(;;){
+		n = read(fd1, buf[npart:], 8192);
+		if(n < 0)
+			error(nil);
+		if(n == 0)
+			continue;
+
+		# squash NULs 
+		for (s = 0; s < n; s++)
+			if (buf[npart+s] == byte 0)
+				break;
+		if(s < n){
+			for(t=s; s<n; s++)
+				if(buf[npart+t] == buf[npart+s])	# assign = 
+					t++;
+			n = t;
+		}
+
+		n += npart;
+
+		# hold on to final partial rune 
+		npart = 0;
+		while(n>0 && (int buf[n-1]&16rC0)){
+			--n;
+			npart++;
+			if((int buf[n]&16rC0)!=16r80){
+				if(utfbytes(buf[n:], npart) > 0){
+					(r, w, ok) = byte2char(buf, n);
+					n += w;
+					npart -= w;
+				}
+				break;
+			}
+		}
+		if(n > 0){
+			hold[0:] = buf[n:n+npart];
+			buf[n] = byte 0;
+			q.l.lock();
+			str := sprint("#%d", q.p);
+			x = array of byte str;
+			m = len x;
+			if(write(afd, x, m) != m)
+				error("stdout writing address");
+			x = nil;
+			if(write(dfd, buf, n) != n)
+				error("stdout writing body");
+			q.p += nrunes(buf, n);
+			q.l.unlock();
+			buf[0:] = hold[0:npart];
+		}
+	}
+}
+
+delete(e : ref Event) : int
+{
+	q0, q1 : int;
+	deltap : int;
+
+	q0 = e.q0;
+	q1 = e.q1;
+	if(q1 <= q.p)
+		return e.q1-e.q0;
+	if(q0 >= q.p+ntyper)
+		return 0;
+	deltap = 0;
+	if(q0 < q.p){
+		deltap = q.p-q0;
+		q0 = 0;
+	}else
+		q0 -= q.p;
+	if(q1 > q.p+ntyper)
+		q1 = ntyper;
+	else
+		q1 -= q.p;
+	deltype(q0, q1);
+	return deltap;
+}
+
+addtype(c : int, p0 : int, b : array of byte, nb : int, nr : int)
+{
+	i, w : int;
+	r, ok : int;
+	p : int;
+	b0 : int;
+
+	for(i=0; i<nb; i+=w){
+		(r, w, ok) = byte2char(b, i);
+		if(r==16r7F && c=='K'){
+			if (pid)
+				postnote(PNGROUP, pid, "kill");
+				# write(pgrpfd, array of byte "interrupt", 9);
+			# toss all typing 
+			q.p += ntyper+nr;
+			ntypebreak = 0;
+			ntypeb = 0;
+			ntyper = 0;
+			# buglet:  more than one delete ignored 
+			return;
+		}
+		if(r=='\n' || r==16r04)
+			ntypebreak++;
+	}
+	ot := typing;
+	typing = array[ntypeb+nb] of byte;
+	if(typing == nil)
+		error("realloc");
+	if (ot != nil)
+		typing[0:] = ot[0:ntypeb];
+	ot = nil;
+	if(p0 == ntyper)
+		typing[ntypeb:] = b[0:nb];
+	else{
+		b0 = 0;
+		for(p=0; p<p0 && b0<ntypeb; p++){
+			(r, w, ok) = byte2char(typing[b0:], i);
+			b0 += w;
+		}
+		if(p != p0)
+			error("typing: findrune");
+		typing[b0+nb:] = typing[b0:ntypeb];
+		typing[b0:] = b[0:nb];
+	}
+	ntypeb += nb;
+	ntyper += nr;
+}
+
+sendtype(fd0 : ref FD, raw : int)
+{
+	i, n, nr : int;
+
+	while(ntypebreak){
+		brkc := 0;
+		for(i=0; i<ntypeb; i++)
+			if(typing[i]==byte '\n' || typing[i]==byte 16r04){
+				n = i + (typing[i] == byte '\n');
+				i++;
+				if(write(fd0, typing, n) != n)
+					error("sending to program");
+				nr = nrunes(typing, i);
+				if (!raw)
+					q.p += nr;
+				ntyper -= nr;
+				ntypeb -= i;
+				typing[0:] = typing[i:i+ntypeb];
+				ntypebreak--;
+				brkc = 1;
+			}
+		if (!brkc) {
+			fprint(stdout, "no breakchar\n");
+			ntypebreak = 0;
+		}
+	}
+}
+
+deltype(p0 : int, p1 : int)
+{
+	w : int;
+	p, b0, b1 : int;
+	r, ok : int;
+
+	# advance to p0 
+	b0 = 0;
+	for(p=0; p<p0 && b0<ntypeb; p++){
+		(r, w, ok) = byte2char(typing, b0);
+		b0 += w;
+	}
+	if(p != p0)
+		error("deltype 1");
+	# advance to p1 
+	b1 = b0;
+	for(; p<p1 && b1<ntypeb; p++){
+		(r, w, ok) = byte2char(typing, b1);
+		b1 += w;
+		if(r=='\n' || r==16r04)
+			ntypebreak--;
+	}
+	if(p != p1)
+		error("deltype 2");
+	typing[b0:] = typing[b1:ntypeb];
+	ntypeb -= b1-b0;
+	ntyper -= p1-p0;
+}
+
+typex(e : ref Event, fd0 : ref FD, afd : ref FD, dfd : ref FD)
+{
+	m, n, nr : int;
+	buf : array of byte;
+
+	if(e.nr > 0)
+		addtype(e.c1, e.q0-q.p, e.b, e.nb, e.nr);
+	else{
+		buf = array[128] of byte;
+		m = e.q0;
+		while(m < e.q1){
+			str := sprint("#%d", m);
+			b := array of byte str;
+			n = len b;
+			write(afd, b, n);
+			b = nil;
+			n = read(dfd, buf, len buf);
+			nr = nrunes(buf, n);
+			while(m+nr > e.q1){
+				do; while(n>0 && (int buf[--n]&16rC0)==16r80);
+				--nr;
+			}
+			if(n == 0)
+				break;
+			addtype(e.c1, m-q.p, buf, n, nr);
+			m += nr;
+		}
+	}
+	buf = nil;
+	sendtype(fd0, 0);
+}
+
+send(e : ref Event, fd0 : ref FD, cfd : ref FD, afd : ref FD, dfd : ref FD, donl : int)
+{
+	l, m, n, nr, lastc, end : int;
+	abuf, buf : array of byte;
+
+	buf = array[128] of byte;
+	end = q.p+ntyper;
+	str := sprint("#%d", end);
+	abuf = array of byte str;
+	l = len abuf;
+	write(afd, abuf, l);
+	abuf = nil;
+	if(e.nr > 0){
+		write(dfd, e.b, e.nb);
+		addtype(e.c1, ntyper, e.b, e.nb, e.nr);
+		lastc = e.r[e.nr-1];
+	}else{
+		m = e.q0;
+		lastc = 0;
+		while(m < e.q1){
+			str = sprint("#%d", m);
+			abuf = array of byte str;
+			n = len abuf;
+			write(afd, abuf, n);
+			abuf = nil;
+			n = read(dfd, buf, len buf);
+			nr = nrunes(buf, n);
+			while(m+nr > e.q1){
+				do; while(n>0 && (int buf[--n]&16rC0)==16r80);
+				--nr;
+			}
+			if(n == 0)
+				break;
+			str = sprint("#%d", end);
+			abuf = array of byte str;
+			l = len abuf;
+			write(afd, abuf, l);
+			abuf = nil;
+			write(dfd, buf, n);
+			addtype(e.c1, ntyper, buf, n, nr);
+			lastc = int buf[n-1];
+			m += nr;
+			end += nr;
+		}
+	}
+	if(donl && lastc!='\n'){
+		write(dfd, array of byte "\n", 1);
+		addtype(e.c1, ntyper, array of byte "\n", 1, 1);
+	}
+	write(cfd, array of byte "dot=addr", 8);
+	sendtype(fd0, 0);
+	buf = nil;
+}
+
--- /dev/null
+++ b/appl/acme/acme/edit/guide
@@ -1,0 +1,4 @@
+e file | x '/regexp/' | c 'replacement'
+e 'file:0,$' | x '/.*word.*\n/' | p -n
+e file | pipe command args ...
+New /absolute/file/name
--- /dev/null
+++ b/appl/acme/acme/edit/mkfile
@@ -1,0 +1,24 @@
+<../../../../mkconfig
+
+BIN=$ROOT/acme/edit
+
+DIRS=\
+	src\
+
+TARG=\
+	guide\
+	readme\
+
+BINTARG=${TARG:%=$BIN/%}
+
+all:V:		$TARG
+
+install:V:	$BINTARG
+
+$BIN/guide : guide
+	rm -f $BIN/guide && cp guide $BIN/guide
+
+$BIN/readme : readme
+	rm -f $BIN/readme && cp readme $BIN/readme
+
+<$ROOT/mkfiles/mksubdirs
--- /dev/null
+++ b/appl/acme/acme/edit/readme
@@ -1,0 +1,31 @@
+The programs collected in /acme/edit offer a sam-like command interface
+to acme windows.  The guide file
+	/acme/edit/guide
+holds templates for several editing operations implemented
+by external programs.  These programs, composed in
+a pipeline, refine the sections of a file to be modified.
+Thus in sam when one says
+	x/.*\n/ g/foo/ p
+in /acme/edit one runs
+	x '/.*\n/' | g '/foo/' | p
+The e command, unrelated to e in sam, disambiguates file names, collects
+lists of names, etc., and produces input suitable for the other tools.
+For example:
+	e '/usr/rob/acme:0,$' | x /oldname/ | c /newname/
+changes oldname to newname in all the files loaded in acme whose names match
+the literal text /usr/rob/acme.
+
+The commands in /acme/edit are
+	e
+	x
+	g
+	c
+	d
+	p
+	pipe	(like sam's | , which can't be used for syntactic reasons)
+
+p takes a -n flag analogous to grep's -n.  There is no s command.
+e has a -l flag to produce line numbers instead of the default character numbers.
+Its implementation is poor but sufficient for the mundane job of recreating
+the occasional line number for tools like acid; its use with the other commands
+in this directory is discouraged.
--- /dev/null
+++ b/appl/acme/acme/edit/src/a.b
@@ -1,0 +1,89 @@
+implement Aa;
+
+include "sys.m";
+include "draw.m";
+include "bufio.m";
+
+sys : Sys;
+bufio : Bufio;
+
+ORDWR, FD, open, read, write, seek, sprint, fprint, fildes, byte2char : import sys;
+Iobuf : import bufio;
+
+Aa : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+stdin, stderr : ref FD;
+
+init(nil : ref Draw->Context, argl : list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	stdin = fildes(0);
+	stderr = fildes(2);
+	main(argl);
+}
+
+include "findfile.b";
+
+prog := "a";
+bin : ref Iobuf;
+
+main(argv : list of string)
+{
+	afd, cfd, dfd : ref FD;
+	i, id : int;
+	nf, n, seq, rlen : int;
+	f, tf : array of File;
+	buf, s : string;
+
+	if(len argv != 2){
+		fprint(stderr, "usage: %s 'replacement'\n", prog);
+		exit;
+	}
+
+include "input.b";
+
+	# sort back to original order, backwards
+	qsort(f, nf, BSCMP);
+
+	# change
+	id = -1;
+	afd = nil;
+	cfd = nil;
+	dfd = nil;
+	ab := array of byte hd tl argv;
+	rlen = len ab;
+	for(i=0; i<nf; i++){
+		if(f[i].ok == 0)
+			continue;
+		if(f[i].id != id){
+			if(id > 0){
+				afd = nil;
+				cfd = nil;
+				dfd = nil;
+			}
+			id = f[i].id;
+			buf = sprint("/mnt/acme/%d/addr", id);
+			afd = open(buf, ORDWR);
+			if(afd == nil)
+				rerror(buf);
+			buf = sprint("/mnt/acme/%d/data", id);
+			dfd = open(buf, ORDWR);
+			if(dfd == nil)
+				rerror(buf);
+			buf = sprint("/mnt/acme/%d/ctl", id);
+			cfd = open(buf, ORDWR);
+			if(cfd == nil)
+				rerror(buf);
+			if(write(cfd, array of byte "mark\nnomark\n", 12) != 12)
+				rerror("setting nomark");
+		}
+		if(fprint(afd, "#%d", f[i].q1) < 0)
+			rerror("writing address");
+		if(write(dfd, ab, rlen) != rlen)
+			rerror("writing replacement");
+	}
+	exit;
+}
--- /dev/null
+++ b/appl/acme/acme/edit/src/c.b
@@ -1,0 +1,104 @@
+implement Cc;
+
+include "sys.m";
+include "draw.m";
+include "bufio.m";
+
+sys : Sys;
+bufio : Bufio;
+
+ORDWR, FD, open, read, write, seek, sprint, fprint, fildes, byte2char : import sys;
+Iobuf : import bufio;
+
+Cc : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+stdin, stderr : ref FD;
+
+init(nil : ref Draw->Context, argl : list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	stdin = fildes(0);
+	stderr = fildes(2);
+	main(argl);
+}
+
+prog := "c";
+
+include "findfile.b";
+
+bin : ref Iobuf;
+
+main(argv : list of string)
+{
+	afd, cfd, dfd : ref FD;
+ 	i, j, id : int;
+	r : string;
+	nf, n, seq, rlen : int;
+	f, tf : array of File;
+	buf, s : string;
+
+	if(len argv != 2){
+		fprint(stderr, "usage: %s 'replacement'\n", prog);
+		exit;
+	}
+
+include "input.b";
+
+	# sort back to original order, backwards
+	qsort(f, nf, BSCMP);
+
+	# change
+	id = -1;
+	afd = nil;
+	cfd = nil;
+	dfd = nil;
+	argv1 := hd tl argv;
+	rlen = len argv1;
+	r = argv1;
+	i = 0;
+	for(j=0; j<rlen; j++){
+		r[i] = argv1[j];
+		if(i>0 && r[i-1]=='\\'){
+			if(r[i] == 'n')
+				r[--i] = '\n';
+			else if(r[i]=='\\')
+				r[--i] = '\\';
+		}
+		i++;
+	}
+	rlen = i;
+	ab := array of byte r[0:rlen];
+	rlen = len ab;
+	for(i=0; i<nf; i++){
+		if(f[i].ok == 0)
+			continue;
+		if(f[i].id != id){
+			if(id > 0){
+				afd = cfd = dfd = nil;
+			}
+			id = f[i].id;
+			buf = sprint("/mnt/acme/%d/addr", id);
+			afd = open(buf, ORDWR);
+			if(afd == nil)
+				rerror(buf);
+			buf = sprint("/mnt/acme/%d/data", id);
+			dfd = open(buf, ORDWR);
+			if(dfd == nil)
+				rerror(buf);
+			buf = sprint("/mnt/acme/%d/ctl", id);
+			cfd = open(buf, ORDWR);
+			if(cfd == nil)
+				rerror(buf);
+			if(write(cfd, array of byte "mark\nnomark\n", 12) != 12)
+				rerror("setting nomark");
+		}
+		if(fprint(afd, "#%d,#%d", f[i].q0, f[i].q1) < 0)
+			rerror("writing address");
+		if(write(dfd, ab, rlen) != rlen)
+			rerror("writing replacement");
+	}
+	exit;
+}
--- /dev/null
+++ b/appl/acme/acme/edit/src/d.b
@@ -1,0 +1,30 @@
+implement Dd;
+
+include "sys.m";
+include "draw.m";
+include "sh.m";
+
+Dd : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+init(ctxt : ref Draw->Context, argl : list of string)
+{
+	sys := load Sys Sys->PATH;
+	stderr := sys->fildes(2);
+	if (len argl != 1) {
+		sys->fprint(stderr, "usage : d\n");
+		return;
+	}
+	cmd := "/acme/edit/c";
+	file := cmd + ".dis";
+	c := load Command file;
+	if(c == nil) {
+		sys->fprint(stderr, "%s: %r\n", cmd);
+		return;
+	}
+	argl = nil;
+	argl = "" :: argl;
+	argl = cmd :: argl;
+	c->init(ctxt, argl);
+}
--- /dev/null
+++ b/appl/acme/acme/edit/src/e.b
@@ -1,0 +1,154 @@
+implement Ee;
+
+include "sys.m";
+include "draw.m";
+include "bufio.m";
+
+sys : Sys;
+bufio : Bufio;
+
+ORDWR, FD, open, read, write, seek, sprint, fprint, fildes, byte2char : import sys;
+Iobuf : import bufio;
+
+Ee : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+stdin, stdout, stderr : ref FD;
+
+init(nil : ref Draw->Context, argl : list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	stdin = fildes(0);
+	stdout = fildes(1);
+	stderr = fildes(2);
+	main(argl);
+}
+
+include "findfile.b";
+
+prog := "e";
+
+main(argv : list of string)
+{
+	afd, cfd : ref FD;
+	i, id : int;
+	buf : string;
+	nf, n, lines, l0, l1 : int;
+	f, tf : array of File;
+
+	lines = 0;
+	if(len argv>1 && hd tl argv == "-l"){
+		lines = 1;
+		argv = tl argv;
+	}
+	if(len argv < 2){
+		fprint(stderr, "usage: %s 'file[:address]' ...\n", prog);
+		exit;
+	}
+	nf = 0;
+	f = nil;
+	for(argv = tl argv; argv != nil; argv = tl argv){
+		(n, tf) = findfile(hd argv);
+		if(n == 0)
+			errors("no files match pattern", hd argv);
+		oldf := f;
+		f = array[n+nf] of File;
+		if(f == nil)
+			rerror("out of memory");
+		if (oldf != nil) {
+			f[0:] = oldf[0:nf];
+			oldf = nil;
+		}
+		f[nf:] = tf[0:n];
+		nf += n;
+		tf = nil;
+	}
+
+	# convert to character positions
+	for(i=0; i<nf; i++){
+		id = f[i].id;
+		buf = sprint("/mnt/acme/%d/addr", id);
+		afd = open(buf, ORDWR);
+		if(afd == nil)
+			rerror(buf);
+		buf = sprint("/mnt/acme/%d/ctl", id);
+		cfd = open(buf, ORDWR);
+		if(cfd == nil)
+			rerror(buf);
+		if(write(cfd, array of byte "addr=dot\n", 9) != 9)
+			rerror("setting address to dot");
+		ab := array of byte f[i].addr;
+		if(write(afd, ab, len ab) != len ab){
+			fprint(stderr, "%s: %s:%s is invalid address\n", prog, f[i].name, f[i].addr);
+			f[i].ok = 0;
+			afd = nil;
+			cfd = nil;
+			continue;
+		}
+		seek(afd, big 0, 0);
+		ab = array[24] of byte;
+		if(read(afd, ab, len ab) != 2*12)
+			rerror("reading address");
+		afd = nil;
+		cfd = nil;
+		buf = string ab;
+		ab = nil;
+		f[i].q0 = int buf;
+		f[i].q1 = int buf[12:];
+		f[i].ok = 1;
+	}
+
+	# sort
+	qsort(f, nf, FCMP);
+
+	# print
+	for(i=0; i<nf; i++){
+		if(f[i].ok)
+			if(lines){
+				(l0, l1) = lineno(f[i]);
+				if(l1 > l0)
+					fprint(stdout, "%s:%d,%d\n", f[i].name, l0, l1);
+				else
+					fprint(stdout, "%s:%d\n", f[i].name, l0);
+			}else{
+				if(f[i].q1 > f[i].q0)
+					fprint(stdout, "%s:#%d,#%d\n", f[i].name, f[i].q0, f[i].q1);
+				else
+					fprint(stdout, "%s:#%d\n", f[i].name, f[i].q0);
+			}
+	}
+	exit;
+}
+
+lineno(f : File) : (int, int)
+{
+	b : ref Iobuf;
+	n0, n1, q, r : int;
+	buf : string;
+
+	buf = sprint("/mnt/acme/%d/body", f.id);
+	b = bufio->open(buf, bufio->OREAD);
+	if(b == nil){
+		fprint(stderr, "%s: can't open %s: %r\n", prog, buf);
+		exit;
+	}
+	n0 = 1;
+	n1 = 1;
+	for(q=0; q<f.q1; q++){
+		r = b.getc();
+		if(r == bufio->EOF){
+			fprint(stderr, "%s: early EOF on %s\n", prog, buf);
+			exit;
+		}
+		if(r=='\n'){
+			if(q < f.q0)
+				n0++;
+			if(q+1 < f.q1)
+				n1++;
+		}
+	}
+	b.close();
+	return (n0, n1);
+}
--- /dev/null
+++ b/appl/acme/acme/edit/src/findfile.b
@@ -1,0 +1,210 @@
+File : adt {
+	id : int;
+	seq : int;
+	ok : int;
+	q0, q1 : int;
+	name : string;
+	addr : string;
+};
+
+BSCMP, SCMP, NCMP, FCMP : con iota;
+
+indexfile := "/mnt/acme/index";
+
+dfd: ref Sys->FD;
+debug(s : string)
+{
+	if (dfd == nil)
+		dfd = sys->create("/usr/jrf/acme/debugedit", Sys->OWRITE, 8r600);
+	sys->fprint(dfd, "%s", s);
+}
+
+error(s : string)
+{
+	fprint(stderr, "%s: %s\n", prog, s);
+	exit;
+}
+
+errors(s, t : string)
+{
+	fprint(stderr, "%s: %s %s\n", prog, s, t);
+	exit;
+}
+
+rerror(s : string)
+{
+	fprint(stderr, "%s: %s: %r\n", prog, s);
+	exit;
+}
+
+strcmp(s, t : string) : int
+{
+	if (s < t) return -1;
+	if (s > t) return 1;
+	return 0;
+}
+
+strstr(s, t : string) : int
+{
+	if (t == nil)
+		return 0;
+	n := len t;
+	if (n > len s)
+		return -1;
+	e := len s - n;
+	for (p := 0; p <= e; p++)
+		if (s[p:p+n] == t)
+			return p;
+	return -1;
+}
+
+nrunes(s : array of byte, nb : int) : int
+{
+	i, n, r, b, ok : int;
+
+	n = 0;
+	for(i=0; i<nb; n++) {
+		(r, b, ok) = byte2char(s, i);
+		i += b;
+	}
+	return n;
+}
+
+index : ref Iobuf;
+
+findfile(pat : string) : (int, array of File)
+{
+	line, pat1, pat2 : string;
+	colon, blank : int;
+	n : int;
+	f : array of File;
+
+	if(index == nil)
+		index = bufio->open(indexfile, bufio->OREAD);
+	else
+		index.seek(big 0, 0);
+	if(index == nil)
+		rerror(indexfile);
+	for(colon=0; colon < len pat && pat[colon]!=':'; colon++)
+		;
+	if (colon == len pat) {
+		pat1 = pat;
+		pat2 = ".";
+	}
+	else {
+		pat1 = pat[0:colon];
+		pat2 = pat[colon+1:];
+	}
+	n = 0;
+	f = nil;
+	while((line=index.gets('\n')) != nil){
+		if(len line < 5*12)
+			rerror("bad index file format");
+		line = line[0:len line - 1];
+		for(blank=5*12; blank < len line && line[blank]!=' '; blank++)
+			;
+		if (blank < len line)
+			line = line[0:blank];
+		if(strcmp(line[5*12:], pat1) == 0){
+			# exact match: take that
+			f = nil;	# should also free t->addr's
+			f = array[1] of File;
+			if(f == nil)
+				rerror("out of memory");
+			f[0].id = int line;
+			f[0].name = line[5*12:];
+			f[0].addr = pat2;
+			n = 1;
+			break;
+		}
+		if(strstr(line[5*12:], pat1) >= 0){
+			# partial match: add to list
+			off := f;
+			f = array[n+1] of File;
+			if(f == nil)
+				rerror("out of memory");
+			f[0:] = off[0:n];
+			off = nil;
+			f[n].id = int line;
+			f[n].name = line[5*12:];
+			f[n].addr = pat2;
+			n++;
+		}
+	}
+	return (n, f);
+}
+
+bscmp(a : File, b : File) : int
+{
+	return b.seq - a.seq;
+}
+
+scmp(a : File, b : File) : int
+{
+	return a.seq - b.seq;
+}
+
+ncmp(a : File, b : File) : int
+{
+	return strcmp(a.name, b.name);
+}
+
+fcmp(a : File, b : File) : int
+{
+	x : int;
+
+	if (a.name < b.name)
+		return -1;
+	if (a.name > b.name)
+		return 1;
+	x = a.q0 - b.q0;
+	if(x != 0)
+		return x;
+	return a.q1-b.q1;
+}
+
+gencmp(a : File, b : File, c : int) : int
+{
+	if (c == BSCMP)
+		return bscmp(a, b);
+	if (c == SCMP)
+		return scmp(a, b);
+	if (c == NCMP)
+		return ncmp(a, b);
+	if (c == FCMP)
+		return fcmp(a, b);
+	return 0;
+}
+
+qsort(a : array of File, n : int, c : int)
+{
+	i, j : int;
+	t : File;
+
+	while(n > 1) {
+		i = n>>1;
+		t = a[0]; a[0] = a[i]; a[i] = t;
+		i = 0;
+		j = n;
+		for(;;) {
+			do
+				i++;
+			while(i < n && gencmp(a[i], a[0], c) < 0);
+			do
+				j--;
+			while(j > 0 && gencmp(a[j], a[0], c) > 0);
+			if(j < i)
+				break;
+			t = a[i]; a[i] = a[j]; a[j] = t;
+		}
+		t = a[0]; a[0] = a[j]; a[j] = t;
+		n = n-j-1;
+		if(j >= n) {
+			qsort(a, j, c);
+			a = a[j+1:];
+		} else {
+			qsort(a[j+1:], n, c);
+			n = j;
+		}
+	}
+}
--- /dev/null
+++ b/appl/acme/acme/edit/src/g.b
@@ -1,0 +1,95 @@
+implement Gg;
+
+include "sys.m";
+include "draw.m";
+include "bufio.m";
+
+sys : Sys;
+bufio : Bufio;
+
+ORDWR, FD, open, read, write, seek, sprint, fprint, fildes, byte2char : import sys;
+Iobuf : import bufio;
+
+Gg : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+stdin, stdout, stderr : ref FD;
+
+init(nil : ref Draw->Context, argl : list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	stdin = fildes(0);
+	stdout = fildes(1);
+	stderr = fildes(2);
+	main(argl);
+}
+
+prog := "g";
+
+include "findfile.b";
+
+bin : ref Iobuf;
+
+main(argv : list of string)
+{
+	afd, cfd, dfd : ref FD;
+	i, id, seq : int;
+	nf, n, plen : int;
+	f, tf : array of File;
+	buf, s : string;
+
+	if(len argv!=2 || len hd tl argv==0 || (hd tl argv)[0]!='/'){
+		fprint(stderr, "usage: %s '/regexp/'\n", prog);
+		exit;
+	}
+
+include "input.b";
+
+	# execute regexp
+	id = -1;
+	afd = nil;
+	dfd = nil;
+	cfd = nil;
+	bufb := array of byte hd tl argv;
+	plen = len bufb;
+	for(i=0; i<nf; i++){
+		if(f[i].ok == 0)
+			continue;
+		if(f[i].id != id){
+			if(id > 0){
+				afd = cfd = dfd = nil;
+			}
+			id = f[i].id;
+			buf = sprint("/mnt/acme/%d/addr", id);
+			afd = open(buf, ORDWR);
+			if(afd == nil)
+				rerror(buf);
+			buf = sprint("/mnt/acme/%d/ctl", id);
+			cfd = open(buf, ORDWR);
+			if(cfd == nil)
+				rerror(buf);
+			buf = sprint("/mnt/acme/%d/data", id);
+			dfd = open(buf, ORDWR);
+			if(dfd == nil)
+				rerror(buf);
+		}
+		ab := array of byte f[i].addr;
+		n = len ab;
+		if(write(afd, ab, n)!=n || fprint(cfd, "limit=addr\n")<0){
+			buf = sprint("%s:%s is invalid limit", f[i].name, f[i].addr);
+			rerror(buf);
+		}
+		if(fprint(afd, "#%d", f[i].q0) < 0)
+			rerror("can't set dot");
+		# look for match
+		if(write(afd, bufb, plen) == plen){
+			if(f[i].q0 == f[i].q1)
+				fprint(stdout, "%s:#%d\n", f[i].name, f[i].q0);
+			else
+				fprint(stdout, "%s:#%d,#%d\n", f[i].name, f[i].q0, f[i].q1);
+		}
+	}
+	exit;
+}
--- /dev/null
+++ b/appl/acme/acme/edit/src/i.b
@@ -1,0 +1,88 @@
+implement Ii;
+
+include "sys.m";
+include "draw.m";
+include "bufio.m";
+
+sys : Sys;
+bufio : Bufio;
+
+ORDWR, FD, open, read, write, seek, sprint, fprint, fildes, byte2char : import sys;
+Iobuf : import bufio;
+
+Ii : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+stdin, stderr : ref FD;
+
+init(nil : ref Draw->Context, argl : list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	stdin = fildes(0);
+	stderr = fildes(2);
+	main(argl);
+}
+
+prog := "i";
+
+include "findfile.b";
+
+bin : ref Iobuf;
+
+main(argv : list of string)
+{
+	afd, cfd, dfd : ref FD;
+	i, id : int;
+	nf, n, seq, rlen : int;
+	f, tf : array of File;
+	s, buf : string;
+
+	if(len argv != 2){
+		fprint(stderr, "usage: %s 'replacement'\n", prog);
+		exit;
+	}
+	
+include "input.b";
+
+	# sort back to original order, backwards
+	qsort(f, nf, BSCMP);
+
+	# change 
+	id = -1;
+	afd = nil;
+	cfd = nil;
+	dfd = nil;
+	ab := array of byte hd tl argv;
+	rlen = len ab;
+	for(i=0; i<nf; i++){
+		if(f[i].ok == 0)
+			continue;
+		if(f[i].id != id){
+			if(id > 0){
+				afd = cfd = dfd = nil;
+			}
+			id = f[i].id;
+			buf = sprint("/mnt/acme/%d/addr", id);
+			afd = open(buf, ORDWR);
+			if(afd == nil)
+				rerror(buf);
+			buf = sprint("/mnt/acme/%d/data", id);
+			dfd = open(buf, ORDWR);
+			if(dfd == nil)
+				rerror(buf);
+			buf = sprint("/mnt/acme/%d/ctl", id);
+			cfd = open(buf, ORDWR);
+			if(cfd == nil)
+				rerror(buf);
+			if(write(cfd, array of byte "mark\nnomark\n", 12) != 12)
+				rerror("setting nomark");
+		}
+		if(fprint(afd, "#%d", f[i].q0) < 0)
+			rerror("writing address");
+		if(write(dfd, ab, rlen) != rlen)
+			rerror("writing replacement");
+	}
+	exit;
+}
--- /dev/null
+++ b/appl/acme/acme/edit/src/input.b
@@ -1,0 +1,84 @@
+	bin = bufio->fopen(stdin, bufio->OREAD);
+	seq = 0;
+	nf = 0;
+	f = nil;
+	while((s=bin.gets('\n')) != nil){
+		s = s[0:len s - 1];
+		(n, tf) = findfile(s);
+		if(n == 0)
+			errors("no files match input", s);
+		for(i=0; i<n; i++)
+			tf[i].seq = seq++;
+		off := f;
+		f = array[n+nf] of File;
+		if(f == nil)
+			rerror("out of memory");
+		if (off != nil) {
+			f[0:] = off[0:nf];
+			off = nil;
+		}
+		f[nf:] = tf[0:n];
+		nf += n;
+		tf = nil;
+	}
+
+	# sort by file name
+	qsort(f, nf, NCMP);
+
+	# convert to character positions if necessary
+	for(i=0; i<nf; i++){
+		f[i].ok = 1;
+		# see if it's easy
+		s = f[i].addr;
+		if(s[0]=='#'){
+			s = s[1:];
+			n = 0;
+			while(len s > 0 && '0'<=s[0] && s[0]<='9'){
+				n = n*10+(s[0]-'0');
+				s = s[1:];
+			}
+			f[i].q0 = n;
+			if(len s == 0){
+				f[i].q1 = n;
+				continue;
+			}
+			if(s[0] == ',') {
+				s = s[1:];
+				n = 0;
+				while(len s > 0 && '0'<=s[0] && s[0]<='9'){
+					n = n*10+(s[0]-'0');
+					s = s[1:];
+				}
+				f[i].q1 = n;
+				if(len s == 0)
+					continue;
+			}
+		}
+		id = f[i].id;
+		buf = sprint("/chan/%d/addr", id);
+		afd = open(buf, ORDWR);
+		if(afd == nil)
+			rerror(buf);
+		buf = sprint("/chan/%d/ctl", id);
+		cfd = open(buf, ORDWR);
+		if(cfd == nil)
+			rerror(buf);
+		if(write(cfd, array of byte "addr=dot\n", 9) != 9)
+			rerror("setting address to dot");
+		ab := array of byte f[i].addr;
+		if(write(afd, ab, len ab) != len ab){
+			fprint(stderr, "%s: %s:%s is invalid address\n", prog, f[i].name, f[i].addr);
+			f[i].ok = 0;
+			afd = cfd = nil;
+			continue;
+		}
+		seek(afd, big 0, 0);
+		bbuf := array[2*12] of byte;
+		if(read(afd, bbuf, len bbuf) != 2*12)
+			rerror("reading address");
+		afd = cfd = nil;
+		buf = string bbuf;
+		bbuf = nil;
+		f[i].q0 = int buf;
+		f[i].q1 = int buf[12:];
+	}
--- /dev/null
+++ b/appl/acme/acme/edit/src/mkfile
@@ -1,0 +1,23 @@
+<../../../../../mkconfig
+
+TARG=\
+	a.dis\
+	c.dis\
+	d.dis\
+	e.dis\
+	g.dis\
+	i.dis\
+	p.dis\
+	x.dis\
+	pipe.dis\
+
+MODULES=\
+	
+SYSMODULES=\
+	sh.m\
+	sys.m\
+	draw.m\
+
+DISBIN=$ROOT/acme/edit
+
+<$ROOT/mkfiles/mkdis
--- /dev/null
+++ b/appl/acme/acme/edit/src/p.b
@@ -1,0 +1,109 @@
+implement Pp;
+
+include "sys.m";
+include "draw.m";
+include "bufio.m";
+
+sys : Sys;
+bufio : Bufio;
+
+ORDWR, FD, open, read, write, seek, sprint, fprint, fildes, byte2char : import sys;
+Iobuf : import bufio;
+
+Pp : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+stdin, stdout, stderr : ref FD;
+
+init(nil : ref Draw->Context, argl : list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	stdin = fildes(0);
+	stdout = fildes(1);
+	stderr = fildes(2);
+	main(argl);
+}
+
+include "findfile.b";
+
+prog := "p";
+bin : ref Iobuf;
+
+main(argv : list of string)
+{
+	afd, cfd, dfd : ref FD;
+	i, id : int;
+	m, nr, nf, n, nflag, seq : int;
+	f, tf : array of File;
+	buf, s : string;
+
+	nflag = 0;
+	if(len argv==2 && hd tl argv == "-n"){
+		argv = tl argv;
+		nflag = 1;
+	}
+	if(len argv != 1){
+		fprint(stderr, "usage: %s [-n]\n", prog);
+		exit;
+	}
+	
+include "input.b";
+
+	# sort back to original order
+	qsort(f, nf, SCMP);
+
+	# print
+	id = -1;
+	afd = nil;
+	cfd = nil;
+	dfd = nil;
+	for(i=0; i<nf; i++){
+		if(f[i].ok == 0)
+			continue;
+		if(f[i].id != id){
+			if(id > 0){
+				afd = cfd = dfd = nil;
+			}
+			id = f[i].id;
+			buf = sprint("/mnt/acme/%d/addr", id);
+			afd = open(buf, ORDWR);
+			if(afd == nil)
+				rerror(buf);
+			buf = sprint("/mnt/acme/%d/data", id);
+			dfd = open(buf, ORDWR);
+			if(dfd == nil)
+				rerror(buf);
+			buf = sprint("/mnt/acme/%d/ctl", id);
+			cfd = open(buf, ORDWR);
+			if(cfd == nil)
+				rerror(buf);
+		}
+		if(nflag){
+			if(f[i].q1 > f[i].q0)
+				fprint(stdout, "%s:#%d,#%d: ", f[i].name, f[i].q0, f[i].q1);
+			else
+				fprint(stdout, "%s:#%d: ", f[i].name, f[i].q0);
+		}
+		m = f[i].q0;
+		while(m < f[i].q1){
+			if(fprint(afd, "#%d", m) < 0){
+				fprint(stderr, "%s: %s:%s is invalid address\n", prog, f[i].name, f[i].addr);
+				continue;
+			}
+			bbuf := array[512] of byte;
+			n = read(dfd, bbuf, len buf);
+			nr = nrunes(bbuf, n);
+			while(m+nr > f[i].q1){
+				do; while(n>0 && (int bbuf[--n]&16rC0)==16r80);
+				--nr;
+			}
+			if(n == 0)
+				break;
+			write(stdout, bbuf, n);
+			m += nr;
+		}
+	}
+	exit;
+}
--- /dev/null
+++ b/appl/acme/acme/edit/src/pipe.b
@@ -1,0 +1,212 @@
+implement Pipe;
+
+include "sys.m";
+include "draw.m";
+include "bufio.m";
+include "sh.m";
+
+sys : Sys;
+bufio : Bufio;
+
+UTFmax, ORDWR, NEWFD, FD, open, read, write, seek, sprint, fprint, fildes, byte2char, pipe, dup, pctl : import sys;
+Iobuf : import bufio;
+
+Pipe : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+stdin, stderr : ref FD;
+pipectxt : ref Draw->Context;
+
+init(ctxt : ref Draw->Context, argl : list of string)
+{
+	pipectxt = ctxt;
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	stdin = fildes(0);
+	stderr = fildes(2);
+	main(argl);
+}
+
+include "findfile.b";
+
+prog := "pipe";
+bin : ref Iobuf;
+
+main(argv : list of string)
+{
+	afd, dfd, cfd : ref FD;
+	nf, nc, nr, npart : int;
+	p1, p2 : array of ref FD;
+	i, n, id, seq : int;
+	buf : string;
+	tmp, data : array of byte;
+	s : string;
+	r, s0 : int;
+	f, tf : array of File;
+	q, q0, q1 : int;
+	cpid : chan of int;
+	w, ok : int;
+
+	if(len argv < 2){
+		fprint(stderr, "usage: pipe command\n");
+		exit;
+	}
+
+include "input.b";
+
+	# sort back to original order
+	qsort(f, nf, SCMP);
+
+	# pipe
+	id = -1;
+	afd = nil;
+	cfd = nil;
+	dfd = nil;
+	tmp = array[8192+UTFmax] of byte;
+	if(tmp == nil)
+		error("malloc");
+	cpid = chan of int;
+	for(i=0; i<nf; i++){
+		if(f[i].id != id){
+			if(id > 0){
+				afd = cfd = dfd = nil;
+			}
+			id = f[i].id;
+			buf = sprint("/mnt/acme/%d/addr", id);
+			afd = open(buf, ORDWR);
+			if(afd == nil)
+				rerror(buf);
+			buf = sprint("/mnt/acme/%d/data", id);
+			dfd = open(buf, ORDWR);
+			if(dfd == nil)
+				rerror(buf);
+			buf = sprint("/mnt/acme/%d/ctl", id);
+			cfd = open(buf, ORDWR);
+			if(cfd == nil)
+				rerror(buf);
+			if(write(cfd, array of byte "mark\nnomark\n", 12) != 12)
+				rerror("setting nomark");
+		}
+
+		if(fprint(afd, "#%ud", f[i].q0) < 0)
+			rerror("writing address");
+
+		q0 = f[i].q0;
+		q1 = f[i].q1;
+		# suck up data
+		data = array[(q1-q0)*UTFmax+1] of byte;
+		if(data == nil)
+			error("malloc failed\n");
+		s0 = 0;
+		q = q0;
+		bbuf := array[12] of byte;
+		while(q < q1){
+			nc = read(dfd, data[s0:], (q1-q)*UTFmax);
+			if(nc <= 0)
+				error("read error from acme");
+			seek(afd, big 0, 0);
+			if(read(afd, bbuf, 12) != 12)
+				rerror("reading address");
+			q = int string bbuf;
+			s0 += nc;
+		}
+		bbuf = nil;
+		s0 = 0;
+		for(nr=0; nr<q1-q0; nr++) {
+			(r, w, ok) = byte2char(data, s0);
+			s0 += w;
+		}
+
+		p1 = array[2] of ref FD;
+		p2 = array[2] of ref FD;
+		if(pipe(p1)<0 || pipe(p2)<0)
+			error("pipe");
+
+		spawn run(tl argv, p1[0], p2[1], cpid);
+		<-cpid;
+		p1[0] = nil;
+		p2[1] = nil;
+
+		spawn send(data, s0, p1[1]);
+		p1[1] = nil;
+
+		# put back data
+		if(fprint(afd, "#%d,#%d", q0, q1) < 0)
+			rerror("writing address");
+
+		npart = 0;
+		q1 = q0;
+		while((nc = read(p2[0], tmp[npart:], 8192)) > 0){
+			nc += npart;
+			s0 = 0;
+			while(s0 <= nc-UTFmax){
+				(r, w, ok) = byte2char(tmp, s0);
+				s0 += w;
+				q1++;
+			}
+			if(s0 > 0)
+				if(write(dfd, tmp, s0) != s0)
+					error("write error to acme");
+			npart = nc - s0;
+			tmp[0:] = tmp[s0:s0+npart];
+		}
+		p2[0] = nil;
+		if(npart){
+			s0 = 0;
+			while(s0 < npart){
+				(r, w, ok) = byte2char(tmp, s0);
+				s0 += w;
+				q1++;
+			}
+			if(write(dfd, tmp, npart) != npart)
+				error("write error to acme");
+		}
+		if(fprint(afd, "#%d,#%d", q0, q1) < 0)
+			rerror("writing address");
+		if(fprint(cfd, "dot=addr\n") < 0)
+			rerror("writing dot");
+		data = nil;
+	}
+}
+
+run(argv : list of string, p1, p2 : ref FD, c : chan of int)
+{
+	pctl(NEWFD, 0::1::2::p1.fd::p2.fd::nil);
+	dup(p1.fd, 0);
+	dup(p2.fd, 1);
+	c <-= pctl(0, nil);
+	exec(hd argv, argv);
+	fprint(stderr, "can't exec");
+	exit;
+}
+
+send(buf : array of byte, nbuf : int, fd : ref FD)
+{
+	if(write(fd, buf, nbuf) != nbuf)
+		error("write error to process");
+	fd = nil;
+}
+
+exec(cmd : string, argl : list of string)
+{
+	file := cmd;
+	if(len file<4 || file[len file-4:]!=".dis")
+		file += ".dis";
+
+	c := load Command file;
+	if(c == nil) {
+		err := sys->sprint("%r");
+		if(file[0]!='/' && file[0:2]!="./"){
+			c = load Command "/dis/"+file;
+			if(c == nil)
+				err = sys->sprint("%r");
+		}
+		if(c == nil){
+			sys->fprint(stderr, "%s: %s\n", cmd, err);
+			return;
+		}
+	}
+
+	c->init(pipectxt, argl);
+}
--- /dev/null
+++ b/appl/acme/acme/edit/src/x.b
@@ -1,0 +1,123 @@
+implement Xx;
+
+include "sys.m";
+include "draw.m";
+include "bufio.m";
+
+sys : Sys;
+bufio : Bufio;
+
+ORDWR, FD, open, read, write, seek, sprint, fprint, fildes, byte2char : import sys;
+Iobuf : import bufio;
+
+Xx : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+stdin, stdout, stderr : ref FD;
+
+init(nil : ref Draw->Context, argl : list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	stdin = fildes(0);
+	stdout = fildes(1);
+	stderr = fildes(2);
+	main(argl);
+}
+
+include "findfile.b";
+
+prog := "x";
+bin : ref Iobuf;
+
+main(argv : list of string)
+{
+	afd, cfd, dfd : ref FD;
+	i, id, seq : int;
+	nf, n, plen : int;
+	addr, aq0, aq1, matched : int;
+	f, tf : array of File;
+	buf, s : string;
+	bbuf0 : array of byte;
+
+	if(len argv!=2 || len hd tl argv==0 || (hd tl argv)[0]!='/'){
+		fprint(stderr, "usage: %s '/regexp/'\n", prog);
+		exit;
+	}
+
+include "input.b";
+
+	# execute regexp
+	id = -1;
+	afd = nil;
+	dfd = nil;
+	cfd = nil;
+	for(i=0; i<nf; i++){
+		if(f[i].ok == 0)
+			continue;
+		if(f[i].id != id){
+			if(id > 0){
+				afd = cfd = dfd = nil;
+			}
+			id = f[i].id;
+			buf = sprint("/mnt/acme/%d/addr", id);
+			afd = open(buf, ORDWR);
+			if(afd == nil)
+				rerror(buf);
+			buf = sprint("/mnt/acme/%d/ctl", id);
+			cfd = open(buf, ORDWR);
+			if(cfd == nil)
+				rerror(buf);
+			buf = sprint("/mnt/acme/%d/data", id);
+			dfd = open(buf, ORDWR);
+			if(dfd == nil)
+				rerror(buf);
+		}
+		bbuf0 = array of byte f[i].addr;
+		n = len bbuf0;
+		if(write(afd, bbuf0, n)!=n || fprint(cfd, "limit=addr\n")<0){
+			buf = sprint("%s:%s is invalid limit", f[i].name, f[i].addr);
+			rerror(buf);
+		}
+		if(fprint(afd, "#%d", f[i].q0) < 0)
+			rerror("can't set address");
+		if(fprint(cfd, "dot=addr") < 0)
+			rerror("can't unset dot");
+		addr = f[i].q0-1;
+		bbuf := array of byte hd tl argv;
+		plen = len bbuf;
+		matched = 0;
+		# scan for matches
+		for(;;){
+			if(write(afd, bbuf, plen) != plen)
+				break;
+			seek(afd, big 0, 0);
+			bbuf0 = array[2*12] of byte;
+			if(read(afd, bbuf0, len bbuf0) != 2*12)
+				rerror("reading address");
+			buf = string bbuf0;
+			bbuf0 = nil;
+			aq0 = int buf;
+			aq1 = int buf[12:];
+			if(matched && aq1==aq0 && addr==aq1){	# repeated null match; advance
+				matched = 0;
+				addr++;
+				if(addr > f[i].q1)
+					break;
+				if(fprint(afd, "#%d", addr) < 0)
+					rerror("writing address");
+				continue;
+			}
+			matched = 1;
+			if(aq0<addr || aq0>=f[i].q1 || aq1>f[i].q1)
+				break;
+			addr = aq1;
+			if(aq0 == aq1)
+				fprint(stdout, "%s:#%d\n", f[i].name, aq0);
+			else
+				fprint(stdout, "%s:#%d,#%d\n", f[i].name, aq0, aq1);
+		}
+	}
+	exit;
+}
--- /dev/null
+++ b/appl/acme/acme/edit/src/xxx.b
@@ -1,0 +1,327 @@
+implement Ee;
+
+include "sys.m";
+include "draw.m";
+include "bufio.m";
+
+sys : Sys;
+bufio : Bufio;
+
+ORDWR, FD, open, read, write, seek, sprint, fprint, fildes, byte2char : import sys;
+Iobuf : import bufio;
+
+Ee : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+stdin, stdout, stderr : ref FD;
+
+init(ctxt : ref Draw->Context, argl : list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	stdin = fildes(0);
+	stdout = fildes(1);
+	stderr = fildes(2);
+	main(argl);
+}
+
+File : adt {
+	id : int;
+	seq : int;
+	ok : int;
+	q0, q1 : int;
+	name : string;
+	addr : string;
+};
+
+BSCMP, SCMP, NCMP, FCMP : con iota;
+
+indexfile := "/usr/jrf/tmp/index";
+
+dfd: ref Sys->FD;
+debug(s : string)
+{
+	if (dfd == nil)
+		dfd = sys->create("/usr/jrf/acme/debugedit", Sys->OWRITE, 8r600);
+	sys->fprint(dfd, "%s", s);
+}
+
+error(s : string)
+{
+debug(sys->sprint("error %s\n", s));
+	fprint(stderr, "%s: %s\n", prog, s);
+	exit;
+}
+
+errors(s, t : string)
+{
+debug(sys->sprint("errors %s %s\n", s, t));
+	fprint(stderr, "%s: %s %s\n", prog, s, t);
+	exit;
+}
+
+rerror(s : string)
+{
+debug(sys->sprint("rerror %s\n", s));
+	fprint(stderr, "%s: %s: %r\n", prog, s);
+	exit;
+}
+
+strcmp(s, t : string) : int
+{
+	if (s < t) return -1;
+	if (s > t) return 1;
+	return 0;
+}
+
+strstr(s, t : string) : int
+{
+	if (t == nil)
+		return 0;
+	n := len t;
+	if (n > len s)
+		return -1;
+	e := len s - n;
+	for (p := 0; p <= e; p++)
+		if (s[p:p+n] == t)
+			return p;
+	return -1;
+}
+
+nrunes(s : array of byte, nb : int) : int
+{
+	i, n, r, b, ok : int;
+
+	n = 0;
+	for(i=0; i<nb; n++) {
+		(r, b, ok) = byte2char(s, i);
+		i += b;
+	}
+	return n;
+}
+
+index : ref Iobuf;
+
+findfile(pat : string) : (int, array of File)
+{
+	line, pat1, pat2 : string;
+	colon, blank : int;
+	n : int;
+	f : array of File;
+
+	if(index == nil)
+		index = bufio->open(indexfile, bufio->OREAD);
+	else
+		index.seek(0, 0);
+	if(index == nil)
+		rerror(indexfile);
+	for(colon=0; colon < len pat && pat[colon]!=':'; colon++)
+		;
+	if (colon == len pat) {
+		pat1 = pat;
+		pat2 = ".";
+	}
+	else {
+		pat1 = pat[0:colon];
+		pat2 = pat[colon+1:];
+	}
+	n = 0;
+	f = nil;
+	while((line=index.gets('\n')) != nil){
+		if(len line < 5*12)
+			rerror("bad index file format");
+		line = line[0:len line - 1];
+		for(blank=5*12; blank < len line && line[blank]!=' '; blank++)
+			;
+		if (blank < len line)
+			line = line[0:blank];
+		if(strcmp(line[5*12:], pat1) == 0){
+			# exact match: take that
+			f = nil;	# should also free t->addr's
+			f = array[1] of File;
+			if(f == nil)
+				rerror("out of memory");
+			f[0].id = int line;
+			f[0].name = line[5*12:];
+			f[0].addr = pat2;
+			n = 1;
+			break;
+		}
+		if(strstr(line[5*12:], pat1) >= 0){
+			# partial match: add to list
+			off := f;
+			f = array[n+1] of File;
+			if(f == nil)
+				rerror("out of memory");
+			f[0:] = off[0:n];
+			off = nil;
+			f[n].id = int line;
+			f[n].name = line[5*12:];
+			f[n].addr = pat2;
+			n++;
+		}
+	}
+	return (n, f);
+}
+
+bscmp(a : File, b : File) : int
+{
+	return b.seq - a.seq;
+}
+
+scmp(a : File, b : File) : int
+{
+	return a.seq - b.seq;
+}
+
+ncmp(a : File, b : File) : int
+{
+	return strcmp(a.name, b.name);
+}
+
+fcmp(a : File, b : File) : int
+{
+	x : int;
+
+	if (a.name < b.name)
+		return -1;
+	if (a.name > b.name)
+		return 1;
+	x = a.q0 - b.q0;
+	if(x != 0)
+		return x;
+	return a.q1-b.q1;
+}
+
+gencmp(a : File, b : File, c : int) : int
+{
+	if (c == BSCMP)
+		return bscmp(a, b);
+	if (c == SCMP)
+		return scmp(a, b);
+	if (c == NCMP)
+		return ncmp(a, b);
+	if (c == FCMP)
+		return fcmp(a, b);
+	return 0;
+}
+
+qsort(a : array of File, n : int, c : int)
+{
+	i, j : int;
+	t : File;
+
+	while(n > 1) {
+		i = n>>1;
+		t = a[0]; a[0] = a[i]; a[i] = t;
+		i = 0;
+		j = n;
+		for(;;) {
+			do
+				i++;
+			while(i < n && gencmp(a[i], a[0], c) < 0);
+			do
+				j--;
+			while(j > 0 && gencmp(a[j], a[0], c) > 0);
+			if(j < i)
+				break;
+			t = a[i]; a[i] = a[j]; a[j] = t;
+		}
+		t = a[0]; a[0] = a[j]; a[j] = t;
+		n = n-j-1;
+		if(j >= n) {
+			qsort(a, j, c);
+			a = a[j+1:];
+		} else {
+			qsort(a[j+1:], n, c);
+			n = j;
+		}
+	}
+}
+
+prog := "e";
+
+main(argv : list of string)
+{
+	afd, cfd : ref FD;
+	i, id : int;
+	buf : string;
+	nf, n, lines, l0, l1 : int;
+	f, tf : array of File;
+
+	if(len argv < 2){
+debug(sys->sprint("usage\n"));
+		fprint(stderr, "usage: %s 'file[:address]' ...\n", prog);
+		exit;
+	}
+	nf = 0;
+	f = nil;
+	for(argv = tl argv; argv != nil; argv = tl argv){
+		(n, tf) = findfile(hd argv);
+		if(n == 0)
+			errors("no files match pattern", hd argv);
+		oldf := f;
+		f = array[n+nf] of File;
+		if(f == nil)
+			rerror("out of memory");
+		if (oldf != nil) {
+			f[0:] = oldf[0:nf];
+			oldf = nil;
+		}
+		f[nf:] = tf[0:n];
+		nf += n;
+		tf = nil;
+	}
+debug(sys->sprint("nf=%d\n", nf));
+	# convert to character positions
+	for(i=0; i<nf; i++){
+		id = f[i].id;
+		buf = sprint("/mnt/acme/%d/addr", id);
+		# afd = open(buf, ORDWR);
+		# if(afd == nil)
+			# rerror(buf);
+		buf = sprint("/mnt/acme/%d/ctl", id);
+		# cfd = open(buf, ORDWR);
+		# if(cfd == nil)
+			# rerror(buf);
+		if(0 && write(cfd, array of byte "addr=dot\n", 9) != 9)
+			rerror("setting address to dot");
+		ab := array of byte f[i].addr;
+		if(0 && write(afd, ab, len ab) != len ab){
+			fprint(stderr, "%s: %s:%s is invalid address\n", prog, f[i].name, f[i].addr);
+			f[i].ok = 0;
+			afd = nil;
+			cfd = nil;
+			continue;
+		}
+		# seek(afd, 0, 0);
+		ab = array[24] of byte;
+		if(0 && read(afd, ab, len ab) != 2*12)
+			rerror("reading address");
+		afd = nil;
+		cfd = nil;
+		# buf = string ab;
+		ab = nil;
+		f[i].q0 = 0; 	# int buf;
+		f[i].q1 = 5;		# int buf[12:];
+		f[i].ok = 1;
+debug(sys->sprint("q0=%d q1=%d\n", f[i].q0, f[i].q1));
+	}
+
+	# sort
+	# qsort(f, nf, FCMP);
+
+	# print
+	for(i=0; i<nf; i++){
+		if(f[i].ok)
+			{
+				if(f[i].q1 > f[i].q0)
+					fprint(stdout, "%s:#%d,#%d\n", f[i].name, f[i].q0, f[i].q1);
+				else
+					fprint(stdout, "%s:#%d\n", f[i].name, f[i].q0);
+			}
+	}
+debug("e exiting\n");
+	exit;
+}
--- /dev/null
+++ b/appl/acme/acme/mail/guide
@@ -1,0 +1,5 @@
+Mail /mail/box/$user/stored
+Mail
+Mailpop3
+mkbox /mail/box/$user/new_box
+mail -'x' someaddress
--- /dev/null
+++ b/appl/acme/acme/mail/mkbox.b
@@ -1,0 +1,25 @@
+implement Mkbox;
+
+include "sys.m";
+include "draw.m";
+
+sys : Sys;
+
+FD : import sys;
+
+Mkbox : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+init(nil : ref Draw->Context, argl : list of string)
+{
+	sys = load Sys Sys->PATH;
+	for (argl = tl argl; argl != nil; argl = tl argl) {
+		nm := hd argl;
+		(ok, dir) := sys->stat(nm);
+		if (ok < 0) {
+			fd := sys->create(nm, Sys->OREAD, 8r600);
+			fd = nil;
+		}
+	}
+}
--- /dev/null
+++ b/appl/acme/acme/mail/mkfile
@@ -1,0 +1,34 @@
+<../../../../mkconfig
+
+BIN=$ROOT/acme/mail
+
+DIRS=\
+	src\
+
+TARG=\
+	guide\
+	readme\
+	mkbox.dis\
+
+MODULES=\
+
+SYSMODULES=\
+	sys.m\
+	draw.m\
+
+BINTARG=${TARG:%=$BIN/%}
+
+DISBIN=$ROOT/acme/mail
+
+all:V:		$TARG
+
+install:V:	$BINTARG
+
+$BIN/guide : guide
+	rm -f $BIN/guide && cp guide $BIN/guide
+
+$BIN/readme : readme
+	rm -f $BIN/readme && cp readme $BIN/readme
+
+<$ROOT/mkfiles/mkdis
+<$ROOT/mkfiles/mksubdirs
--- /dev/null
+++ b/appl/acme/acme/mail/readme
@@ -1,0 +1,29 @@
+Mail is the single program in this directory.  Its argument specifies
+the mail box to read, default /mail/box/$user/mbox.
+For example, running
+	Mail /mail/box/$user/stored
+(a line in the guide file) looks at saved mail.
+
+Mail maintains a window containing headers for all the
+messages in the mailbox and monitors the mailbox for new messages.
+Using button 3 to indicate a message number opens
+a window on that message.   commands in the mailbox window are
+	Put		Write the mailbox back to the file (never done automatically)
+	Mail		Make a new message window ready to mail someone.
+			Takes argument names analogously to acme's New.
+	Del		Exit Mail, after checking that mailbox isn't modified.
+New messages appear at the top of the window and are highlighted upon arrival.
+(The messages are numbered oldest to newest, the opposite of regular mail.)
+
+Message windows have a simple format: the first line, up to the first tab or newline,
+holds the sender or, when sending, the addressee.  Edit the line to change who the
+message goes to.  Message windows contain the commands
+	Reply	Make a new window to compose a reply to this message
+	Delmesg	Delete the message from the screen and from the mailbox
+	Del		Delete the window, leaving the message in the mailbox
+	Post		Send the message to the addressee
+	Save		Save to the named mailbox, default/mail/box/$user/stored
+Save takes a full file name; if that name has no slashes, the file is taken
+to be in /mail/box/$user and must already exist. Use mkbox in the guide to
+create target mailboxes in /mail/box/$user.
+Reply and mail windows contain an obvious subset of the commands.
--- /dev/null
+++ b/appl/acme/acme/mail/src/Mail.b
@@ -1,0 +1,1715 @@
+implement mail;
+
+include "sys.m";
+include "draw.m";
+include "bufio.m";
+include "daytime.m";
+include "sh.m";
+
+mail : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+sys : Sys;
+bufio : Bufio;
+daytime : Daytime;
+
+OREAD, OWRITE, ORDWR, NEWFD, FORKFD, FORKENV, NEWPGRP, UTFmax : import Sys;
+FD, Dir : import sys;
+fprint, sprint, sleep, create, open, read, write, remove, stat, fstat, fwstat, fildes, pctl, pipe, dup, byte2char : import sys;
+Context : import Draw;
+EOF : import Bufio;
+Iobuf : import bufio;
+time : import daytime;
+
+DIRLEN : con 116;
+PNPROC, PNGROUP : con iota;
+False : con 0;
+True : con 1;
+EVENTSIZE : con 256;
+Runeself : con 16r80;
+OCEXEC : con 0;
+CHEXCL : con 0; # 16r20000000;	# for the moment
+CHAPPEND : con 0; # 16r40000000;
+
+MAILDIR : con "/mail";
+
+Win : adt {
+	winid : int;
+	addr : ref FD;
+	body : ref Iobuf;
+	ctl : ref FD;
+	data : ref FD;
+	event : ref FD;
+	buf : array of byte;
+	bufp : int;
+	nbuf : int;
+
+	wnew : fn() : ref Win;
+	wwritebody : fn(w : self ref Win, s : string);
+	wread : fn(w : self ref Win, m : int, n : int) : string;
+	wclean : fn(w : self ref Win);
+	wname : fn(w : self ref Win, s : string);
+	wdormant : fn(w : self ref Win);
+	wevent : fn(w : self ref Win, e : ref Event);
+	wshow : fn(w : self ref Win);
+	wtagwrite : fn(w : self ref Win, s : string);
+	wwriteevent : fn(w : self ref Win, e : ref Event);
+	wslave : fn(w : self ref Win, c : chan of Event);
+	wreplace : fn(w : self ref Win, s : string, t : string);
+	wselect : fn(w : self ref Win, s : string);
+	wsetdump : fn(w : self ref Win, s : string, t : string);
+	wdel : fn(w : self ref Win, n : int) : int;
+	wreadall : fn(w : self ref Win) : string;
+
+ 	ctlwrite : fn(w : self ref Win, s : string);
+ 	getec : fn(w : self ref Win) : int;
+ 	geten : fn(w : self ref Win) : int;
+ 	geter : fn(w : self ref Win, s : array of byte) : (int, int);
+ 	openfile : fn(w : self ref Win, s : string) : ref FD;
+ 	openbody : fn(w : self ref Win, n : int);
+};
+
+Mesg : adt {
+	w : ref Win;
+	id : int;
+	hdr : string;
+	realhdr : string;
+	replyto : string;
+	text : string;
+	subj : string;
+	next : cyclic ref Mesg;
+ 	lline1 : int;
+	box : cyclic ref Box;
+	isopen : int;
+	posted : int;
+
+	read : fn(b : ref Box) : ref Mesg;
+	open : fn(m : self ref Mesg);
+	slave : fn(m : self ref Mesg);
+	free : fn(m : self ref Mesg);
+	save : fn(m : self ref Mesg, s : string);
+	mkreply : fn(m : self ref Mesg);
+	mkmail : fn(b : ref Box, s : string);
+	putpost : fn(m : self ref Mesg, e : ref Event);
+
+ 	command : fn(m : self ref Mesg, s : string) : int;
+ 	send : fn(m : self ref Mesg);
+};
+
+Box : adt {
+	w : ref Win;
+	nm : int;
+	readonly : int;
+	m : cyclic ref Mesg;
+	file : string;
+	io : ref Iobuf;
+	clean : int;
+ 	leng : big;
+ 	cdel : chan of ref Mesg;
+	cevent : chan of Event;
+	cmore : chan of int;
+	
+	line : string;
+	peekline : string;
+
+	read : fn(s : string, n : int) : ref Box;
+	readmore : fn(b : self ref Box);
+	readline : fn(b : self ref Box) : string;
+	unreadline : fn(b : self ref Box);
+	slave : fn(b : self ref Box);
+	mopen : fn(b : self ref Box, n : int);
+	rewrite : fn(b : self ref Box);
+	mdel : fn(b : self ref Box, m : ref Mesg);
+	event : fn(b : self ref Box, e : ref Event);
+
+	command : fn(b : self ref Box, s : string) : int;
+};
+
+Event : adt {
+	c1 : int;
+	c2 : int;
+	q0 : int;
+	q1 : int;
+	flag : int;
+	nb : int;
+	nr : int;
+	b : array of byte;
+	r : array of int;
+};
+
+Lock : adt {
+	cnt : int;
+	chann : chan of int;
+
+	init : fn() : ref Lock;
+	lock : fn(l : self ref Lock);
+	unlock : fn(l : self ref Lock);
+};
+
+Ref : adt {
+	l : ref Lock;
+	cnt : int;
+
+	init : fn() : ref Ref;
+	inc : fn(r : self ref Ref) : int;
+};
+
+mbox : ref Box;
+mboxfile : string;
+usermboxfile : string;
+usermboxdir : string;
+lockfile : string;
+user : string;
+date : string;
+mailctxt : ref Context;
+stdout, stderr : ref FD;
+
+killing : int = 0;
+
+init(ctxt : ref Context, argl : list of string)
+{
+	mailctxt = ctxt;
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	daytime = load Daytime Daytime->PATH;
+	stdout = fildes(1);
+	stderr = fildes(2);
+	main(argl);
+}
+
+dlock : ref Lock;
+dfd : ref Sys->FD;
+
+debug(s : string)
+{
+	if (dfd == nil) {
+		dfd = sys->create("/usr/jrf/acme/debugmail", Sys->OWRITE, 8r600);
+		dlock = Lock.init();
+	}
+	if (dfd == nil)
+		return;
+	dlock.lock();
+	sys->fprint(dfd, "%s", s);	
+	dlock.unlock();
+}
+
+postnote(t : int, pid : int, note : string) : int
+{
+	fd := open("#p/" + string pid + "/ctl", OWRITE);
+	if (fd == nil)
+		return -1;
+	if (t == PNGROUP)
+		note += "grp";
+	fprint(fd, "%s", note);
+	fd = nil;
+	return 0;
+}
+
+exec(cmd : string, argl : list of string)
+{
+	file := cmd;
+	if(len file<4 || file[len file-4:]!=".dis")
+		file += ".dis";
+
+	c := load Command file;
+	if(c == nil) {
+		err := sprint("%r");
+		if(file[0]!='/' && file[0:2]!="./"){
+			c = load Command "/dis/"+file;
+			if(c == nil)
+				err = sprint("%r");
+		}
+		if(c == nil){
+			fprint(stderr, "%s: %s\n", cmd, err);
+			return;
+		}
+	}
+	c->init(mailctxt, argl);
+}
+
+swrite(fd : ref FD, s : string) : int
+{
+	ab := array of byte s;
+	m := len ab;
+	p := write(fd, ab, m);
+	if (p == m)
+		return len s;
+	if (p <= 0)
+		return p;
+	return 0;
+}
+
+strchr(s : string, c : int) : int
+{
+	for (i := 0; i < len s; i++)
+		if (s[i] == c)
+			return i;
+	return -1;
+} 
+
+strrchr(s : string, c : int) : int
+{
+	for (i := len s - 1; i >= 0; i--)
+		if (s[i] == c)
+			return i;
+	return -1;
+}
+
+strtoi(s : string) : (int, int)
+{
+	m := 0;
+	neg := 0;
+	t := 0;
+	ls := len s;
+	while (t < ls && (s[t] == ' ' || s[t] == '\t'))
+		t++;
+	if (t < ls && s[t] == '+')
+		t++;
+	else if (t < ls && s[t] == '-') {
+		neg = 1;
+		t++;
+	}
+	while (t < ls && (s[t] >= '0' && s[t] <= '9')) {
+		m = 10*m + s[t]-'0';
+		t++;
+	}
+	if (neg)
+		m = -m;
+	return (m, t);	
+}
+
+access(s : string) : int
+{
+	fd := open(s, 0);
+	if (fd == nil)
+		return -1;
+	fd = nil;
+	return 0;
+}
+
+newevent() : ref Event
+{
+	e := ref Event;
+	e.b = array[EVENTSIZE*UTFmax+1] of byte;
+	e.r = array[EVENTSIZE+1] of int;
+	return e;
+}	
+
+newmesg() : ref Mesg
+{
+	m := ref Mesg;
+	m.id = m.lline1 = m.isopen = m.posted = 0;
+	return m;
+}
+
+lc, uc : chan of ref Lock;
+
+initlock()
+{
+	lc = chan of ref Lock;
+	uc = chan of ref Lock;
+	spawn lockmgr();
+}
+
+lockmgr()
+{
+	l : ref Lock;
+
+	for (;;) {
+		alt {
+			l = <- lc =>
+				if (l.cnt++ == 0)
+					l.chann <-= 1;
+			l = <- uc =>
+				if (--l.cnt > 0)
+					l.chann <-= 1;
+		}
+	}
+}
+
+Lock.init() : ref Lock
+{
+	return ref Lock(0, chan of int);
+}
+
+Lock.lock(l : self ref Lock)
+{
+	lc <-= l;
+	<- l.chann;
+}
+
+Lock.unlock(l : self ref Lock)
+{
+	uc <-= l;
+}
+
+Ref.init() : ref Ref
+{
+	r := ref Ref;
+	r.l = Lock.init();
+	r.cnt = 0;
+	return r;
+}
+
+Ref.inc(r : self ref Ref) : int
+{
+	r.l.lock();
+	i := r.cnt;
+	r.cnt++;
+	r.l.unlock();
+	return i;
+}
+
+error(s : string)
+{
+	if(s != nil)
+		fprint(stderr, "mail: %s\n", s);
+	rmlockfile();
+# debug(sprint("error %s\n", s));
+	postnote(PNGROUP, pctl(0, nil), "kill");
+	killing = 1;
+	exit;
+}
+
+rmlockfile()
+{
+	if (lockfile != nil) {
+		remove(lockfile);
+		lockfile = nil;
+	}
+}
+
+#
+#  try opening a lock file.  If it doesn't exist try creating it.
+#
+openlockfile(path : string) : ref FD
+{
+	try : int;
+	fd : ref FD;
+
+	try = 0;
+	for(;;){
+		# fd = open(path, OWRITE);
+		# if(fd!=nil || ++try>3)
+		# 	return fd;
+		if(++try > 3)
+			return fd;
+		(ok, d) := stat(path);
+		if(ok >= 0)
+			sleep(1000);
+		else{
+			fd = create(path, OWRITE, CHEXCL|8r666);
+			if(fd != nil){
+				(ok, d) = fstat(fd);
+				if(ok >= 0){
+					d.mode |= CHEXCL|8r666;
+					fwstat(fd, d);
+				}
+				return fd;
+			}
+			break;
+		}
+	}
+	return nil;
+}
+
+tryopen(s : string, mode : int) : ref FD
+{
+	fd : ref FD;
+	try : int;
+
+	for(try=0; try<3; try++){
+		fd = open(s, mode);
+		if(fd != nil)
+			return fd;
+		sleep(1000);
+	}
+	return nil;
+}
+
+run(argv : list of string, c : chan of int, p0 : ref FD)
+{
+	# pctl(FORKFD|NEWPGRP, nil);	# had RFMEM
+	pctl(FORKENV|NEWFD|NEWPGRP, 0::1::2::p0.fd::nil);
+	c <-= pctl(0, nil);
+	dup(p0.fd, 0);
+	p0 = nil;
+	exec(hd argv, argv);
+	exit;
+}
+
+getuser() : string
+{
+  	fd := open("/dev/user", OREAD);
+  	if(fd == nil)
+    		return "";
+  	buf := array[128] of byte;
+  	n := read(fd, buf, len buf);
+  	if(n < 0)
+    		return "";
+  	return string buf[0:n];	
+}
+
+main(argv : list of string)
+{
+	fd : ref FD;
+	readonly : int;
+	buf : string;
+
+	initlock();
+	initreply();
+	date = time();
+	if(date==nil)
+		error("can't get current time");
+	user = getuser();
+	if(user == nil)
+		user = "Wile.E.Coyote";
+	usermboxdir = MAILDIR + "/box/" + user + "/";
+	usermboxfile = MAILDIR + "/box/" + user + "/mbox";
+	if(len argv > 1)
+		mboxfile = hd tl argv;
+	else
+		mboxfile = usermboxfile;
+
+	fd = nil;
+	readonly = False;
+	if(mboxfile == usermboxfile){
+		buf = MAILDIR + "/box/" + user + "/L.reading";
+		fd = openlockfile(buf);
+		if(fd == nil){
+			fprint(stderr, "Mail: %s in use; opened read-only\n", mboxfile);
+			readonly = True;
+		}
+		else
+			lockfile = buf;
+	}
+	mbox = mbox.read(mboxfile, readonly);
+	spawn timeslave(fd, mbox, mbox.cmore);
+	mbox.slave();
+	error(nil);
+}
+
+timeslave(rlock : ref FD, b : ref Box, c : chan of int)
+{
+	buf := array[DIRLEN] of byte;
+	for(;;){
+		sleep(30*1000);
+		if(rlock != nil && write(rlock, buf, 0)<0)
+			error("can't maintain L.reading: %r");
+		(ok, d) := stat(mboxfile);
+		if (ok >= 0 && d.length > b.leng)
+			c <-= 0;
+	}
+}
+
+Win.wnew() : ref Win
+{
+	w := ref Win;
+	buf := array[12] of byte;
+	w.ctl = open("/chan/new/ctl", ORDWR);
+	if(w.ctl==nil || read(w.ctl, buf, 12)!=12)
+		error("can't open window ctl file: %r");
+	w.ctlwrite("noscroll\n");
+	w.winid = int string buf;
+	w.event = w.openfile("event");
+	w.addr = nil;	# will be opened when needed
+	w.body = nil;
+	w.data = nil;
+	w.bufp = w.nbuf = 0;
+	w.buf = array[512] of byte;
+	return w;
+}
+
+Win.openfile(w : self ref Win, f : string) : ref FD
+{
+	buf := sprint("/chan/%d/%s", w.winid, f);
+	fd := open(buf, ORDWR|OCEXEC);
+	if(fd == nil)
+		error(sprint("can't open window %s file: %r", f));
+	return fd;
+}
+
+Win.openbody(w : self ref Win, mode : int)
+{
+	buf := sprint("/chan/%d/body", w.winid);
+	w.body = bufio->open(buf, mode|OCEXEC);
+	if(w.body == nil)
+		error("can't open window body file: %r");
+}
+
+Win.wwritebody(w : self ref Win, s : string)
+{
+	n := len s;
+	if(w.body == nil)
+		w.openbody(OWRITE);
+	if(w.body.puts(s) != n)
+		error("write error to window: %r");
+}
+
+Win.wreplace(w : self ref Win, addr : string, repl : string)
+{
+	if(w.addr == nil)
+		w.addr = w.openfile("addr");
+	if(w.data == nil)
+		w.data = w.openfile("data");
+	if(swrite(w.addr, addr) < 0){
+		fprint(stderr, "mail: warning: bad address %s:%r\n", addr);
+		return;
+	}
+	if(swrite(w.data, repl) != len repl)
+		error("writing data: %r");
+}
+
+nrunes(s : array of byte, nb : int) : int
+{
+	i, n : int;
+
+	n = 0;
+	for(i=0; i<nb; n++) {
+		(r, b, ok) := byte2char(s, i);
+		if (!ok)
+			error("help needed in nrunes()");
+		i += b;
+	}
+	return n;
+}
+
+Win.wread(w : self ref Win, q0 : int, q1 : int) : string
+{
+	m, n, nr : int;
+	s, buf : string;
+	b : array of byte;
+
+	b = array[256] of byte;
+	if(w.addr == nil)
+		w.addr = w.openfile("addr");
+	if(w.data == nil)
+		w.data = w.openfile("data");
+	s = nil;
+	m = q0;
+	while(m < q1){
+		buf = sprint("#%d", m);
+		if(swrite(w.addr, buf) != len buf)
+			error("writing addr: %r");
+		n = read(w.data, b, len b);
+		if(n <= 0)
+			error("reading data: %r");
+		nr = nrunes(b, n);
+		while(m+nr >q1){
+			do; while(n>0 && (int b[--n]&16rC0)==16r80);
+			--nr;
+		}
+		if(n == 0)
+			break;
+		s += string b[0:n];
+		m += nr;
+	}
+	return s;
+}
+
+Win.wshow(w : self ref Win)
+{
+	w.ctlwrite("show\n");
+}
+
+Win.wsetdump(w : self ref Win, dir : string, cmd : string)
+{
+	t : string;
+
+	if(dir != nil){
+		t = "dumpdir " + dir + "\n";
+		w.ctlwrite(t);
+		t = nil;
+	}
+	if(cmd != nil){
+		t = "dump " + cmd + "\n";
+		w.ctlwrite(t);
+		t = nil;
+	}
+}
+
+Win.wselect(w : self ref Win, addr : string)
+{
+	if(w.addr == nil)
+		w.addr = w.openfile("addr");
+	if(swrite(w.addr, addr) < 0)
+		error("writing addr");
+	w.ctlwrite("dot=addr\n");
+}
+
+Win.wtagwrite(w : self ref Win, s : string)
+{
+	fd : ref FD;
+
+	fd = w.openfile("tag");
+	if(swrite(fd, s) != len s)
+		error("tag write: %r");
+	fd = nil;
+}
+
+Win.ctlwrite(w : self ref Win, s : string)
+{
+	if(swrite(w.ctl, s) != len s)
+		error("write error to ctl file: %r");
+}
+
+Win.wdel(w : self ref Win, sure : int) : int
+{
+	if (w == nil)
+		return False;
+	if(sure)
+		swrite(w.ctl, "delete\n");
+	else if(swrite(w.ctl, "del\n") != 4)
+		return False;
+	w.wdormant();
+	w.ctl = nil;
+	w.event = nil;
+	return True;
+}
+
+Win.wname(w : self ref Win, s : string)
+{
+	w.ctlwrite("name " + s + "\n");
+}
+
+Win.wclean(w : self ref Win)
+{
+	if(w.body != nil)
+		w.body.flush();
+	w.ctlwrite("clean\n");
+}
+
+Win.wdormant(w : self ref Win)
+{
+	w.addr = nil;
+	if(w.body != nil){
+		w.body.close();
+		w.body = nil;
+	}
+	w.data = nil;
+}
+
+Win.getec(w : self ref Win) : int
+{
+	if(w.nbuf == 0){
+		w.nbuf = read(w.event, w.buf, len w.buf);
+		if(w.nbuf <= 0 && !killing) {
+			error("event read error: %r");
+		}
+		w.bufp = 0;
+	}
+	w.nbuf--;
+	return int w.buf[w.bufp++];
+}
+
+Win.geten(w : self ref Win) : int
+{
+	n, c : int;
+
+	n = 0;
+	while('0'<=(c=w.getec()) && c<='9')
+		n = n*10+(c-'0');
+	if(c != ' ')
+		error("event number syntax");
+	return n;
+}
+
+Win.geter(w : self ref Win, buf : array of byte) : (int, int)
+{
+	r, m, n, ok : int;
+
+	r = w.getec();
+	buf[0] = byte r;
+	n = 1;
+	if(r >= Runeself) {
+		for (;;) {
+			(r, m, ok) = byte2char(buf[0:n], 0);
+			if (m > 0)
+				return (r, n);
+			buf[n++] = byte w.getec();
+		}
+	}
+	return (r, n);
+}
+
+Win.wevent(w : self ref Win, e : ref Event)
+{
+	i, nb : int;
+
+	e.c1 = w.getec();
+	e.c2 = w.getec();
+	e.q0 = w.geten();
+	e.q1 = w.geten();
+	e.flag = w.geten();
+	e.nr = w.geten();
+	if(e.nr > EVENTSIZE)
+		error("event string too long");
+	e.nb = 0;
+	for(i=0; i<e.nr; i++){
+		(e.r[i], nb) = w.geter(e.b[e.nb:]);
+		e.nb += nb;
+	}
+	e.r[e.nr] = 0;
+	e.b[e.nb] = byte 0;
+	c := w.getec();
+	if(c != '\n')
+		error("event syntax 2");
+}
+
+Win.wslave(w : self ref Win, ce : chan of Event)
+{
+	e : ref Event;
+
+	e = newevent();
+	for(;;){
+		w.wevent(e);
+		ce <-= *e;
+	}
+}
+
+Win.wwriteevent(w : self ref Win, e : ref Event)
+{
+	fprint(w.event, "%c%c%d %d\n", e.c1, e.c2, e.q0, e.q1);
+}
+
+Win.wreadall(w : self ref Win) : string
+{
+	s, t : string;
+
+	if(w.body != nil)
+		w.body.close();
+	w.openbody(OREAD);
+	s = nil;
+	while ((t = w.body.gets('\n')) != nil)
+		s += t;
+	w.body.close();
+	w.body = nil;
+	return s;
+}
+
+ignored : int;
+
+None,Unknown,Ignore,CC,From,ReplyTo,Sender,Subject,Re,To, Date : con iota;
+NHeaders : con 200;
+
+Hdrs : adt {
+	name : string;
+	typex : int;
+};
+
+
+hdrs := array[NHeaders+1] of {
+	Hdrs ( "CC:",				CC ),
+	Hdrs ( "From:",				From ),
+	Hdrs ( "Reply-To:",			ReplyTo ),
+	Hdrs ( "Sender:",			Sender ),
+	Hdrs ( "Subject:",			Subject ),
+	Hdrs ( "Re:",				Re ),
+	Hdrs ( "To:",				To ),
+	Hdrs ( "Date:",				Date),
+ * => Hdrs ( "",					0 ),
+};
+
+StRnCmP(s : string, t : string, n : int) : int
+{
+	c, d, i, j : int;
+
+	i = j = 0;
+	if (len s < n || len t < n)
+		return -1;
+	while(n > 0){
+		c = s[i++];
+		d = t[j++];
+		--n;
+		if(c != d){
+			if('a'<=c && c<='z')
+				c -= 'a'-'A';
+			if('a'<=d && d<='z')
+				d -= 'a'-'A';
+			if(c != d)
+				return c-d;
+		}
+	}
+	return 0;
+}
+
+ignore()
+{
+	b : ref Iobuf;
+	s : string;
+	i : int;
+
+	ignored = True;
+	b = bufio->open(MAILDIR + "/lib/ignore", OREAD);
+	if(b == nil)
+		return;
+	for(i=0; hdrs[i].name != nil; i++)
+		;
+	while((s = b.gets('\n')) != nil){
+		s = s[0:len s - 1];
+		hdrs[i].name = s;
+		hdrs[i].typex = Ignore;
+		if(++i >= NHeaders){
+			fprint(stderr, "%s/lib/ignore has more than %d headers\n", MAILDIR, NHeaders);
+			break;
+		}
+	}
+	b.close();
+}
+
+readhdr(b : ref Box) : (string, int)
+{
+	i, j, n, m, typex : int;
+	s, t : string;
+
+	{
+		if(!ignored)
+			ignore();
+		s = b.readline();
+		n = len s;
+		if(n <= 0)
+			raise("e");
+		for(i=0; i<n; i++){
+			j = s[i];
+			if(i>0 && j == ':')
+				break;
+			if(j<'!' || '~'<j){
+				b.unreadline();
+				raise("e");
+			}
+		}
+		typex = Unknown;
+		for(i=0; hdrs[i].name != nil; i++){
+			j = len hdrs[i].name;
+			if(StRnCmP(hdrs[i].name, s, j) == 0){
+				typex = hdrs[i].typex;
+				break;
+			}
+		}
+		# scan for multiple sublines 
+		for(;;){
+			t = b.readline();
+			m = len t;
+			if(m<=0 || (t[0]!=' ' && t[0]!='\t')){
+				b.unreadline();
+				break;
+			}
+			# absorb 
+			s += t;
+		}
+		return(s, typex);
+	}
+	exception{
+		"*" =>
+			return (nil, None);
+	}
+}
+
+Mesg.read(b : ref Box) : ref Mesg
+{
+	m : ref Mesg;
+	s : string;
+	n, typex : int;
+
+	s = b.readline();
+	n = len s;
+	if(n <= 0)
+		return nil;
+
+{
+	if(n < 5 || s[0:5] !="From ")
+		raise("e");
+	m = newmesg();
+	m.realhdr = s;
+	# toss 'From ' 
+	s = s[5:];
+	n -= 5;
+	# toss spaces/tabs
+	while (n > 0 && (s[0] == ' ' || s[0] == '\t')) {
+		s = s[1:];
+		n--;
+	}
+	m.hdr = s;
+	# convert first blank to tab 
+	s0 := strchr(m.hdr, ' ');
+	if(s0 >= 0){
+		m.hdr[s0] = '\t';
+		# drop trailing seconds, time zone, and year if match local year 
+		t := n-6;
+		if(t <= 0)
+			raise("e");
+		if(m.hdr[t:n-1] == date[23:]){
+			m.hdr = m.hdr[0:t] + "\n";	# drop year for sure
+			t = -1;
+			s1 := strchr(m.hdr[s0:], ':');
+			if(s1 >= 0)
+				t = strchr(m.hdr[s0+s1+1:], ':');
+			if(t >= 0)	# drop seconds and time zone 
+				m.hdr = m.hdr[0:s0+s1+t+1] + "\n";
+			else{	# drop time zone 
+				t = strchr(m.hdr[s0+s1+1:], ' ');
+				if(t >= 0)
+					m.hdr = m.hdr[0:s0+s1+t+1] + "\n";
+			}
+			n = len m.hdr;
+		}
+	}
+	m.lline1 = n;
+	m.text = nil;
+	# read header 
+loop:
+	for(;;){
+		(s, typex) = readhdr(b);
+		case(typex){
+		None =>
+			break loop;
+		ReplyTo =>
+			m.replyto = s[9:];
+			break;
+		From =>
+			if(m.replyto == nil)
+				m.replyto = s[5:];
+			break;
+		Subject =>
+			m.subj = s[8:];
+			break;
+		Re =>
+			m.subj = s[3:];
+			break;
+		Date =>
+			break;
+		}
+		m.realhdr += s;
+		if(typex != Ignore)
+			m.hdr += s;
+	}
+	# read body 
+	for(;;){
+		s = b.readline();
+		n = len s;
+		if(n <= 0)
+			break;
+		if(len s >= 5 && s[0:5] == "From "){
+			b.unreadline();
+			break;
+		}
+		m.text += s;
+	}
+	# remove trailing "morF\n" 
+	l := len m.text;
+	if(l>6 && m.text[l-6:] == "\nmorF\n")
+		m.text = m.text[0:l-5];
+	m.box = b;
+	return m;
+}
+exception{
+	"*" =>
+		error("malformed header " + s);
+		return nil;
+}
+}
+
+Mesg.mkmail(b : ref Box, hdr : string)
+{
+	r : ref Mesg;
+
+	r = newmesg();
+	r.hdr = hdr + "\n";
+	r.lline1 = len r.hdr;
+	r.text = nil;
+	r.box = b;
+	r.open();
+	r.w.wdormant();
+}
+
+replyaddr(r : string) : string
+{
+	p, q, rr : int;
+
+	rr = 0;
+	while(r[rr]==' ' || r[rr]=='\t')
+		rr++;
+	r = r[rr:];
+	p = strchr(r, '<');
+	if(p >= 0){
+		q = strchr(r[p+1:], '>');
+		if(q < 0)
+			r = r[p+1:];
+		else
+			r = r[p+1:p+q] + "\n";
+		return r;
+	}
+	p = strchr(r, '(');
+	if(p >= 0){
+		q = strchr(r[p:], ')');
+		if(q < 0)
+			r = r[0:p];
+		else
+			r = r[0:p] + r[p+q+1:];
+	}
+	return r;
+}
+
+Mesg.mkreply(m : self ref Mesg)
+{
+	r : ref Mesg;
+
+	r = newmesg();
+	if(m.replyto != nil){
+		r.hdr = replyaddr(m.replyto);
+		r.lline1 = len r.hdr;
+	}else{
+		r.hdr = m.hdr[0:m.lline1];
+		r.lline1 = m.lline1;	# was len m.hdr;
+	}
+	if(m.subj != nil){
+		if(StRnCmP(m.subj, "re:", 3)==0 || StRnCmP(m.subj, " re:", 4)==0)
+			r.text = "Subject:" + m.subj + "\n";
+		else
+			r.text = "Subject: Re:" + m.subj + "\n";
+	}
+	else
+		r.text = nil;
+	r.box = m.box;
+	r.open();
+	r.w.wselect("$");
+	r.w.wdormant();
+}
+
+Mesg.free(m : self ref Mesg)
+{
+	m.text = nil;
+	m.hdr = nil;
+	m.subj = nil;
+	m.realhdr = nil;
+	m.replyto = nil;
+	m = nil;
+}
+
+replyid : ref Ref;
+
+initreply()
+{
+	replyid = Ref.init();
+}
+
+Mesg.open(m : self ref Mesg)
+{
+	buf: string;
+
+	if(m.isopen)
+		return;
+	m.w = Win.wnew();
+	if(m.id != 0)
+		m.w.wwritebody("From ");
+	m.w.wwritebody(m.hdr);
+	m.w.wwritebody(m.text);
+	if(m.id){
+		buf = sprint("%s/%d", m.box.file , m.id);
+		m.w.wtagwrite("Reply Delmesg Save");
+	}else{
+		buf = sprint("%s/Reply%d", m.box.file, replyid.inc());
+		m.w.wtagwrite("Post");
+	}
+	m.w.wname(buf);
+	m.w.wclean();
+	m.w.wselect("0");
+	m.isopen = True;
+	m.posted = False;
+	spawn m.slave();
+}
+
+Mesg.putpost(m : self ref Mesg, e : ref Event)
+{
+	if(m.posted || m.id==0)
+		return;
+	if(e.q0 >= len m.hdr+5)	# include "From " 
+		return;
+	m.w.wtagwrite(" Post");
+	m.posted = True;
+	return;
+}
+
+Mesg.slave(m : self ref Mesg)
+{
+	e, e2, ea, etoss, eq : ref Event;
+	s : string;
+	na : int;
+
+	e = newevent();
+	e2 = newevent();
+	ea = newevent();
+	etoss = newevent();
+	for(;;){
+		m.w.wevent(e);
+		case(e.c1){
+		'E' =>	# write to body; can't affect us 
+			break;
+		'F' =>	# generated by our actions; ignore 
+			break;
+		'K' or 'M' =>	# type away; we don't care 
+			case(e.c2){
+			'x' or 'X' =>	# mouse only 
+				eq = e;
+				if(e.flag & 2){
+					m.w.wevent(e2);
+					eq = e2;
+				}
+				if(e.flag & 8){
+					m.w.wevent(ea);
+					m.w.wevent(etoss);
+					na = ea.nb;
+				}else
+					na = 0;
+				if(eq.q1>eq.q0 && eq.nb==0)
+					s = m.w.wread(eq.q0, eq.q1);
+				else
+					s = string eq.b[0:eq.nb];
+				if(na)
+					s = s + " " + string ea.b[0:ea.nb];
+				if(!m.command(s))	# send it back 
+					m.w.wwriteevent(e);
+				s = nil;
+				break;
+			'l' or 'L' =>	# mouse only 
+				if(e.flag & 2)
+					m.w.wevent(e2);
+				# just send it back 
+				m.w.wwriteevent(e);
+				break;
+			'I' or 'D' =>	# modify away; we don't care 
+				m.putpost(e);
+				break;
+			'd' or 'i' =>
+				break;
+			* =>
+				fprint(stdout, "unknown message %c%c\n", e.c1, e.c2);
+				break;
+			}
+		* =>
+			fprint(stdout, "unknown message %c%c\n", e.c1, e.c2);
+			break;
+		}
+	}
+}
+
+Mesg.command(m : self ref Mesg, s : string) : int
+{
+	while(s[0]==' ' || s[0]=='\t' || s[0]=='\n')
+		s = s[1:];
+	if(s == "Post"){
+		m.send();
+		return True;
+	}
+	if(len s >= 4 && s[0:4] == "Save"){
+		s = s[4:];
+		while(s[0]==' ' || s[0]=='\t' || s[0]=='\n')
+			s = s[1:];
+		if(s == nil)
+			m.save("stored");
+		else{
+			ss := 0;
+			while(ss < len s && s[ss]!=' ' && s[ss]!='\t' && s[ss]!='\n')
+				ss++;
+			m.save(s[0:ss]);
+		}
+		return True;
+	}
+	if(s == "Reply"){
+		m.mkreply();
+		return True;
+	}
+	if(s == "Del"){
+		if(m.w.wdel(False)){
+			m.isopen = False;
+			exit;
+		}
+		return True;
+	}
+	if(s == "Delmesg"){
+		if(m.w.wdel(False)){
+			m.isopen = False;
+			m.box.cdel <-= m;
+			exit;
+		}
+		return True;
+	}
+	return False;
+}
+
+Mesg.save(m : self ref Mesg, base : string)
+{
+	s, buf : string;
+	n : int;
+	fd : ref FD;
+	b : ref Iobuf;
+
+	if(m.id <= 0){
+		fprint(stderr, "can't save reply message; mail it to yourself\n");
+		return;
+	}
+	buf = nil;
+	if(strchr(base, '/') >= 0)
+		s = base;
+	else
+		s = usermboxdir + base;
+	{
+		if(access(s) < 0)
+			raise("e");
+		fd = tryopen(s, OWRITE);
+		if(fd == nil)
+			raise("e");
+		buf = nil;
+		b = bufio->fopen(fd, OWRITE);
+		# seek to end in case file isn't append-only 
+		b.seek(big 0, 2);
+		# use edited headers: first line of real header followed by remainder of selected ones 
+		for(n=0; n<len m.realhdr && m.realhdr[n++]!='\n'; )
+			;
+		b.puts(m.realhdr[0:n]);
+		b.puts(m.hdr[m.lline1:]);
+		b.puts(m.text);
+		b.close();
+		b = nil;
+		fd = nil;
+	}
+	exception{
+		"*" =>
+			buf = nil;
+			fprint(stderr, "mail: can't open %s: %r\n", base);
+			return;
+	}
+}
+
+Mesg.send(m : self ref Mesg)
+{
+	s, buf : string;
+	t, u : int;
+	a, b : list of string;
+	n : int;
+	p : array of ref FD;
+	c : chan of int;
+
+	p = array[2] of ref FD;
+	s = m.w.wreadall();
+	a = "sendmail" :: nil;
+	if(len s >= 5 && s[0:5] == "From ")
+		s = s[5:];
+	for(t=0; t < len s && s[t]!='\n' && s[t]!='\t';){
+		while(t < len s && (s[t]==' ' || s[t]==','))
+			t++;
+		u = t;
+		while(t < len s && s[t]!=' ' && s[t]!=',' && s[t]!='\t' && s[t]!='\n')
+			t++;
+		if(t == u)
+			break;
+		a = s[u:t] :: a;
+	}
+	b = nil;
+	for ( ; a != nil; a = tl a)
+		b = hd a :: b;
+	a = b;
+	while(t < len s && s[t]!='\n')
+		t++;
+	if(s[t] == '\n')
+		t++;
+	if(pipe(p) < 0)
+		error("can't pipe: %r");
+	c = chan of int;
+	spawn run(a, c, p[0]);
+	<-c;
+	c = nil;
+	p[0] = nil;
+	n = len s - t;
+	if(swrite(p[1], s[t:]) != n)
+		fprint(stderr, "write to pipe failed: %r\n");
+	p[1] = nil;
+	# run() frees the arg list 
+	buf = sprint("%s/%d-R", m.box.file, m.id);
+	m.w.wname(buf);
+	m.w.wclean();
+}
+
+Box.read(f : string, readonly : int) : ref Box
+{
+	b : ref Box;
+	m : ref Mesg;
+	s, buf : string;
+
+	b = ref Box;
+	b.nm = 0;
+	b.readonly = readonly;
+	b.file = f;
+	b.io = bufio->open(f, OREAD|OCEXEC);
+	if(b.io == nil)
+		error(sprint("can't open %s: %r", f));
+	b.w = Win.wnew();
+	if (!readonly)
+		spawn lockfilemon(b.w.winid);
+	while((m = m.read(b)) != nil){
+		m.next = b.m;
+		b.m = m;
+		b.nm++;
+		m.id = b.nm;
+	}
+	b.leng = b.io.offset();
+	b.io.close();
+	b.io = nil;
+	# b.w = Win.wnew();
+	for(m=b.m; m != nil; m=m.next){
+		if(m.subj != nil)
+			buf = sprint("%d\t%s\t %s", m.id, m.hdr[0:m.lline1], m.subj);
+		else
+			buf = sprint("%d\t%s", m.id, m.hdr[0:m.lline1]);
+		b.w.wwritebody(buf);
+	}
+	buf = sprint("Mail/%s/", s);
+	b.w.wname(f);
+	if(b.readonly)
+		b.w.wtagwrite("Mail");
+	else
+		b.w.wtagwrite("Put Mail");
+	buf = "Mail " + f; 
+	b.w.wsetdump("/acme/mail", buf);
+	b.w.wclean();
+	b.w.wselect("0");
+	b.w.wdormant();
+	b.cdel= chan of ref Mesg;
+	b.cevent = chan of Event;
+	b.cmore = chan of int;
+	spawn b.w.wslave(b.cevent);
+	b.clean = True;
+	return b;
+}
+
+Box.readmore(b : self ref Box)
+{
+	m : ref Mesg;
+	new : int;
+	buf : string;
+	doclose : int;
+
+	doclose = False;
+	if(b.io == nil){
+		b.io = bufio->open(b.file, OREAD|OCEXEC);
+		if(b.io == nil)
+			error(sprint("can't open %s: %r", b.file));
+		b.io.seek(b.leng, 0);
+		doclose = True;
+	}
+	new = False;
+	while((m = m.read(b)) != nil){
+		m.next = b.m;
+		b.m = m;
+		b.nm++;
+		m.id = b.nm;
+		if(m.subj != nil)
+			buf  = sprint("%d\t%s\t  %s", m.id, m.hdr[0:m.lline1], m.subj);
+		else
+			buf = sprint("%d\t%s", m.id, m.hdr[0:m.lline1]);
+		b.w.wreplace("0", buf);
+		new = True;
+	}
+	b.leng = b.io.offset();
+	if(doclose){
+		b.io.close();
+		b.io = nil;
+	}
+	if(new){
+		if(b.clean)
+			b.w.wclean();
+		b.w.wselect("0;/.*(\\n[ \t].*)*");
+		b.w.wshow();
+	}
+	b.w.wdormant();
+}
+
+Box.readline(b : self ref Box) : string
+{
+    	for (;;) {
+		if(b.peekline != nil){
+			b.line = b.peekline;
+			b.peekline = nil;
+		}else
+			b.line = b.io.gets('\n');
+		# nulls appear in mailboxes! 
+		if(b.line != nil && strchr(b.line, 0) >= 0)
+			;
+		else
+			break;
+	}
+	return b.line;
+}
+
+Box.unreadline(b : self ref Box)
+{
+	b.peekline = b.line;
+}
+
+Box.slave(b : self ref Box)
+{
+	e : ref Event;
+	m : ref Mesg;
+
+	e = newevent();
+	for(;;){
+		alt{
+		*e = <-b.cevent =>
+			b.event(e);
+			break;
+		<-b.cmore =>
+			b.readmore();
+			break;
+		m = <-b.cdel =>
+			b.mdel(m);
+			break;
+		}
+	}
+}
+
+Box.event(b : self ref Box, e : ref Event)
+{
+	e2, ea, eq : ref Event;
+	s : string;
+	t : int;
+	n, na, nopen : int;
+
+	e2 = newevent();
+	ea = newevent();
+	case(e.c1){
+	'E' =>	# write to body; can't affect us 
+		break;
+	'F' =>	# generated by our actions; ignore 
+		break;
+	'K' =>	# type away; we don't care 
+		break;
+	'M' =>
+		case(e.c2){
+		'x' or 'X' =>
+			if(e.flag & 2)
+				*e2 = <-b.cevent;
+			if(e.flag & 8){
+				*ea = <-b.cevent;
+				na = ea.nb;
+				<- b.cevent;
+			}else
+				na = 0;
+			s = string e.b[0:e.nb];
+			# if it's a known command, do it 
+			if((e.flag&2) && e.nb==0)
+				s = string e2.b[0:e2.nb];
+			if(na)
+				s = sprint("%s %s", s, string ea.b[0:ea.nb]);
+			# if it's a long message, it can't be for us anyway 
+			if(!b.command(s))	# send it back 
+				b.w.wwriteevent(e);
+			if(na)
+				s = nil;
+			break;
+		'l' or 'L' =>
+			eq = e;
+			if(e.flag & 2){
+				*e2 = <-b.cevent;
+				eq = e2;
+			}
+			s = string eq.b[0:eq.nb];
+			if(eq.q1>eq.q0 && eq.nb==0)
+				s = b.w.wread(eq.q0, eq.q1);
+			nopen = 0;
+			do{
+				t = 0;
+				(n, t) = strtoi(s);
+				if(n>0 && (t == len s || s[t]==' ' || s[t]=='\t' || s[t]=='\n')){
+					b.mopen(n);
+					nopen++;
+					s = s[t:];
+				}
+				while(s != nil && s[0]!='\n')
+					s = s[1:];
+			}while(s != nil);
+			if(nopen == 0)	# send it back 
+				b.w.wwriteevent(e);
+			break;
+		'I' or 'D' or 'd' or 'i' =>	# modify away; we don't care 
+			break;
+		* =>
+			fprint(stdout, "unknown message %c%c\n", e.c1, e.c2);
+			break;
+		}
+	* =>
+		fprint(stdout, "unknown message %c%c\n", e.c1, e.c2);
+		break;
+	}
+}
+
+Box.mopen(b : self ref Box, id : int)
+{
+	m : ref Mesg;
+
+	for(m=b.m; m != nil; m=m.next)
+		if(m.id == id){
+			m.open();
+			break;
+		}
+}
+
+Box.mdel(b : self ref Box, dm : ref Mesg)
+{
+	prev, m : ref Mesg;
+	buf : string;
+
+	if(dm.id){
+		prev = nil;
+		for(m=b.m; m!=nil && m!=dm; m=m.next)
+			prev = m;
+		if(m == nil)
+			error(sprint("message %d not found", dm.id));
+		if(prev == nil)
+			b.m = m.next;
+		else
+			prev.next = m.next;
+		# remove from screen: use acme to help 
+		buf = sprint("/^%d	.*\\n(^[ \t].*\\n)*/", m.id);
+		b.w.wreplace(buf, "");
+	}
+	dm.free();
+	b.clean = False;
+}
+
+Box.command(b : self ref Box, s : string) : int
+{
+	t : int;
+	m : ref Mesg;
+
+	while(s[0]==' ' || s[0]=='\t' || s[0]=='\n')
+		s = s[1:];
+	if(len s >= 4 && s[0:4] == "Mail"){
+		s = s[4:];
+		while(s != nil && (s[0]==' ' || s[0]=='\t' || s[0]=='\n'))
+			s = s[1:];
+		t = 0;
+		while(t < len s && s[t] && s[t]!=' ' && s[t]!='\t' && s[t]!='\n')
+			t++;
+		m = b.m;		# avoid warning message on b.m.mkmail(...)
+		m.mkmail(b, s[0:t]);
+		return True;
+	}
+	if(s == "Del"){
+
+		if(!b.clean){
+			b.clean = True;
+			fprint(stderr, "mail: mailbox not written\n");
+			return True;
+		}
+		rmlockfile();
+		postnote(PNGROUP, pctl(0, nil), "kill");
+		killing = 1;
+		pctl(NEWPGRP, nil);
+		b.w.wdel(True);
+		for(m=b.m; m != nil; m=m.next)
+			m.w.wdel(False);
+		exit;
+		return True;
+	}
+	if(s == "Put"){
+		if(b.readonly)
+			fprint(stderr, "Mail: %s is read-only\n", b.file);
+		else
+			b.rewrite();
+		return True;
+	}
+	return False;
+}
+
+Box.rewrite(b : self ref Box)
+{
+	mbox, Lmbox, mboxtmp : ref FD;
+	i, t, ok : int;
+	buf : string;
+	s : string;
+	m : ref Mesg;
+	d : Dir;
+	Lmboxs : string = nil;
+
+	if(b.clean){
+		b.w.wclean();
+		return;
+	}
+	t = strrchr(b.file, '/');
+	if(t >= 0)
+		s = b.file[t+1:];
+	else
+		s = b.file;
+	if(mboxfile == usermboxfile){
+		buf = sprint("%sL.%s", b.file[0:t+1], s);
+		Lmbox = openlockfile(buf);
+		if(Lmbox == nil)
+			error(sprint("can't open lock file %s: %r", buf));
+		else
+			Lmboxs = buf;
+	}else
+		Lmbox = nil;
+	buf = sprint("%s.tmp", b.file);
+	mbox = tryopen(mboxfile, OREAD);
+	if(mbox != nil){
+		b.io = bufio->fopen(mbox, OREAD);
+		b.io.seek(b.leng, 0);
+		b.readmore();
+	}else if(access(buf)){
+		fprint(stderr, "mail: mailbox missing; using %s\n", buf);
+		mboxtmp = tryopen(buf, ORDWR);
+		b.io = bufio->fopen(mboxtmp, OREAD);
+		b.readmore();
+		b.io.close();
+	}else
+		error(sprint("can't open %s to rewrite: %r", s));
+	remove(buf);
+	mboxtmp = create(buf, OWRITE, 0622|CHAPPEND|CHEXCL);
+	if(mboxtmp == nil)
+			error(sprint("can't create %s: %r", buf));
+	(ok, d) = fstat(mboxtmp);
+	if(ok < 0)
+		error(sprint("can't fstat %s: %r", buf));
+	d.mode |= 0622;
+	if(fwstat(mboxtmp, d) < 0)
+		error(sprint("can't change mode of %s: %r", buf));
+	b.io = bufio->fopen(mboxtmp, OWRITE);
+	# write it backwards: stupid code 
+	for(i=1; i<=b.nm; i++){
+		for(m=b.m; m!=nil && m.id!=i; m=m.next)
+			;
+		if(m != nil){
+			b.io.puts(m.realhdr);
+			b.io.puts(m.text);
+		}
+	}
+	if(remove(mboxfile) < 0)
+		error(sprint("can't unlink %s: %r", mboxfile));
+	d.name = s;
+	if(fwstat(mboxtmp, d) < 0)
+		error(sprint("can't change name of %s: %r", buf));
+	b.leng = b.io.offset();
+	b.io.close();
+	mboxtmp = nil;
+	b.io = nil;
+	Lmbox = nil;
+	if (Lmboxs != nil)
+		remove(Lmboxs);
+	b.w.wclean();
+	b.clean = True;
+}
+
+lockfilemon(id : int)
+{
+	pctl(NEWPGRP, nil);
+	p := "/chan/" + string id;
+	for (;;) {
+		if (lockfile == nil)
+			error(nil);
+		sleep(60*1000);
+		(ok, d) := stat(p);
+		if (ok < 0)
+			error(nil);
+	}
+}
--- /dev/null
+++ b/appl/acme/acme/mail/src/Mailp.b
@@ -1,0 +1,1684 @@
+implement mailpop3;
+
+include "sys.m";
+include "draw.m";
+include "bufio.m";
+include "daytime.m";
+include "sh.m";
+include "pop3.m";
+
+mailpop3 : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+sys : Sys;
+bufio : Bufio;
+daytime : Daytime;
+pop3 : Pop3;
+
+OREAD, OWRITE, ORDWR, NEWFD, FORKENV, FORKFD, NEWPGRP, UTFmax, EXCEPTION, ONCE : import Sys;
+FD, Dir, Exception : import sys;
+fprint, sprint, sleep, create, open, read, write, remove, stat, fstat, fwstat, fildes, pctl, pipe, dup, byte2char : import sys;
+Context : import Draw;
+EOF : import Bufio;
+Iobuf : import bufio;
+time : import daytime;
+
+DIRLEN : con 116;
+PNPROC, PNGROUP : con iota;
+False : con 0;
+True : con 1;
+EVENTSIZE : con 256;
+Runeself : con 16r80;
+OCEXEC : con 0;
+CHEXCL : con 0; # 16r20000000;
+CHAPPEND : con 0; # 16r40000000;
+
+Win : adt {
+	winid : int;
+	addr : ref FD;
+	body : ref Iobuf;
+	ctl : ref FD;
+	data : ref FD;
+	event : ref FD;
+	buf : array of byte;
+	bufp : int;
+	nbuf : int;
+
+	wnew : fn() : ref Win;
+	wwritebody : fn(w : self ref Win, s : string);
+	wread : fn(w : self ref Win, m : int, n : int) : string;
+	wclean : fn(w : self ref Win);
+	wname : fn(w : self ref Win, s : string);
+	wdormant : fn(w : self ref Win);
+	wevent : fn(w : self ref Win, e : ref Event);
+	wshow : fn(w : self ref Win);
+	wtagwrite : fn(w : self ref Win, s : string);
+	wwriteevent : fn(w : self ref Win, e : ref Event);
+	wslave : fn(w : self ref Win, c : chan of Event);
+	wreplace : fn(w : self ref Win, s : string, t : string);
+	wselect : fn(w : self ref Win, s : string);
+	wsetdump : fn(w : self ref Win, s : string, t : string);
+	wdel : fn(w : self ref Win, n : int) : int;
+	wreadall : fn(w : self ref Win) : string;
+
+ 	ctlwrite : fn(w : self ref Win, s : string);
+ 	getec : fn(w : self ref Win) : int;
+ 	geten : fn(w : self ref Win) : int;
+ 	geter : fn(w : self ref Win, s : array of byte) : (int, int);
+ 	openfile : fn(w : self ref Win, s : string) : ref FD;
+ 	openbody : fn(w : self ref Win, n : int);
+};
+
+Mesg : adt {
+	w : ref Win;
+	id : int;
+	popno : int;
+	hdr : string;
+	realhdr : string;
+	replyto : string;
+	text : string;
+	subj : string;
+	next : cyclic ref Mesg;
+ 	lline1 : int;
+	box : cyclic ref Box;
+	isopen : int;
+	posted : int;
+	deleted : int;
+
+	read : fn(b : ref Box) : ref Mesg;
+	open : fn(m : self ref Mesg);
+	slave : fn(m : self ref Mesg);
+	free : fn(m : self ref Mesg);
+	save : fn(m : self ref Mesg, s : string);
+	mkreply : fn(m : self ref Mesg);
+	mkmail : fn(b : ref Box, s : string);
+	putpost : fn(m : self ref Mesg, e : ref Event);
+
+ 	command : fn(m : self ref Mesg, s : string) : int;
+ 	send : fn(m : self ref Mesg);
+};
+
+Box : adt {
+	w : ref Win;
+	nm : int;
+	readonly : int;
+	m : cyclic ref Mesg;
+#	io : ref Iobuf;
+	clean : int;
+ 	leng : int;
+ 	cdel : chan of ref Mesg;
+	cevent : chan of Event;
+	cmore : chan of int;
+	lst : list of int;
+	s : string;
+	
+	line : string;
+	popno : int;
+	peekline : string;
+
+	read : fn(n : int) : ref Box;
+	readmore : fn(b : self ref Box);
+	readline : fn(b : self ref Box) : string;
+	unreadline : fn(b : self ref Box);
+	slave : fn(b : self ref Box);
+	mopen : fn(b : self ref Box, n : int);
+	rewrite : fn(b : self ref Box);
+	mdel : fn(b : self ref Box, m : ref Mesg);
+	event : fn(b : self ref Box, e : ref Event);
+
+	command : fn(b : self ref Box, s : string) : int;
+};
+
+Event : adt {
+	c1 : int;
+	c2 : int;
+	q0 : int;
+	q1 : int;
+	flag : int;
+	nb : int;
+	nr : int;
+	b : array of byte;
+	r : array of int;
+};
+
+Lock : adt {
+	cnt : int;
+	chann : chan of int;
+
+	init : fn() : ref Lock;
+	lock : fn(l : self ref Lock);
+	unlock : fn(l : self ref Lock);
+};
+
+Ref : adt {
+	l : ref Lock;
+	cnt : int;
+
+	init : fn() : ref Ref;
+	inc : fn(r : self ref Ref) : int;
+};
+
+mbox : ref Box;
+user : string;
+date : string;
+mailctxt : ref Context;
+stdout, stderr : ref FD;
+
+killing : int = 0;
+
+init(ctxt : ref Context, argl : list of string)
+{
+	mailctxt = ctxt;
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	daytime = load Daytime Daytime->PATH;
+	pop3 = load Pop3 Pop3->PATH;
+	stdout = fildes(1);
+	stderr = fildes(2);
+	main();
+}
+
+dlock : ref Lock;
+dfd : ref Sys->FD;
+
+debug(s : string)
+{
+	if (dfd == nil) {
+		dfd = sys->create("/usr/jrf/acme/debugmail", Sys->OWRITE, 8r600);
+		dlock = Lock.init();
+	}
+	if (dfd == nil)
+		return;
+	dlock.lock();
+	sys->fprint(dfd, "%s", s);	
+	dlock.unlock();
+}
+
+postnote(t : int, pid : int, note : string) : int
+{
+	fd := open("#p/" + string pid + "/ctl", OWRITE);
+	if (fd == nil)
+		return -1;
+	if (t == PNGROUP)
+		note += "grp";
+	fprint(fd, "%s", note);
+	fd = nil;
+	return 0;
+}
+
+exec(cmd : string, argl : list of string)
+{
+	file := cmd;
+	if(len file<4 || file[len file-4:]!=".dis")
+		file += ".dis";
+
+	c := load Command file;
+	if(c == nil) {
+		err := sprint("%r");
+		if(file[0]!='/' && file[0:2]!="./"){
+			c = load Command "/dis/"+file;
+			if(c == nil)
+				err = sprint("%r");
+		}
+		if(c == nil){
+			fprint(stderr, "%s: %s\n", cmd, err);
+			return;
+		}
+	}
+	c->init(mailctxt, argl);
+}
+
+swrite(fd : ref FD, s : string) : int
+{
+	ab := array of byte s;
+	m := len ab;
+	p := write(fd, ab, m);
+	if (p == m)
+		return len s;
+	if (p <= 0)
+		return p;
+	return 0;
+}
+
+strchr(s : string, c : int) : int
+{
+	for (i := 0; i < len s; i++)
+		if (s[i] == c)
+			return i;
+	return -1;
+} 
+
+strrchr(s : string, c : int) : int
+{
+	for (i := len s - 1; i >= 0; i--)
+		if (s[i] == c)
+			return i;
+	return -1;
+}
+
+strtoi(s : string) : (int, int)
+{
+	m := 0;
+	neg := 0;
+	t := 0;
+	ls := len s;
+	while (t < ls && (s[t] == ' ' || s[t] == '\t'))
+		t++;
+	if (t < ls && s[t] == '+')
+		t++;
+	else if (t < ls && s[t] == '-') {
+		neg = 1;
+		t++;
+	}
+	while (t < ls && (s[t] >= '0' && s[t] <= '9')) {
+		m = 10*m + s[t]-'0';
+		t++;
+	}
+	if (neg)
+		m = -m;
+	return (m, t);	
+}
+
+access(s : string) : int
+{
+	fd := open(s, 0);
+	if (fd == nil)
+		return -1;
+	fd = nil;
+	return 0;
+}
+
+newevent() : ref Event
+{
+	e := ref Event;
+	e.b = array[EVENTSIZE*UTFmax+1] of byte;
+	e.r = array[EVENTSIZE+1] of int;
+	return e;
+}	
+
+newmesg() : ref Mesg
+{
+	m := ref Mesg;
+	m.id = m.lline1 = m.isopen = m.posted = m.deleted = 0;
+	return m;
+}
+
+lc, uc : chan of ref Lock;
+
+initlock()
+{
+	lc = chan of ref Lock;
+	uc = chan of ref Lock;
+	spawn lockmgr();
+}
+
+lockmgr()
+{
+	l : ref Lock;
+
+	for (;;) {
+		alt {
+			l = <- lc =>
+				if (l.cnt++ == 0)
+					l.chann <-= 1;
+			l = <- uc =>
+				if (--l.cnt > 0)
+					l.chann <-= 1;
+		}
+	}
+}
+
+Lock.init() : ref Lock
+{
+	return ref Lock(0, chan of int);
+}
+
+Lock.lock(l : self ref Lock)
+{
+	lc <-= l;
+	<- l.chann;
+}
+
+Lock.unlock(l : self ref Lock)
+{
+	uc <-= l;
+}
+
+Ref.init() : ref Ref
+{
+	r := ref Ref;
+	r.l = Lock.init();
+	r.cnt = 0;
+	return r;
+}
+
+Ref.inc(r : self ref Ref) : int
+{
+	r.l.lock();
+	i := r.cnt;
+	r.cnt++;
+	r.l.unlock();
+	return i;
+}
+
+error(s : string)
+{
+	if(s != nil)
+		fprint(stderr, "mail: %s\n", s);
+	postnote(PNGROUP, pctl(0, nil), "kill");
+	killing = 1;
+	exit;
+}
+
+tryopen(s : string, mode : int) : ref FD
+{
+	fd : ref FD;
+	try : int;
+
+	for(try=0; try<3; try++){
+		fd = open(s, mode);
+		if(fd != nil)
+			return fd;
+		sleep(1000);
+	}
+	return nil;
+}
+
+run(argv : list of string, c : chan of int, p0 : ref FD)
+{
+	# pctl(FORKFD|NEWPGRP, nil);	# had RFMEM
+	pctl(FORKENV|NEWFD|NEWPGRP, 0::1::2::p0.fd::nil);
+	c <-= pctl(0, nil);
+	dup(p0.fd, 0);
+	p0 = nil;
+	exec(hd argv, argv);
+	exit;
+}
+
+getuser() : string
+{
+  	fd := open("/dev/user", OREAD);
+  	if(fd == nil)
+    		return "";
+  	buf := array[128] of byte;
+  	n := read(fd, buf, len buf);
+  	if(n < 0)
+    		return "";
+  	return string buf[0:n];	
+}
+
+pop3conn : int = 0;
+pop3bad : int = 0;
+pop3lock : ref Lock;
+
+pop3open()
+{
+	pop3lock.lock();
+	if (!pop3conn) {
+		(ok, s) := pop3->open(user, "********", nil);	# password now got from user in Mailpop3.b
+		if (ok < 0) {
+			if (!pop3bad) {
+				fprint(stderr, "mail: could not connect to POP3 mail server : %s\n", s);
+				pop3bad = 1;
+			}
+			return;
+		}
+	}
+	pop3conn = 1;
+	pop3bad = 0;
+}
+
+pop3close()
+{
+	if (pop3conn) {
+		(ok, s) := pop3->close();
+		if (ok < 0) {
+			fprint(stderr, "mail: could not close POP3 connection : %s\n", s);
+			pop3lock.unlock();
+			return;
+		}
+	}
+	pop3conn = 0;
+	pop3lock.unlock();
+}
+
+pop3stat(b : ref Box) : int
+{
+	(ok, s, nm, nil) := pop3->stat();
+	if (ok < 0 && pop3conn) {
+		fprint(stderr, "mail: could not stat POP3 server : %s\n", s);
+		return b.leng;
+	}
+	return nm;
+}
+
+pop3list() : list of int
+{
+	(ok, s, l) := pop3->msgnolist();
+	if (ok < 0 && pop3conn) {
+		fprint(stderr, "mail: could not get list from POP3 server : %s\n", s);
+		return nil;
+	}
+	return l;
+}
+
+pop3mesg(mno : int) : string
+{
+	(ok, s, msg) := pop3->get(mno);
+	if (ok < 0 && pop3conn) {
+		fprint(stderr, "mail: could not retrieve a message from server : %s\n", s);
+		return "Acme Mail : FAILED TO RETRIEVE MESSAGE\n";
+	}
+	return msg;
+}
+
+pop3del(mno : int) : int
+{
+	(ok, s) := pop3->delete(mno);
+	if (ok < 0) 
+		fprint(stderr, "mail: could not delete message : %s\n", s);
+	return ok;
+}
+
+pop3init(b : ref Box)
+{
+	b.leng = pop3stat(b);
+	b.lst = pop3list();
+	b.s = nil;
+	b.popno = 0;
+}
+
+pop3more(b : ref Box)
+{
+	nl : list of int;
+
+	leng := b.leng;
+	b.leng = pop3stat(b);
+	b.lst = pop3list();
+	b.s = nil;
+	b.popno = 0;
+	if (len b.lst != b.leng || b.leng <= leng)
+		error("bad lengths in pop3more()");
+	# is this ok ?
+	nl = nil;
+	for (i := 0; i < leng; i++) {
+		nl = hd b.lst :: nl;
+		b.lst = tl b.lst;
+	}
+	# now update pop nos.
+	for (m := b.m; m != nil; m = m.next) {
+		# opopno := m.popno;
+		if (nl == nil)
+			error("message list too big");
+		m.popno = hd nl;
+		nl = tl nl;
+		# debug(sys->sprint("%d : popno from %d to %d\n", m.id, opopno, m.popno));
+	}
+	if (nl != nil)
+		error("message list too small");
+}
+
+pop3next(b : ref Box) : string
+{
+	mno : int = 0;
+	r : string;
+
+	if (b.s == nil) {
+		if (b.lst == nil)
+			return nil;	# end of box
+		first := b.popno == 0;
+		mno = hd b.lst;
+		b.lst = tl b.lst;
+		b.s = pop3mesg(mno);
+		b.popno = mno;
+		if (!first)
+			return nil;	# end of message
+	}
+	t := strchr(b.s, '\n');
+	if (t >= 0) {
+		r = b.s[0:t+1];
+		b.s = b.s[t+1:];
+	}
+	else {
+		r = b.s;
+		b.s = nil;
+	}
+	return r;
+}
+
+main()
+{
+	readonly : int;
+
+	initlock();
+	initreply();
+	date = time();
+	if(date==nil)
+		error("can't get current time");
+	user = getuser();
+	if(user == nil)
+		user = "Wile.E.Coyote";
+	readonly = False;
+	pop3lock = Lock.init();
+	mbox = mbox.read(readonly);
+	spawn timeslave(mbox, mbox.cmore);
+	mbox.slave();
+	error(nil);
+}
+
+timeslave(b : ref Box, c : chan of int)
+{
+	for(;;){
+		sleep(30*1000);
+		pop3open();
+		leng := pop3stat(b);
+		pop3close();
+		if (leng > b.leng)
+			c <-= 0;
+	}
+}
+
+Win.wnew() : ref Win
+{
+	w := ref Win;
+	buf := array[12] of byte;
+	w.ctl = open("/chan/new/ctl", ORDWR);
+	if(w.ctl==nil || read(w.ctl, buf, 12)!=12)
+		error("can't open window ctl file: %r");
+	w.ctlwrite("noscroll\n");
+	w.winid = int string buf;
+	w.event = w.openfile("event");
+	w.addr = nil;	# will be opened when needed
+	w.body = nil;
+	w.data = nil;
+	w.bufp = w.nbuf = 0;
+	w.buf = array[512] of byte;
+	return w;
+}
+
+Win.openfile(w : self ref Win, f : string) : ref FD
+{
+	buf := sprint("/chan/%d/%s", w.winid, f);
+	fd := open(buf, ORDWR|OCEXEC);
+	if(fd == nil)
+		error(sprint("can't open window %s file: %r", f));
+	return fd;
+}
+
+Win.openbody(w : self ref Win, mode : int)
+{
+	buf := sprint("/chan/%d/body", w.winid);
+	w.body = bufio->open(buf, mode|OCEXEC);
+	if(w.body == nil)
+		error("can't open window body file: %r");
+}
+
+Win.wwritebody(w : self ref Win, s : string)
+{
+	n := len s;
+	if(w.body == nil)
+		w.openbody(OWRITE);
+	if(w.body.puts(s) != n)
+		error("write error to window: %r");
+}
+
+Win.wreplace(w : self ref Win, addr : string, repl : string)
+{
+	if(w.addr == nil)
+		w.addr = w.openfile("addr");
+	if(w.data == nil)
+		w.data = w.openfile("data");
+	if(swrite(w.addr, addr) < 0){
+		fprint(stderr, "mail: warning: bad address %s:%r\n", addr);
+		return;
+	}
+	if(swrite(w.data, repl) != len repl)
+		error("writing data: %r");
+}
+
+nrunes(s : array of byte, nb : int) : int
+{
+	i, n : int;
+
+	n = 0;
+	for(i=0; i<nb; n++) {
+		(r, b, ok) := byte2char(s, i);
+		if (!ok)
+			error("help needed in nrunes()");
+		i += b;
+	}
+	return n;
+}
+
+Win.wread(w : self ref Win, q0 : int, q1 : int) : string
+{
+	m, n, nr : int;
+	s, buf : string;
+	b : array of byte;
+
+	b = array[256] of byte;
+	if(w.addr == nil)
+		w.addr = w.openfile("addr");
+	if(w.data == nil)
+		w.data = w.openfile("data");
+	s = nil;
+	m = q0;
+	while(m < q1){
+		buf = sprint("#%d", m);
+		if(swrite(w.addr, buf) != len buf)
+			error("writing addr: %r");
+		n = read(w.data, b, len b);
+		if(n <= 0)
+			error("reading data: %r");
+		nr = nrunes(b, n);
+		while(m+nr >q1){
+			do; while(n>0 && (int b[--n]&16rC0)==16r80);
+			--nr;
+		}
+		if(n == 0)
+			break;
+		s += string b[0:n];
+		m += nr;
+	}
+	return s;
+}
+
+Win.wshow(w : self ref Win)
+{
+	w.ctlwrite("show\n");
+}
+
+Win.wsetdump(w : self ref Win, dir : string, cmd : string)
+{
+	t : string;
+
+	if(dir != nil){
+		t = "dumpdir " + dir + "\n";
+		w.ctlwrite(t);
+		t = nil;
+	}
+	if(cmd != nil){
+		t = "dump " + cmd + "\n";
+		w.ctlwrite(t);
+		t = nil;
+	}
+}
+
+Win.wselect(w : self ref Win, addr : string)
+{
+	if(w.addr == nil)
+		w.addr = w.openfile("addr");
+	if(swrite(w.addr, addr) < 0)
+		error("writing addr");
+	w.ctlwrite("dot=addr\n");
+}
+
+Win.wtagwrite(w : self ref Win, s : string)
+{
+	fd : ref FD;
+
+	fd = w.openfile("tag");
+	if(swrite(fd, s) != len s)
+		error("tag write: %r");
+	fd = nil;
+}
+
+Win.ctlwrite(w : self ref Win, s : string)
+{
+	if(swrite(w.ctl, s) != len s)
+		error("write error to ctl file: %r");
+}
+
+Win.wdel(w : self ref Win, sure : int) : int
+{
+	if (w == nil)
+		return False;
+	if(sure)
+		swrite(w.ctl, "delete\n");
+	else if(swrite(w.ctl, "del\n") != 4)
+		return False;
+	w.wdormant();
+	w.ctl = nil;
+	w.event = nil;
+	return True;
+}
+
+Win.wname(w : self ref Win, s : string)
+{
+	w.ctlwrite("name " + s + "\n");
+}
+
+Win.wclean(w : self ref Win)
+{
+	if(w.body != nil)
+		w.body.flush();
+	w.ctlwrite("clean\n");
+}
+
+Win.wdormant(w : self ref Win)
+{
+	w.addr = nil;
+	if(w.body != nil){
+		w.body.close();
+		w.body = nil;
+	}
+	w.data = nil;
+}
+
+Win.getec(w : self ref Win) : int
+{
+	if(w.nbuf == 0){
+		w.nbuf = read(w.event, w.buf, len w.buf);
+		if(w.nbuf <= 0 && !killing) {
+			error("event read error: %r");
+		}
+		w.bufp = 0;
+	}
+	w.nbuf--;
+	return int w.buf[w.bufp++];
+}
+
+Win.geten(w : self ref Win) : int
+{
+	n, c : int;
+
+	n = 0;
+	while('0'<=(c=w.getec()) && c<='9')
+		n = n*10+(c-'0');
+	if(c != ' ')
+		error("event number syntax");
+	return n;
+}
+
+Win.geter(w : self ref Win, buf : array of byte) : (int, int)
+{
+	r, m, n, ok : int;
+
+	r = w.getec();
+	buf[0] = byte r;
+	n = 1;
+	if(r >= Runeself) {
+		for (;;) {
+			(r, m, ok) = byte2char(buf[0:n], 0);
+			if (m > 0)
+				return (r, n);
+			buf[n++] = byte w.getec();
+		}
+	}
+	return (r, n);
+}
+
+Win.wevent(w : self ref Win, e : ref Event)
+{
+	i, nb : int;
+
+	e.c1 = w.getec();
+	e.c2 = w.getec();
+	e.q0 = w.geten();
+	e.q1 = w.geten();
+	e.flag = w.geten();
+	e.nr = w.geten();
+	if(e.nr > EVENTSIZE)
+		error("event string too long");
+	e.nb = 0;
+	for(i=0; i<e.nr; i++){
+		(e.r[i], nb) = w.geter(e.b[e.nb:]);
+		e.nb += nb;
+	}
+	e.r[e.nr] = 0;
+	e.b[e.nb] = byte 0;
+	c := w.getec();
+	if(c != '\n')
+		error("event syntax 2");
+}
+
+Win.wslave(w : self ref Win, ce : chan of Event)
+{
+	e : ref Event;
+
+	e = newevent();
+	for(;;){
+		w.wevent(e);
+		ce <-= *e;
+	}
+}
+
+Win.wwriteevent(w : self ref Win, e : ref Event)
+{
+	fprint(w.event, "%c%c%d %d\n", e.c1, e.c2, e.q0, e.q1);
+}
+
+Win.wreadall(w : self ref Win) : string
+{
+	s, t : string;
+
+	if(w.body != nil)
+		w.body.close();
+	w.openbody(OREAD);
+	s = nil;
+	while ((t = w.body.gets('\n')) != nil)
+		s += t;
+	w.body.close();
+	w.body = nil;
+	return s;
+}
+
+None,Unknown,Ignore,CC,From,ReplyTo,Sender,Subject,Re,To, Date : con iota;
+NHeaders : con 200;
+
+Hdrs : adt {
+	name : string;
+	typex : int;
+};
+
+
+hdrs := array[NHeaders+1] of {
+	Hdrs ( "CC:",				CC ),
+	Hdrs ( "From:",				From ),
+	Hdrs ( "Reply-To:",			ReplyTo ),
+	Hdrs ( "Sender:",			Sender ),
+	Hdrs ( "Subject:",			Subject ),
+	Hdrs ( "Re:",				Re ),
+	Hdrs ( "To:",				To ),
+	Hdrs ( "Date:",				Date),
+ * => Hdrs ( "",					0 ),
+};
+
+StRnCmP(s : string, t : string, n : int) : int
+{
+	c, d, i, j : int;
+
+	i = j = 0;
+	if (len s < n || len t < n)
+		return -1;
+	while(n > 0){
+		c = s[i++];
+		d = t[j++];
+		--n;
+		if(c != d){
+			if('a'<=c && c<='z')
+				c -= 'a'-'A';
+			if('a'<=d && d<='z')
+				d -= 'a'-'A';
+			if(c != d)
+				return c-d;
+		}
+	}
+	return 0;
+}
+
+readhdr(b : ref Box) : (string, int)
+{
+	i, j, n, m, typex : int;
+	s, t : string;
+
+{
+	s = b.readline();
+	n = len s;
+	if(n <= 0) {
+		b.unreadline();
+		raise("e");
+	}
+	for(i=0; i<n; i++){
+		j = s[i];
+		if(i>0 && j == ':')
+			break;
+		if(j<'!' || '~'<j){
+			b.unreadline();
+			raise("e");
+		}
+	}
+	typex = Unknown;
+	for(i=0; hdrs[i].name != nil; i++){
+		j = len hdrs[i].name;
+		if(StRnCmP(hdrs[i].name, s, j) == 0){
+			typex = hdrs[i].typex;
+			break;
+		}
+	}
+	# scan for multiple sublines 
+	for(;;){
+		t = b.readline();
+		m = len t;
+		if(m<=0 || (t[0]!=' ' && t[0]!='\t')){
+			b.unreadline();
+			break;
+		}
+		# absorb 
+		s += t;
+	}
+	return(s, typex);
+}
+exception{
+	"*" =>
+		return (nil, None);
+}
+}
+
+Mesg.read(b : ref Box) : ref Mesg
+{
+	m : ref Mesg;
+	s : string;
+	n, typex : int;
+
+	s = b.readline();
+	n = len s;
+	if(n <= 0)
+		return nil;
+	
+{
+	if(n < 5 || (s[0:5] !="From " && s[0:5] != "From:"))
+		raise("e");
+	m = newmesg();
+	m.popno = b.popno;
+	if (m.popno == 0)
+		error("bad pop3 id");
+	m.realhdr = s;
+	# toss 'From ' 
+	s = s[5:];
+	n -= 5;
+	# toss spaces/tabs
+	while (n > 0 && (s[0] == ' ' || s[0] == '\t')) {
+		s = s[1:];
+		n--;
+	}
+	m.hdr = s;
+	# convert first blank to tab 
+	s0 := strchr(m.hdr, ' ');
+	if(s0 >= 0){
+		m.hdr[s0] = '\t';
+		# drop trailing seconds, time zone, and year if match local year 
+		t := n-6;
+		if(t <= 0)
+			raise("e");
+		if(m.hdr[t:n-1] == date[23:]){
+			m.hdr = m.hdr[0:t] + "\n";	# drop year for sure
+			t = -1;
+			s1 := strchr(m.hdr[s0:], ':');
+			if(s1 >= 0)
+				t = strchr(m.hdr[s0+s1+1:], ':');
+			if(t >= 0)	# drop seconds and time zone 
+				m.hdr = m.hdr[0:s0+s1+t+1] + "\n";
+			else{	# drop time zone 
+				t = strchr(m.hdr[s0+s1+1:], ' ');
+				if(t >= 0)
+					m.hdr = m.hdr[0:s0+s1+t+1] + "\n";
+			}
+			n = len m.hdr;
+		}
+	}
+	m.lline1 = n;
+	m.text = nil;
+	# read header 
+loop:
+	for(;;){
+		(s, typex) = readhdr(b);
+		case(typex){
+		None =>
+			break loop;
+		ReplyTo =>
+			m.replyto = s[9:];
+			break;
+		From =>
+			if(m.replyto == nil)
+				m.replyto = s[5:];
+			break;
+		Subject =>
+			m.subj = s[8:];
+			break;
+		Re =>
+			m.subj = s[3:];
+			break;
+		Date =>
+			break;
+		}
+		m.realhdr += s;
+		if(typex != Ignore)
+			m.hdr += s;
+	}
+	# read body 
+	for(;;){
+		s = b.readline();
+		n = len s;
+		if(n <= 0)
+			break;
+#		if(len s >= 5 && (s[0:5] == "From " || s[0:5] == "From:")){
+#			b.unreadline();
+#			break;
+#		}
+		m.text += s;
+	}
+	# remove trailing "morF\n" 
+	l := len m.text;
+	if(l>6 && m.text[l-6:] == "\nmorF\n")
+		m.text = m.text[0:l-5];
+	m.box = b;
+	return m;
+}
+exception{
+	"*" =>
+		error("malformed header " + s);
+		return nil;
+}
+}
+
+Mesg.mkmail(b : ref Box, hdr : string)
+{
+	r : ref Mesg;
+
+	r = newmesg();
+	r.hdr = hdr + "\n";
+	r.lline1 = len r.hdr;
+	r.text = nil;
+	r.box = b;
+	r.open();
+	r.w.wdormant();
+}
+
+replyaddr(r : string) : string
+{
+	p, q, rr : int;
+
+	rr = 0;
+	while(r[rr]==' ' || r[rr]=='\t')
+		rr++;
+	r = r[rr:];
+	p = strchr(r, '<');
+	if(p >= 0){
+		q = strchr(r[p+1:], '>');
+		if(q < 0)
+			r = r[p+1:];
+		else
+			r = r[p+1:p+q] + "\n";
+		return r;
+	}
+	p = strchr(r, '(');
+	if(p >= 0){
+		q = strchr(r[p:], ')');
+		if(q < 0)
+			r = r[0:p];
+		else
+			r = r[0:p] + r[p+q+1:];
+	}
+	return r;
+}
+
+Mesg.mkreply(m : self ref Mesg)
+{
+	r : ref Mesg;
+
+	r = newmesg();
+	if(m.replyto != nil){
+		r.hdr = replyaddr(m.replyto);
+		r.lline1 = len r.hdr;
+	}else{
+		r.hdr = m.hdr[0:m.lline1];
+		r.lline1 = m.lline1;	# was len m.hdr;
+	}
+	if(m.subj != nil){
+		if(StRnCmP(m.subj, "re:", 3)==0 || StRnCmP(m.subj, " re:", 4)==0)
+			r.text = "Subject:" + m.subj + "\n";
+		else
+			r.text = "Subject: Re:" + m.subj + "\n";
+	}
+	else
+		r.text = nil;
+	r.box = m.box;
+	r.open();
+	r.w.wselect("$");
+	r.w.wdormant();
+}
+
+Mesg.free(m : self ref Mesg)
+{
+	m.text = nil;
+	m.hdr = nil;
+	m.subj = nil;
+	m.realhdr = nil;
+	m.replyto = nil;
+	m = nil;
+}
+
+replyid : ref Ref;
+
+initreply()
+{
+	replyid = Ref.init();
+}
+
+Mesg.open(m : self ref Mesg)
+{
+	buf, s : string;
+
+	if(m.isopen)
+		return;
+	m.w = Win.wnew();
+	if(m.id != 0)
+		m.w.wwritebody("From ");
+	m.w.wwritebody(m.hdr);
+	m.w.wwritebody(m.text);
+	if(m.id){
+		buf = sprint("Mail/box/%d", m.id);
+		m.w.wtagwrite("Reply Delmesg Save");
+	}else{
+		buf = sprint("Mail/%s/Reply%d", s, replyid.inc());
+		m.w.wtagwrite("Post");
+	}
+	m.w.wname(buf);
+	m.w.wclean();
+	m.w.wselect("0");
+	m.isopen = True;
+	m.posted = False;
+	spawn m.slave();
+}
+
+Mesg.putpost(m : self ref Mesg, e : ref Event)
+{
+	if(m.posted || m.id==0)
+		return;
+	if(e.q0 >= len m.hdr+5)	# include "From " 
+		return;
+	m.w.wtagwrite(" Post");
+	m.posted = True;
+	return;
+}
+
+Mesg.slave(m : self ref Mesg)
+{
+	e, e2, ea, etoss, eq : ref Event;
+	s : string;
+	na : int;
+
+	e = newevent();
+	e2 = newevent();
+	ea = newevent();
+	etoss = newevent();
+	for(;;){
+		m.w.wevent(e);
+		case(e.c1){
+		'E' =>	# write to body; can't affect us 
+			break;
+		'F' =>	# generated by our actions; ignore 
+			break;
+		'K' or 'M' =>	# type away; we don't care 
+			case(e.c2){
+			'x' or 'X' =>	# mouse only 
+				eq = e;
+				if(e.flag & 2){
+					m.w.wevent(e2);
+					eq = e2;
+				}
+				if(e.flag & 8){
+					m.w.wevent(ea);
+					m.w.wevent(etoss);
+					na = ea.nb;
+				}else
+					na = 0;
+				if(eq.q1>eq.q0 && eq.nb==0)
+					s = m.w.wread(eq.q0, eq.q1);
+				else
+					s = string eq.b[0:eq.nb];
+				if(na)
+					s = s + " " + string ea.b[0:ea.nb];
+				if(!m.command(s))	# send it back 
+					m.w.wwriteevent(e);
+				s = nil;
+				break;
+			'l' or 'L' =>	# mouse only 
+				if(e.flag & 2)
+					m.w.wevent(e2);
+				# just send it back 
+				m.w.wwriteevent(e);
+				break;
+			'I' or 'D' =>	# modify away; we don't care 
+				m.putpost(e);
+				break;
+			'd' or 'i' =>
+				break;
+			* =>
+				fprint(stdout, "unknown message %c%c\n", e.c1, e.c2);
+				break;
+			}
+		* =>
+			fprint(stdout, "unknown message %c%c\n", e.c1, e.c2);
+			break;
+		}
+	}
+}
+
+Mesg.command(m : self ref Mesg, s : string) : int
+{
+	while(s[0]==' ' || s[0]=='\t' || s[0]=='\n')
+		s = s[1:];
+	if(s == "Post"){
+		m.send();
+		return True;
+	}
+	if(len s >= 4 && s[0:4] == "Save"){
+		s = s[4:];
+		while(s[0]==' ' || s[0]=='\t' || s[0]=='\n')
+			s = s[1:];
+		if(s == nil)
+			m.save("stored");
+		else{
+			ss := 0;
+			while(ss < len s && s[ss]!=' ' && s[ss]!='\t' && s[ss]!='\n')
+				ss++;
+			m.save(s[0:ss]);
+		}
+		return True;
+	}
+	if(s == "Reply"){
+		m.mkreply();
+		return True;
+	}
+	if(s == "Del"){
+		if(m.w.wdel(False)){
+			m.isopen = False;
+			exit;
+		}
+		return True;
+	}
+	if(s == "Delmesg"){
+		if(m.w.wdel(False)){
+			m.isopen = False;
+			m.box.cdel <-= m;
+			exit;
+		}
+		return True;
+	}
+	return False;
+}
+
+Mesg.save(m : self ref Mesg, base : string)
+{
+	s, buf : string;
+	n : int;
+	fd : ref FD;
+	b : ref Iobuf;
+
+	if(m.id <= 0){
+		fprint(stderr, "can't save reply message; mail it to yourself\n");
+		return;
+	}
+	buf = nil;
+	s = base;
+{
+	if(access(s) < 0)
+		raise("e");
+	fd = tryopen(s, OWRITE);
+	if(fd == nil)
+		raise("e");
+	buf = nil;
+	b = bufio->fopen(fd, OWRITE);
+	# seek to end in case file isn't append-only 
+	b.seek(big 0, 2);
+	# use edited headers: first line of real header followed by remainder of selected ones 
+	for(n=0; n<len m.realhdr && m.realhdr[n++]!='\n'; )
+		;
+	b.puts(m.realhdr[0:n]);
+	b.puts(m.hdr[m.lline1:]);
+	b.puts(m.text);
+	b.close();
+	b = nil;
+	fd = nil;
+}
+exception{
+	"*" =>
+		buf = nil;
+		fprint(stderr, "mail: can't open %s: %r\n", base);
+		return;
+}
+}
+
+Mesg.send(m : self ref Mesg)
+{
+	s, buf : string;
+	t, u : int;
+	a, b : list of string;
+	n : int;
+	p : array of ref FD;
+	c : chan of int;
+
+	p = array[2] of ref FD;
+	s = m.w.wreadall();
+	a = "sendmail" :: nil;
+	if(len s >= 5 && (s[0:5] == "From " || s[0:5] == "From:"))
+		s = s[5:];
+	for(t=0; t < len s && s[t]!='\n' && s[t]!='\t';){
+		while(t < len s && (s[t]==' ' || s[t]==','))
+			t++;
+		u = t;
+		while(t < len s && s[t]!=' ' && s[t]!=',' && s[t]!='\t' && s[t]!='\n')
+			t++;
+		if(t == u)
+			break;
+		a = s[u:t] :: a;
+	}
+	b = nil;
+	for ( ; a != nil; a = tl a)
+		b = hd a :: b;
+	a = b;
+	while(t < len s && s[t]!='\n')
+		t++;
+	if(s[t] == '\n')
+		t++;
+	if(pipe(p) < 0)
+		error("can't pipe: %r");
+	c = chan of int;
+	spawn run(a, c, p[0]);
+	<-c;
+	c = nil;
+	p[0] = nil;
+	n = len s - t;
+	if(swrite(p[1], s[t:]) != n)
+		fprint(stderr, "write to pipe failed: %r\n");
+	p[1] = nil;
+	# run() frees the arg list 
+	buf = sprint("Mail/box/%d-R", m.id);
+	m.w.wname(buf);
+	m.w.wclean();
+}
+
+Box.read(readonly : int) : ref Box
+{
+	b : ref Box;
+	m : ref Mesg;
+	buf : string;
+
+	b = ref Box;
+	b.nm = 0;
+	b.leng = 0;
+	b.readonly = readonly;
+	pop3open();
+	pop3init(b);
+	while((m = m.read(b)) != nil){
+		m.next = b.m;
+		b.m = m;
+		b.nm++;
+		m.id = b.nm;
+	}
+	pop3close();
+	if (b.leng != b.nm)
+		error("bad message count in Box.read()");
+	b.w = Win.wnew();
+	for(m=b.m; m != nil; m=m.next){
+		if(m.subj != nil)
+			buf = sprint("%d\t%s\t %s", m.id, m.hdr[0:m.lline1], m.subj);
+		else
+			buf = sprint("%d\t%s", m.id, m.hdr[0:m.lline1]);
+		b.w.wwritebody(buf);
+	}
+	buf = sprint("Mail/box/");
+	b.w.wname(buf);
+	if(b.readonly)
+		b.w.wtagwrite("Mail");
+	else
+		b.w.wtagwrite("Put Mail");
+	buf = "Mail " + "box"; 
+	b.w.wsetdump("/acme/mail", buf);
+	b.w.wclean();
+	b.w.wselect("0");
+	b.w.wdormant();
+	b.cdel= chan of ref Mesg;
+	b.cevent = chan of Event;
+	b.cmore = chan of int;
+	spawn b.w.wslave(b.cevent);
+	b.clean = True;
+	return b;
+}
+
+Box.readmore(b : self ref Box)
+{
+	m : ref Mesg;
+	new : int;
+	buf : string;
+
+	new = False;
+	leng := b.leng;
+	n := 0;
+	pop3open();
+	pop3more(b);
+	while((m = m.read(b)) != nil){
+		m.next = b.m;
+		b.m = m;
+		b.nm++;
+		n++;
+		m.id = b.nm;
+		if(m.subj != nil)
+			buf  = sprint("%d\t%s\t  %s", m.id, m.hdr[0:m.lline1], m.subj);
+		else
+			buf = sprint("%d\t%s", m.id, m.hdr[0:m.lline1]);
+		b.w.wreplace("0", buf);
+		new = True;
+	}
+	pop3close();
+	if (b.leng != leng+n)
+		error("bad message count in Box.readmore()");
+	if(new){
+		if(b.clean)
+			b.w.wclean();
+		b.w.wselect("0;/.*(\\n[ \t].*)*");
+		b.w.wshow();
+	}
+	b.w.wdormant();
+}
+
+Box.readline(b : self ref Box) : string
+{
+    	for (;;) {
+		if(b.peekline != nil){
+			b.line = b.peekline;
+			b.peekline = nil;
+		}else
+			b.line = pop3next(b);
+		# nulls appear in mailboxes! 
+		if(b.line != nil && strchr(b.line, 0) >= 0)
+			;
+		else
+			break;
+	}
+	return b.line;
+}
+
+Box.unreadline(b : self ref Box)
+{
+	b.peekline = b.line;
+}
+
+Box.slave(b : self ref Box)
+{
+	e : ref Event;
+	m : ref Mesg;
+
+	e = newevent();
+	for(;;){
+		alt{
+		*e = <-b.cevent =>
+			b.event(e);
+			break;
+		<-b.cmore =>
+			b.readmore();
+			break;
+		m = <-b.cdel =>
+			b.mdel(m);
+			break;
+		}
+	}
+}
+
+Box.event(b : self ref Box, e : ref Event)
+{
+	e2, ea, eq : ref Event;
+	s : string;
+	t : int;
+	n, na, nopen : int;
+
+	e2 = newevent();
+	ea = newevent();
+	case(e.c1){
+	'E' =>	# write to body; can't affect us 
+		break;
+	'F' =>	# generated by our actions; ignore 
+		break;
+	'K' =>	# type away; we don't care 
+		break;
+	'M' =>
+		case(e.c2){
+		'x' or 'X' =>
+			if(e.flag & 2)
+				*e2 = <-b.cevent;
+			if(e.flag & 8){
+				*ea = <-b.cevent;
+				na = ea.nb;
+				<- b.cevent;
+			}else
+				na = 0;
+			s = string e.b[0:e.nb];
+			# if it's a known command, do it 
+			if((e.flag&2) && e.nb==0)
+				s = string e2.b[0:e2.nb];
+			if(na)
+				s = sprint("%s %s", s, string ea.b[0:ea.nb]);
+			# if it's a long message, it can't be for us anyway 
+			if(!b.command(s))	# send it back 
+				b.w.wwriteevent(e);
+			if(na)
+				s = nil;
+			break;
+		'l' or 'L' =>
+			eq = e;
+			if(e.flag & 2){
+				*e2 = <-b.cevent;
+				eq = e2;
+			}
+			s = string eq.b[0:eq.nb];
+			if(eq.q1>eq.q0 && eq.nb==0)
+				s = b.w.wread(eq.q0, eq.q1);
+			nopen = 0;
+			do{
+				t = 0;
+				(n, t) = strtoi(s);
+				if(n>0 && (t == len s || s[t]==' ' || s[t]=='\t' || s[t]=='\n')){
+					b.mopen(n);
+					nopen++;
+					s = s[t:];
+				}
+				while(s != nil && s[0]!='\n')
+					s = s[1:];
+			}while(s != nil);
+			if(nopen == 0)	# send it back 
+				b.w.wwriteevent(e);
+			break;
+		'I' or 'D' or 'd' or 'i' =>	# modify away; we don't care 
+			break;
+		* =>
+			fprint(stdout, "unknown message %c%c\n", e.c1, e.c2);
+			break;
+		}
+	* =>
+		fprint(stdout, "unknown message %c%c\n", e.c1, e.c2);
+		break;
+	}
+}
+
+Box.mopen(b : self ref Box, id : int)
+{
+	m : ref Mesg;
+
+	for(m=b.m; m != nil; m=m.next)
+		if(m.id == id){
+			m.open();
+			break;
+		}
+}
+
+Box.mdel(b : self ref Box, dm : ref Mesg)
+{
+	m : ref Mesg;
+	buf : string;
+
+	if(dm.id){
+		for(m=b.m; m!=nil && m!=dm; m=m.next)
+			;
+		if(m == nil)
+			error(sprint("message %d not found", dm.id));
+		m.deleted = 1;
+		# remove from screen: use acme to help 
+		buf = sprint("/^%d	.*\\n(^[ \t].*\\n)*/", m.id);
+		b.w.wreplace(buf, "");
+	}
+	dm.free();
+	b.clean = False;
+}
+
+Box.command(b : self ref Box, s : string) : int
+{
+	t : int;
+	m : ref Mesg;
+
+	while(s[0]==' ' || s[0]=='\t' || s[0]=='\n')
+		s = s[1:];
+	if(len s >= 4 && s[0:4] == "Mail"){
+		s = s[4:];
+		while(s != nil && (s[0]==' ' || s[0]=='\t' || s[0]=='\n'))
+			s = s[1:];
+		t = 0;
+		while(t < len s && s[t] && s[t]!=' ' && s[t]!='\t' && s[t]!='\n')
+			t++;
+		m = b.m;		# avoid warning message on b.m.mkmail(...)
+		m.mkmail(b, s[0:t]);
+		return True;
+	}
+	if(s == "Del"){
+
+		if(!b.clean){
+			b.clean = True;
+			fprint(stderr, "mail: mailbox not written\n");
+			return True;
+		}
+		postnote(PNGROUP, pctl(0, nil), "kill");
+		killing = 1;
+		pctl(NEWPGRP, nil);
+		b.w.wdel(True);
+		for(m=b.m; m != nil; m=m.next)
+			m.w.wdel(False);
+		exit;
+		return True;
+	}
+	if(s == "Put"){
+		if(b.readonly)
+			fprint(stderr, "Mail is read-only\n");
+		else
+			b.rewrite();
+		return True;
+	}
+	return False;
+}
+
+Box.rewrite(b : self ref Box)
+{
+	prev, m : ref Mesg;
+
+	if(b.clean){
+		b.w.wclean();
+		return;
+	}
+	prev = nil;
+	pop3open();
+	for(m=b.m; m!=nil; m=m.next) {
+		if (m.deleted && pop3del(m.popno) >= 0) {
+			b.leng--;
+			if (prev == nil)
+				b.m=m.next;
+			else
+				prev.next=m.next;
+		}
+		else
+			prev = m;
+	}
+	pop3close();
+	b.w.wclean();
+	b.clean = True;
+}
--- /dev/null
+++ b/appl/acme/acme/mail/src/Mailpop3.b
@@ -1,0 +1,1720 @@
+implement Mailpop3;
+
+include "sys.m";
+include "draw.m";
+include "bufio.m";
+include "daytime.m";
+include "sh.m";
+include "pop3.m";
+
+Mailpop3 : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+sys : Sys;
+bufio : Bufio;
+daytime : Daytime;
+pop3 : Pop3;
+
+OREAD, OWRITE, ORDWR, FORKENV, NEWFD, FORKFD, NEWPGRP, UTFmax : import Sys;
+FD, Dir : import sys;
+fprint, sprint, sleep, create, open, read, write, remove, stat, fstat, fwstat, fildes, pctl, pipe, dup, byte2char : import sys;
+Context : import Draw;
+EOF : import Bufio;
+Iobuf : import bufio;
+time : import daytime;
+
+DIRLEN : con 116;
+PNPROC, PNGROUP : con iota;
+False : con 0;
+True : con 1;
+EVENTSIZE : con 256;
+Runeself : con 16r80;
+OCEXEC : con 0;
+CHEXCL : con 0; # 16r20000000;
+CHAPPEND : con 0; # 16r40000000;
+
+Win : adt {
+	winid : int;
+	addr : ref FD;
+	body : ref Iobuf;
+	ctl : ref FD;
+	data : ref FD;
+	event : ref FD;
+	buf : array of byte;
+	bufp : int;
+	nbuf : int;
+
+	wnew : fn() : ref Win;
+	wwritebody : fn(w : self ref Win, s : string);
+	wread : fn(w : self ref Win, m : int, n : int) : string;
+	wclean : fn(w : self ref Win);
+	wname : fn(w : self ref Win, s : string);
+	wdormant : fn(w : self ref Win);
+	wevent : fn(w : self ref Win, e : ref Event);
+	wshow : fn(w : self ref Win);
+	wtagwrite : fn(w : self ref Win, s : string);
+	wwriteevent : fn(w : self ref Win, e : ref Event);
+	wslave : fn(w : self ref Win, c : chan of Event);
+	wreplace : fn(w : self ref Win, s : string, t : string);
+	wselect : fn(w : self ref Win, s : string);
+	wsetdump : fn(w : self ref Win, s : string, t : string);
+	wdel : fn(w : self ref Win, n : int) : int;
+	wreadall : fn(w : self ref Win) : string;
+
+ 	ctlwrite : fn(w : self ref Win, s : string);
+ 	getec : fn(w : self ref Win) : int;
+ 	geten : fn(w : self ref Win) : int;
+ 	geter : fn(w : self ref Win, s : array of byte) : (int, int);
+ 	openfile : fn(w : self ref Win, s : string) : ref FD;
+ 	openbody : fn(w : self ref Win, n : int);
+};
+
+Mesg : adt {
+	w : ref Win;
+	id : int;
+	popno : int;
+	hdr : string;
+	realhdr : string;
+	replyto : string;
+	text : string;
+	subj : string;
+	next : cyclic ref Mesg;
+ 	lline1 : int;
+	box : cyclic ref Box;
+	isopen : int;
+	posted : int;
+	deleted : int;
+
+	read : fn(b : ref Box) : ref Mesg;
+	open : fn(m : self ref Mesg);
+	slave : fn(m : self ref Mesg);
+	free : fn(m : self ref Mesg);
+	save : fn(m : self ref Mesg, s : string);
+	mkreply : fn(m : self ref Mesg);
+	mkmail : fn(b : ref Box, s : string);
+	putpost : fn(m : self ref Mesg, e : ref Event);
+
+ 	command : fn(m : self ref Mesg, s : string) : int;
+ 	send : fn(m : self ref Mesg);
+};
+
+Box : adt {
+	w : ref Win;
+	nm : int;
+	readonly : int;
+	m : cyclic ref Mesg;
+#	io : ref Iobuf;
+	clean : int;
+ 	leng : int;
+ 	cdel : chan of ref Mesg;
+	cevent : chan of Event;
+	cmore : chan of int;
+	lst : list of int;
+	s : string;
+	
+	line : string;
+	popno : int;
+	peekline : string;
+
+	read : fn(n : int) : ref Box;
+	readmore : fn(b : self ref Box, lck : int);
+	readline : fn(b : self ref Box) : string;
+	unreadline : fn(b : self ref Box);
+	slave : fn(b : self ref Box);
+	mopen : fn(b : self ref Box, n : int);
+	rewrite : fn(b : self ref Box);
+	mdel : fn(b : self ref Box, m : ref Mesg);
+	event : fn(b : self ref Box, e : ref Event);
+
+	command : fn(b : self ref Box, s : string) : int;
+};
+
+Event : adt {
+	c1 : int;
+	c2 : int;
+	q0 : int;
+	q1 : int;
+	flag : int;
+	nb : int;
+	nr : int;
+	b : array of byte;
+	r : array of int;
+};
+
+Lock : adt {
+	cnt : int;
+	chann : chan of int;
+
+	init : fn() : ref Lock;
+	lock : fn(l : self ref Lock);
+	unlock : fn(l : self ref Lock);
+};
+
+Ref : adt {
+	l : ref Lock;
+	cnt : int;
+
+	init : fn() : ref Ref;
+	inc : fn(r : self ref Ref) : int;
+};
+
+mbox : ref Box;
+user : string;
+pwd : string;
+date : string;
+mailctxt : ref Context;
+stdout, stderr : ref FD;
+
+killing : int = 0;
+
+init(ctxt : ref Context, nil : list of string)
+{
+	mailctxt = ctxt;
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	daytime = load Daytime Daytime->PATH;
+	pop3 = load Pop3 Pop3->PATH;
+	stdout = fildes(1);
+	stderr = fildes(2);
+	main();
+}
+
+dlock : ref Lock;
+dfd : ref Sys->FD;
+
+debug(s : string)
+{
+	if (dfd == nil) {
+		dfd = sys->create("/usr/jrf/acme/debugmail", Sys->OWRITE, 8r600);
+		dlock = Lock.init();
+	}
+	if (dfd == nil)
+		return;
+	dlock.lock();
+	sys->fprint(dfd, "%s", s);	
+	dlock.unlock();
+}
+
+postnote(t : int, pid : int, note : string) : int
+{
+	fd := open("#p/" + string pid + "/ctl", OWRITE);
+	if (fd == nil)
+		return -1;
+	if (t == PNGROUP)
+		note += "grp";
+	fprint(fd, "%s", note);
+	fd = nil;
+	return 0;
+}
+
+exec(cmd : string, argl : list of string)
+{
+	file := cmd;
+	if(len file<4 || file[len file-4:]!=".dis")
+		file += ".dis";
+
+	c := load Command file;
+	if(c == nil) {
+		err := sprint("%r");
+		if(file[0]!='/' && file[0:2]!="./"){
+			c = load Command "/dis/"+file;
+			if(c == nil)
+				err = sprint("%r");
+		}
+		if(c == nil){
+			fprint(stderr, "%s: %s\n", cmd, err);
+			return;
+		}
+	}
+	c->init(mailctxt, argl);
+}
+
+swrite(fd : ref FD, s : string) : int
+{
+	ab := array of byte s;
+	m := len ab;
+	p := write(fd, ab, m);
+	if (p == m)
+		return len s;
+	if (p <= 0)
+		return p;
+	return 0;
+}
+
+strchr(s : string, c : int) : int
+{
+	for (i := 0; i < len s; i++)
+		if (s[i] == c)
+			return i;
+	return -1;
+} 
+
+strrchr(s : string, c : int) : int
+{
+	for (i := len s - 1; i >= 0; i--)
+		if (s[i] == c)
+			return i;
+	return -1;
+}
+
+strtoi(s : string) : (int, int)
+{
+	m := 0;
+	neg := 0;
+	t := 0;
+	ls := len s;
+	while (t < ls && (s[t] == ' ' || s[t] == '\t'))
+		t++;
+	if (t < ls && s[t] == '+')
+		t++;
+	else if (t < ls && s[t] == '-') {
+		neg = 1;
+		t++;
+	}
+	while (t < ls && (s[t] >= '0' && s[t] <= '9')) {
+		m = 10*m + s[t]-'0';
+		t++;
+	}
+	if (neg)
+		m = -m;
+	return (m, t);	
+}
+
+access(s : string) : int
+{
+	fd := open(s, 0);
+	if (fd == nil)
+		return -1;
+	fd = nil;
+	return 0;
+}
+
+newevent() : ref Event
+{
+	e := ref Event;
+	e.b = array[EVENTSIZE*UTFmax+1] of byte;
+	e.r = array[EVENTSIZE+1] of int;
+	return e;
+}	
+
+newmesg() : ref Mesg
+{
+	m := ref Mesg;
+	m.id = m.lline1 = m.isopen = m.posted = m.deleted = 0;
+	return m;
+}
+
+lc, uc : chan of ref Lock;
+
+initlock()
+{
+	lc = chan of ref Lock;
+	uc = chan of ref Lock;
+	spawn lockmgr();
+}
+
+lockmgr()
+{
+	l : ref Lock;
+
+	for (;;) {
+		alt {
+			l = <- lc =>
+				if (l.cnt++ == 0)
+					l.chann <-= 1;
+			l = <- uc =>
+				if (--l.cnt > 0)
+					l.chann <-= 1;
+		}
+	}
+}
+
+Lock.init() : ref Lock
+{
+	return ref Lock(0, chan of int);
+}
+
+Lock.lock(l : self ref Lock)
+{
+	lc <-= l;
+	<- l.chann;
+}
+
+Lock.unlock(l : self ref Lock)
+{
+	uc <-= l;
+}
+
+Ref.init() : ref Ref
+{
+	r := ref Ref;
+	r.l = Lock.init();
+	r.cnt = 0;
+	return r;
+}
+
+Ref.inc(r : self ref Ref) : int
+{
+	r.l.lock();
+	i := r.cnt;
+	r.cnt++;
+	r.l.unlock();
+	return i;
+}
+
+error(s : string)
+{
+	if(s != nil)
+		fprint(stderr, "mail: %s\n", s);
+	postnote(PNGROUP, pctl(0, nil), "kill");
+	killing = 1;
+	exit;
+}
+
+tryopen(s : string, mode : int) : ref FD
+{
+	fd : ref FD;
+	try : int;
+
+	for(try=0; try<3; try++){
+		fd = open(s, mode);
+		if(fd != nil)
+			return fd;
+		sleep(1000);
+	}
+	return nil;
+}
+
+run(argv : list of string, c : chan of int, p0 : ref FD)
+{
+	# pctl(FORKFD|NEWPGRP, nil);	# had RFMEM
+	pctl(FORKENV|NEWFD|NEWPGRP, 0::1::2::p0.fd::nil);
+	c <-= pctl(0, nil);
+	dup(p0.fd, 0);
+	p0 = nil;
+	exec(hd argv, argv);
+	exit;
+}
+
+getuser() : string
+{
+  	fd := open("/dev/user", OREAD);
+  	if(fd == nil)
+    		return "";
+  	buf := array[128] of byte;
+  	n := read(fd, buf, len buf);
+  	if(n < 0)
+    		return "";
+  	return string buf[0:n];	
+}
+
+pop3conn : int = 0;
+pop3bad : int = 0;
+pop3lock : ref Lock;
+
+pop3open(lck : int)
+{
+	if (lck)
+		pop3lock.lock();
+	if (!pop3conn) {
+		(ok, s) := pop3->open(user, pwd, nil);
+		if (ok < 0) {
+			if (!pop3bad) {
+				fprint(stderr, "mail: could not connect to POP3 mail server : %s\n", s);
+				pop3bad = 1;
+			}
+			return;
+		}
+	}
+	pop3conn = 1;
+	pop3bad = 0;
+}
+
+pop3close(unlck : int)
+{
+	if (pop3conn) {
+		(ok, s) := pop3->close();
+		if (ok < 0) {
+			fprint(stderr, "mail: could not close POP3 connection : %s\n", s);
+			pop3lock.unlock();
+			return;
+		}
+	}
+	pop3conn = 0;
+	if (unlck)
+		pop3lock.unlock();
+}
+
+pop3stat(b : ref Box) : int
+{
+	(ok, s, nm, nil) := pop3->stat();
+	if (ok < 0 && pop3conn) {
+		fprint(stderr, "mail: could not stat POP3 server : %s\n", s);
+		return b.leng;
+	}
+	return nm;
+}
+
+pop3list() : list of int
+{
+	(ok, s, l) := pop3->msgnolist();
+	if (ok < 0 && pop3conn) {
+		fprint(stderr, "mail: could not get list from POP3 server : %s\n", s);
+		return nil;
+	}
+	return l;
+}
+
+pop3mesg(mno : int) : string
+{
+	(ok, s, msg) := pop3->get(mno);
+	if (ok < 0 && pop3conn) {
+		fprint(stderr, "mail: could not retrieve a message from server : %s\n", s);
+		return "Acme Mail : FAILED TO RETRIEVE MESSAGE\n";
+	}
+	return msg;
+}
+
+pop3del(mno : int) : int
+{
+	(ok, s) := pop3->delete(mno);
+	if (ok < 0) 
+		fprint(stderr, "mail: could not delete message : %s\n", s);
+	return ok;
+}
+
+pop3init(b : ref Box)
+{
+	b.leng = pop3stat(b);
+	b.lst = pop3list();
+	b.s = nil;
+	b.popno = 0;
+	if (len b.lst != b.leng)
+		error("bad lengths in pop3init()");
+}
+
+pop3more(b : ref Box)
+{
+	nl : list of int;
+
+	leng := b.leng;
+	b.leng = pop3stat(b);
+	b.lst = pop3list();
+	b.s = nil;
+	b.popno = 0;
+	if (len b.lst != b.leng || b.leng < leng)
+		error("bad lengths in pop3more()");
+	# is this ok ?
+	nl = nil;
+	for (i := 0; i < leng; i++) {
+		nl = hd b.lst :: nl;
+		b.lst = tl b.lst;
+	}
+	# now update pop nos.
+	for (m := b.m; m != nil; m = m.next) {
+		# opopno := m.popno;
+		if (nl == nil)
+			error("message list too big");
+		m.popno = hd nl;
+		nl = tl nl;
+		# debug(sys->sprint("%d : popno from %d to %d\n", m.id, opopno, m.popno));
+	}
+	if (nl != nil)
+		error("message list too small");
+}
+
+pop3next(b : ref Box) : string
+{
+	mno : int = 0;
+	r : string;
+
+	if (b.s == nil) {
+		if (b.lst == nil)
+			return nil;	# end of box
+		first := b.popno == 0;
+		mno = hd b.lst;
+		b.lst = tl b.lst;
+		b.s = pop3mesg(mno);
+		b.popno = mno;
+		if (!first)
+			return nil;	# end of message
+	}
+	t := strchr(b.s, '\n');
+	if (t >= 0) {
+		r = b.s[0:t+1];
+		b.s = b.s[t+1:];
+	}
+	else {
+		r = b.s;
+		b.s = nil;
+	}
+	return r;
+}
+
+main()
+{
+	readonly : int;
+
+	initlock();
+	initreply();
+	date = time();
+	if(date==nil)
+		error("can't get current time");
+	user = getuser();
+	if(user == nil)
+		user = "Wile.E.Coyote";
+	readonly = False;
+	pop3lock = Lock.init();
+	mbox = mbox.read(readonly);
+	spawn timeslave(mbox, mbox.cmore);
+	mbox.slave();
+	error(nil);
+}
+
+timeslave(b : ref Box, c : chan of int)
+{
+	for(;;){
+		sleep(30*1000);
+		pop3open(1);
+		leng := pop3stat(b);
+		pop3close(1);
+		if (leng > b.leng)
+			c <-= 0;
+	}
+}
+
+Win.wnew() : ref Win
+{
+	w := ref Win;
+	buf := array[12] of byte;
+	w.ctl = open("/chan/new/ctl", ORDWR);
+	if(w.ctl==nil || read(w.ctl, buf, 12)!=12)
+		error("can't open window ctl file: %r");
+	w.ctlwrite("noscroll\n");
+	w.winid = int string buf;
+	w.event = w.openfile("event");
+	w.addr = nil;	# will be opened when needed
+	w.body = nil;
+	w.data = nil;
+	w.bufp = w.nbuf = 0;
+	w.buf = array[512] of byte;
+	return w;
+}
+
+Win.openfile(w : self ref Win, f : string) : ref FD
+{
+	buf := sprint("/chan/%d/%s", w.winid, f);
+	fd := open(buf, ORDWR|OCEXEC);
+	if(fd == nil)
+		error(sprint("can't open window %s file: %r", f));
+	return fd;
+}
+
+Win.openbody(w : self ref Win, mode : int)
+{
+	buf := sprint("/chan/%d/body", w.winid);
+	w.body = bufio->open(buf, mode|OCEXEC);
+	if(w.body == nil)
+		error("can't open window body file: %r");
+}
+
+Win.wwritebody(w : self ref Win, s : string)
+{
+	n := len s;
+	if(w.body == nil)
+		w.openbody(OWRITE);
+	if(w.body.puts(s) != n)
+		error("write error to window: %r");
+}
+
+Win.wreplace(w : self ref Win, addr : string, repl : string)
+{
+	if(w.addr == nil)
+		w.addr = w.openfile("addr");
+	if(w.data == nil)
+		w.data = w.openfile("data");
+	if(swrite(w.addr, addr) < 0){
+		fprint(stderr, "mail: warning: bad address %s:%r\n", addr);
+		return;
+	}
+	if(swrite(w.data, repl) != len repl)
+		error("writing data: %r");
+}
+
+nrunes(s : array of byte, nb : int) : int
+{
+	i, n : int;
+
+	n = 0;
+	for(i=0; i<nb; n++) {
+		(nil, b, ok) := byte2char(s, i);
+		if (!ok)
+			error("help needed in nrunes()");
+		i += b;
+	}
+	return n;
+}
+
+Win.wread(w : self ref Win, q0 : int, q1 : int) : string
+{
+	m, n, nr : int;
+	s, buf : string;
+	b : array of byte;
+
+	b = array[256] of byte;
+	if(w.addr == nil)
+		w.addr = w.openfile("addr");
+	if(w.data == nil)
+		w.data = w.openfile("data");
+	s = nil;
+	m = q0;
+	while(m < q1){
+		buf = sprint("#%d", m);
+		if(swrite(w.addr, buf) != len buf)
+			error("writing addr: %r");
+		n = read(w.data, b, len b);
+		if(n <= 0)
+			error("reading data: %r");
+		nr = nrunes(b, n);
+		while(m+nr >q1){
+			do; while(n>0 && (int b[--n]&16rC0)==16r80);
+			--nr;
+		}
+		if(n == 0)
+			break;
+		s += string b[0:n];
+		m += nr;
+	}
+	return s;
+}
+
+Win.wshow(w : self ref Win)
+{
+	w.ctlwrite("show\n");
+}
+
+Win.wsetdump(w : self ref Win, dir : string, cmd : string)
+{
+	t : string;
+
+	if(dir != nil){
+		t = "dumpdir " + dir + "\n";
+		w.ctlwrite(t);
+		t = nil;
+	}
+	if(cmd != nil){
+		t = "dump " + cmd + "\n";
+		w.ctlwrite(t);
+		t = nil;
+	}
+}
+
+Win.wselect(w : self ref Win, addr : string)
+{
+	if(w.addr == nil)
+		w.addr = w.openfile("addr");
+	if(swrite(w.addr, addr) < 0)
+		error("writing addr");
+	w.ctlwrite("dot=addr\n");
+}
+
+Win.wtagwrite(w : self ref Win, s : string)
+{
+	fd : ref FD;
+
+	fd = w.openfile("tag");
+	if(swrite(fd, s) != len s)
+		error("tag write: %r");
+	fd = nil;
+}
+
+Win.ctlwrite(w : self ref Win, s : string)
+{
+	if(swrite(w.ctl, s) != len s)
+		error("write error to ctl file: %r");
+}
+
+Win.wdel(w : self ref Win, sure : int) : int
+{
+	if (w == nil)
+		return False;
+	if(sure)
+		swrite(w.ctl, "delete\n");
+	else if(swrite(w.ctl, "del\n") != 4)
+		return False;
+	w.wdormant();
+	w.ctl = nil;
+	w.event = nil;
+	return True;
+}
+
+Win.wname(w : self ref Win, s : string)
+{
+	w.ctlwrite("name " + s + "\n");
+}
+
+Win.wclean(w : self ref Win)
+{
+	if(w.body != nil)
+		w.body.flush();
+	w.ctlwrite("clean\n");
+}
+
+Win.wdormant(w : self ref Win)
+{
+	w.addr = nil;
+	if(w.body != nil){
+		w.body.close();
+		w.body = nil;
+	}
+	w.data = nil;
+}
+
+Win.getec(w : self ref Win) : int
+{
+	if(w.nbuf == 0){
+		w.nbuf = read(w.event, w.buf, len w.buf);
+		if(w.nbuf <= 0 && !killing) {
+			error("event read error: %r");
+		}
+		w.bufp = 0;
+	}
+	w.nbuf--;
+	return int w.buf[w.bufp++];
+}
+
+Win.geten(w : self ref Win) : int
+{
+	n, c : int;
+
+	n = 0;
+	while('0'<=(c=w.getec()) && c<='9')
+		n = n*10+(c-'0');
+	if(c != ' ')
+		error("event number syntax");
+	return n;
+}
+
+Win.geter(w : self ref Win, buf : array of byte) : (int, int)
+{
+	r, m, n, ok : int;
+
+	r = w.getec();
+	buf[0] = byte r;
+	n = 1;
+	if(r >= Runeself) {
+		for (;;) {
+			(r, m, ok) = byte2char(buf[0:n], 0);
+			if (m > 0)
+				return (r, n);
+			buf[n++] = byte w.getec();
+		}
+	}
+	return (r, n);
+}
+
+Win.wevent(w : self ref Win, e : ref Event)
+{
+	i, nb : int;
+
+	e.c1 = w.getec();
+	e.c2 = w.getec();
+	e.q0 = w.geten();
+	e.q1 = w.geten();
+	e.flag = w.geten();
+	e.nr = w.geten();
+	if(e.nr > EVENTSIZE)
+		error("event string too long");
+	e.nb = 0;
+	for(i=0; i<e.nr; i++){
+		(e.r[i], nb) = w.geter(e.b[e.nb:]);
+		e.nb += nb;
+	}
+	e.r[e.nr] = 0;
+	e.b[e.nb] = byte 0;
+	c := w.getec();
+	if(c != '\n')
+		error("event syntax 2");
+}
+
+Win.wslave(w : self ref Win, ce : chan of Event)
+{
+	e : ref Event;
+
+	e = newevent();
+	for(;;){
+		w.wevent(e);
+		ce <-= *e;
+	}
+}
+
+Win.wwriteevent(w : self ref Win, e : ref Event)
+{
+	fprint(w.event, "%c%c%d %d\n", e.c1, e.c2, e.q0, e.q1);
+}
+
+Win.wreadall(w : self ref Win) : string
+{
+	s, t : string;
+
+	if(w.body != nil)
+		w.body.close();
+	w.openbody(OREAD);
+	s = nil;
+	while ((t = w.body.gets('\n')) != nil)
+		s += t;
+	w.body.close();
+	w.body = nil;
+	return s;
+}
+
+None, Unknown, Ignore, CC, From, ReplyTo, Sender, Subject, Re, To, Date, Received : con iota;
+NHeaders : con 200;
+
+Hdrs : adt {
+	name : string;
+	typex : int;
+};
+
+
+hdrs := array[NHeaders+1] of {
+	Hdrs ( "CC:",				CC ),
+	Hdrs ( "From:",				From ),
+	Hdrs ( "Reply-To:",			ReplyTo ),
+	Hdrs ( "Sender:",			Sender ),
+	Hdrs ( "Subject:",			Subject ),
+	Hdrs ( "Re:",				Re ),
+	Hdrs ( "To:",				To ),
+	Hdrs ( "Date:",				Date),
+	Hdrs ( "Received:",			Received),
+ * => Hdrs ( "",					0 ),
+};
+
+StRnCmP(s : string, t : string, n : int) : int
+{
+	c, d, i, j : int;
+
+	i = j = 0;
+	if (len s < n || len t < n)
+		return -1;
+	while(n > 0){
+		c = s[i++];
+		d = t[j++];
+		--n;
+		if(c != d){
+			if('a'<=c && c<='z')
+				c -= 'a'-'A';
+			if('a'<=d && d<='z')
+				d -= 'a'-'A';
+			if(c != d)
+				return c-d;
+		}
+	}
+	return 0;
+}
+
+readhdr(b : ref Box) : (string, int)
+{
+	i, j, n, m, typex : int;
+	s, t : string;
+
+{
+	s = b.readline();
+	n = len s;
+	if(n <= 0) {
+		b.unreadline();
+		raise("e");
+	}
+	for(i=0; i<n; i++){
+		j = s[i];
+		if(i>0 && j == ':')
+			break;
+		if(j<'!' || '~'<j){
+			b.unreadline();
+			raise("e");
+		}
+	}
+	typex = Unknown;
+	for(i=0; hdrs[i].name != nil; i++){
+		j = len hdrs[i].name;
+		if(StRnCmP(hdrs[i].name, s, j) == 0){
+			typex = hdrs[i].typex;
+			break;
+		}
+	}
+	# scan for multiple sublines 
+	for(;;){
+		t = b.readline();
+		m = len t;
+		if(m<=0 || (t[0]!=' ' && t[0]!='\t')){
+			b.unreadline();
+			break;
+		}
+		# absorb 
+		s += t;
+	}
+	return(s, typex);
+}
+exception{
+	"*" =>
+		return (nil, None);
+}
+}
+
+Mesg.read(b : ref Box) : ref Mesg
+{
+	m : ref Mesg;
+	s : string;
+	n, typex : int;
+
+	for(;;){
+		s = b.readline();
+		n = len s;
+		if(n <= 0)
+			return nil;
+		if(n >= 5 && (s[0:5] == "From:" || s[0:5] == "From "))
+			break;
+	}
+{
+	if(n < 5 || (s[0:5] !="From " && s[0:5] != "From:"))
+		raise("e");
+	m = newmesg();
+	m.popno = b.popno;
+	if (m.popno == 0)
+		error("bad pop3 id");
+	m.realhdr = s;
+	# toss 'From ' 
+	s = s[5:];
+	n -= 5;
+	# toss spaces/tabs
+	while (n > 0 && (s[0] == ' ' || s[0] == '\t')) {
+		s = s[1:];
+		n--;
+	}
+	m.hdr = s;
+	# convert first blank to tab 
+	s0 := strchr(m.hdr, ' ');
+	if(s0 >= 0){
+		m.hdr[s0] = '\t';
+		# drop trailing seconds, time zone, and year if match local year 
+		t := n-6;
+		if(t <= 0)
+			raise("e");
+		if(m.hdr[t:n-1] == date[23:]){
+			m.hdr = m.hdr[0:t] + "\n";	# drop year for sure
+			t = -1;
+			s1 := strchr(m.hdr[s0:], ':');
+			if(s1 >= 0)
+				t = strchr(m.hdr[s0+s1+1:], ':');
+			if(t >= 0)	# drop seconds and time zone 
+				m.hdr = m.hdr[0:s0+s1+t+1] + "\n";
+			else{	# drop time zone 
+				t = strchr(m.hdr[s0+s1+1:], ' ');
+				if(t >= 0)
+					m.hdr = m.hdr[0:s0+s1+t+1] + "\n";
+			}
+			n = len m.hdr;
+		}
+	}
+	m.lline1 = n;
+	m.text = nil;
+	# read header 
+loop:
+	for(;;){
+		(s, typex) = readhdr(b);
+		case(typex){
+		None =>
+			break loop;
+		ReplyTo =>
+			m.replyto = s[9:];
+			break;
+		From =>
+			if(m.replyto == nil)
+				m.replyto = s[5:];
+			break;
+		Subject =>
+			m.subj = s[8:];
+			break;
+		Re =>
+			m.subj = s[3:];
+			break;
+		Date =>
+			break;
+		}
+		m.realhdr += s;
+		if(typex != Ignore)
+			m.hdr += s;
+	}
+	# read body 
+	for(;;){
+		s = b.readline();
+		n = len s;
+		if(n <= 0)
+			break;
+#		if(len s >= 5 && (s[0:5] == "From " || s[0:5] == "From:")){
+#			b.unreadline();
+#			break;
+#		}
+		m.text += s;
+	}
+	# remove trailing "morF\n" 
+	l := len m.text;
+	if(l>6 && m.text[l-6:] == "\nmorF\n")
+		m.text = m.text[0:l-5];
+	m.box = b;
+	return m;
+}
+exception{
+	"*" =>
+		error("malformed header " + s);
+		return nil;
+}
+}
+
+Mesg.mkmail(b : ref Box, hdr : string)
+{
+	r : ref Mesg;
+
+	r = newmesg();
+	r.hdr = hdr + "\n";
+	r.lline1 = len r.hdr;
+	r.text = nil;
+	r.box = b;
+	r.open();
+	r.w.wdormant();
+}
+
+replyaddr(r : string) : string
+{
+	p, q, rr : int;
+
+	rr = 0;
+	while(r[rr]==' ' || r[rr]=='\t')
+		rr++;
+	r = r[rr:];
+	p = strchr(r, '<');
+	if(p >= 0){
+		q = strchr(r[p+1:], '>');
+		if(q < 0)
+			r = r[p+1:];
+		else
+			r = r[p+1:p+q] + "\n";
+		return r;
+	}
+	p = strchr(r, '(');
+	if(p >= 0){
+		q = strchr(r[p:], ')');
+		if(q < 0)
+			r = r[0:p];
+		else
+			r = r[0:p] + r[p+q+1:];
+	}
+	return r;
+}
+
+Mesg.mkreply(m : self ref Mesg)
+{
+	r : ref Mesg;
+
+	r = newmesg();
+	if(m.replyto != nil){
+		r.hdr = replyaddr(m.replyto);
+		r.lline1 = len r.hdr;
+	}else{
+		r.hdr = m.hdr[0:m.lline1];
+		r.lline1 = m.lline1;	# was len m.hdr;
+	}
+	if(m.subj != nil){
+		if(StRnCmP(m.subj, "re:", 3)==0 || StRnCmP(m.subj, " re:", 4)==0)
+			r.text = "Subject:" + m.subj + "\n";
+		else
+			r.text = "Subject: Re:" + m.subj + "\n";
+	}
+	else
+		r.text = nil;
+	r.box = m.box;
+	r.open();
+	r.w.wselect("$");
+	r.w.wdormant();
+}
+
+Mesg.free(m : self ref Mesg)
+{
+	m.text = nil;
+	m.hdr = nil;
+	m.subj = nil;
+	m.realhdr = nil;
+	m.replyto = nil;
+	m = nil;
+}
+
+replyid : ref Ref;
+
+initreply()
+{
+	replyid = Ref.init();
+}
+
+Mesg.open(m : self ref Mesg)
+{
+	buf, s : string;
+
+	if(m.isopen)
+		return;
+	m.w = Win.wnew();
+	if(m.id != 0)
+		m.w.wwritebody("From ");
+	m.w.wwritebody(m.hdr);
+	m.w.wwritebody(m.text);
+	if(m.id){
+		buf = sprint("Mail/box/%d", m.id);
+		m.w.wtagwrite("Reply Delmesg Save");
+	}else{
+		buf = sprint("Mail/%s/Reply%d", s, replyid.inc());
+		m.w.wtagwrite("Post");
+	}
+	m.w.wname(buf);
+	m.w.wclean();
+	m.w.wselect("0");
+	m.isopen = True;
+	m.posted = False;
+	spawn m.slave();
+}
+
+Mesg.putpost(m : self ref Mesg, e : ref Event)
+{
+	if(m.posted || m.id==0)
+		return;
+	if(e.q0 >= len m.hdr+5)	# include "From " 
+		return;
+	m.w.wtagwrite(" Post");
+	m.posted = True;
+	return;
+}
+
+Mesg.slave(m : self ref Mesg)
+{
+	e, e2, ea, etoss, eq : ref Event;
+	s : string;
+	na : int;
+
+	e = newevent();
+	e2 = newevent();
+	ea = newevent();
+	etoss = newevent();
+	for(;;){
+		m.w.wevent(e);
+		case(e.c1){
+		'E' =>	# write to body; can't affect us 
+			break;
+		'F' =>	# generated by our actions; ignore 
+			break;
+		'K' or 'M' =>	# type away; we don't care 
+			case(e.c2){
+			'x' or 'X' =>	# mouse only 
+				eq = e;
+				if(e.flag & 2){
+					m.w.wevent(e2);
+					eq = e2;
+				}
+				if(e.flag & 8){
+					m.w.wevent(ea);
+					m.w.wevent(etoss);
+					na = ea.nb;
+				}else
+					na = 0;
+				if(eq.q1>eq.q0 && eq.nb==0)
+					s = m.w.wread(eq.q0, eq.q1);
+				else
+					s = string eq.b[0:eq.nb];
+				if(na)
+					s = s + " " + string ea.b[0:ea.nb];
+				if(!m.command(s))	# send it back 
+					m.w.wwriteevent(e);
+				s = nil;
+				break;
+			'l' or 'L' =>	# mouse only 
+				if(e.flag & 2)
+					m.w.wevent(e2);
+				# just send it back 
+				m.w.wwriteevent(e);
+				break;
+			'I' or 'D' =>	# modify away; we don't care 
+				m.putpost(e);
+				break;
+			'd' or 'i' =>
+				break;
+			* =>
+				fprint(stdout, "unknown message %c%c\n", e.c1, e.c2);
+				break;
+			}
+		* =>
+			fprint(stdout, "unknown message %c%c\n", e.c1, e.c2);
+			break;
+		}
+	}
+}
+
+Mesg.command(m : self ref Mesg, s : string) : int
+{
+	while(s[0]==' ' || s[0]=='\t' || s[0]=='\n')
+		s = s[1:];
+	if(s == "Post"){
+		m.send();
+		return True;
+	}
+	if(len s >= 4 && s[0:4] == "Save"){
+		s = s[4:];
+		while(len s > 0 && (s[0]==' ' || s[0]=='\t' || s[0]=='\n'))
+			s = s[1:];
+		if(s == nil)
+			m.save("stored");
+		else{
+			ss := 0;
+			while(ss < len s && s[ss]!=' ' && s[ss]!='\t' && s[ss]!='\n')
+				ss++;
+			m.save(s[0:ss]);
+		}
+		return True;
+	}
+	if(s == "Reply"){
+		m.mkreply();
+		return True;
+	}
+	if(s == "Del"){
+		if(m.w.wdel(False)){
+			m.isopen = False;
+			exit;
+		}
+		return True;
+	}
+	if(s == "Delmesg"){
+		if(m.w.wdel(False)){
+			m.isopen = False;
+			m.box.cdel <-= m;
+			exit;
+		}
+		return True;
+	}
+	return False;
+}
+
+Mesg.save(m : self ref Mesg, base : string)
+{
+	s, buf : string;
+	n : int;
+	fd : ref FD;
+	b : ref Iobuf;
+
+	if(m.id <= 0){
+		fprint(stderr, "can't save reply message; mail it to yourself\n");
+		return;
+	}
+	buf = nil;
+	s = base;
+{
+	if(access(s) < 0)
+		raise("e");
+	fd = tryopen(s, OWRITE);
+	if(fd == nil)
+		raise("e");
+	buf = nil;
+	b = bufio->fopen(fd, OWRITE);
+	# seek to end in case file isn't append-only 
+	b.seek(big 0, 2);
+	# use edited headers: first line of real header followed by remainder of selected ones 
+	for(n=0; n<len m.realhdr && m.realhdr[n++]!='\n'; )
+		;
+	b.puts(m.realhdr[0:n]);
+	b.puts(m.hdr[m.lline1:]);
+	b.puts(m.text);
+	b.close();
+	b = nil;
+	fd = nil;
+}
+exception{
+	"*" =>
+		buf = nil;
+		fprint(stderr, "mail: can't open %s: %r\n", base);
+		return;
+}
+}
+
+Mesg.send(m : self ref Mesg)
+{
+	s, buf : string;
+	t, u : int;
+	a, b : list of string;
+	n : int;
+	p : array of ref FD;
+	c : chan of int;
+
+	p = array[2] of ref FD;
+	s = m.w.wreadall();
+	a = "sendmail" :: nil;
+	if(len s >= 5 && (s[0:5] == "From " || s[0:5] == "From:"))
+		s = s[5:];
+	for(t=0; t < len s && s[t]!='\n' && s[t]!='\t';){
+		while(t < len s && (s[t]==' ' || s[t]==','))
+			t++;
+		u = t;
+		while(t < len s && s[t]!=' ' && s[t]!=',' && s[t]!='\t' && s[t]!='\n')
+			t++;
+		if(t == u)
+			break;
+		a = s[u:t] :: a;
+	}
+	b = nil;
+	for ( ; a != nil; a = tl a)
+		b = hd a :: b;
+	a = b;
+	while(t < len s && s[t]!='\n')
+		t++;
+	if(s[t] == '\n')
+		t++;
+	if(pipe(p) < 0)
+		error("can't pipe: %r");
+	c = chan of int;
+	spawn run(a, c, p[0]);
+	<-c;
+	c = nil;
+	p[0] = nil;
+	n = len s - t;
+	if(swrite(p[1], s[t:]) != n)
+		fprint(stderr, "write to pipe failed: %r\n");
+	p[1] = nil;
+	# run() frees the arg list 
+	buf = sprint("Mail/box/%d-R", m.id);
+	m.w.wname(buf);
+	m.w.wclean();
+}
+
+Box.read(readonly : int) : ref Box
+{
+	b : ref Box;
+	m : ref Mesg;
+	buf : string;
+
+	b = ref Box;
+	b.nm = 0;
+	b.leng = 0;
+	b.readonly = readonly;
+	b.w = Win.wnew();
+	b.w.wwritebody("Password:");
+	b.w.wname("Mail/box/");
+	b.w.wclean();
+	b.w.wselect("$");
+	b.w.ctlwrite("noecho\n");
+	b.cevent = chan of Event;
+	spawn b.w.wslave(b.cevent);
+	e := ref Event;
+	for (;;) {
+		sleep(1000);
+		s := b.w.wreadall();
+		lens := len s;
+		if (lens >= 10 && s[0:9] == "Password:" && s[lens-1] == '\n') {
+			pwd = s[9:lens-1];
+			for (i := 0; i < lens; i++)
+				s[i] = '\b';
+			b.w.wwritebody(s);
+			break;
+		}
+		alt {
+			*e = <-b.cevent =>
+				b.event(e);
+				break;
+			* =>
+				break;
+		}
+	}
+	b.w.ctlwrite("echo\n");
+	pop3open(1);
+	pop3init(b);
+	while((m = m.read(b)) != nil){
+		m.next = b.m;
+		b.m = m;
+		b.nm++;
+		m.id = b.nm;
+	}
+	pop3close(1);
+	if (b.leng != b.nm)
+		error("bad message count in Box.read()");
+	# b.w = Win.wnew();
+	for(m=b.m; m != nil; m=m.next){
+		if(m.subj != nil)
+			buf = sprint("%d\t%s\t %s", m.id, m.hdr[0:m.lline1], m.subj);
+		else
+			buf = sprint("%d\t%s", m.id, m.hdr[0:m.lline1]);
+		b.w.wwritebody(buf);
+	}
+	# b.w.wname("Mail/box/");
+	if(b.readonly)
+		b.w.wtagwrite("Mail");
+	else
+		b.w.wtagwrite("Put Mail");
+	b.w.wsetdump("/acme/mail", "Mail box");
+	b.w.wclean();
+	b.w.wselect("0");
+	b.w.wdormant();
+	b.cdel= chan of ref Mesg;
+	b.cmore = chan of int;
+	b.clean = True;
+	return b;
+}
+
+Box.readmore(b : self ref Box, lck : int)
+{
+	m : ref Mesg;
+	new : int;
+	buf : string;
+
+	new = False;
+	leng := b.leng;
+	n := 0;
+	pop3open(lck);
+	pop3more(b);
+	while((m = m.read(b)) != nil){
+		m.next = b.m;
+		b.m = m;
+		b.nm++;
+		n++;
+		m.id = b.nm;
+		if(m.subj != nil)
+			buf  = sprint("%d\t%s\t  %s", m.id, m.hdr[0:m.lline1], m.subj);
+		else
+			buf = sprint("%d\t%s", m.id, m.hdr[0:m.lline1]);
+		b.w.wreplace("0", buf);
+		new = True;
+	}
+	pop3close(1);
+	if (b.leng != leng+n)
+		error("bad message count in Box.readmore()");
+	if(new){
+		if(b.clean)
+			b.w.wclean();
+		b.w.wselect("0;/.*(\\n[ \t].*)*");
+		b.w.wshow();
+	}
+	b.w.wdormant();
+}
+
+Box.readline(b : self ref Box) : string
+{
+    	for (;;) {
+		if(b.peekline != nil){
+			b.line = b.peekline;
+			b.peekline = nil;
+		}else
+			b.line = pop3next(b);
+		# nulls appear in mailboxes! 
+		if(b.line != nil && strchr(b.line, 0) >= 0)
+			;
+		else
+			break;
+	}
+	return b.line;
+}
+
+Box.unreadline(b : self ref Box)
+{
+	b.peekline = b.line;
+}
+
+Box.slave(b : self ref Box)
+{
+	e : ref Event;
+	m : ref Mesg;
+
+	e = newevent();
+	for(;;){
+		alt{
+		*e = <-b.cevent =>
+			b.event(e);
+			break;
+		<-b.cmore =>
+			b.readmore(1);
+			break;
+		m = <-b.cdel =>
+			b.mdel(m);
+			break;
+		}
+	}
+}
+
+Box.event(b : self ref Box, e : ref Event)
+{
+	e2, ea, eq : ref Event;
+	s : string;
+	t : int;
+	n, na, nopen : int;
+
+	e2 = newevent();
+	ea = newevent();
+	case(e.c1){
+	'E' =>	# write to body; can't affect us 
+		break;
+	'F' =>	# generated by our actions; ignore 
+		break;
+	'K' =>	# type away; we don't care 
+		break;
+	'M' =>
+		case(e.c2){
+		'x' or 'X' =>
+			if(e.flag & 2)
+				*e2 = <-b.cevent;
+			if(e.flag & 8){
+				*ea = <-b.cevent;
+				na = ea.nb;
+				<- b.cevent;
+			}else
+				na = 0;
+			s = string e.b[0:e.nb];
+			# if it's a known command, do it 
+			if((e.flag&2) && e.nb==0)
+				s = string e2.b[0:e2.nb];
+			if(na)
+				s = sprint("%s %s", s, string ea.b[0:ea.nb]);
+			# if it's a long message, it can't be for us anyway 
+			if(!b.command(s))	# send it back 
+				b.w.wwriteevent(e);
+			if(na)
+				s = nil;
+			break;
+		'l' or 'L' =>
+			eq = e;
+			if(e.flag & 2){
+				*e2 = <-b.cevent;
+				eq = e2;
+			}
+			s = string eq.b[0:eq.nb];
+			if(eq.q1>eq.q0 && eq.nb==0)
+				s = b.w.wread(eq.q0, eq.q1);
+			nopen = 0;
+			do{
+				t = 0;
+				(n, t) = strtoi(s);
+				if(n>0 && (t == len s || s[t]==' ' || s[t]=='\t' || s[t]=='\n')){
+					b.mopen(n);
+					nopen++;
+					s = s[t:];
+				}
+				while(s != nil && s[0]!='\n')
+					s = s[1:];
+			}while(s != nil);
+			if(nopen == 0)	# send it back 
+				b.w.wwriteevent(e);
+			break;
+		'I' or 'D' or 'd' or 'i' =>	# modify away; we don't care 
+			break;
+		* =>
+			fprint(stdout, "unknown message %c%c\n", e.c1, e.c2);
+			break;
+		}
+	* =>
+		fprint(stdout, "unknown message %c%c\n", e.c1, e.c2);
+		break;
+	}
+}
+
+Box.mopen(b : self ref Box, id : int)
+{
+	m : ref Mesg;
+
+	for(m=b.m; m != nil; m=m.next)
+		if(m.id == id){
+			m.open();
+			break;
+		}
+}
+
+Box.mdel(b : self ref Box, dm : ref Mesg)
+{
+	m : ref Mesg;
+	buf : string;
+
+	if(dm.id){
+		for(m=b.m; m!=nil && m!=dm; m=m.next)
+			;
+		if(m == nil)
+			error(sprint("message %d not found", dm.id));
+		m.deleted = 1;
+		# remove from screen: use acme to help 
+		buf = sprint("/^%d	.*\\n(^[ \t].*\\n)*/", m.id);
+		b.w.wreplace(buf, "");
+	}
+	dm.free();
+	b.clean = False;
+}
+
+Box.command(b : self ref Box, s : string) : int
+{
+	t : int;
+	m : ref Mesg;
+
+	while(s[0]==' ' || s[0]=='\t' || s[0]=='\n')
+		s = s[1:];
+	if(len s >= 4 && s[0:4] == "Mail"){
+		s = s[4:];
+		while(s != nil && (s[0]==' ' || s[0]=='\t' || s[0]=='\n'))
+			s = s[1:];
+		t = 0;
+		while(t < len s && s[t] && s[t]!=' ' && s[t]!='\t' && s[t]!='\n')
+			t++;
+		m = b.m;		# avoid warning message on b.m.mkmail(...)
+		m.mkmail(b, s[0:t]);
+		return True;
+	}
+	if(s == "Del"){
+
+		if(!b.clean){
+			b.clean = True;
+			fprint(stderr, "mail: mailbox not written\n");
+			return True;
+		}
+		postnote(PNGROUP, pctl(0, nil), "kill");
+		killing = 1;
+		pctl(NEWPGRP, nil);
+		b.w.wdel(True);
+		for(m=b.m; m != nil; m=m.next)
+			m.w.wdel(False);
+		exit;
+		return True;
+	}
+	if(s == "Put"){
+		if(b.readonly)
+			fprint(stderr, "Mail is read-only\n");
+		else
+			b.rewrite();
+		return True;
+	}
+	return False;
+}
+
+Box.rewrite(b : self ref Box)
+{
+	prev, m : ref Mesg;
+
+	if(b.clean){
+		b.w.wclean();
+		return;
+	}
+	prev = nil;
+	pop3open(1);
+	for(m=b.m; m!=nil; m=m.next) {
+		if (m.deleted && pop3del(m.popno) >= 0) {
+			b.leng--;
+			if (prev == nil)
+				b.m=m.next;
+			else
+				prev.next=m.next;
+		}
+		else
+			prev = m;
+	}
+	# must update pop nos now so don't unlock pop3
+	pop3close(0);
+	b.w.wclean();
+	b.clean = True;
+	b.readmore(0);	# updates pop nos
+}
--- /dev/null
+++ b/appl/acme/acme/mail/src/mashfile
@@ -1,0 +1,18 @@
+make -clear;
+
+MOD=module;
+DISBIN=/acme/mail;
+LFLAGS=-I/$MOD -gw;
+
+fn lcom {
+	limbo $LFLAGS $args;
+};
+
+TARG=mail.dis;
+
+*.dis 			:~ $1.b 		{ lcom $1.b };
+$DISBIN/*.dis 	:~ $1.dis 		{ cp $1.dis $DISBIN };
+
+default : $TARG {};
+all : $TARG {};
+install : $DISBIN/*.dis {};
--- /dev/null
+++ b/appl/acme/acme/mail/src/mkfile
@@ -1,0 +1,16 @@
+<../../../../../mkconfig
+
+TARG=\
+	Mail.dis\
+	Mailpop3.dis\
+
+MODULES=\
+
+SYSMODULES=\
+	sh.m\
+	sys.m\
+	draw.m\
+
+DISBIN=$ROOT/acme/mail
+
+<$ROOT/mkfiles/mkdis
--- /dev/null
+++ b/appl/acme/acme/mkfile
@@ -1,0 +1,9 @@
+<../../../mkconfig
+
+DIRS=\
+	acid\
+	bin\
+	edit\
+	mail\
+
+<$ROOT/mkfiles/mksubdirs
--- /dev/null
+++ b/appl/acme/buff.b
@@ -1,0 +1,380 @@
+implement Bufferm;
+
+include "common.m";
+
+sys : Sys;
+dat : Dat;
+utils : Utils;
+diskm : Diskm;
+ecmd: Editcmd;
+
+FALSE, TRUE, XXX, Maxblock, Astring : import Dat;
+Block : import Dat;
+disk : import dat;
+Disk : import diskm;
+File: import Filem;
+error, warning, min : import utils;
+
+init(mods : ref Dat->Mods)
+{
+	sys = mods.sys;
+	dat = mods.dat;
+	utils = mods.utils;
+	diskm = mods.diskm;
+	ecmd = mods.editcmd;
+}
+
+nullbuffer : Buffer;
+
+newbuffer() : ref Buffer
+{
+	b := ref nullbuffer;
+	return b;
+}
+
+Slop : con 100;	# room to grow with reallocation
+
+Buffer.sizecache(b : self ref Buffer, n : int)
+{
+	if(n <= b.cmax)
+		return;
+	b.cmax = n+Slop;
+	os := b.c;
+	b.c = utils->stralloc(b.cmax);
+	if (os != nil) {
+		loss := len os.s;
+		c := b.c;
+		oss := os.s;
+		for (i := 0; i < loss && i < b.cmax; i++)
+			c.s[i] = oss[i];
+		utils->strfree(os);
+	}
+}
+
+#
+# Move cache so b.cq <= q0 < b.cq+b.cnc.
+# If at very end, q0 will fall on end of cache block.
+#
+
+Buffer.flush(b : self ref Buffer)
+{
+	if(b.cdirty || b.cnc==0){
+		if(b.cnc == 0)
+			b.delblock(b.cbi);
+		else
+			b.bl[b.cbi] = disk.write(b.bl[b.cbi], b.c.s, b.cnc);
+		b.cdirty = FALSE;
+	}
+}
+
+Buffer.setcache(b : self ref Buffer, q0 : int)
+{
+	blp, bl : ref Block;
+	i, q : int;
+
+	if (q0 > b.nc)
+		error("bad assert in 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;
+	b.flush();
+	# 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 = b.bl[i];
+		if(i >= b.nbl)
+			error("block not found");
+	}
+	bl = blp;
+	# remember position 
+	b.cbi = i;
+	b.cq = q;
+	b.sizecache(bl.n);
+	b.cnc = bl.n;
+	#read block
+	disk.read(bl, b.c, b.cnc);
+}
+
+Buffer.addblock(b : self ref Buffer, i : int, n : int)
+{
+	if (i > b.nbl)
+		error("bad assert in addblock");
+
+	obl := b.bl;
+	b.bl = array[b.nbl+1] of ref Block;
+	b.bl[0:] = obl[0:i];
+	if(i < b.nbl)
+		b.bl[i+1:] = obl[i:b.nbl];
+	b.bl[i] = disk.new(n);
+	b.nbl++;
+	obl = nil;
+}
+
+Buffer.delblock(b : self ref Buffer, i : int)
+{
+	if (i >= b.nbl)
+		error("bad assert in delblock");
+
+	disk.release(b.bl[i]);
+	obl := b.bl;
+	b.bl = array[b.nbl-1] of ref Block;
+	b.bl[0:] = obl[0:i];
+	if(i < b.nbl-1)
+		b.bl[i:] = obl[i+1:b.nbl];
+	b.nbl--;
+	obl = nil;
+}
+
+Buffer.insert(b : self ref Buffer, q0 : int, s : string, n : int)
+{
+	i, j,  m, t, off, p : int;
+
+	if (q0>b.nc)
+		error("bad assert in insert");
+	p = 0;
+	while(n > 0){
+		b.setcache(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("bad assert in insert");
+				b.addblock(0, t);
+				b.cbi = 0;
+			}
+			b.sizecache(t);
+			c := b.c;
+			# cs := c.s;
+			for (j = b.cnc-1; j >= off; j--)
+				c.s[j+m] = c.s[j];
+			for (j = 0; j < m; j++)
+				c.s[off+j] = s[p+j];
+			b.cnc = t;
+		}
+		#
+		# 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.
+		#
+		else if(q0==b.cq || q0==b.cq+b.cnc){
+			if(b.cdirty)
+				b.flush();
+			m = min(n, Maxblock);
+			if(b.bl == nil){	# allocate 
+				if (b.cnc != 0)
+					error("bad assert in insert");
+				i = 0;
+			}else{
+				i = b.cbi;
+				if(q0 > b.cq)
+					i++;
+			}
+			b.addblock(i, m);
+			b.sizecache(m);
+			c := b.c;
+			for (j = 0; j < m; j++)
+				c.s[j] = s[p+j];
+			b.cq = q0;
+			b.cbi = i;
+			b.cnc = m;
+		}
+		else {
+			#
+		 	# Split the block; cut off the right side and
+		 	# let go of it.
+			#
+		 
+			m = b.cnc-off;
+			if(m > 0){
+				i = b.cbi+1;
+				b.addblock(i, m);
+				b.bl[i] = disk.write(b.bl[i], b.c.s[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);
+			b.sizecache(b.cnc+m);
+			c := b.c;
+			for (j = 0; j < m; j++)
+				c.s[j+b.cnc] = s[p+j];
+			b.cnc += m;
+		}
+		b.nc += m;
+		q0 += m;
+		p += m;
+		n -= m;
+		b.cdirty = TRUE;
+	}
+}
+
+Buffer.delete(b : self ref Buffer, q0 : int, q1 : int)
+{
+	m, n, off : int;
+
+	if (q0>q1 || q0>b.nc || q1>b.nc)
+		error("bad assert in delete");
+
+	while(q1 > q0){
+		b.setcache(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) {
+			c := b.c;
+			# cs := c.s;
+			p := m+off;
+			for (j := off; j < p; j++)
+				c.s[j] = c.s[j+n];
+		}
+		b.cnc -= n;
+		b.cdirty = TRUE;
+		q1 -= n;
+		b.nc -= n;
+	}
+}
+
+# Buffer.replace(b: self ref Buffer, q0: int, q1: int, s: string, n: int)
+# {
+#	if(q0>q1 || q0>b.nc || q1>b.nc || n != q1-q0)
+#		error("bad assert in replace");
+#	p := 0;
+#	while(q1 > q0){
+#		b.setcache(q0);
+#		off := q0-b.cq;
+#		if(q1 > b.cq+b.cnc)
+#			n = b.cnc-off;
+#		else
+#			n = q1-q0;
+#		c := b.c;
+#		for(i := 0; i < n; i++)
+#			c.s[i+off] = s[i+p];
+#		b.cdirty = TRUE;
+#		q0 += n;
+#		p += n;
+#	}	
+# }
+
+pbuf : array of byte;
+
+bufloader(b: ref Buffer, q0: int, r: string, nr: int): int
+{
+	b.insert(q0, r, nr);
+	return nr;
+}
+
+loadfile(fd: ref Sys->FD, q0: int, fun: int, b: ref Buffer, f: ref File): int
+{
+	p : array of byte;
+	r : string;
+	m, n, nb, nr : int;
+	q1 : int;
+
+	if (pbuf == nil)
+		pbuf = array[Maxblock+Sys->UTFmax] of byte;
+	p = pbuf;
+	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 = sys->read(fd, p[m:], Maxblock);
+		if(n < 0){
+			warning(nil, "read error in Buffer.load");
+			break;
+		}
+		m += n;
+		nb = sys->utfbytes(p, m);
+		r = string p[0:nb];
+		p[0:] = p[nb:m];
+		m -= nb;
+		nr = len r;
+		if(fun == Dat->BUFL)
+			q1 += bufloader(b, q1, r, nr);
+		else
+			q1 += ecmd->readloader(f, q1, r, nr);
+	}
+	p = nil;
+	r = nil;
+	return q1-q0;
+}
+
+Buffer.loadx(b : self ref Buffer, q0 : int, fd : ref Sys->FD) : int
+{
+	if (q0>b.nc)
+		error("bad assert in load");
+	return loadfile(fd, q0, Dat->BUFL, b, nil);
+}
+
+Buffer.read(b : self ref Buffer, q0 : int, s : ref Astring, p : int, n : int)
+{
+	m : int;
+
+	if (q0>b.nc || q0+n>b.nc)
+		error("bad assert in read");
+	while(n > 0){
+		b.setcache(q0);
+		m = min(n, b.cnc-(q0-b.cq));
+		c := b.c;
+		cs := c.s;
+		for (j := 0; j < m; j++)
+			s.s[p+j] = cs[j+q0-b.cq];
+		q0 += m;
+		p += m;
+		n -= m;
+	}
+}
+
+Buffer.reset(b : self ref Buffer)
+{
+	i : int;
+
+	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; )
+		b.delblock(i);
+}
+
+Buffer.close(b : self ref Buffer)
+{
+	b.reset();
+	if (b.c != nil) {
+		utils->strfree(b.c);
+		b.c = nil;
+	}
+	b.cnc = 0;
+	b.bl = nil;
+	b.nbl = 0;
+}
--- /dev/null
+++ b/appl/acme/buff.m
@@ -1,0 +1,34 @@
+Bufferm : module {
+	PATH : con "/dis/acme/buff.dis";
+
+	init : fn(mods : ref Dat->Mods);
+
+	newbuffer : fn() : ref Buffer;
+
+	Buffer : adt {
+		nc : int;
+		c : ref Dat->Astring;		# cache
+		cnc : int;		# bytes in cache
+		cmax : int;	# size of allocated cache
+		cq : int;		# position of cache
+		cdirty : int;	# cache needs to be written
+		cbi : int;		# index of cache Block
+		bl : array of ref Dat->Block;	# array of blocks
+		nbl : int;		# number of blocks
+
+		insert : fn(b : self ref Buffer, n : int, s : string, m : int);
+		delete : fn(b : self ref Buffer, n : int, m : int);
+		# replace : fn(b : self ref Buffer, q0 : int, q1 : int, s : string, n : int);
+		loadx : fn(b : self ref Buffer, n : int, fd : ref Sys->FD) : int;
+		read : fn(b : self ref Buffer, n : int, s : ref Dat->Astring, p, m : int);
+		close : fn(b : self ref Buffer);
+		reset : fn(b : self ref Buffer);
+		sizecache : fn(b : self ref Buffer, n : int);
+		flush : fn(b : self ref Buffer);
+		setcache : fn(b : self ref Buffer, n : int);
+		addblock : fn(b : self ref Buffer, n : int, m : int);
+		delblock : fn(b : self ref Buffer, n : int);
+	};
+
+	loadfile: fn(fd: ref Sys->FD, q1: int, fun: int, b: ref Bufferm->Buffer, f: ref Filem->File): int;
+};
--- /dev/null
+++ b/appl/acme/col.b
@@ -1,0 +1,610 @@
+implement Columnm;
+
+include "common.m";
+
+sys : Sys;
+utils : Utils;
+drawm : Draw;
+acme : Acme;
+graph : Graph;
+gui : Gui;
+dat : Dat;
+textm : Textm;
+rowm : Rowm;
+filem : Filem;
+windowm : Windowm;
+
+FALSE, TRUE, XXX : import Dat;
+Border : import Dat;
+mouse, colbutton : import dat;
+Point, Rect, Image : import drawm;
+draw : import graph;
+min, max, abs, error, clearmouse : import utils;
+black, white, mainwin : import gui;
+Text : import textm;
+Row : import rowm;
+Window : import windowm;
+File : import filem;
+Columntag : import Textm;
+BACK : import Framem;
+tagcols, textcols : import acme;
+
+init(mods : ref Dat->Mods)
+{
+	sys = mods.sys;
+	dat = mods.dat;
+	utils = mods.utils;
+	drawm = mods.draw;
+	acme = mods.acme;
+	graph = mods.graph;
+	gui = mods.gui;
+	textm = mods.textm;
+	rowm = mods.rowm;
+	filem = mods.filem;
+	windowm = mods.windowm;
+}
+
+Column.init(c : self ref Column, r : Rect)
+{
+	r1 : Rect;
+	t : ref Text;
+	dummy : ref File = nil;
+
+	draw(mainwin, r, white, nil, (0, 0));
+	c.r = r;
+	c.row = nil;
+	c.w = nil;
+	c.nw = 0;
+	c.tag = textm->newtext();
+	t = c.tag;
+	t.w = nil;
+	t.col = c;
+	r1 = r;
+	r1.max.y = r1.min.y + (graph->font).height;
+	t.init(dummy.addtext(t), r1, dat->reffont, tagcols);
+	t.what = Columntag;
+	r1.min.y = r1.max.y;
+	r1.max.y += Border;
+	draw(mainwin, r1, black, nil, (0, 0));
+	t.insert(0, "New Cut Paste Snarf Sort Zerox Delcol ", 38, TRUE, 0);
+	t.setselect(t.file.buf.nc, t.file.buf.nc);
+	draw(mainwin, t.scrollr, colbutton, nil, colbutton.r.min);
+	c.safe = TRUE;
+}
+
+Column.add(c : self ref Column, w : ref Window, clone : ref Window, y : int) : ref Window
+{
+	r, r1 : Rect;
+	v : ref Window;
+	i, t : int;
+
+	v = nil;
+	r = c.r;
+	r.min.y = c.tag.frame.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.frame.r.min.y+v.body.frame.r.dy()/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.frame.maxlines<=3){
+			c.grow(v, 1, 1);
+			y = v.body.frame.r.min.y+v.body.frame.r.dy()/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(mainwin, r, textcols[BACK], nil, (0, 0));
+		r1 = r;
+		y = min(y, t-(v.tag.frame.font.height+v.body.frame.font.height+Border+1));
+		r1.max.y = min(y, v.body.frame.r.min.y+v.body.frame.nlines*v.body.frame.font.height);
+		r1.min.y = v.reshape(r1, FALSE);
+		r1.max.y = r1.min.y+Border;
+		draw(mainwin, r1, black, nil, (0, 0));
+		r.min.y = r1.max.y;
+	}
+	if(w == nil){
+		w = ref Window;
+		draw(mainwin, r, textcols[BACK], nil, (0, 0));
+		w.col = c;
+		w.init(clone, r);
+	}else{
+		w.col = c;
+		w.reshape(r, FALSE);
+	}
+	w.tag.col = c;
+	w.tag.row = c.row;
+	w.body.col = c;
+	w.body.row = c.row;
+	ocw := c.w;
+	c.w = array[c.nw+1] of ref Window;
+	c.w[0:] = ocw[0:i];
+	c.w[i+1:] = ocw[i:c.nw];
+	ocw = nil;
+	c.nw++;
+	c.w[i] = w;
+	utils->savemouse(w);
+	# near but not on the button 
+	graph->cursorset(w.tag.scrollr.max.add(Point(3, 3)));
+	dat->barttext = w.body;
+	c.safe = TRUE;
+	return w;
+}
+
+Column.close(c : self ref Column, w : ref Window, dofree : int)
+{
+	r : Rect;
+	i : int;
+
+	# w is locked 
+	if(!c.safe)
+		c.grow(w, 1, 1);
+	for(i=0; i<c.nw; i++)
+		if(c.w[i] == w)
+			break;
+	if (i == c.nw)
+		error("can't find window");
+	r = w.r;
+	w.tag.col = nil;
+	w.body.col = nil;
+	w.col = nil;
+	utils->restoremouse(w);
+	if(dofree){
+		w.delete();
+		w.close();
+	}
+	ocw := c.w;
+	c.w = array[c.nw-1] of ref Window;
+	c.w[0:] = ocw[0:i];
+	c.w[i:] = ocw[i+1:c.nw];
+	ocw = nil;
+	c.nw--;
+	if(c.nw == 0){
+		draw(mainwin, r, white, nil, (0, 0));
+		return;
+	}
+	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 
+		w = c.w[i];
+		r.max.y = w.r.max.y;
+	}
+	draw(mainwin, r, textcols[BACK], nil, (0, 0));
+	if(c.safe)
+		w.reshape(r, FALSE);
+}
+
+Column.closeall(c : self ref Column)
+{
+	i : int;
+	w : ref Window;
+
+	if(c == dat->activecol)
+		dat->activecol = nil;
+	c.tag.close();
+	for(i=0; i<c.nw; i++){
+		w = c.w[i];
+		w.close();
+	}
+	c.nw = 0;
+	c.w = nil;
+	c = nil;
+	clearmouse();
+}
+
+Column.mousebut(c : self ref Column)
+{
+	graph->cursorset(c.tag.scrollr.min.add(c.tag.scrollr.max).div(2));
+}
+
+Column.reshape(c : self ref Column, r : Rect)
+{
+	i : int;
+	r1, r2 : Rect;
+	w : ref Window;
+
+	clearmouse();
+	r1 = r;
+	r1.max.y = r1.min.y + c.tag.frame.font.height;
+	c.tag.reshape(r1);
+	draw(mainwin, c.tag.scrollr, colbutton, nil, colbutton.r.min);
+	r1.min.y = r1.max.y;
+	r1.max.y += Border;
+	draw(mainwin, r1, black, nil, (0, 0));
+	r1.max.y = r.max.y;
+	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+(w.r.dy()+Border)*r.dy()/c.r.dy();
+		r2 = r1;
+		r2.max.y = r2.min.y+Border;
+		draw(mainwin, r2, black, nil, (0, 0));
+		r1.min.y = r2.max.y;
+		r1.min.y = w.reshape(r1, FALSE);
+	}
+	c.r = r;
+}
+
+colcmp(a : ref Window, b : ref Window) : int
+{
+	r1, r2 : string;
+
+	r1 = a.body.file.name;
+	r2 = b.body.file.name;
+	if (r1 < r2)
+		return -1;
+	if (r1 > r2)
+		return 1;
+	return 0;
+}
+
+qsort(a : array of ref Window, n : int)
+{
+	i, j : int;
+	t : ref Window;
+
+	while(n > 1) {
+		i = n>>1;
+		t = a[0]; a[0] = a[i]; a[i] = t;
+		i = 0;
+		j = n;
+		for(;;) {
+			do
+				i++;
+			while(i < n && colcmp(a[i], a[0]) < 0);
+			do
+				j--;
+			while(j > 0 && colcmp(a[j], a[0]) > 0);
+			if(j < i)
+				break;
+			t = a[i]; a[i] = a[j]; a[j] = t;
+		}
+		t = a[0]; a[0] = a[j]; a[j] = t;
+		n = n-j-1;
+		if(j >= n) {
+			qsort(a, j);
+			a = a[j+1:];
+		} else {
+			qsort(a[j+1:], n);
+			n = j;
+		}
+	}
+}
+
+Column.sort(c : self ref Column)
+{
+	i, y : int;
+	r, r1 : Rect;
+	rp : array of Rect;
+	w : ref Window;
+	wp : array of ref Window;
+
+	if(c.nw == 0)
+		return;
+	clearmouse();
+	rp = array[c.nw] of Rect;
+	wp = array[c.nw] of ref Window;
+	wp[0:] = c.w[0:c.nw];
+	qsort(wp, c.nw);
+	for(i=0; i<c.nw; i++)
+		rp[i] = wp[i].r;
+	r = c.r;
+	r.min.y = c.tag.frame.r.max.y;
+	draw(mainwin, r, textcols[BACK], nil, (0, 0));
+	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+w.r.dy()+Border;
+		r1 = r;
+		r1.max.y = r1.min.y+Border;
+		draw(mainwin, r1, black, nil, (0, 0));
+		r.min.y = r1.max.y;
+		y = w.reshape(r, FALSE);
+	}
+	rp = nil;
+	c.w = wp;
+}
+
+Column.grow(c : self ref Column, w : ref Window, but : int, mv : int)
+{
+	r, cr : Rect;
+	i, j, k, l, y1, y2, tot, nnl, onl, dnl, h : int;
+	nl, ny : array of int;
+	v : ref Window;
+
+	for(i=0; i<c.nw; i++)
+		if(c.w[i] == w)
+			break;
+	if (i == c.nw)
+		error("can't find window");
+
+	cr = c.r;
+	if(but < 0){	# make sure window fills its own space properly 
+		r = w.r;
+		if(i == c.nw-1)
+			r.max.y = cr.max.y;
+		else
+			r.max.y = c.w[i+1].r.min.y;
+		w.reshape(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(mainwin, cr, textcols[BACK], nil, (0, 0));
+		w.reshape(cr, FALSE);
+		for(i=1; i<c.nw; i++)
+			c.w[i].body.frame.maxlines = 0;
+		c.safe = FALSE;
+		return;
+	}
+	# store old #lines for each window 
+	onl = w.body.frame.maxlines;
+	nl = array[c.nw] of int;
+	ny = array[c.nw] of int;
+	tot = 0;
+	for(j=0; j<c.nw; j++){
+		l = c.w[j].body.frame.maxlines;
+		nl[j] = l;
+		tot += l;
+	}
+	# approximate new #lines for this window 
+	if(but == 2){	# as big as can be 
+		for (j = 0; j < c.nw; j++)
+			nl[j] = 0;
+		nl[i] = tot;
+	}
+	else {
+		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 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+v.tag.all.dy();
+		if(nl[j])
+			r.max.y += 1 + nl[j]*v.body.frame.font.height;
+		if(!c.safe || !v.r.eq(r)){
+			draw(mainwin, r, textcols[BACK], nil, (0, 0));
+			v.reshape(r, c.safe);
+		}
+		r.min.y = v.r.max.y;
+		r.max.y += Border;
+		draw(mainwin, r, black, nil, (0, 0));
+		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-v.tag.all.dy();
+		if(nl[j])
+			r.min.y -= 1 + nl[j]*v.body.frame.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+w.tag.all.dy();
+	h = w.body.frame.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 || !w.r.eq(r)){
+		draw(mainwin, r, textcols[BACK], nil, (0, 0));
+		w.reshape(r, c.safe);
+	}
+	if(i < c.nw-1){
+		r.min.y = r.max.y;
+		r.max.y += Border;
+		draw(mainwin, r, black, nil, (0, 0));
+		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+v.tag.all.dy();
+		if(nl[j])
+			r.max.y += 1 + nl[j]*v.body.frame.font.height;
+		if(!c.safe || !v.r.eq(r)){
+			draw(mainwin, r, textcols[BACK], nil, (0, 0));
+			v.reshape(r, c.safe);
+		}
+		if(j < c.nw-1){	# no border on last window 
+			r.min.y = v.r.max.y;
+			r.max.y += Border;
+			draw(mainwin, r, black, nil, (0, 0));
+		}
+		y1 = r.max.y;
+	}
+	r = w.r;
+	r.min.y = y1;
+	r.max.y = c.r.max.y;
+	draw(mainwin, r, textcols[BACK], nil, (0, 0));
+	nl = nil;
+	ny = nil;
+	c.safe = TRUE;
+	if (mv)
+		w.mousebut();
+}
+
+Column.dragwin(c : self ref Column, w : ref Window, but : int)
+{
+	r : Rect;
+	i, b : int;
+	p, op : Point;
+	v : ref Window;
+	nc : ref Column;
+
+	clearmouse();
+	graph->cursorswitch(dat->boxcursor);
+	b = mouse.buttons;
+	op = mouse.xy;
+	while(mouse.buttons == b)
+		acme->frgetmouse();
+	graph->cursorswitch(dat->arrowcursor);
+	if(mouse.buttons){
+		while(mouse.buttons)
+			acme->frgetmouse();
+		return;
+	}
+
+	for(i=0; i<c.nw; i++)
+		if(c.w[i] == w)
+			break;
+	if (i == c.nw)
+		error("can't find window");
+
+	p = mouse.xy;
+	if(abs(p.x-op.x)<5 && abs(p.y-op.y)<5){
+		c.grow(w, but, 1);
+		w.mousebut();
+		return;
+	}
+	# is it a flick to the right? 
+	if(abs(p.y-op.y)<10 && p.x>op.x+30 && c.row.whichcol(p) == c)
+		p.x += w.r.dx();	# yes: toss to next column 
+	nc = c.row.whichcol(p);
+	if(nc!=nil && nc!=c){
+		c.close(w, FALSE);
+		nc.add(w, nil, p.y);
+		w.mousebut();
+		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 
+		c.close(w, FALSE);
+		c.add(w, nil, p.y);
+		w.mousebut();
+		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-w.tag.all.dy()-Border)
+		p.y = w.r.max.y-w.tag.all.dy()-Border;
+	r = v.r;
+	r.max.y = p.y;
+	if(r.max.y > v.body.frame.r.min.y){
+		r.max.y -= (r.max.y-v.body.frame.r.min.y)%v.body.frame.font.height;
+		if(v.body.frame.r.min.y == v.body.frame.r.max.y)
+			r.max.y++;
+	}
+	if(!r.eq(v.r)){
+		draw(mainwin, r, textcols[BACK], nil, (0, 0));
+		v.reshape(r, c.safe);
+	}
+	r.min.y = v.r.max.y;
+	r.max.y = r.min.y+Border;
+	draw(mainwin, r, black, nil, (0, 0));
+	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;
+	# r.max.y = w.r.max.y;
+	if(!r.eq(w.r)){
+		draw(mainwin, r, textcols[BACK], nil, (0, 0));
+		w.reshape(r, c.safe);
+	}
+	c.safe = TRUE;
+    	w.mousebut();
+}
+
+Column.which(c : self ref Column, p : Point) : ref Text
+{
+	i : int;
+	w : ref Window;
+
+	if(!p.in(c.r))
+		return nil;
+	if(p.in(c.tag.all))
+		return c.tag;
+	for(i=0; i<c.nw; i++){
+		w = c.w[i];
+		if(p.in(w.r)){
+			if(p.in(w.tag.all))
+				return w.tag;
+			return w.body;
+		}
+	}
+	return nil;
+}
+
+Column.clean(c : self ref Column, exiting : int) : int
+{
+	clean : int;
+	i : int;
+
+	clean = TRUE;
+	for(i=0; i<c.nw; i++)
+		clean &= c.w[i].clean(TRUE, exiting);
+	return clean;
+}
--- /dev/null
+++ b/appl/acme/col.m
@@ -1,0 +1,26 @@
+Columnm : module {
+	PATH : con "/dis/acme/col.dis";
+
+	init : fn(mods : ref Dat->Mods);
+
+	Column : adt {
+		r : Draw->Rect;
+		tag : cyclic ref Textm->Text;
+		row : cyclic ref Rowm->Row;
+		w : cyclic array of ref Windowm->Window;
+		nw : int;
+		safe : int;
+
+		init : fn (c : self ref Column, r : Draw->Rect);
+		add : fn (c : self ref Column, w : ref Windowm->Window, w0 : ref Windowm->Window, n : int) : ref Windowm->Window;
+		close : fn (c : self ref Column, w : ref Windowm->Window, n : int);
+		closeall : fn (c : self ref Column);
+		reshape : fn (c : self ref Column, r : Draw->Rect);
+		which : fn (c : self ref Column, p : Draw->Point) : ref Textm->Text;
+		dragwin : fn (c : self ref Column, w : ref Windowm->Window, n : int);
+		grow : fn (c : self ref Column, w : ref Windowm->Window, m, n : int);
+		clean : fn (c : self ref Column, exiting : int) : int;
+		sort : fn (c : self ref Column);
+		mousebut : fn (c : self ref Column);
+	};
+};
--- /dev/null
+++ b/appl/acme/common.m
@@ -1,0 +1,30 @@
+include "sys.m";
+include "bufio.m";
+include "plumbmsg.m";
+include "workdir.m";
+include "draw.m";
+include "styx.m";
+include "acme.m";
+include "dat.m";
+include "gui.m";
+include "graph.m";
+include "frame.m";
+include "util.m";
+include "regx.m";
+include "text.m";
+include "file.m";
+include "wind.m";
+include "row.m";
+include "col.m";
+include "buff.m";
+include "disk.m";
+include "xfid.m";
+include "exec.m";
+include "look.m";
+include "time.m";
+include "scrl.m";
+include "fsys.m";
+include "edit.m";
+include "elog.m";
+include "ecmd.m";
+include "styxaux.m";
--- /dev/null
+++ b/appl/acme/dat.b
@@ -1,0 +1,107 @@
+implement Dat;
+
+include "common.m";
+
+sys : Sys;
+acme : Acme;
+utils : Utils;
+
+# lc, uc : chan of ref Lock;
+
+init(mods : ref Dat->Mods)
+{
+	sys = mods.sys;
+	acme = mods.acme;
+	utils = mods.utils;
+
+	mouse = ref Draw->Pointer;
+	mouse.buttons = mouse.msec = 0;
+	mouse.xy = (0, 0);
+	# lc = chan of ref Lock;
+	# uc = chan of ref Lock;
+	# spawn lockmgr();
+}
+
+# lockmgr()
+# {
+# 	l : ref Lock;
+# 
+# 	acme->lockpid = sys->pctl(0, nil);
+# 	for (;;) {
+# 		alt {
+# 			l = <- lc =>
+# 				if (l.cnt++ == 0)
+# 					l.chann <-= 1;
+# 			l = <- uc =>
+# 				if (--l.cnt > 0)
+# 					l.chann <-= 1;
+# 		}
+# 	}
+# }
+
+Lock.init() : ref Lock
+{
+	return ref Lock(0, chan[1] of int);
+	# return ref Lock(0, chan of int);
+}
+
+Lock.lock(l : self ref Lock)
+{
+	l.cnt++;
+	l.chann <-= 0;
+	# lc <-= l;
+	# <- l.chann;
+}
+
+Lock.unlock(l : self ref Lock)
+{
+	<-l.chann;
+	l.cnt--;
+	# uc <-= l;
+}
+
+Lock.locked(l : self ref Lock) : int
+{
+	return l.cnt > 0;
+}
+
+Ref.init() : ref Ref
+{
+	r := ref Ref;
+	r.l = Lock.init();
+	r.cnt = 0;
+	return r;
+}
+
+Ref.inc(r : self ref Ref) : int
+{
+	r.l.lock();
+	i := r.cnt;
+	r.cnt++;
+	r.l.unlock();
+	return i;
+}
+
+Ref.dec(r : self ref Ref) : int
+{
+	r.l.lock();
+	r.cnt--;
+	i := r.cnt;
+	r.l.unlock();
+	return i;
+}
+
+Ref.refx(r : self ref Ref) : int
+{
+	return r.cnt;
+}
+
+Reffont.get(p, q, r : int, b : string) : ref Reffont
+{
+	return acme->get(p, q, r, b);
+}
+
+Reffont.close(r : self ref Reffont)
+{
+	return acme->close(r);
+}
--- /dev/null
+++ b/appl/acme/dat.m
@@ -1,0 +1,283 @@
+Dat : module {
+	PATH : con "/dis/acme/dat.dis";
+
+	init : fn(mods : ref Mods);
+
+	Mods : adt {
+		sys : Sys;
+		bufio : Bufio;
+		draw : Draw;
+		styx : Styx;
+		styxaux : Styxaux;
+		acme : Acme;
+		gui : Gui;
+		graph : Graph;
+		dat : Dat;
+		framem : Framem;
+		utils : Utils;
+		regx : Regx;
+		scroll : Scroll;
+		textm : Textm;
+		filem : Filem;
+		windowm : Windowm;
+		rowm : Rowm;
+		columnm : Columnm;
+		bufferm : Bufferm;
+		diskm : Diskm;
+		exec : Exec;
+		look : Look;
+		timerm : Timerm;
+		fsys : Fsys;
+		xfidm : Xfidm;
+		plumbmsg : Plumbmsg;
+		edit: Edit;
+		editlog: Editlog;
+		editcmd: Editcmd;
+	};
+
+	SZSHORT : con 2;
+	SZINT : con 4;
+
+	FALSE, TRUE, XXX : con iota;
+
+	EM_NORMAL, EM_RAW, EM_MASK : con iota;
+
+	Qdir,Qacme,Qcons,Qconsctl,Qdraw,Qeditout,Qindex,Qlabel,Qnew,QWaddr,QWbody,QWconsctl,QWctl,QWdata,QWeditout,QWevent,QWrdsel,QWwrsel,QWtag,QMAX : con iota;
+
+	Blockincr : con 256;
+	Maxblock : con 8*1024;
+	NRange : con 10;
+	Infinity : con 16r7fffffff; 	# huge value for regexp address
+
+	# fbufalloc() guarantees room off end of BUFSIZE
+	MAXRPC : con 8192+Styx->IOHDRSZ;
+	BUFSIZE : con MAXRPC;
+	EVENTSIZE : con 256;
+	PLUMBSIZE : con 1024;
+	Scrollwid : con 12;	# width of scroll bar
+	Scrollgap : con 4;	# gap right of scroll bar
+	Margin : con 4;		# margin around text
+	Border : con 2;		#  line between rows, cols, windows
+	Maxtab : con 4;		# size of a tab, in units of the '0' character
+	
+	Empty: con 0;
+	Null : con '-';
+	Delete : con 'd';
+	Insert : con 'i';
+	Replace: con 'r';
+	Filename : con 'f';
+
+	# editing
+	Inactive, Inserting, Collecting: con iota;
+
+	# alphabets
+	ALPHA_LATIN: con '\0';
+	ALPHA_GREEK: con '*';
+	ALPHA_CYRILLIC: con '@';
+
+	Kscrollup: con 16re050;
+	Kscrolldown: con 16re051;
+
+	Astring : adt {
+		s : string;
+	};
+
+	Lock : adt {
+		cnt : int;
+		chann : chan of int;
+
+		init : fn() : ref Lock;
+		lock : fn(l : self ref Lock);
+		unlock : fn(l : self ref Lock);
+		locked : fn(l : self ref Lock) : int;
+	};
+
+# 	Lockx : adt {
+#		sem : ref Lock->Semaphore;
+#
+#		init : fn() : ref Lockx;
+#		lock : fn(l : self ref Lockx);
+#		unlock : fn(l : self ref Lockx);
+#	};
+
+	Ref : adt {
+		l : ref Lock;
+		cnt : int;
+
+		init : fn() : ref Ref;
+		inc : fn(r : self ref Ref) : int;
+		dec : fn(r : self ref Ref) : int;
+		refx : fn(r : self ref Ref) : int;
+	};
+
+	Runestr : adt {
+		r: string;
+		nr: int;
+	};
+
+	Range : adt {
+		q0 : int;
+		q1 : int;
+	};
+
+	Block : adt {
+		addr : int;			# disk address in bytes
+		n : int;			# number of used runes in block
+		next : cyclic ref Block;	# pointer to next in free list
+	};
+
+	Timer : adt {
+		dt : int;
+		c : chan of int;
+		next : cyclic ref Timer;
+	};
+
+	Command : adt {
+		pid : int;
+		name : string;
+		text : string;
+		av : list of string;
+		iseditcmd: int;
+		md : ref Mntdir;
+		next : cyclic ref Command;
+	};
+
+	Dirtab : adt {
+		name : string;
+		qtype : int;
+		qid : int;
+		perm : int;
+	};
+
+	Mntdir : adt {
+		id : int;
+		refs : int;
+		dir : string;
+		ndir : int;
+		next : cyclic ref Mntdir;
+		nincl : int;
+		incl : array of string;
+	};
+
+	Fid : adt {
+		fid : int;
+		busy : int;
+		open : int;
+		qid : Sys->Qid;
+		w : cyclic ref Windowm->Window;
+		dir : array of Dirtab;
+		next : cyclic ref Fid;
+		mntdir : ref Mntdir;
+		nrpart : int;
+		rpart : array of byte;
+	};
+
+	Rangeset : type array of Range;
+
+	Expand : adt {
+		q0 : int;
+		q1 : int;
+		name : string;
+		bname : string;
+		jump : int;
+		at : ref Textm->Text;
+		ar : string;
+		a0 : int;
+		a1 : int;
+	};
+
+	Dirlist : adt {
+		r : string;
+		wid : int;
+	};
+
+	Reffont : adt {
+		r : ref Ref;
+		f : ref Draw->Font;
+
+		get : fn(p : int, q : int, r : int, b : string) : ref Reffont;
+		close : fn(r : self ref Reffont);
+	};
+
+	Cursor : adt {
+		hot : Draw->Point;
+		size : Draw->Point;
+		bits : array of byte;
+	};
+
+	Smsg0 : adt {
+		msize : int;
+		version : string;
+		iounit: int;
+		qid : Sys->Qid;
+		count : int;
+		data : array of byte;
+		stat : Sys->Dir;
+		qids: array of Sys->Qid;
+	};
+
+	# loadfile function ptr
+
+	BUFL, READL: con iota;
+
+	# allwindows pick type
+
+	Looper: adt{
+		cp: ref Edit->Cmd;
+		XY: int;
+		w: array of ref Windowm->Window;
+		nw: int;
+	};	# only one; X and Y can't nest
+
+	Tofile: adt {
+		f: ref Filem->File;
+		r: ref Edit->String;
+	};
+
+	Filecheck: adt{
+		f: ref Filem->File;
+		r: string;
+		nr: int;
+	};
+
+	Allwin: adt{
+		pick{
+			LP => lp: ref Looper;
+			FF => ff: ref Tofile;
+			FC => fc: ref Filecheck;
+		}
+	};
+
+	seq : int;
+	maxtab : int;
+	mouse : ref Draw->Pointer;
+	reffont : ref Reffont;
+	modbutton : ref Draw->Image;
+	colbutton : ref Draw->Image;
+	button : ref Draw->Image;
+	arrowcursor, boxcursor : ref Cursor;
+	row : ref Rowm->Row;
+	disk : ref Diskm->Disk;
+	seltext : ref Textm->Text;
+	argtext : ref Textm->Text;
+	mousetext : ref Textm->Text; 	# global because Text.close needs to clear it
+	typetext : ref Textm->Text;		# ditto
+	barttext : ref Textm->Text;		# shared between mousetask and keyboardtask
+	bartflag : int;
+	activewin : ref Windowm->Window;
+	activecol : ref Columnm->Column;
+	nullrect : Draw->Rect;
+	home : string;
+	plumbed : int;
+
+	ckeyboard : chan of int;
+	cmouse : chan of ref Draw->Pointer;
+	cwait : chan of string;
+	ccommand : chan of ref Command;
+	ckill : chan of string;
+	cxfidalloc : chan of ref Xfidm->Xfid;
+	cxfidfree : chan of ref Xfidm->Xfid;
+	cerr : chan of string;
+	cplumb : chan of ref Plumbmsg->Msg;
+	cedit: chan of int;
+};
--- /dev/null
+++ b/appl/acme/ecmd.b
@@ -1,0 +1,1350 @@
+implement Editcmd;
+
+include "common.m";
+
+sys: Sys;
+utils: Utils;
+edit: Edit;
+editlog: Editlog;
+windowm: Windowm;
+look: Look;
+columnm: Columnm;
+bufferm: Bufferm;
+exec: Exec;
+dat: Dat;
+textm: Textm;
+regx: Regx;
+filem: Filem;
+rowm: Rowm;
+
+Dir: import Sys;
+Allwin, Filecheck, Tofile, Looper, Astring: import Dat;
+aNo, aDot, aAll: import Edit;
+C_nl, C_a, C_b, C_c, C_d, C_B, C_D, C_e, C_f, C_g, C_i, C_k, C_m, C_n, C_p, C_s, C_u, C_w, C_x, C_X, C_pipe, C_eq: import Edit;
+TRUE, FALSE: import Dat;
+Inactive, Inserting, Collecting: import Dat;
+BUFSIZE, Runestr: import Dat;
+Addr, Address, String, Cmd: import Edit;
+Window: import windowm;
+File: import filem;
+NRange, Range, Rangeset: import Dat;
+Text: import textm;
+Column: import columnm;
+Buffer: import bufferm;
+
+sprint: import sys;
+elogterm, elogclose, eloginsert, elogdelete, elogreplace, elogapply: import editlog;
+cmdtab, allocstring, freestring, Straddc, curtext, editing, newaddr, cmdlookup, editerror: import edit;
+error, stralloc, strfree, warning, skipbl, findbl: import utils;
+lookfile, cleanname, dirname: import look;
+undo, run: import exec;
+Ref, Lock, row, cedit: import dat;
+rxcompile, rxexecute, rxbexecute: import regx;
+allwindows: import rowm;
+
+init(mods : ref Dat->Mods)
+{
+	sys = mods.sys;
+	utils = mods.utils;
+	edit = mods.edit;
+	editlog = mods.editlog;
+	windowm = mods.windowm;
+	look = mods.look;
+	columnm = mods.columnm;
+	bufferm = mods.bufferm;
+	exec = mods.exec;
+	dat = mods.dat;
+	textm = mods.textm;
+	regx = mods.regx;
+	filem = mods.filem;
+	rowm = mods.rowm;
+
+	none.r.q0 = none.r.q1 = 0;
+	none.f = nil;
+}
+
+cmdtabexec(i: int, t: ref Text, cp: ref Cmd): int
+{
+	case (cmdtab[i].fnc){
+		C_nl	=> i = nl_cmd(t, cp);
+		C_a 	=> i = a_cmd(t, cp);
+		C_b	=> i = b_cmd(t, cp);
+		C_c	=> i = c_cmd(t, cp);
+		C_d	=> i = d_cmd(t, cp);
+		C_e	=> i = e_cmd(t, cp);
+		C_f	=> i = f_cmd(t, cp);
+		C_g	=> i = g_cmd(t, cp);
+		C_i	=> i = i_cmd(t, cp);
+		C_m	=> i = m_cmd(t, cp);
+		C_p	=> i = p_cmd(t, cp);
+		C_s	=> i = s_cmd(t, cp);
+		C_u	=> i = u_cmd(t, cp);
+		C_w	=> i = w_cmd(t, cp);
+		C_x	=> i = x_cmd(t, cp);
+		C_eq => i = eq_cmd(t, cp);
+		C_B	=> i = B_cmd(t, cp);
+		C_D	=> i = D_cmd(t, cp);
+		C_X	=> i = X_cmd(t, cp);
+		C_pipe	=> i = pipe_cmd(t, cp);
+		* =>	error("bad case in cmdtabexec");
+	}
+	return i;
+}
+
+Glooping: int;
+nest: int;
+Enoname := "no file name given";
+
+addr: Address;
+menu: ref File;
+sel: Rangeset;
+collection: string;
+ncollection: int;
+
+clearcollection()
+{
+	collection = nil;
+	ncollection = 0;
+}
+
+resetxec()
+{
+	Glooping = nest = 0;
+	clearcollection();
+}
+
+mkaddr(f: ref File): Address
+{
+	a: Address;
+
+	a.r.q0 = f.curtext.q0;
+	a.r.q1 = f.curtext.q1;
+	a.f = f;
+	return a;
+}
+
+none: Address;
+
+cmdexec(t: ref Text, cp: ref Cmd): int
+{
+	i: int;
+	ap: ref Addr;
+	f: ref File;
+	w: ref Window;
+	dot: Address;
+
+	if(t == nil)
+		w = nil;
+	else
+		w = t.w;
+	if(w==nil && (cp.addr==nil || cp.addr.typex!='"') &&
+	    utils->strchr("bBnqUXY!", cp.cmdc) < 0&&
+	    !(cp.cmdc=='D' && cp.text!=nil))
+		editerror("no current window");
+	i = cmdlookup(cp.cmdc);	# will be -1 for '{' 
+	f = nil;
+	if(t!=nil && t.w!=nil){
+		t = t.w.body;
+		f = t.file;
+		f.curtext = t;
+	}
+	if(i>=0 && cmdtab[i].defaddr != aNo){
+		if((ap=cp.addr)==nil && cp.cmdc!='\n'){
+			cp.addr = ap = newaddr();
+			ap.typex = '.';
+			if(cmdtab[i].defaddr == aAll)
+				ap.typex = '*';
+		}else if(ap!=nil && ap.typex=='"' && ap.next==nil && cp.cmdc!='\n'){
+			ap.next = newaddr();
+			ap.next.typex = '.';
+			if(cmdtab[i].defaddr == aAll)
+				ap.next.typex = '*';
+		}
+		if(cp.addr!=nil){	# may be false for '\n' (only)
+			if(f!=nil){
+				dot = mkaddr(f);
+				addr = cmdaddress(ap, dot, 0);
+			}else	# a "
+				addr = cmdaddress(ap, none, 0);
+			f = addr.f;
+			t = f.curtext;
+		}
+	}
+	case(cp.cmdc){
+	'{' =>
+		dot = mkaddr(f);
+		if(cp.addr != nil)
+			dot = cmdaddress(cp.addr, dot, 0);
+		for(cp = cp.cmd; cp!=nil; cp = cp.next){
+			t.q0 = dot.r.q0;
+			t.q1 = dot.r.q1;
+			cmdexec(t, cp);
+		}
+		break;
+	* =>
+		if(i < 0)
+			editerror(sprint("unknown command %c in cmdexec", cp.cmdc));
+		i = cmdtabexec(i, t, cp);
+		return i;
+	}
+	return 1;
+}
+
+edittext(f: ref File, q: int, r: string, nr: int): string
+{
+	case(editing){
+	Inactive =>
+		return "permission denied";
+	Inserting =>
+		eloginsert(f, q, r, nr);
+		return nil;
+	Collecting =>
+		collection += r[0: nr];
+		ncollection += nr;
+		return nil;
+	* =>
+		return "unknown state in edittext";
+	}
+}
+
+# string is known to be NUL-terminated
+filelist(t: ref Text, r: string, nr: int): string
+{
+	if(nr == 0)
+		return nil;
+	(r, nr) = skipbl(r, nr);
+	if(r[0] != '<')
+		return r;
+	# use < command to collect text 
+	clearcollection();
+	runpipe(t, '<', r[1:], nr-1, Collecting);
+	return collection;
+}
+
+a_cmd(t: ref Text, cp: ref Cmd): int
+{
+	return append(t.file, cp, addr.r.q1);
+}
+
+b_cmd(nil: ref Text, cp: ref Cmd): int
+{
+	f: ref File;
+
+	f = tofile(cp.text);
+	if(nest == 0)
+		pfilename(f);
+	curtext = f.curtext;
+	return TRUE;
+}
+
+B_cmd(t: ref Text, cp: ref Cmd): int
+{
+	listx, r, s: string;
+	nr: int;
+
+	listx = filelist(t, cp.text.r, cp.text.n);
+	if(listx == nil)
+		editerror(Enoname);
+	r = listx;
+	nr = len r;
+	(r, nr) = skipbl(r, nr);
+	if(nr == 0)
+		look->new(t, t, nil, 0, 0, r, 0);
+	else while(nr > 0){
+		(s, nr) = findbl(r, nr);
+		look->new(t, t, nil, 0, 0, r, len r);
+		if(nr > 0)
+			(r, nr) = skipbl(s[1:], nr-1);
+	}
+	clearcollection();
+	return TRUE;
+}
+
+c_cmd(t: ref Text, cp: ref Cmd): int
+{
+	elogreplace(t.file, addr.r.q0, addr.r.q1, cp.text.r, cp.text.n);
+	return TRUE;
+}
+
+d_cmd(t: ref Text, nil: ref Cmd): int
+{
+	if(addr.r.q1 > addr.r.q0)
+		elogdelete(t.file, addr.r.q0, addr.r.q1);
+	return TRUE;
+}
+
+D1(t: ref Text)
+{
+	if(t.w.body.file.ntext>1 || t.w.clean(FALSE, FALSE))
+		t.col.close(t.w, TRUE);
+}
+
+D_cmd(t: ref Text, cp: ref Cmd): int
+{
+	listx, r, s, n: string;
+	nr, nn: int;
+	w: ref Window;
+	dir, rs: Runestr;
+	buf: string;
+
+	listx = filelist(t, cp.text.r, cp.text.n);
+	if(listx == nil){
+		D1(t);
+		return TRUE;
+	}
+	dir = dirname(t, nil, 0);
+	r = listx;
+	nr = len r;
+	(r, nr) = skipbl(r, nr);
+	do{
+		(s, nr) = findbl(r, nr);
+		# first time through, could be empty string, meaning delete file empty name
+		nn = len r;
+		if(r[0]=='/' || nn==0 || dir.nr==0){
+			rs.r = r;
+			rs.nr = nn;
+		}else{
+			n = dir.r + "/" + r;
+			rs = cleanname(n, dir.nr+1+nn);
+		}
+		w = lookfile(rs.r, rs.nr);
+		if(w == nil){
+			buf = sprint("no such file %s", rs.r);
+			rs.r = nil;
+			editerror(buf);
+		}
+		rs.r = nil;
+		D1(w.body);
+		if(nr > 0)
+			(r, nr) = skipbl(s[1:], nr-1);
+	}while(nr > 0);
+	clearcollection();
+	dir.r = nil;
+	return TRUE;
+}
+
+readloader(f: ref File, q0: int, r: string, nr: int): int
+{
+	if(nr > 0)
+		eloginsert(f, q0, r, nr);
+	return 0;
+}
+
+e_cmd(t: ref Text , cp: ref Cmd): int
+{
+	name: string;
+	f: ref File;
+	i, q0, q1, nulls, samename, allreplaced, ok: int;
+	fd: ref Sys->FD;
+	s, tmp: string;
+	d: Dir;
+
+	f = t.file;
+	q0 = addr.r.q0;
+	q1 = addr.r.q1;
+	if(cp.cmdc == 'e'){
+		if(t.w.clean(TRUE, FALSE)==FALSE)
+			editerror("");	# winclean generated message already 
+		q0 = 0;
+		q1 = f.buf.nc;
+	}
+	allreplaced = (q0==0 && q1==f.buf.nc);
+	name = cmdname(f, cp.text, cp.cmdc=='e');
+	if(name == nil)
+		editerror(Enoname);
+	i = len name;
+	samename = name == t.file.name;
+	s = name;
+	name = nil;
+	fd = sys->open(s, Sys->OREAD);
+	if(fd == nil){
+		tmp = sprint("can't open %s: %r", s);
+		s = nil;
+		editerror(tmp);
+	}
+	(ok, d) = sys->fstat(fd);
+	if(ok >=0 && (d.mode&Sys->DMDIR)){
+		fd = nil;
+		tmp = sprint("%s is a directory", s);
+		s = nil;
+		editerror(tmp);
+	}
+	elogdelete(f, q0, q1);
+	nulls = 0;
+	bufferm->loadfile(fd, q1, Dat->READL, nil, f);
+	s = nil;
+	fd = nil;
+	if(nulls)
+		warning(nil, sprint("%s: NUL bytes elided\n", s));
+	else if(allreplaced && samename)
+		f.editclean = TRUE;
+	return TRUE;
+}
+
+f_cmd(t: ref Text, cp: ref Cmd): int
+{
+	name: string;
+
+	name = cmdname(t.file, cp.text, TRUE);
+	name = nil;
+	pfilename(t.file);
+	return TRUE;
+}
+
+g_cmd(t: ref Text, cp: ref Cmd): int
+{
+	ok: int;
+
+	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");
+	(ok, sel) = rxexecute(t, nil, addr.r.q0, addr.r.q1);
+	if(ok ^ cp.cmdc=='v'){
+		t.q0 = addr.r.q0;
+		t.q1 = addr.r.q1;
+		return cmdexec(t, cp.cmd);
+	}
+	return TRUE;
+}
+
+i_cmd(t: ref Text, cp: ref Cmd): int
+{
+	return append(t.file, cp, addr.r.q0);
+}
+
+# int
+# k_cmd(File *f, Cmd *cp)
+# {
+# 	USED(cp);
+#	f->mark = addr.r;
+#	return TRUE;
+# }
+
+copy(f: ref File, addr2: Address)
+{
+	p: int;
+	ni: int;
+	buf: ref Astring;
+
+	buf = stralloc(BUFSIZE);
+	for(p=addr.r.q0; p<addr.r.q1; p+=ni){
+		ni = addr.r.q1-p;
+		if(ni > BUFSIZE)
+			ni = BUFSIZE;
+		f.buf.read(p, buf, 0, ni);
+		eloginsert(addr2.f, addr2.r.q1, buf.s, ni);
+	}
+	strfree(buf);
+}
+
+move(f: ref File, addr2: Address)
+{
+	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");
+}
+
+m_cmd(t: ref Text, cp: ref Cmd): int
+{
+	dot, addr2: Address;
+
+	dot = mkaddr(t.file);
+	addr2 = cmdaddress(cp.mtaddr, dot, 0);
+	if(cp.cmdc == 'm')
+		move(t.file, addr2);
+	else
+		copy(t.file, addr2);
+	return TRUE;
+}
+
+# int
+# n_cmd(File *f, Cmd *cp)
+# {
+#	int i;
+#	USED(f);
+#	USED(cp);
+#	for(i = 0; i<file.nused; i++){
+#		if(file.filepptr[i] == cmd)
+#			continue;
+#		f = file.filepptr[i];
+#		Strduplstr(&genstr, &f->name);
+#		filename(f);
+#	}
+#	return TRUE;
+#}
+
+p_cmd(t: ref Text, nil: ref Cmd): int
+{
+	return pdisplay(t.file);
+}
+
+s_cmd(t: ref Text, cp: ref Cmd): int
+{
+	i, j, k, c, m, n, nrp, didsub, ok: int;
+	p1, op, delta: int;
+	buf: ref String;
+	rp: array of Rangeset;
+	err: string;
+	rbuf: ref Astring;
+
+	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; ){
+		(ok, sel) = rxexecute(t, nil, p1, addr.r.q1);
+		if(!ok)
+			break;
+		if(sel[0].q0 == sel[0].q1){	# empty match?
+			if(sel[0].q0 == op){
+				p1++;
+				continue;
+			}
+			p1 = sel[0].q1+1;
+		}else
+			p1 = sel[0].q1;
+		op = sel[0].q1;
+		if(--n>0)
+			continue;
+		nrp++;
+		orp := rp;
+		rp = array[nrp] of Rangeset;
+		rp[0: ] = orp[0:nrp-1];
+		rp[nrp-1] = copysel(sel);
+		orp = nil;
+	}
+	rbuf = stralloc(BUFSIZE);
+	buf = allocstring(0);
+	for(m=0; m<nrp; m++){
+		buf.n = 0;
+		buf.r = nil;
+		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[j].q1-sel[j].q0>BUFSIZE){
+						err = "replacement string too long";
+						rp = nil;
+						freestring(buf);
+						strfree(rbuf);
+						editerror(err);
+						return FALSE;
+					}
+					t.file.buf.read(sel[j].q0, rbuf, 0, sel[j].q1-sel[j].q0);
+					for(k=0; k<sel[j].q1-sel[j].q0; k++)
+						Straddc(buf, rbuf.s[k]);
+				}else
+				 	Straddc(buf, c);
+			}else if(c!='&')
+				Straddc(buf, c);
+			else{
+				if(sel[0].q1-sel[0].q0>BUFSIZE){
+					err = "right hand side too long in substitution";
+					rp = nil;
+					freestring(buf);
+					strfree(rbuf);
+					editerror(err);
+					return FALSE;
+				}
+				t.file.buf.read(sel[0].q0, rbuf, 0, sel[0].q1-sel[0].q0);
+				for(k=0; k<sel[0].q1-sel[0].q0; k++)
+					Straddc(buf, rbuf.s[k]);
+			}
+		elogreplace(t.file, sel[0].q0, sel[0].q1, buf.r, buf.n);
+		delta -= sel[0].q1-sel[0].q0;
+		delta += buf.n;
+		didsub = 1;
+		if(!cp.flag)
+			break;
+	}
+	rp = nil;
+	freestring(buf);
+	strfree(rbuf);
+	if(!didsub && nest==0)
+		editerror("no substitution");
+	t.q0 = addr.r.q0;
+	t.q1 = addr.r.q1+delta;
+	return TRUE;
+}
+
+u_cmd(t: ref Text, cp: ref Cmd): int
+{
+	n, oseq, flag: int;
+
+	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;
+warning(nil, sprint("seq %d\n", t.file.seq));
+		undo(t, flag);
+	}
+	return TRUE;
+}
+
+w_cmd(t: ref Text, cp: ref Cmd): int
+{
+	r: string;
+	f: ref File;
+
+	f = t.file;
+	if(f.seq == dat->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");
+	exec->putfile(f, addr.r.q0, addr.r.q1, r);
+	# r is freed by putfile
+	return TRUE;
+}
+
+x_cmd(t: ref Text, cp: ref Cmd): int
+{
+	if(cp.re!=nil)
+		looper(t.file, cp, cp.cmdc=='x');
+	else
+		linelooper(t.file, cp);
+	return TRUE;
+}
+
+X_cmd(nil: ref Text, cp: ref Cmd): int
+{
+	filelooper(cp, cp.cmdc=='X');
+	return TRUE;
+}
+
+runpipe(t: ref Text, cmd: int, cr: string, ncr: int, state: int)
+{
+	r, s: string;
+	n: int;
+	dir: Runestr;
+	w: ref Window;
+
+	(r, n) = skipbl(cr, ncr);
+	if(n == 0)
+		editerror("no command specified for >");
+	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);
+	}
+	tmps := "z";
+	tmps[0] = cmd;
+	s = tmps + r;
+	n++;
+	dir.r = nil;
+	dir.nr = 0;
+	if(t != nil)
+		dir = dirname(t, nil, 0);
+	if(dir.nr==1 && dir.r[0]=='.'){	# sigh 
+		dir.r = nil;
+		dir.nr = 0;
+	}
+	editing = state;
+	if(t!=nil && t.w!=nil)
+		t.w.refx.inc();	# run will decref
+	spawn run(w, s, dir.r, dir.nr, TRUE, nil, nil, TRUE);
+	s = nil;
+	if(t!=nil && t.w!=nil)
+		t.w.unlock();
+	row.qlock.unlock();
+	<- cedit;
+	row.qlock.lock();
+	editing = Inactive;
+	if(t!=nil && t.w!=nil)
+		t.w.lock('M');
+}
+
+pipe_cmd(t: ref Text, cp: ref Cmd): int
+{
+	runpipe(t, cp.cmdc, cp.text.r, cp.text.n, Inserting);
+	return TRUE;
+}
+
+nlcount(t: ref Text, q0: int, q1: int): int
+{
+	nl: int;
+	buf: ref Astring;
+	i, nbuf: int;
+
+	buf = stralloc(BUFSIZE);
+	nbuf = 0;
+	i = nl = 0;
+	while(q0 < q1){
+		if(i == nbuf){
+			nbuf = q1-q0;
+			if(nbuf > BUFSIZE)
+				nbuf = BUFSIZE;
+			t.file.buf.read(q0, buf, 0, nbuf);
+			i = 0;
+		}
+		if(buf.s[i++] == '\n')
+			nl++;
+		q0++;
+	}
+	strfree(buf);
+	return nl;
+}
+
+printposn(t: ref Text, charsonly: int)
+{
+	l1, l2: int;
+
+	if(t != nil && t.file != nil && t.file.name != nil)
+		warning(nil, t.file.name + ":");
+	if(!charsonly){
+		l1 = 1+nlcount(t, 0, addr.r.q0);
+		l2 = l1+nlcount(t, addr.r.q0, addr.r.q1);
+		# check if addr ends with '\n' 
+		if(addr.r.q1>0 && addr.r.q1>addr.r.q0 && t.readc(addr.r.q1-1)=='\n')
+			--l2;
+		warning(nil, sprint("%ud", l1));
+		if(l2 != l1)
+			warning(nil, sprint(",%ud", l2));
+		warning(nil, "\n");
+		# warning(nil, "; ");
+		return;
+	}
+	warning(nil, sprint("#%d", addr.r.q0));
+	if(addr.r.q1 != addr.r.q0)
+		warning(nil, sprint(",#%d", addr.r.q1));
+	warning(nil, "\n");
+}
+
+eq_cmd(t: ref Text, cp: ref Cmd): int
+{
+	charsonly: int;
+
+	case(cp.text.n){
+	0 =>
+		charsonly = FALSE;
+		break;
+	1 =>
+		if(cp.text.r[0] == '#'){
+			charsonly = TRUE;
+			break;
+		}
+	* =>
+		charsonly = TRUE;
+		editerror("newline expected");
+	}
+	printposn(t, charsonly);
+	return TRUE;
+}
+
+nl_cmd(t: ref Text, cp: ref Cmd): int
+{
+	a: Address;
+	f: ref File;
+
+	f = t.file;
+	if(cp.addr == nil){
+		# First put it on newline boundaries
+		a = mkaddr(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){
+			a = mkaddr(f);
+			addr = lineaddr(1, a, 1);
+		}
+	}
+	t.show(addr.r.q0, addr.r.q1, TRUE);
+	return TRUE;
+}
+
+append(f: ref File, cp: ref Cmd, p: int): int
+{
+	if(cp.text.n > 0)
+		eloginsert(f, p, cp.text.r, cp.text.n);
+	return TRUE;
+}
+
+pdisplay(f: ref File): int
+{
+	p1, p2: int;
+	np: int;
+	buf: ref Astring;
+
+	p1 = addr.r.q0;
+	p2 = addr.r.q1;
+	if(p2 > f.buf.nc)
+		p2 = f.buf.nc;
+	buf = stralloc(BUFSIZE);
+	while(p1 < p2){
+		np = p2-p1;
+		if(np>BUFSIZE-1)
+			np = BUFSIZE-1;
+		f.buf.read(p1, buf, 0, np);
+		warning(nil, sprint("%s", buf.s[0:np]));
+		p1 += np;
+	}
+	strfree(buf);
+	f.curtext.q0 = addr.r.q0;
+	f.curtext.q1 = addr.r.q1;
+	return TRUE;
+}
+
+pfilename(f: ref File)
+{
+	dirty: int;
+	w: ref Window;
+
+	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, sprint("%c%c%c %s\n", " '"[dirty],
+		'+', " ."[curtext!=nil && curtext.file==f], f.name));
+}
+
+loopcmd(f: ref File, cp: ref Cmd, rp: array of Range, nrp: int)
+{
+	i: int;
+
+	for(i=0; i<nrp; i++){
+		f.curtext.q0 = rp[i].q0;
+		f.curtext.q1 = rp[i].q1;
+		cmdexec(f.curtext, cp);
+	}
+}
+
+looper(f: ref File, cp: ref Cmd, xy: int)
+{
+	p, op, nrp, ok: int;
+	r, tr: Range;
+	rp: array of  Range;
+
+	r = addr.r;
+	if(xy)
+		op = -1;
+	else
+		op = r.q0;
+	nest++;
+	if(rxcompile(cp.re.r) == FALSE)
+		editerror(sprint("bad regexp in %c command", cp.cmdc));
+	nrp = 0;
+	rp = nil;
+	for(p = r.q0; p<=r.q1; ){
+		(ok, sel) = rxexecute(f.curtext, nil, p, r.q1);
+		if(!ok){ # 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[0].q0==sel[0].q1){	# empty match?
+				if(sel[0].q0==op){
+					p++;
+					continue;
+				}
+				p = sel[0].q1+1;
+			}else
+				p = sel[0].q1;
+			if(xy)
+				tr = sel[0];
+			else{
+				tr.q0 = op;
+				tr.q1 = sel[0].q0;
+			}
+		}
+		op = sel[0].q1;
+		nrp++;
+		orp := rp;
+		rp = array[nrp] of Range;
+		rp[0: ] = orp[0: nrp-1];
+		rp[nrp-1] = tr;
+		orp = nil;
+	}
+	loopcmd(f, cp.cmd, rp, nrp);
+	rp = nil;
+	--nest;
+}
+
+linelooper(f: ref File, cp: ref Cmd)
+{
+	nrp, p: int;
+	r, linesel: Range;
+	a, a3: Address;
+	rp: array of Range;
+
+	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++;
+				orp := rp;
+				rp = array[nrp] of Range;
+				rp[0: ] = orp[0: nrp-1];
+				rp[nrp-1] = linesel;
+				orp = nil;
+				continue;
+			}
+		break;
+	}
+	loopcmd(f, cp.cmd, rp, nrp);
+	rp = nil;
+	--nest;
+}
+
+loopstruct: ref Looper;
+
+alllooper(w: ref Window, lp: ref Looper)
+{
+	t: ref Text;
+	cp: ref Cmd;
+
+	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.name==nil)
+		return;
+	if(cp.re==nil || filematch(t.file, cp.re)==lp.XY){
+		olpw := lp.w;
+		lp.w = array[lp.nw+1] of ref Window;
+		lp.w[0: ] = olpw[0: lp.nw];
+		lp.w[lp.nw++] = w;
+		olpw = nil;
+	}
+}
+
+filelooper(cp: ref Cmd, XY: int)
+{
+	i: int;
+
+	if(Glooping++)
+		editerror(sprint("can't nest %c command", "YX"[XY]));
+	nest++;
+
+	if(loopstruct == nil)
+		loopstruct = ref Looper;
+	loopstruct.cp = cp;
+	loopstruct.XY = XY;
+	if(loopstruct.w != nil)	# error'ed out last time
+		loopstruct.w = nil;
+	loopstruct.w = nil;
+	loopstruct.nw = 0;
+	aw := ref Allwin.LP(loopstruct);
+	allwindows(Edit->ALLLOOPER, aw);
+	aw = nil;
+	for(i=0; i<loopstruct.nw; i++)
+		cmdexec(loopstruct.w[i].body, cp.cmd);
+	loopstruct.w = nil;
+
+	--Glooping;
+	--nest;
+}
+
+nextmatch(f: ref File, r: ref String, p: int, sign: int)
+{
+	ok: int;
+
+	if(rxcompile(r.r) == FALSE)
+		editerror("bad regexp in command address");
+	if(sign >= 0){
+		(ok, sel) = rxexecute(f.curtext, nil, p, 16r7FFFFFFF);
+		if(!ok)
+			editerror("no match for regexp");
+		if(sel[0].q0==sel[0].q1 && sel[0].q0==p){
+			if(++p>f.buf.nc)
+				p = 0;
+			(ok, sel) = rxexecute(f.curtext, nil, p, 16r7FFFFFFF);
+			if(!ok)
+				editerror("address");
+		}
+	}else{
+		(ok, sel) = rxbexecute(f.curtext, p);
+		if(!ok)
+			editerror("no match for regexp");
+		if(sel[0].q0==sel[0].q1 && sel[0].q1==p){
+			if(--p<0)
+				p = f.buf.nc;
+			(ok, sel) = rxbexecute(f.curtext, p);
+			if(!ok)
+				editerror("address");
+		}
+	}
+}
+
+cmdaddress(ap: ref Addr, a: Address, sign: int): Address
+{
+	f := a.f;
+	a1, a2: Address;
+
+	do{
+		case(ap.typex){
+		'l' or
+		'#' =>
+			if(ap.typex == '#')
+				a = charaddr(ap.num, a, sign);
+			else
+				a = lineaddr(ap.num, a, sign);
+			break;
+
+		'.' =>
+			a = mkaddr(f);
+			break;
+
+		'$' =>
+			a.r.q0 = a.r.q1 = f.buf.nc;
+			break;
+
+		'\'' =>
+editerror("can't handle '");
+#			a.r = f.mark;
+			break;
+
+		'?' =>
+			sign = -sign;
+			if(sign == 0)
+				sign = -1;
+			if(sign >= 0)
+				v := a.r.q1;
+			else
+				v = a.r.q0;
+			nextmatch(f, ap.re, v, sign);
+			a.r = sel[0];
+			break;
+
+		'/' =>
+			if(sign >= 0)
+				v := a.r.q1;
+			else
+				v = a.r.q0;
+			nextmatch(f, ap.re, v, sign);
+			a.r = sel[0];
+			break;
+
+		'"' =>
+			f = matchfile(ap.re);
+			a = mkaddr(f);
+			break;
+
+		'*' =>
+			a.r.q0 = 0;
+			a.r.q1 = f.buf.nc;
+			return a;
+
+		',' or
+		';' =>
+			if(ap.left!=nil)
+				a1 = cmdaddress(ap.left, a, 0);
+			else{
+				a1.f = a.f;
+				a1.r.q0 = a1.r.q1 = 0;
+			}
+			if(ap.typex == ';'){
+				f = a1.f;
+				a = a1;
+				f.curtext.q0 = a1.r.q0;
+				f.curtext.q1 = a1.r.q1;
+			}
+			if(ap.next!=nil)
+				a2 = cmdaddress(ap.next, a, 0);
+			else{
+				a2.f = a.f;
+				a2.r.q0 = a2.r.q1 = f.buf.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;
+
+		'+' or
+		'-' =>
+			sign = 1;
+			if(ap.typex == '-')
+				sign = -1;
+			if(ap.next==nil || ap.next.typex=='+' || ap.next.typex=='-')
+				a = lineaddr(1, a, sign);
+			break;
+		* =>
+			error("cmdaddress");
+			return a;
+		}
+	}while((ap = ap.next)!=nil);	# assign =
+	return a;
+}
+
+alltofile(w: ref Window, tp: ref Tofile)
+{
+	t: ref Text;
+
+	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(tp.r.r == t.file.name)
+		tp.f = t.file;
+}
+
+tofile(r: ref String): ref File
+{
+	t: ref Tofile;
+	rr: String;
+
+	(rr.r, r.n) = skipbl(r.r, r.n);
+	t = ref Tofile;
+	t.f = nil;
+	t.r = ref String;
+	*t.r = rr;
+	aw := ref Allwin.FF(t);
+	allwindows(Edit->ALLTOFILE, aw);
+	aw = nil;
+	if(t.f == nil)
+		editerror(sprint("no such file\"%s\"", rr.r));
+	return t.f;
+}
+
+allmatchfile(w: ref Window, tp: ref Tofile)
+{
+	t: ref Text;
+
+	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(sprint("too many files match \"%s\"", tp.r.r));
+		tp.f = w.body.file;
+	}
+}
+
+matchfile(r: ref String): ref File
+{
+	tf: ref Tofile;
+
+	tf = ref Tofile;
+	tf.f = nil;
+	tf.r = r;
+	aw := ref Allwin.FF(tf);
+	allwindows(Edit->ALLMATCHFILE, aw);
+	aw = nil;
+
+	if(tf.f == nil)
+		editerror(sprint("no file matches \"%s\"", r.r));
+	return tf.f;
+}
+
+filematch(f: ref File, r: ref String): int
+{
+	buf: string;
+	w: ref Window;
+	match, i, dirty: int;
+	s: Rangeset;
+
+	# 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");
+	w = f.curtext.w;
+	# same check for dirty as in settag, but we know ncache==0
+	dirty = !w.isdir && !w.isscratch && f.mod;
+	buf = sprint("%c%c%c %s\n", " '"[dirty],
+		'+', " ."[curtext!=nil && curtext.file==f], f.name);
+	(match, s) = rxexecute(nil, buf, 0, i);
+	buf = nil;
+	return match;
+}
+
+charaddr(l: int, addr: Address, sign: int): Address
+{
+	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.buf.nc)
+		editerror("address out of range");
+	return addr;
+}
+
+lineaddr(l: int, addr: Address, sign: int): Address
+{
+	n: int;
+	c: int;
+	f := addr.f;
+	a: Address;
+	p: int;
+
+	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 = f.curtext.readc(p++)=='\n';
+			}
+			while(n < l){
+				if(p >= f.buf.nc)
+					editerror("address out of range");
+				if(f.curtext.readc(p++) == '\n')
+					n++;
+			}
+			a.r.q0 = p;
+		}
+		while(p < f.buf.nc && f.curtext.readc(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 = f.curtext.readc(p-1);
+					if(c != '\n' || ++n != l)
+						p--;
+				}
+			}
+			a.r.q1 = p;
+			if(p > 0)
+				p--;
+		}
+		while(p > 0 && f.curtext.readc(p-1)!='\n')	# lines start after a newline
+			p--;
+		a.r.q0 = p;
+	}
+	return a;
+}
+
+allfilecheck(w: ref Window, fp: ref Filecheck)
+{
+	f: ref File;
+
+	f = w.body.file;
+	if(w.body.file == fp.f)
+		return;
+	if(fp.r == f.name)
+		warning(nil, sprint("warning: duplicate file name \"%s\"\n", fp.r));
+}
+
+cmdname(f: ref File, str: ref String , set: int): string
+{
+	r, s: string;
+	n: int;
+	fc: ref Filecheck;
+	newname: Runestr;
+
+	r = nil;
+	n = str.n;
+	s = str.r;
+	if(n == 0){
+		# no name; use existing
+		if(f.name == nil)
+			return nil;
+		return f.name;
+	}
+	(s, n) = skipbl(s, n);
+	if(n == 0)
+		;
+	else{
+		if(s[0] == '/'){
+			r = s;
+		}else{
+			newname = dirname(f.curtext, s, n);
+			r = newname.r;
+			n = newname.nr;
+		}
+		fc = ref Filecheck;
+		fc.f = f;
+		fc.r = r;
+		fc.nr = n;
+		aw := ref Allwin.FC(fc);
+		allwindows(Edit->ALLFILECHECK, aw);
+		aw = nil;
+		if(f.name == nil)
+			set = TRUE;
+	}
+
+	if(set && r[0: n] != f.name){
+		f.mark();
+		f.mod = TRUE;
+		f.curtext.w.dirty = TRUE;
+		f.curtext.w.setname(r, n);
+	}
+	return r;
+}
+
+copysel(rs: Rangeset): Rangeset
+{
+	nrs := array[NRange] of Range;
+	for(i := 0; i < NRange; i++)
+		nrs[i] = rs[i];
+	return nrs;
+}
--- /dev/null
+++ b/appl/acme/ecmd.m
@@ -1,0 +1,18 @@
+Editcmd: module {
+
+	PATH: con "/dis/acme/ecmd.dis";
+
+	init : fn(mods : ref Dat->Mods);
+
+	cmdexec: fn(a0: ref Textm->Text, a1: ref Edit->Cmd): int;
+	resetxec: fn();
+	cmdaddress: fn(a0: ref Edit->Addr, a1: Edit->Address, a2: int): Edit->Address;
+	edittext: fn(f: ref Filem->File, q: int, r: string, nr: int): string;
+
+	alllooper: fn(w: ref Windowm->Window, lp: ref Dat->Looper);
+	alltofile: fn(w: ref Windowm->Window, tp: ref Dat->Tofile);
+	allmatchfile: fn(w: ref Windowm->Window, tp: ref Dat->Tofile);
+	allfilecheck: fn(w: ref Windowm->Window, fp: ref Dat->Filecheck);
+
+	readloader: fn(f: ref Filem->File, q0: int, r: string, nr: int): int;
+};
--- /dev/null
+++ b/appl/acme/edit.b
@@ -1,0 +1,676 @@
+implement Edit;
+
+include "common.m";
+
+sys: Sys;
+dat: Dat;
+utils: Utils;
+textm: Textm;
+windowm: Windowm;
+rowm: Rowm;
+scroll: Scroll;
+editlog: Editlog;
+editcomd: Editcmd;
+
+sprint, print: import sys;
+FALSE, TRUE, BUFSIZE, Null, Empty, Inactive: import Dat;
+warning, error, strchr: import utils;
+Text: import textm;
+File: import Filem;
+Window: import windowm;
+allwindows: import rowm;
+scrdraw: import scroll;
+elogterm, elogapply: import editlog;
+cmdexec, resetxec: import editcomd;
+
+init(mods : ref Dat->Mods)
+{
+	sys = mods.sys;
+	dat = mods.dat;
+	utils = mods.utils;
+	textm = mods.textm;
+	windowm = mods.windowm;
+	rowm = mods.rowm;
+	scroll = mods.scroll;
+	editlog = mods.editlog;
+	editcomd = mods.editcmd;
+	editing = Inactive;
+}
+
+linex: con "\n";
+wordx: con "\t\n";
+
+cmdtab = array[28] of {
+#		cmdc	text	regexp	addr	defcmd	defaddr	count	token	 fn
+	Cmdt ( '\n',	0,	0,	0,	0,	aDot,	0,	nil,		C_nl ),
+	Cmdt ( 'a',		1,	0,	0,	0,	aDot,	0,	nil,		C_a ),
+	Cmdt ( 'b',		0,	0,	0,	0,	aNo,		0,	linex,	C_b ),
+	Cmdt ( 'c',		1,	0,	0,	0,	aDot,	0,	nil,		C_c ),
+	Cmdt ( 'd',		0,	0,	0,	0,	aDot,	0,	nil,		C_d ),
+	Cmdt ( 'e',		0,	0,	0,	0,	aNo,		0,	wordx,	C_e ),
+	Cmdt ( 'f',		0,	0,	0,	0,	aNo,		0,	wordx,	C_f ),
+	Cmdt ( 'g',		0,	1,	0,	'p',	aDot,	0,	nil,		C_g ),
+	Cmdt ( 'i',		1,	0,	0,	0,	aDot,	0,	nil,		C_i ),
+	Cmdt ( 'm',	0,	0,	1,	0,	aDot,	0,	nil,		C_m ),
+	Cmdt ( 'p',		0,	0,	0,	0,	aDot,	0,	nil,		C_p ),
+	Cmdt ( 'r',		0,	0,	0,	0,	aDot,	0,	wordx,	C_e ),
+	Cmdt ( 's',		0,	1,	0,	0,	aDot,	1,	nil,		C_s ),
+	Cmdt ( 't',		0,	0,	1,	0,	aDot,	0,	nil,		C_m ),
+	Cmdt ( 'u',		0,	0,	0,	0,	aNo,		2,	nil,		C_u ),
+	Cmdt ( 'v',		0,	1,	0,	'p',	aDot,	0,	nil,		C_g ),
+	Cmdt ( 'w',	0,	0,	0,	0,	aAll,		0,	wordx,	C_w ),
+	Cmdt ( 'x',		0,	1,	0,	'p',	aDot,	0,	nil,		C_x ),
+	Cmdt ( 'y',		0,	1,	0,	'p',	aDot,	0,	nil,		C_x ),
+	Cmdt ( '=',		0,	0,	0,	0,	aDot,	0,	linex,	C_eq ),
+	Cmdt ( 'B',		0,	0,	0,	0,	aNo,		0,	linex,	C_B ),
+	Cmdt ( 'D',	0,	0,	0,	0,	aNo,		0,	linex,	C_D ),
+	Cmdt ( 'X',		0,	1,	0,	'f',	aNo,		0,	nil,		C_X ),
+	Cmdt ( 'Y',		0,	1,	0,	'f',	aNo,		0,	nil,		C_X ),
+	Cmdt ( '<',		0,	0,	0,	0,	aDot,	0,	linex,	C_pipe ),
+	Cmdt ( '|',		0,	0,	0,	0,	aDot,	0,	linex,	C_pipe ),
+	Cmdt ( '>',		0,	0,	0,	0,	aDot,	0,	linex,	C_pipe ),
+	# deliberately unimplemented
+	# Cmdt ( 'k',	0,	0,	0,	0,	aDot,	0,	nil,		C_k ),
+	# Cmdt ( 'n',	0,	0,	0,	0,	aNo,		0,	nil,		C_n ),
+	# Cmdt ( 'q',	0,	0,	0,	0,	aNo,		0,	nil,		C_q ),
+	# Cmdt ( '!',	0,	0,	0,	0,	aNo,		0,	linex,	C_plan9 ),
+	Cmdt (0,		0,	0,	0,	0,	0,		0,	nil,		-1 )
+};
+
+cmdstartp: string;
+cmdendp: int;
+cmdp: int;
+editerrc: chan of string;
+
+lastpat : ref String;
+patset: int;
+
+# cmdlist: ref List;
+# addrlist: ref List;
+# stringlist: ref List;
+
+editwaitproc(pid : int, sync: chan of int)
+{
+	fd : ref Sys->FD;
+	n : int;
+
+	sys->pctl(Sys->FORKFD, nil);
+	w := sprint("#p/%d/wait", pid);
+	fd = sys->open(w, Sys->OREAD);
+	if (fd == nil)
+		error("fd == nil in editwaitproc");
+	sync <-= sys->pctl(0, nil);
+	buf := array[Sys->WAITLEN] of byte;
+	status := "";
+	for(;;){
+		if ((n = sys->read(fd, buf, len buf))<0)
+			error("bad read in editwaitproc");
+		status = string buf[0:n];
+		dat->cwait <-= status;
+	}
+}
+
+editthread()
+{
+	cmdp: ref Cmd;
+
+	mypid := sys->pctl(0, nil);
+	sync := chan of int;
+	spawn editwaitproc(mypid, sync);
+	yourpid := <- sync;
+	while((cmdp=parsecmd(0)) != nil){
+#		ocurfile = curfile;
+#		loaded = curfile && !curfile->unread;
+		if(cmdexec(curtext, cmdp) == 0)
+			break;
+		freecmd();
+	}
+	editerrc <-= nil;
+	utils->postnote(Utils->PNPROC, mypid, yourpid, "kill");
+}
+
+allelogterm(w: ref Window)
+{
+	elogterm(w.body.file);
+}
+
+alleditinit(w: ref Window)
+{
+	w.tag.commit(TRUE);
+	w.body.commit(TRUE);
+	w.body.file.editclean = FALSE;
+}
+
+allupdate(w: ref Window)
+{
+	t: ref Text;
+	i: int;
+	f: ref File;
+
+	t = w.body;
+	f = t.file;
+	if(f.curtext != t)		# do curtext only
+		return;
+	if(f.elog.typex == Null)
+		elogterm(f);
+	else if(f.elog.typex != Empty){
+		elogapply(f);
+		if(f.editclean){
+			f.mod = FALSE;
+			for(i=0; i<f.ntext; i++)
+				f.text[i].w.dirty = FALSE;
+		}
+		t.setselect(t.q0, t.q1);
+		scrdraw(t);
+		w.settag();
+	}
+}
+
+editerror(s: string)
+{
+	# print("%s", s);
+	freecmd();
+	allwindows(ALLELOGTERM, nil);	# truncate the edit logs
+	editerrc <-= s;
+	exit;
+}
+
+editcmd(ct: ref Text, r: string, n: int)
+{
+	err: string;
+
+	if(n == 0)
+		return;
+	if(2*n > BUFSIZE){
+		warning(nil, "string too long\n");
+		return;
+	}
+
+	allwindows(ALLEDITINIT, nil);
+	cmdstartp = r[0:n];
+	if(r[n-1] != '\n')
+		cmdstartp[n++] = '\n';
+	cmdendp = n;
+	cmdp = 0;
+	if(ct.w == nil)
+		curtext = nil;
+	else
+		curtext = ct.w.body;
+	resetxec();
+	if(editerrc == nil){
+		editerrc = chan of string;
+		lastpat = allocstring(0);
+	}
+	spawn editthread();
+	err = <- editerrc;
+	editing = Inactive;
+	if(err != nil)
+		warning(nil, sprint("Edit: %s\n", err));
+
+	# update everyone whose edit log has data
+	allwindows(ALLUPDATE, nil);
+}
+
+getch(): int
+{
+	if(cmdp == cmdendp)
+		return -1;
+	return cmdstartp[cmdp++];
+}
+
+nextc(): int
+{
+	if(cmdp == cmdendp)
+		return -1;
+	return cmdstartp[cmdp];
+}
+
+ungetch()
+{
+	if(--cmdp < 0)
+		error("ungetch");
+}
+
+getnum(signok: int): int
+{
+	n: int;
+	c, sign: int;
+
+	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;
+}
+
+cmdskipbl(): int
+{
+	c: int;
+	do
+		c = getch();
+	while(c==' ' || c=='\t');
+	if(c >= 0)
+		ungetch();
+	return c;
+}
+
+# Check that list has room for one more element.
+# growlist(l: ref List)
+# {
+#	if(l.elems == nil || l.nalloc==0){
+#		l.nalloc = INCR;
+#		l.elems = array[INCR] of Listelement;
+#		l.nused = 0;
+#	}else if(l.nused == l.nalloc){
+#		old := l.elems;
+#		l.elems = array[l.nalloc+INCR] of Listelement;
+#		l.elems[0:] = old[0:l.nalloc];
+#		l.nalloc += INCR;
+#	}
+# }
+
+# Remove the ith element from the list
+# dellist(l: ref List, i: int)
+# {
+#	l.elems[i:] = l.elems[i+1:l.nused];
+#	l.nused--;
+# }
+
+# Add a new element, whose position is i, to the list
+# inslist(l: ref List, i: int, val: int)
+# {
+#	growlist(l);
+#	l.elems[i+1:] = l.elems[i:l.nused];
+#	l.elems[i] = val;
+#	l.nused++;
+# }
+
+# listfree(l: ref List)
+# {
+#	l.elems = nil;
+# }
+
+allocstring(n: int): ref String
+{
+	s: ref String;
+
+	s = ref String;
+	s.n = n;
+	s.r = string array[s.n] of { * => byte '\0' };
+	return s;
+}
+
+freestring(s: ref String)
+{
+	s.r = nil;
+}
+
+newcmd(): ref Cmd
+{
+	p: ref Cmd;
+
+	p = ref Cmd;
+	# inslist(cmdlist, cmdlist.nused, p);
+	return p;
+}
+
+newstring(n: int): ref String
+{
+	p: ref String;
+
+	p = allocstring(n);
+	# inslist(stringlist, stringlist.nused, p);
+	return p;
+}
+
+newaddr(): ref Addr
+{
+	p: ref Addr;
+
+	p = ref Addr;
+	# inslist(addrlist, addrlist.nused, p);
+	return p;
+}
+
+freecmd()
+{
+	# i: int;
+
+	# cmdlist.elems = nil;
+	# addrlist.elems = nil;
+	# stringlist.elems = nil;
+	# cmdlist.nused = addrlist.nused = stringlist.nused = 0;
+}
+
+okdelim(c: int)
+{
+	if(c=='\\' || ('a'<=c && c<='z')
+	|| ('A'<=c && c<='Z') || ('0'<=c && c<='9'))
+		editerror(sprint("bad delimiter %c\n", c));
+}
+
+atnl()
+{
+	c: int;
+
+	cmdskipbl();
+	c = getch();
+	if(c != '\n')
+		editerror(sprint("newline expected (saw %c)", c));
+}
+
+Straddc(s: ref String, c: int)
+{
+	s.r[s.n++] = c;
+}
+
+getrhs(s: ref String, delim: int, cmd: int)
+{
+	c: int;
+
+	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
+}
+
+collecttoken(end: string): ref String
+{
+	c: int;
+
+	s := newstring(0);
+
+	while((c=nextc())==' ' || c=='\t')
+		Straddc(s, getch()); # blanks significant for getname()
+	while((c=getch())>0 && strchr(end, c)<0)
+		Straddc(s, c);
+	if(c != '\n')
+		atnl();
+	return s;
+}
+
+collecttext(): ref String
+{
+	s: ref String;
+	begline, i, c, delim: int;
+
+	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)
+				return s;
+		}while(s.r[begline]!='.' || s.r[begline+1]!='\n');
+		s.r[s.n-2] = '\0';
+	}else{
+		okdelim(delim = getch());
+		getrhs(s, delim, 'a');
+		if(nextc()==delim)
+			getch();
+		atnl();
+	}
+	return s;
+}
+
+cmdlookup(c: int): int
+{
+	i: int;
+
+	for(i=0; cmdtab[i].cmdc; i++)
+		if(cmdtab[i].cmdc == c)
+			return i;
+	return -1;
+}
+
+parsecmd(nest: int): ref Cmd
+{
+	i, c: int;
+	cp, ncp: ref Cmd;
+	cmd: ref Cmd;
+
+	cmd = ref Cmd;
+	cmd.next = cmd.cmd = nil;
+	cmd.re = nil;
+	cmd.flag = cmd.num = 0;
+	cmd.addr = compoundaddr();
+	if(cmdskipbl() == -1)
+		return nil;
+	if((c=getch())==-1)
+		return nil;
+	cmd.cmdc = c;
+	if(cmd.cmdc=='c' && nextc()=='d'){	# sleazy two-character case
+		getch();		# the 'd'
+		cmd.cmdc='c'|16r100;
+	}
+	i = cmdlookup(cmd.cmdc);
+	if(i >= 0){
+		if(cmd.cmdc == '\n'){
+			cp = newcmd();
+			*cp = *cmd;
+			return cp;
+			# let nl_cmd work it all out
+		}
+		ct := cmdtab[i];
+		if(ct.defaddr==aNo && cmd.addr != nil)
+			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())==nil)
+			editerror("bad address");
+		if(ct.defcmd){
+			if(cmdskipbl() == '\n'){
+				getch();
+				cmd.cmd = newcmd();
+				cmd.cmd.cmdc = ct.defcmd;
+			}else if((cmd.cmd = parsecmd(nest))==nil)
+				error("defcmd");
+		}else if(ct.text)
+			cmd.text = collecttext();
+		else if(ct.token != nil)
+			cmd.text = collecttoken(ct.token);
+		else
+			atnl();
+	}else
+		case(cmd.cmdc){
+		'{' =>
+			cp = nil;
+			do{
+				if(cmdskipbl()=='\n')
+					getch();
+				ncp = parsecmd(nest+1);
+				if(cp != nil)
+					cp.next = ncp;
+				else
+					cmd.cmd = ncp;
+			}while((cp = ncp) != nil);
+			break;
+		'}' =>
+			atnl();
+			if(nest==0)
+				editerror("right brace with no left brace");
+			return nil;
+		'c'|16r100 =>
+			editerror("unimplemented command cd");
+		* =>
+			editerror(sprint("unknown command %c", cmd.cmdc));
+		}
+	cp = newcmd();
+	*cp = *cmd;
+	return cp;
+}
+
+getregexp(delim: int): ref String
+{
+	buf, r: ref String;
+	i, c: int;
+
+	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 >= BUFSIZE)
+			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);
+	k := lastpat.n;
+	for(j := 0; j < k; j++)
+		r.r[j] = lastpat.r[j];	# newstring put \0 at end
+	return r;
+}
+
+simpleaddr(): ref Addr
+{
+	addr: Addr;
+	ap, nap: ref Addr;
+
+	addr.next = nil;
+	addr.left = nil;
+	case(cmdskipbl()){
+	'#' =>
+		addr.typex = getch();
+		addr.num = getnum(1);
+		break;
+	'0' to '9' =>
+		addr.num = getnum(1);
+		addr.typex='l';
+		break;
+	'/' or '?' or '"' =>
+		addr.re = getregexp(addr.typex = getch());
+		break;
+	'.' or
+	'$' or
+	'+' or
+	'-' or
+	'\'' =>
+		addr.typex = getch();
+		break;
+	* =>
+		return nil;
+	}
+	if((addr.next = simpleaddr()) != nil)
+		case(addr.next.typex){
+		'.' or
+		'$' or
+		'\'' =>
+			if(addr.typex!='"')
+				editerror("bad address syntax");
+			break;
+		'"' =>
+			editerror("bad address syntax");
+			break;
+		'l' or
+		'#' =>
+			if(addr.typex=='"')
+				break;
+			if(addr.typex!='+' && addr.typex!='-'){
+				# insert the missing '+'
+				nap = newaddr();
+				nap.typex='+';
+				nap.next = addr.next;
+				addr.next = nap;
+			}
+			break;
+		'/' or
+		'?' =>
+			if(addr.typex!='+' && addr.typex!='-'){
+				# insert the missing '+'
+				nap = newaddr();
+				nap.typex='+';
+				nap.next = addr.next;
+				addr.next = nap;
+			}
+			break;
+		'+' or
+		'-' =>
+			break;
+		* =>
+			error("simpleaddr");
+		}
+	ap = newaddr();
+	*ap = addr;
+	return ap;
+}
+
+compoundaddr(): ref Addr
+{
+	addr: Addr;
+	ap, next: ref Addr;
+
+	addr.left = simpleaddr();
+	if((addr.typex = cmdskipbl())!=',' && addr.typex!=';')
+		return addr.left;
+	getch();
+	next = addr.next = compoundaddr();
+	if(next != nil && (next.typex==',' || next.typex==';') && next.left==nil)
+		editerror("bad address syntax");
+	ap = newaddr();
+	*ap = addr;
+	return ap;
+}
+
--- /dev/null
+++ b/appl/acme/edit.m
@@ -1,0 +1,85 @@
+Edit: module {
+	#pragma	varargck	argpos	editerror	1
+
+	PATH: con "/dis/acme/edit.dis";
+
+	String: adt{
+		n: int;
+		r: string;
+	};
+
+	Addr: adt{
+		typex: int;		# # (char addr), l (line addr), / ? . $ + - , ;
+		num: int;
+		next: cyclic ref Addr;		# or right side of , and ; 
+		re: ref String;
+		left: cyclic ref Addr;		# left side of , and ; 
+	};
+
+	Address: adt{
+		r: Dat->Range;
+		f: ref Filem->File;
+	};
+
+	Cmd: adt{
+		addr: ref Addr;			# address (range of text)
+		re: ref String;			# regular expression for e.g. 'x'
+		next: cyclic ref Cmd;		# pointer to next element in {}
+		num: int;
+		flag: int;				# whatever
+		cmdc: int;				# command character; 'x' etc.
+		cmd: cyclic ref Cmd;			# target of x, g, {, etc.
+		text: ref String;			# text of a, c, i; rhs of s
+		mtaddr: ref Addr;		# address for m, t
+	};
+
+	Cmdt: adt{
+		cmdc: int;			# command character
+		text: int;			# takes a textual argument?
+		regexp: int;		# takes a regular expression?
+		addr: int;		# takes an address (m or t)?
+		defcmd: int;		# default command; 0==>none
+		defaddr: int;		# default address
+		count: int;		# takes a count e.g. s2///
+		token: string;		# takes text terminated by one of these
+		fnc: int;			# function to call with parse tree
+	};
+
+	cmdtab: array of Cmdt;
+
+	INCR: con 25;	# delta when growing list
+
+	List: adt{
+		nalloc: int;
+		nused: int;
+		pick{
+			C => cmdptr: array of ref Cmd;
+			S => stringptr: array of ref String;
+			A => addrptr: array of ref Addr;
+		}
+	};
+
+	aNo, aDot, aAll: con iota;	# default addresses
+
+	ALLLOOPER, ALLTOFILE, ALLMATCHFILE, ALLFILECHECK, ALLELOGTERM, ALLEDITINIT, ALLUPDATE: con iota;
+
+	C_nl, C_a, C_b, C_c, C_d, C_B, C_D, C_e, C_f, C_g, C_i, C_k, C_m, C_n, C_p, C_s, C_u, C_w, C_x, C_X, C_pipe, C_eq: con iota;
+
+	editing: int;
+	curtext: ref Textm->Text;
+
+	init : fn(mods : ref Dat->Mods);
+
+	allocstring: fn(a0: int): ref String;
+	freestring: fn(a0: ref String);
+	getregexp: fn(a0: int): ref String;
+	newaddr: fn(): ref Addr;
+	editcmd: fn(t: ref Textm->Text, r: string, n: int);
+	editerror: fn(a0: string);
+	cmdlookup: fn(a0: int): int;
+	Straddc: fn(a0: ref String, a1: int);
+
+	allelogterm: fn(w: ref Windowm->Window);
+	alleditinit: fn(w: ref Windowm->Window);
+	allupdate: fn(w: ref Windowm->Window);
+};
--- /dev/null
+++ b/appl/acme/elog.b
@@ -1,0 +1,353 @@
+implement Editlog;
+
+include "common.m";
+
+sys: Sys;
+utils: Utils;
+buffm: Bufferm;
+filem: Filem;
+textm: Textm;
+edit: Edit;
+
+sprint, fprint: import sys;
+FALSE, TRUE, BUFSIZE, Empty, Null, Delete, Insert, Replace, Filename, Astring: import Dat;
+File: import filem;
+Buffer: import buffm;
+Text: import textm;
+error, warning, stralloc, strfree: import utils;
+editerror: import edit;
+
+init(mods : ref Dat->Mods)
+{
+	sys = mods.sys;
+	utils = mods.utils;
+	buffm = mods.bufferm;
+	filem = mods.filem;
+	textm = mods.textm;
+	edit = mods.edit;
+}
+
+Wsequence := "warning: changes out of sequence\n";
+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
+#
+
+Buflog: adt{
+	typex: int;		# Replace, Filename
+	q0: int;		# location of change (unused in f)
+	nd: int;		# runes to delete
+	nr: int;		# runes in string or file name
+};
+
+Buflogsize: con 7;
+SHM : con 16rffff;
+
+pack(b: Buflog) : string
+{
+	a := "0123456";
+	a[0] = b.typex;
+	a[1] = b.q0&SHM;
+	a[2] = (b.q0>>16)&SHM;
+	a[3] = b.nd&SHM;
+	a[4] = (b.nd>>16)&SHM;
+	a[5] = b.nr&SHM;
+	a[6] = (b.nr>>16)&SHM;
+	return a;
+}
+
+scopy(s1: ref Astring, m: int, s2: string, n: int, o: int)
+{
+	p := o-n;
+	for(i := 0; i < p; i++)
+		s1.s[m++] = s2[n++];
+}
+
+#
+# Minstring shouldn't be very big or we will do lots of I/O for small changes.
+# Maxstring is BUFSIZE so we can fbufalloc() once and not realloc elog.r.
+#
+Minstring: con 16;	# distance beneath which we merge changes
+Maxstring: con BUFSIZE;	# maximum length of change we will merge into one
+
+eloginit(f: ref File)
+{
+	if(f.elog.typex != Empty)
+		return;
+	f.elog.typex = Null;
+	if(f.elogbuf == nil)
+		f.elogbuf = buffm->newbuffer();
+		# f.elogbuf = ref Buffer;
+	if(f.elog.r == nil)
+		f.elog.r = stralloc(BUFSIZE);
+	f.elogbuf.reset();
+}
+
+elogclose(f: ref File)
+{
+	if(f.elogbuf != nil){
+		f.elogbuf.close();
+		f.elogbuf = nil;
+	}
+}
+
+elogreset(f: ref File)
+{
+	f.elog.typex = Null;
+	f.elog.nd = 0;
+	f.elog.nr = 0;
+}
+
+elogterm(f: ref File)
+{
+	elogreset(f);
+	if(f.elogbuf != nil)
+		f.elogbuf.reset();
+	f.elog.typex = Empty;
+	if(f.elog.r != nil){
+		strfree(f.elog.r);
+		f.elog.r = nil;
+	}
+	warned = FALSE;
+}
+
+elogflush(f: ref File)
+{
+	b: Buflog;
+
+	b.typex = f.elog.typex;
+	b.q0 = f.elog.q0;
+	b.nd = f.elog.nd;
+	b.nr = f.elog.nr;
+	case(f.elog.typex){
+	* =>
+		warning(nil, sprint("unknown elog type 0x%ux\n", f.elog.typex));
+		break;
+	Null =>
+		break;
+	Insert or
+	Replace =>
+		if(f.elog.nr > 0)
+			f.elogbuf.insert(f.elogbuf.nc, f.elog.r.s, f.elog.nr);
+		f.elogbuf.insert(f.elogbuf.nc, pack(b), Buflogsize);
+		break;
+	Delete =>
+		f.elogbuf.insert(f.elogbuf.nc, pack(b), Buflogsize);
+		break;
+	}
+	elogreset(f);
+}
+
+elogreplace(f: ref File, q0: int, q1: int, r: string, nr: int)
+{
+	gap: int;
+
+	if(q0==q1 && nr==0)
+		return;
+	eloginit(f);
+	if(f.elog.typex!=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.typex==Replace && f.elog.nr+gap+nr<Maxstring){
+		if(gap < Minstring){
+			if(gap > 0){
+				f.buf.read(f.elog.q0+f.elog.nd, f.elog.r, f.elog.nr, gap);
+				f.elog.nr += gap;
+			}
+			f.elog.nd += gap + q1-q0;
+			scopy(f.elog.r, f.elog.nr, r, 0, nr);
+			f.elog.nr += nr;
+			return;
+		}
+	}
+	elogflush(f);
+	f.elog.typex = Replace;
+	f.elog.q0 = q0;
+	f.elog.nd = q1-q0;
+	f.elog.nr = nr;
+	if(nr > BUFSIZE)
+		editerror(sprint("internal error: replacement string too large(%d)", nr));
+	scopy(f.elog.r, 0, r, 0, nr);
+}
+
+eloginsert(f: ref File, q0: int, r: string, nr: int)
+{
+	n: int;
+
+	if(nr == 0)
+		return;
+	eloginit(f);
+	if(f.elog.typex!=Null && q0<f.elog.q0){
+		if(warned++ == 0)
+			warning(nil, Wsequence);
+		elogflush(f);
+	}
+	# try to merge with previous
+	if(f.elog.typex==Insert && q0==f.elog.q0 && f.elog.nr+nr<Maxstring){
+		ofer := f.elog.r;
+		f.elog.r = stralloc(f.elog.nr+nr);
+		scopy(f.elog.r, 0, ofer.s, 0, f.elog.nr);
+		scopy(f.elog.r, f.elog.nr, r, 0, nr);
+		f.elog.nr += nr;
+		strfree(ofer);
+		return;
+	}
+	while(nr > 0){
+		elogflush(f);
+		f.elog.typex = Insert;
+		f.elog.q0 = q0;
+		n = nr;
+		if(n > BUFSIZE)
+			n = BUFSIZE;
+		f.elog.nr = n;
+		scopy(f.elog.r, 0, r, 0, n);
+		r = r[n:];
+		nr -= n;
+	}
+}
+
+elogdelete(f: ref File, q0: int, q1: int)
+{
+	if(q0 == q1)
+		return;
+	eloginit(f);
+	if(f.elog.typex!=Null && q0<f.elog.q0+f.elog.nd){
+		if(warned++ == 0)
+			warning(nil, Wsequence);
+		elogflush(f);
+	}
+	#  try to merge with previous
+	if(f.elog.typex==Delete && f.elog.q0+f.elog.nd==q0){
+		f.elog.nd += q1-q0;
+		return;
+	}
+	elogflush(f);
+	f.elog.typex = Delete;
+	f.elog.q0 = q0;
+	f.elog.nd = q1-q0;
+}
+
+elogapply(f: ref File)
+{
+	b: Buflog;
+	buf: ref Astring;
+	i, n, up, mod : int;
+	log: ref Buffer;
+
+	elogflush(f);
+	log = f.elogbuf;
+	t := f.curtext;
+
+	a := stralloc(Buflogsize);
+	buf = stralloc(BUFSIZE);
+	mod = FALSE;
+
+	#
+	# The edit commands have already updated the selection in t.q0, t.q1.
+	# The text.insert and text.delete calls below will update it again, so save the
+	# current setting and restore it at the end.
+	#
+	q0 := t.q0;
+	q1 := t.q1;
+
+	while(log.nc > 0){
+		up = log.nc-Buflogsize;
+		log.read(up, a, 0, Buflogsize);
+		b.typex = a.s[0];
+		b.q0 = a.s[1]|(a.s[2]<<16);
+		b.nd = a.s[3]|(a.s[4]<<16);
+		b.nr = a.s[5]|(a.s[6]<<16);
+		case(b.typex){
+		* =>
+			error(sprint("elogapply: 0x%ux\n", b.typex));
+			break;
+
+		Replace =>
+			if(!mod){
+				mod = TRUE;
+				f.mark();
+			}
+			# if(b.nd == b.nr && b.nr <= BUFSIZE){
+			#	up -= b.nr;
+			#	log.read(up, buf, 0, b.nr);
+			#	t.replace(b.q0, b.q0+b.nd, buf.s, b.nr, TRUE, 0);
+			#	break;
+			# }
+			t.delete(b.q0, b.q0+b.nd, TRUE);
+			up -= b.nr;
+			for(i=0; i<b.nr; i+=n){
+				n = b.nr - i;
+				if(n > BUFSIZE)
+					n = BUFSIZE;
+				log.read(up+i, buf, 0, n);
+				t.insert(b.q0+i, buf.s, n, TRUE, 0);
+			}
+			# t.q0 = b.q0;
+			# t.q1 = b.q0+b.nr;
+			break;
+
+		Delete =>
+			if(!mod){
+				mod = TRUE;
+				f.mark();
+			}
+			t.delete(b.q0, b.q0+b.nd, TRUE);
+			# t.q0 = b.q0;
+			# t.q1 = b.q0;
+			break;
+
+		Insert =>
+			if(!mod){
+				mod = TRUE;
+				f.mark();
+			}
+			up -= b.nr;
+			for(i=0; i<b.nr; i+=n){
+				n = b.nr - i;
+				if(n > BUFSIZE)
+					n = BUFSIZE;
+				log.read(up+i, buf, 0, n);
+				t.insert(b.q0+i, buf.s, n, TRUE, 0);
+			}
+			# t.q0 = b.q0;
+			# t.q1 = b.q0+b.nr;
+			break;
+
+#		Filename =>
+#			f.seq = u.seq;
+#			f.unsetname(epsilon);
+#			f.mod = u.mod;
+#			up -= u.n;
+#			if(u.n == 0)
+#				f.name = nil;
+#			else{
+#				fn0 := stralloc(u.n);
+#				delta.read(up, fn0, 0, u.n);
+#				f.name = fn0.s;
+#				strfree(fn0);
+#			}
+#			break;
+#
+		}
+		log.delete(up, log.nc);
+	}
+	strfree(buf);
+	strfree(a);
+	elogterm(f);
+
+	t.q0 = q0;
+	t.q1 = q1;
+	if(t.q1 > f.buf.nc)	# can't happen
+		t.q1 = f.buf.nc;
+}
--- /dev/null
+++ b/appl/acme/elog.m
@@ -1,0 +1,24 @@
+Editlog: module {
+
+	PATH: con "/dis/acme/elog.dis";
+
+	Elog: adt{
+		typex: int;		# Delete, Insert, Filename
+		q0: int;		# location of change (unused in f)
+		nd: int;		# number of deleted characters
+		nr: int;		# runes in string or file name
+		r: ref Dat->Astring;
+	};
+
+	init : fn(mods : ref Dat->Mods);
+
+	elogterm: fn(a0: ref Filem->File);
+	elogclose: fn(a0: ref Filem->File);
+	eloginsert: fn(a0: ref Filem->File, a1: int, a2: string, a3: int);
+	elogdelete: fn(a0: ref Filem->File, a1: int, a2: int);
+	elogreplace: fn(a0: ref Filem->File, a1: int, a2: int, a3: string, a4: int);
+	elogapply: fn(a0: ref Filem->File);
+
+};
+
+	
--- /dev/null
+++ b/appl/acme/exec.b
@@ -1,0 +1,1350 @@
+implement Exec;
+
+include "common.m";
+
+sys : Sys;
+dat : Dat;
+acme : Acme;
+utils : Utils;
+graph : Graph;
+gui : Gui;
+lookx : Look;
+bufferm : Bufferm;
+textm : Textm;
+scrl : Scroll;
+filem : Filem;
+windowm : Windowm;
+rowm : Rowm;
+columnm : Columnm;
+fsys : Fsys;
+editm: Edit;
+
+Dir, OREAD, OWRITE : import Sys;
+EVENTSIZE, QWaddr, QWdata, QWevent, Astring : import dat;
+Lock, Reffont, Ref, seltext, seq, row : import dat;
+warning, error, skipbl, findbl, stralloc, strfree, exec : import utils;
+dirname : import lookx;
+Body, Text : import textm;
+File : import filem;
+sprint : import sys;
+TRUE, FALSE, XXX, BUFSIZE : import Dat;
+Buffer : import bufferm;
+Row : import rowm;
+Column : import columnm;
+Window : import windowm;
+setalphabet: import textm;
+
+# snarfbuf : ref Buffer;
+
+init(mods : ref Dat->Mods)
+{
+	sys = mods.sys;
+	dat = mods.dat;
+	acme = mods.acme;
+	utils = mods.utils;
+	graph = mods.graph;
+	gui = mods.gui;
+	lookx = mods.look;
+	bufferm = mods.bufferm;
+	textm = mods.textm;
+	scrl = mods.scroll;
+	filem = mods.filem;
+	rowm = mods.rowm;
+	windowm = mods.windowm;
+	columnm = mods.columnm;
+	fsys = mods.fsys;
+	editm = mods.edit;
+
+	snarfbuf = bufferm->newbuffer();
+}
+
+Exectab : adt {
+	name : string;
+	fun : int;
+	mark : int;
+	flag1 : int;
+	flag2 : int;
+};
+
+F_ALPHABET, F_CUT, F_DEL, F_DELCOL, F_DUMP, F_EDIT, F_EXITX, F_FONTX, F_GET, F_ID, F_INCL, F_KILL, F_LIMBO, F_LINENO, F_LOCAL, F_LOOK, F_NEW, F_NEWCOL, F_PASTE, F_PUT, F_PUTALL, F_UNDO, F_SEND, F_SORT, F_TAB, F_ZEROX : con iota;
+
+exectab := array[] of {
+	Exectab ( "Alphabet",	F_ALPHABET,	FALSE,	XXX,		XXX		),
+	Exectab ( "Cut",		F_CUT,		TRUE,	TRUE,	TRUE	),
+	Exectab ( "Del",			F_DEL,		FALSE,	FALSE,	XXX		),
+	Exectab ( "Delcol",		F_DELCOL,	FALSE,	XXX,		XXX		),
+	Exectab ( "Delete",		F_DEL,		FALSE,	TRUE,	XXX		),
+	Exectab ( "Dump",		F_DUMP,		FALSE,	TRUE,	XXX		),
+	Exectab ( "Edit",		F_EDIT,		FALSE,	XXX,		XXX		),
+	Exectab ( "Exit",		F_EXITX,		FALSE,	XXX,		XXX		),
+	Exectab ( "Font",		F_FONTX,		FALSE,	XXX,		XXX		),
+	Exectab ( "Get",			F_GET,		FALSE,	TRUE,	XXX		),
+	Exectab ( "ID",			F_ID,		FALSE,	XXX,		XXX		),
+	Exectab ( "Incl",		F_INCL,		FALSE,	XXX,		XXX		),
+	Exectab ( "Kill",			F_KILL,		FALSE,	XXX,		XXX		),
+	Exectab ( "Limbo",		F_LIMBO,		FALSE,	XXX,		XXX   	),
+	Exectab ( "Lineno",		F_LINENO,	FALSE,	XXX,		XXX		),
+	Exectab ( "Load",		F_DUMP,		FALSE,	FALSE,	XXX		),
+	Exectab ( "Local",		F_LOCAL,		FALSE,	XXX,		XXX		),
+	Exectab ( "Look",		F_LOOK,		FALSE,	XXX,		XXX		),
+	Exectab ( "New",		F_NEW,		FALSE,	XXX,		XXX		),
+	Exectab ( "Newcol",		F_NEWCOL,	FALSE,	XXX,		XXX		),
+	Exectab ( "Paste",		F_PASTE,		TRUE,	TRUE,	XXX		),
+	Exectab ( "Put",			F_PUT,		FALSE,	XXX,		XXX		),
+	Exectab ( "Putall",		F_PUTALL,	FALSE,	XXX,		XXX		),
+	Exectab ( "Redo",		F_UNDO,		FALSE,	FALSE,	XXX		),
+	Exectab ( "Send",		F_SEND,		TRUE,	XXX,		XXX		),
+	Exectab ( "Snarf",		F_CUT,		FALSE,	TRUE,	FALSE	),
+	Exectab ( "Sort",		F_SORT,		FALSE,	XXX,		XXX		),
+	Exectab ( "Tab",		F_TAB,		FALSE,	XXX,		XXX		),
+	Exectab ( "Undo",		F_UNDO,		FALSE,	TRUE,	XXX		),
+	Exectab ( "Zerox",		F_ZEROX,		FALSE,	XXX,		XXX		),
+	Exectab ( nil, 			0,			0,		0,		0		),
+};
+
+runfun(fun : int, et, t, argt : ref Text, flag1, flag2 : int, arg : string, narg : int)
+{
+	case (fun) {
+		F_ALPHABET	=> alphabet(et, argt, arg, narg);
+		F_CUT 	 	=> cut(et, t, flag1, flag2);
+		F_DEL 		=> del(et, flag1);
+		F_DELCOL	=> delcol(et);
+		F_DUMP 		=> dump(argt, flag1, arg, narg);
+		F_EDIT		=> edit(et, argt, arg, narg);
+		F_EXITX		=> exitx();
+		F_FONTX		=> fontx(et, t, argt, arg, narg);
+		F_GET 		=> get(et, t, argt, flag1, arg, narg);
+		F_ID 		=> id(et);
+		F_INCL 		=> incl(et, argt, arg, narg);
+		F_KILL 		=> kill(argt, arg, narg);
+		F_LIMBO		=> limbo(et);
+		F_LINENO		=> lineno(et);
+		F_LOCAL 		=> local(et, argt, arg);
+		F_LOOK 		=> look(et, t, argt);
+		F_NEW 		=> lookx->new(et, t, argt, flag1, flag2, arg, narg);
+		F_NEWCOL	=> newcol(et);
+		F_PASTE		=> paste(et, t, flag1, flag2);
+		F_PUT		=> put(et, argt, arg, narg);
+		F_PUTALL 	=> putall();
+		F_UNDO 		=> undo(et, flag1);
+		F_SEND		=> send(et, t);
+		F_SORT		=> sort(et);
+		F_TAB		=> tab(et, argt, arg, narg);
+		F_ZEROX		=> zerox(et, t);
+		*			=> error("bad case in runfun()");
+	}
+}	
+		
+lookup(r : string, n : int) : int
+{
+	nr : int;
+
+	(r, n) = skipbl(r, n);
+	if(n == 0)
+		return -1;
+	(nil, nr) = findbl(r, n);
+	nr = n-nr;
+	for(i := 0; exectab[i].name != nil; i++)
+		if (r[0:nr] == exectab[i].name)
+			return i;
+	return -1;
+}
+
+isexecc(c : int) : int
+{
+	if(lookx->isfilec(c))
+		return 1;
+	return c=='<' || c=='|' || c=='>';
+}
+
+execute(t : ref Text, aq0 : int, aq1 : int, external : int, argt : ref Text)
+{
+	q0, q1 : int;
+	r : ref Astring;
+	s, dir, aa, a : string;
+	e : int;
+	c, n, f : int;
+
+	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.buf.nc && isexecc(c=t.readc(q1)) && c!=':')
+				q1++;
+			while(q0>0 && isexecc(c=t.readc(q0-1)) && c!=':')
+				q0--;
+			if(q1 == q0)
+				return;
+		}
+	}
+	r = stralloc(q1-q0);
+	t.file.buf.read(q0, r, 0, q1-q0);
+	e = lookup(r.s, q1-q0);
+	if(!external && t.w!=nil && t.w.nopen[QWevent]>byte 0){
+		f = 0;
+		if(e >= 0)
+			f |= 1;
+		if(q0!=aq0 || q1!=aq1){
+			t.file.buf.read(aq0, r, 0, aq1-aq0);
+			f |= 2;
+		}
+		(aa, a) = getbytearg(argt, TRUE, TRUE);
+		if(a != nil){	
+			if(len a > EVENTSIZE){	# too big; too bad 
+				aa = a = nil;
+				warning(nil, "`argument string too long\n");
+				return;
+			}
+			f |= 8;
+		}
+		c = 'x';
+		if(t.what == Body)
+			c = 'X';
+		n = aq1-aq0;
+		if(n <= EVENTSIZE)
+			t.w.event(sprint("%c%d %d %d %d %s\n", c, aq0, aq1, f, n, r.s[0:n]));
+		else
+			t.w.event(sprint("%c%d %d %d 0 \n", c, aq0, aq1, f));
+		if(q0!=aq0 || q1!=aq1){
+			n = q1-q0;
+			t.file.buf.read(q0, r, 0, n);
+			if(n <= EVENTSIZE)
+				t.w.event(sprint("%c%d %d 0 %d %s\n", c, q0, q1, n, r.s[0:n]));
+			else
+				t.w.event(sprint("%c%d %d 0 0 \n", c, q0, q1));
+		}
+		if(a != nil){
+			t.w.event(sprint("%c0 0 0 %d %s\n", c, len a, a));
+			if(aa != nil)
+				t.w.event(sprint("%c0 0 0 %d %s\n", c, len aa, aa));
+			else
+				t.w.event(sprint("%c0 0 0 0 \n", c));
+		}
+		strfree(r);
+		r = nil;
+		a = aa = nil;
+		return;
+	}
+	if(e >= 0){
+		if(exectab[e].mark && seltext!=nil)
+		if(seltext.what == Body){
+			seq++;
+			seltext.w.body.file.mark();
+		}
+		(s, n) = skipbl(r.s, q1-q0);
+		(s, n) = findbl(s, n);
+		(s, n) = skipbl(s, n);
+		runfun(exectab[e].fun, t, seltext, argt, exectab[e].flag1, exectab[e].flag2, s, n);
+		strfree(r);
+		r = nil;
+		return;
+	}
+
+	(dir, n) = dirname(t, nil, 0);
+	if(n==1 && dir[0]=='.'){	# sigh 
+		dir = nil;
+		n = 0;
+	}
+	(aa, a) = getbytearg(argt, TRUE, TRUE);
+	if(t.w != nil)
+		t.w.refx.inc();
+	spawn run(t.w, r.s, dir, n, TRUE, aa, a, FALSE);
+}
+
+printarg(argt : ref Text, q0 : int, q1 : int) : string
+{
+	buf : string;
+
+	if(argt.what!=Body || argt.file.name==nil)
+		return nil;
+	if(q0 == q1)
+		buf = sprint("%s:#%d", argt.file.name, q0);
+	else
+		buf = sprint("%s:#%d,#%d", argt.file.name, q0, q1);
+	return buf;
+}
+
+getarg(argt : ref Text, doaddr : int, dofile : int) : (string, string, int)
+{
+	r : ref Astring;
+	n : int;
+	e : Dat->Expand;
+	a : string;
+	ok : int;
+
+	if(argt == nil)
+		return (nil, nil, 0);
+	a = nil;
+	argt.commit(TRUE);
+	(ok, e) = lookx->expand(argt, argt.q0, argt.q1);
+	if (ok) {
+		e.bname = nil;
+		if(len e.name && dofile){
+			if(doaddr)
+				a = printarg(argt, e.q0, e.q1);
+			return (a, e.name, len e.name);
+		}
+		e.name = nil;
+	}else{
+		e.q0 = argt.q0;
+		e.q1 = argt.q1;
+	}
+	n = e.q1 - e.q0;
+	r = stralloc(n);
+	argt.file.buf.read(e.q0, r, 0, n);
+	if(doaddr)
+		a = printarg(argt, e.q0, e.q1);
+	return(a, r.s, n);
+}
+
+getbytearg(argt : ref Text, doaddr : int, dofile : int) : (string, string)
+{
+	r : string;
+	n : int;
+	aa : string;
+
+	(aa, r, n) = getarg(argt, doaddr, dofile);
+	if(r == nil)
+		return (nil, nil);
+	return (aa, r);
+}
+
+newcol(et : ref Text)
+{
+	c : ref Column;
+
+	c = et.row.add(nil, -1);
+	if(c != nil)
+		c.add(nil, nil, -1).settag();
+}
+
+delcol(et : ref Text)
+{
+	c := et.col;
+	if(c==nil || !c.clean(FALSE))
+		return;
+	for(i:=0; i<c.nw; i++){
+		w := c.w[i];
+		if(int w.nopen[QWevent]+int w.nopen[QWaddr]+int w.nopen[QWdata] > 0){
+			warning(nil, sys->sprint("can't delete column; %s is running an external command\n", w.body.file.name));
+			return;
+		}
+	}
+	c.row.close(c, TRUE);
+}
+
+del(et : ref Text, flag1 : int)
+{
+	if(et.col==nil || et.w == nil)
+		return;
+	if(flag1 || et.w.body.file.ntext>1 || et.w.clean(FALSE, FALSE))
+		et.col.close(et.w, TRUE);
+}
+
+sort(et : ref Text)
+{
+	if(et.col != nil)
+		et.col.sort();
+}
+
+seqof(w: ref Window, isundo: int): int
+{
+	# 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 w.body.file.redoseq();
+}
+
+undo(et : ref Text, flag1 : int)
+{
+	i, j: int;
+	c: ref Column;
+	w: ref Window;
+	seq: int;
+
+	if(et==nil || et.w== nil)
+		return;
+	seq = seqof(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(seqof(w, flag1) == seq)
+				w.undo(flag1);
+		}
+	}
+	# et.w.undo(flag1);
+}
+
+getname(t : ref Text, argt : ref Text, arg : string, narg : int, isput : int) : string
+{
+	r, dir : string;
+	i, n, ndir, promote : int;
+
+	(nil, r, n) = getarg(argt, FALSE, TRUE);
+	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)
+			return t.file.name;
+		# prefix with directory name if necessary 
+		dir = nil;
+		ndir = 0;
+		if(n>0 && arg[0]!='/'){
+			(dir, ndir) = dirname(t, nil, 0);
+			if(n==1 && dir[0]=='.'){	# sigh 
+				dir = nil;
+				ndir = 0;
+			}
+		}
+		if(dir != nil){
+			r = dir[0:ndir] + arg[0:n];
+			dir = nil;
+			n += ndir;
+		}else
+			r = arg[0:n];
+	}
+	return r;
+}
+
+zerox(et : ref Text, t : ref Text)
+{
+	nw : ref Window;
+	c, locked : int;
+
+	locked = FALSE;
+	if(t!=nil && t.w!=nil && t.w!=et.w){
+		locked = TRUE;
+		c = 'M';
+		if(et.w != nil)
+			c = et.w.owner;
+		t.w.lock(c);
+	}
+	if(t == nil)
+		t = et;
+	if(t==nil || t.w==nil)
+		return;
+	t = t.w.body;
+	if(t.w.isdir)
+		warning(nil, sprint("%s is a directory; Zerox illegal\n", t.file.name));
+	else{
+		nw = t.w.col.add(nil, t.w, -1);
+		# ugly: fix locks so w.unlock works 
+		nw.lock1(t.w.owner);
+	}
+	if(locked)
+		t.w.unlock();
+}
+
+get(et : ref Text, t : ref Text, argt : ref Text, flag1 : int, arg : string, narg : int)
+{
+	name : string;
+	r : string;
+	i, n, dirty : int;
+	w : ref Window;
+	u : ref Text;
+	d : Dir;
+	ok : int;
+
+	if(flag1)
+		if(et==nil || et.w==nil)
+			return;
+	if(!et.w.isdir && (et.w.body.file.buf.nc>0 && !et.w.clean(TRUE, FALSE)))
+		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){
+		(ok, d) = sys->stat(name);
+		if (ok == 0 && d.qid.qtype & Sys->QTDIR) {
+			warning(nil, sprint("%s is a directory; can't read with multiple windows on it\n", name));
+			return;
+		}
+	}
+	r = name;
+	n = len name;
+	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 
+		u.reset();
+		u.w.dirfree();
+	}
+	samename := r[0:n] == t.file.name;
+	t.loadx(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;
+	name = nil;
+	r = nil;
+	w.settag();
+	t.file.unread = FALSE;
+	for(i=0; i<t.file.ntext; i++){
+		u = t.file.text[i];
+		u.w.tag.setselect(u.w.tag.file.buf.nc, u.w.tag.file.buf.nc);
+		scrl->scrdraw(u);
+	}
+}
+
+putfile(f: ref File, q0: int, q1: int, name: string)
+{
+	n : int;
+	r, s : ref Astring;
+	w : ref Window;
+	i, q : int;
+	fd : ref Sys->FD;
+	d : Dir;
+	ok : int;
+
+	w = f.curtext.w;
+	
+	{
+		if(name == f.name){
+			(ok, d) = sys->stat(name);
+			if(ok >= 0 && (f.dev!=d.dev || f.qidpath!=d.qid.path || f.mtime<d.mtime)){
+				f.dev = d.dev;
+				f.qidpath = d.qid.path;
+				f.mtime = d.mtime;
+				if(f.unread)
+					warning(nil, sys->sprint("%s not written; file already exists\n", name));
+				else
+					warning(nil, sys->sprint("%s modified since last read\n", name));
+				raise "e";
+			}
+		}
+		fd = sys->create(name, OWRITE, 8r664);	# was 666
+		if(fd == nil){
+			warning(nil, sprint("can't create file %s: %r\n", name));
+			raise "e";
+		}
+		r = stralloc(BUFSIZE);
+		s = stralloc(BUFSIZE);
+		
+		{
+			(ok, d) = sys->fstat(fd);
+			if(ok>=0 && (d.mode&Sys->DMAPPEND) && d.length>big 0){
+				warning(nil, sprint("%s not written; file is append only\n", name));
+				raise "e";
+			}
+			for(q = q0; q < q1; q += n){
+				n = q1 - q;
+				if(n > BUFSIZE)
+					n = BUFSIZE;
+				f.buf.read(q, r, 0, n);
+				ab := array of byte r.s[0:n];
+				if(sys->write(fd, ab, len ab) != len ab){
+					ab = nil;
+					warning(nil, sprint("can't write file %s: %r\n", name));
+					raise "e";
+				}
+				ab = nil;
+			}
+			if(name == f.name){
+				d0 : Dir;
+		
+				if(q0 != 0 || q1 != f.buf.nc){
+					f.mod = TRUE;
+					w.dirty = TRUE;
+					f.unread = TRUE;
+				}
+				else{
+					(ok, d0) = sys->fstat(fd);	# use old values if we failed
+					if (ok >= 0)
+						d = d0;
+					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;
+				}
+			}
+			strfree(s);
+			strfree(r);
+			s = r = nil;
+			name = nil;
+			fd = nil;
+			w.settag();
+		}
+		exception{
+			* =>
+				strfree(s);
+				strfree(r);
+				s = r = nil;
+				fd = nil;
+				raise "e";
+		}
+	}
+	exception{
+		* =>
+			name = nil;
+			return;
+	}
+}
+
+put(et : ref Text, argt : ref Text, arg : string, narg : int)
+{
+	namer : string;
+	name : string;
+	w : ref Window;
+
+	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 = name;
+	putfile(f, 0, f.buf.nc, namer);
+	name = nil;
+}
+
+dump(argt : ref Text, isdump : int, arg : string, narg : int)
+{
+	name : string;
+
+	if(narg)
+		name = arg;
+	else
+		(nil, name) = getbytearg(argt, FALSE, TRUE);
+	if(isdump)
+		row.dump(name);
+	else {
+		if (!row.qlock.locked())
+			error("row not locked in dump()");
+		row.loadx(name, FALSE);
+	}
+	name = nil;
+}
+
+cut(et : ref Text, t : ref Text, dosnarf : int, docut : int)
+{
+	q0, q1, n, locked, c : int;
+	r : ref Astring;
+
+	# use current window if snarfing and its selection is non-null 
+	if(et!=t && dosnarf && et.w!=nil){
+		if(et.w.body.q1>et.w.body.q0){
+			t = et.w.body;
+			t.file.mark();	# seq has been incremented by execute
+		}
+		else if(et.w.tag.q1>et.w.tag.q0)
+			t = et.w.tag;
+	}
+	if(t == nil)
+		return;
+	locked = FALSE;
+	if(t.w!=nil && et.w!=t.w){
+		locked = TRUE;
+		c = 'M';
+		if(et.w != nil)
+			c = et.w.owner;
+		t.w.lock(c);
+	}
+	if(t.q0 == t.q1){
+		if(locked)
+			t.w.unlock();
+		return;
+	}
+	if(dosnarf){
+		q0 = t.q0;
+		q1 = t.q1;
+		snarfbuf.delete(0, snarfbuf.nc);
+		r = stralloc(BUFSIZE);
+		while(q0 < q1){
+			n = q1 - q0;
+			if(n > BUFSIZE)
+				n = BUFSIZE;
+			t.file.buf.read(q0, r, 0, n);
+			snarfbuf.insert(snarfbuf.nc, r.s, n);
+			q0 += n;
+		}
+		strfree(r);
+		r = nil;
+		acme->putsnarf();
+	}
+	if(docut){
+		t.delete(t.q0, t.q1, TRUE);
+		t.setselect(t.q0, t.q0);
+		if(t.w != nil){
+			scrl->scrdraw(t);
+			t.w.settag();
+		}
+	}else if(dosnarf)	# Snarf command 
+		dat->argtext = t;
+	if(locked)
+		t.w.unlock();
+}
+
+paste(et : ref Text, t : ref Text, selectall : int, tobody: int)
+{
+	c : int;
+	q, q0, q1, n : int;
+	r : ref Astring;
+
+	# if(tobody), use body of executing window  (Paste or Send command)
+	if(tobody && et!=nil && et.w!=nil){
+		t = et.w.body;
+		t.file.mark();	# seq has been incremented by execute
+	}
+	if(t == nil)
+		return;
+
+	acme->getsnarf();
+	if(t==nil || snarfbuf.nc==0)
+		return;
+	if(t.w!=nil && et.w!=t.w){
+		c = 'M';
+		if(et.w != nil)
+			c = et.w.owner;
+		t.w.lock(c);
+	}
+	cut(t, t, FALSE, TRUE);
+	q = 0;
+	q0 = t.q0;
+	q1 = t.q0+snarfbuf.nc;
+	r = stralloc(BUFSIZE);
+	while(q0 < q1){
+		n = q1 - q0;
+		if(n > BUFSIZE)
+			n = BUFSIZE;
+		if(r == nil)
+			r = stralloc(n);
+		snarfbuf.read(q, r, 0, n);
+		t.insert(q0, r.s, n, TRUE, 0);
+		q += n;
+		q0 += n;
+	}
+	strfree(r);
+	r = nil;
+	if(selectall)
+		t.setselect(t.q0, q1);
+	else
+		t.setselect(q1, q1);
+	if(t.w != nil){
+		scrl->scrdraw(t);
+		t.w.settag();
+	}
+	if(t.w!=nil && et.w!=t.w)
+		t.w.unlock();
+}
+
+look(et : ref Text, t : ref Text, argt : ref Text)
+{
+	r : string;
+	s : ref Astring;
+	n : int;
+
+	if(et != nil && et.w != nil){
+		t = et.w.body;
+		(nil, r, n) = getarg(argt, FALSE, FALSE);
+		if(r == nil){
+			n = t.q1-t.q0;
+			s = stralloc(n);
+			t.file.buf.read(t.q0, s, 0, n);
+			r = s.s;
+		}
+		lookx->search(t, r, n);
+		r = nil;
+	}
+}
+
+send(et : ref Text, t : ref Text)
+{
+	if(et.w==nil)
+		return;
+	t = et.w.body;
+	if(t.q0 != t.q1)
+		cut(t, t, TRUE, FALSE);
+	t.setselect(t.file.buf.nc, t.file.buf.nc);
+	paste(t, t, TRUE, TRUE);
+	if(t.readc(t.file.buf.nc-1) != '\n'){
+		t.insert(t.file.buf.nc, "\n", 1, TRUE, 0);
+		t.setselect(t.file.buf.nc, t.file.buf.nc);
+	}
+}
+
+edit(et: ref Text, argt: ref Text, arg: string, narg: int)
+{
+	r: string;
+	leng: int;
+
+	if(et == nil)
+		return;
+	(nil, r, leng) = getarg(argt, FALSE, TRUE);
+	seq++;
+	if(r != nil){
+		editm->editcmd(et, r, leng);
+		r = nil;
+	}else
+		editm->editcmd(et, arg, narg);
+}
+
+exitx()
+{
+	if(row.clean(TRUE))
+		acme->acmeexit(nil);
+}
+
+putall()
+{
+	i, j, e : int;
+	w : ref Window;
+	c : ref Column;
+	a : string;
+
+	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 || len w.body.file.name==0)
+				continue;
+			if(w.nopen[QWevent] > byte 0)
+				continue;
+			a = w.body.file.name;
+			e = utils->access(a);
+			if(w.body.file.mod || w.body.ncache)
+				if(e < 0)
+					warning(nil, sprint("no auto-Put of %s: %r\n", a));
+				else{
+					w.commit(w.body);
+					put(w.body, nil, nil, 0);
+				}
+			a = nil;
+		}
+	}
+}
+
+id(et : ref Text)
+{
+	if(et != nil && et.w != nil)
+		warning(nil, sprint("/mnt/acme/%d/\n", et.w.id));
+}
+
+limbo(et: ref Text)
+{
+	s := getname(et.w.body, nil, nil, 0, 0);
+	if(s == nil)
+		return;
+	for(l := len s; l > 0 && s[--l] != '/'; )
+		;
+	if(s[l] == '/')
+		s = s[l+1: ];
+	s = "limbo -gw " + s;
+	(dir, n) := dirname(et, nil, 0);
+	if(n==1 && dir[0]=='.'){	# sigh 
+		dir = nil;
+		n = 0;
+	}
+	spawn run(nil, s, dir, n, TRUE, nil, nil, FALSE);
+}
+
+local(et : ref Text, argt : ref Text, arg : string)
+{
+	a, aa : string;
+	dir : string;
+	n : int;
+
+	(aa, a) = getbytearg(argt, TRUE, TRUE);
+
+	(dir, n) = dirname(et, nil, 0);
+	if(n==1 && dir[0]=='.'){	# sigh 
+		dir = nil;
+		n = 0;
+	}
+	spawn run(nil, arg, dir, n, FALSE, aa, a, FALSE);
+}
+
+kill(argt : ref Text, arg : string, narg : int)
+{
+	a, cmd, r : string;
+	na : int;
+
+	(nil, r, na) = getarg(argt, FALSE, FALSE);
+	if(r != nil)
+		kill(nil, r, na);
+	# loop condition: *arg is not a blank 
+	for(;;){
+		(a, na) = findbl(arg, narg);
+		if(a == arg)
+			break;
+		cmd = arg[0:narg-na];
+		dat->ckill <-= cmd;
+		(arg, narg) = skipbl(a, na);
+	}
+}
+
+lineno(et : ref Text)
+{
+	n : int;
+
+	if (et == nil || et.w == nil || (et = et.w.body) == nil)
+		return;
+	q0 := et.q0;
+	q1 := et.q1;
+	if (q0 < 0 || q1 < 0 || q0 > q1)
+		return;
+	ln0 := 1;
+	ln1 := 1;
+	rp := stralloc(BUFSIZE);
+	nc := et.file.buf.nc;
+	if (q0 >= nc)
+		q0 = nc-1;
+	if (q1 >= nc)
+		q1 = nc-1;
+	for (q := 0; q < q1; ) {
+		if (q+BUFSIZE > nc)
+			n = nc-q;
+		else
+			n = BUFSIZE;
+		et.file.buf.read(q, rp, 0, n);
+		for (i := 0; i < n && q < q1; i++) {
+			if (rp.s[i] == '\n') {
+				if (q < q0)
+					ln0++;
+				if (q < q1-1)
+					ln1++;
+			}
+			q++;
+		}
+	}
+	rp = nil;
+	if (et.file.name != nil)
+		file := et.file.name + ":";
+	else
+		file = nil;
+	if (ln0 == ln1)
+		warning(nil, sprint("%s%d\n", file, ln0));
+	else
+		warning(nil, sprint("%s%d,%d\n", file, ln0, ln1));
+}
+
+fontx(et : ref Text, t : ref Text, argt : ref Text, arg : string, narg : int)
+{
+	a, r, flag, file : string;
+	na, nf : int;
+	aa : string;
+	newfont : ref Reffont;
+	dp : ref Dat->Dirlist;
+	i, fix : int;
+
+	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, na) = findbl(arg, narg);
+		if(a == arg)
+			break;
+		r = arg[0:narg-na];
+		if(r == "fix" || r == "var"){
+			flag = nil;
+			flag = r;
+		}else{
+			file = r;
+			nf = narg-na;
+		}
+		(arg, narg) = skipbl(a, na);
+	}
+	(nil, r, na) = getarg(argt, FALSE, TRUE);
+	if(r != nil)
+		if(r == "fix" || r == "var"){
+			flag = nil;
+			flag = r;
+		}else{
+			file = r;
+			nf = na;
+		}
+	fix = 1;
+	if(flag != nil)
+		fix = flag == "fix";
+	else if(file == nil){
+		newfont = Reffont.get(FALSE, FALSE, FALSE, nil);
+		if(newfont != nil)
+			fix = newfont.f.name == t.frame.font.name;
+	}
+	if(file != nil){
+		aa = file[0:nf];
+		newfont = Reffont.get(fix, flag!=nil, FALSE, aa);
+		aa = nil;
+	}else
+		newfont = Reffont.get(fix, FALSE, FALSE, nil);
+	if(newfont != nil){
+		graph->draw(gui->mainwin, t.w.r, acme->textcols[Framem->BACK], nil, (0, 0));
+		t.reffont.close();
+		t.reffont = newfont;
+		t.frame.font = newfont.f;
+		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 = dp.r;
+				dp.wid = graph->strwidth(newfont.f, aa);
+				aa = nil;
+			}
+		}
+		# avoid shrinking of window due to quantization 
+		t.w.col.grow(t.w, -1, 1);
+	}
+	file = nil;
+	flag = nil;
+}
+
+incl(et : ref Text, argt : ref Text, arg : string, narg : int)
+{
+	a, r : string;
+	w : ref Window;
+	na, n, leng : int;
+
+	if(et==nil || et.w==nil)
+		return;
+	w = et.w;
+	n = 0;
+	(nil, r, leng) = getarg(argt, FALSE, TRUE);
+	if(r != nil){
+		n++;
+		w.addincl(r, leng);
+	}
+	# loop condition: *arg is not a blank 
+	for(;;){
+		(a, na) = findbl(arg, narg);
+		if(a == arg)
+			break;
+		r = arg[0:narg-na];
+		n++;
+		w.addincl(r, narg-na);
+		(arg, narg) = skipbl(a, na);
+	}
+	if(n==0 && w.nincl){
+		for(n=w.nincl; --n>=0; )
+			warning(nil, sprint("%s ", w.incl[n]));
+		warning(nil, "\n");
+	}
+}
+
+tab(et : ref Text, argt : ref Text, arg : string, narg : int)
+{
+	a, r, p : string;
+	w : ref Window;
+	na, leng, tab : int;
+
+	if(et==nil || et.w==nil)
+		return;
+	w = et.w;
+	(nil, r, leng) = getarg(argt, FALSE, TRUE);
+	tab = 0;
+	if(r!=nil && leng>0){
+		p = r[0:leng];
+		if('0'<=p[0] && p[0]<='9')
+			tab = int p;
+		p = nil;
+	}else{
+		(a, na) = findbl(arg, narg);
+		if(a != arg){
+			p = arg[0:narg-na];
+			if('0'<=p[0] && p[0]<='9')
+				tab = int p;
+			p = nil;
+		}
+	}
+	if(tab > 0){
+		if(w.body.tabstop != tab){
+			w.body.tabstop = tab;
+			w.reshape(w.r, 1);
+		}
+	}else
+		warning(nil, sys->sprint("%s: Tab %d\n", w.body.file.name, w.body.tabstop));
+}
+
+alphabet(et: ref Text, argt: ref Text, arg: string, narg: int)
+{
+	r: string;
+	leng: int;
+
+	if(et == nil)
+		return;
+	(nil, r, leng) = getarg(argt, FALSE, FALSE);
+	if(r != nil)
+		setalphabet(r[0:leng]);
+	else
+		setalphabet(arg[0:narg]);
+}
+
+runfeed(p : array of ref Sys->FD, c : chan of int)
+{
+	n : int;
+	buf : array of byte;
+	s : string;
+
+	sys->pctl(Sys->FORKFD, nil);
+	c <-= 1;
+	# p[1] = nil;
+	buf = array[256] of byte;
+	for(;;){
+		if((n = sys->read(p[0], buf, 256)) <= 0)
+			break;
+		s = string buf[0:n];
+		dat->cerr <-= s;
+		s = nil;
+	}
+	buf = nil;
+	exit;
+}
+
+run(win : ref Window, s : string, rdir : string, ndir : int, newns : int, argaddr : string, arg : string, iseditcmd: int)
+{
+	c : ref Dat->Command;
+	name, dir : string;
+	e, t : int;
+	av : list of string;
+	r : int;
+	incl : array of string;
+	inarg, i, nincl : int;
+	tfd : ref Sys->FD;
+	p : array of ref Sys->FD;
+	pc : chan of int;
+	winid : int;
+
+	c = ref Dat->Command;
+	t = 0;
+	while(t < len s && (s[t]==' ' || s[t]=='\n' || s[t]=='\t'))
+		t++;
+	for(e=t; e < len s; e++)
+		if(s[e]==' ' || s[e]=='\n' || s[e]=='\t' )
+			break;
+	name = s[t:e];
+	e = utils->strrchr(name, '/');
+	if(e >= 0)
+		name = name[e+1:];
+	name += " ";	# add blank here for ease in waittask 
+	c.name = name;
+	name = nil;
+	pipechar := 0;
+	if (t < len s && (s[t] == '<' || s[t] == '|' || s[t] == '>')){
+		pipechar = s[t++];
+		s = s[t:];
+	}
+	c.pid = sys->pctl(0, nil);
+	c.iseditcmd = iseditcmd;
+	c.text = s;
+	dat->ccommand <-= c;
+	#
+	# must pctl() after communication because rendezvous name
+	# space is part of RFNAMEG.
+	#
+	 
+	if(newns){
+		wids : string = "";
+		filename: string;
+
+		if(win != nil){
+			filename = win.body.file.name;
+			wids = string win.id;
+			nincl = win.nincl;
+			incl = array[nincl] of string;
+			for(i=0; i<nincl; i++)
+				incl[i] = win.incl[i];
+			winid = win.id;
+			win.close();
+		}else{
+			winid = 0;
+			nincl = 0;
+			incl = nil;
+			if(dat->activewin != nil)
+				winid = (dat->activewin).id;
+		}
+		# sys->pctl(Sys->FORKNS|Sys->FORKFD|Sys->NEWPGRP, nil);
+		sys->pctl(Sys->FORKNS|Sys->NEWFD|Sys->FORKENV|Sys->NEWPGRP, 0::1::2::fsys->fsyscfd()::nil);
+		if(rdir != nil){
+			dir = rdir[0:ndir];
+			sys->chdir(dir);	# ignore error: probably app. window 
+			dir = nil;
+		}
+		if(filename != nil)
+			utils->setenv("%", filename);
+		c.md = fsys->fsysmount(rdir, ndir, incl, nincl);
+		if(c.md == nil){
+			# error("child: can't mount /mnt/acme");
+			warning(nil, "can't mount /mnt/acme");
+			exit;
+		}
+		if(winid > 0 && (pipechar=='|' || pipechar=='>')){
+			buf := sys->sprint("/mnt/acme/%d/rdsel", winid);
+			tfd = sys->open(buf, OREAD);
+		}
+		else
+			tfd = sys->open("/dev/null", OREAD);
+		sys->dup(tfd.fd, 0);
+		tfd = nil;
+		if((winid > 0 || iseditcmd) && (pipechar=='|' || pipechar=='<')){
+			buf: string;
+
+			if(iseditcmd){
+				if(winid > 0)
+					buf = sprint("/mnt/acme/%d/editout", winid);
+				else
+ 					buf = sprint("/mnt/acme/editout");
+			}
+			else
+				buf = sys->sprint("/mnt/acme/%d/wrsel", winid);
+			tfd = sys->open(buf, OWRITE);
+		}
+		else
+			tfd = sys->open("/dev/cons", OWRITE);
+		sys->dup(tfd.fd, 1);
+		tfd = nil;
+		if(winid > 0 && (pipechar=='|' || pipechar=='<')){
+			tfd = sys->open("/dev/cons", OWRITE);
+			sys->dup(tfd.fd, 2);
+		}
+		else
+			sys->dup(1, 2);
+		tfd = nil;
+		utils->setenv("acmewin", wids);
+	}else{
+		if(win != nil)
+			win.close();
+		sys->pctl(Sys->FORKFD|Sys->NEWPGRP, nil);
+		if(rdir != nil){
+			dir = rdir[0:ndir];
+			sys->chdir(dir);	# ignore error: probably app. window 
+			dir = nil;
+		}
+		p = array[2] of ref Sys->FD;
+		if(sys->pipe(p) < 0){
+			error("child: can't pipe");
+			exit;
+		}
+		pc = chan of int;
+		spawn runfeed(p, pc);
+		<-pc;
+		pc = nil;
+		fsys->fsysclose();
+		tfd = sys->open("/dev/null", OREAD);
+		sys->dup(tfd.fd, 0);
+		tfd = nil;
+		sys->dup(p[1].fd, 1);
+		sys->dup(1, 2);
+		p[0] = p[1] = nil;
+	}
+
+	if(argaddr != nil)
+		utils->setenv("acmeaddr", argaddr);
+	hard := 0;
+	if(len s > 512-10)	# may need to print into stack 
+		hard = 1;
+	else {
+		inarg = FALSE;
+		for(e=0; e < len s; e++){
+			r = s[e];
+			if(r==' ' || r=='\t')
+				continue;
+			if(r < ' ') {
+				hard = 1;
+				break;
+			}
+			if(utils->strchr("#;&|^$=`'{}()<>[]*?^~`", r) >= 0) {
+				hard = 1;
+				break;
+			}
+			inarg = TRUE;
+		}
+		if (!hard) {
+			if(!inarg)
+				exit;
+			av = nil;
+			sa := -1;
+			for(e=0; e < len s; e++){
+				r = s[e];
+				if(r==' ' || r=='\t'){
+					if (sa >= 0) {
+						av = s[sa:e] :: av;
+						sa = -1;
+					}
+					continue;
+				}
+				if (sa < 0)
+					sa = e;
+			}
+			if (sa >= 0)
+				av = s[sa:e] :: av;
+			if (arg != nil)
+				av = arg :: av;
+			av = utils->reverse(av);
+			c.av = av;
+			exec(hd av, av);
+			dat->cwait <-= string c.pid + " \"Exec\":";
+			exit;
+		}
+	}
+
+	if(arg != nil){
+		s = sprint("%s '%s'", s, arg);	# BUG: what if quote in arg? 
+		c.text = s;
+	}
+	av = nil;
+	av = s :: av;
+	av = "-c" :: av;
+	av = "/dis/sh" :: av;
+	exec(hd av, av);
+	dat->cwait <-= string c.pid + " \"Exec\":";
+	exit;
+}
+
+# Nasty bug causes
+# Edit ,|nonexistentcommand
+# (or ,> or ,<) to lock up acme.  Easy fix.  Add these two lines
+# to the failure case of runwaittask():
+#
+# /sys/src/cmd/acme/exec.c:1287 a exec.c:1288,1289
+# else{
+# if(c->iseditcmd)
+# sendul(cedit, 0);
+# free(c->name);
+# free(c->text);
+# free(c);
+# }
+
+
--- /dev/null
+++ b/appl/acme/exec.m
@@ -1,0 +1,19 @@
+Exec : module {
+	PATH : con "/dis/acme/exec.dis";
+
+	snarfbuf : ref Bufferm->Buffer;
+
+	init : fn(mods : ref Dat->Mods);
+
+	fontx : fn(et : ref Textm->Text, t : ref Textm->Text, argt : ref Textm->Text, arg : string, narg : int);
+	get : fn(et, t, argt : ref Textm->Text, flag1 : int, arg : string, narg : int);
+	put : fn(et, argt : ref Textm->Text, arg : string, narg : int);
+	cut : fn(et, t : ref Textm->Text, flag1, flag2 : int);
+	paste : fn(et, t : ref Textm->Text, flag1 : int, flag2: int);
+
+	getarg : fn(t : ref Textm->Text, m : int, n : int) : (string, string, int);
+	execute : fn(t : ref Textm->Text, aq0, aq1, external : int, argt : ref Textm->Text);
+	run : fn(w : ref Windowm->Window, s : string, rdir : string, ndir : int, newns : int, argaddr : string, arg : string, ise: int);
+	undo: fn(t: ref Textm->Text, flag: int);
+	putfile: fn(f: ref Filem->File, q0: int, q1: int, r: string);
+};
--- /dev/null
+++ b/appl/acme/file.b
@@ -1,0 +1,331 @@
+implement Filem;
+
+include "common.m";
+
+sys : Sys;
+dat : Dat;
+utils : Utils;
+buffm : Bufferm;
+textm : Textm;
+editlog: Editlog;
+
+FALSE, TRUE, XXX, Delete, Insert, Filename, BUFSIZE, Astring : import Dat;
+Buffer, newbuffer : import buffm;
+Text : import textm;
+error : import utils;
+
+init(mods : ref Dat->Mods)
+{
+	sys = mods.sys;
+	dat = mods.dat;
+	utils = mods.utils;
+	buffm = mods.bufferm;
+	textm = mods.textm;
+	editlog = mods.editlog;
+}
+
+#
+# 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.
+#
+ 
+Undo : adt
+{
+	typex : int;	# Delete, Insert, Filename 
+	mod : int;		# modify bit 
+	seq : int;		# sequence number 
+	p0 : int;		# location of change (unused in f) 
+	n : int;		# # runes in string or file name 
+};
+
+Undosize : con 8;
+SHM : con 16rffff;
+
+undotostr(t, m, s, p, n : int) : string
+{
+	a := "01234567";
+	a[0] = t;
+	a[1] = m;
+	a[2] = s&SHM;
+	a[3] = (s>>16)&SHM;
+	a[4] = p&SHM;
+	a[5] = (p>>16)&SHM;
+	a[6] = n&SHM;
+	a[7] = (n>>16)&SHM;
+	return a;
+}
+
+strtoundo(s: string): Undo
+{
+	u: Undo;
+
+	u.typex = s[0];
+	u.mod = s[1];
+	u.seq = s[2]|(s[3]<<16);
+	u.p0 = s[4]|(s[5]<<16);
+	u.n = s[6]|(s[7]<<16);
+	return u;
+}
+
+nullfile : File;
+
+File.addtext(f : self ref File, t : ref Text) : ref File
+{
+	if(f == nil) {
+		f = ref nullfile;
+		f.buf = newbuffer();
+		f.delta = newbuffer();
+		f.epsilon = newbuffer();
+		f.ntext = 0;
+		f.unread = TRUE;
+	}
+	oft := f.text;
+	f.text = array[f.ntext+1] of ref Text;
+	f.text[0:] = oft[0:f.ntext];
+	oft = nil;
+	f.text[f.ntext++] = t;
+	f.curtext = t;
+	return f;
+}
+
+File.deltext(f : self ref File, t : ref Text)
+{
+	i : int;
+
+	for(i=0; i<f.ntext; i++)
+		if(f.text[i] == t)
+			break;
+	if (i == f.ntext)
+		error("can't find text in File.deltext");
+
+	f.ntext--;
+	if(f.ntext == 0){
+		f.close();
+		return;
+	}
+	f.text[i:] = f.text[i+1:f.ntext+1];
+	if(f.curtext == t)
+		f.curtext = f.text[0];
+}
+
+File.insert(f : self ref File, p0 : int, s : string, ns : int)
+{
+	if (p0 > f.buf.nc)
+		error("bad assert in File.insert");
+	if(f.seq > 0)
+		f.uninsert(f.delta, p0, ns);
+	f.buf.insert(p0, s, ns);
+	if(ns)
+		f.mod = TRUE;
+}
+
+File.uninsert(f : self ref File, delta : ref Buffer, p0 : int, ns : int)
+{
+	# undo an insertion by deleting 
+	a := undotostr(Delete, f.mod, f.seq, p0, ns);
+	delta.insert(delta.nc, a, Undosize);
+}
+
+File.delete(f : self ref File, p0 : int, p1 : int)
+{
+	if (p0>p1 || p0>f.buf.nc || p1>f.buf.nc)
+		error("bad assert in File.delete");
+	if(f.seq > 0)
+		f.undelete(f.delta, p0, p1);
+	f.buf.delete(p0, p1);
+	if(p1 > p0)
+		f.mod = TRUE;
+}
+
+File.undelete(f : self ref File, delta : ref Buffer, p0 : int, p1 : int)
+{
+	buf : ref Astring;
+	i, n : int;
+
+	# undo a deletion by inserting 
+	a := undotostr(Insert, f.mod, f.seq, p0, p1-p0);
+	m := p1-p0;
+	if(m > BUFSIZE)
+		m = BUFSIZE;
+	buf = utils->stralloc(m);
+	for(i=p0; i<p1; i+=n){
+		n = p1 - i;
+		if(n > BUFSIZE)
+			n = BUFSIZE;
+		f.buf.read(i, buf, 0, n);
+		delta.insert(delta.nc, buf.s, n);
+	}
+	utils->strfree(buf);
+	buf = nil;
+	delta.insert(delta.nc, a, Undosize);
+}
+
+File.setname(f : self ref File, name : string, n : int)
+{
+	if(f.seq > 0)
+		f.unsetname(f.delta);
+	f.name = name[0:n];
+	f.unread = TRUE;
+}
+
+File.unsetname(f : self ref File, delta : ref Buffer)
+{
+	# undo a file name change by restoring old name 
+	a := undotostr(Filename, f.mod, f.seq, 0, len f.name);
+	if(f.name != nil)
+		delta.insert(delta.nc, f.name, len f.name);
+	delta.insert(delta.nc, a, Undosize);
+}
+
+File.loadx(f : self ref File, p0 : int, fd : ref Sys->FD) : int
+{
+	if(f.seq > 0)
+		error("undo in file.load unimplemented");
+	return f.buf.loadx(p0, fd);
+}
+
+File.undo(f : self ref File, isundo : int, q0 : int, q1 : int) : (int, int)
+{
+	buf : ref Astring;
+	i, j, n, up : int;
+	stop : int;
+	delta, epsilon : ref Buffer;
+	u : Undo;
+
+	a := utils->stralloc(Undosize);
+	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 = utils->stralloc(BUFSIZE);
+	while(delta.nc > 0){
+		up = delta.nc-Undosize;
+		delta.read(up, a, 0, Undosize);
+		u = strtoundo(a.s);
+		if(isundo){
+			if(u.seq < stop){
+				f.seq = u.seq;
+				utils->strfree(buf);
+				utils->strfree(a);
+				return (q0, q1);
+			}
+		}else{
+			if(stop == 0)
+				stop = u.seq;
+			if(u.seq > stop){
+				utils->strfree(buf);
+				utils->strfree(a);
+				return (q0, q1);
+			}
+		}
+		case(u.typex){
+		Delete =>
+			f.seq = u.seq;
+			f.undelete(epsilon, u.p0, u.p0+u.n);
+			f.mod = u.mod;
+			f.buf.delete(u.p0, u.p0+u.n);
+			for(j=0; j<f.ntext; j++)
+				f.text[j].delete(u.p0, u.p0+u.n, FALSE);
+			q0 = u.p0;
+			q1 = u.p0;
+		Insert =>
+			f.seq = u.seq;
+			f.uninsert(epsilon, u.p0, u.n);
+			f.mod = u.mod;
+			up -= u.n;
+			# buf = utils->stralloc(BUFSIZE);
+			for(i=0; i<u.n; i+=n){
+				n = u.n - i;
+				if(n > BUFSIZE)
+					n = BUFSIZE;
+				delta.read(up+i, buf, 0, n);
+				f.buf.insert(u.p0+i, buf.s, n);
+				for(j=0; j<f.ntext; j++)
+					f.text[j].insert(u.p0+i, buf.s, n, FALSE, 0);
+			}
+			# utils->strfree(buf);
+			# buf = nil;
+			q0 = u.p0;
+			q1 = u.p0+u.n;
+		Filename =>
+			f.seq = u.seq;
+			f.unsetname(epsilon);
+			f.mod = u.mod;
+			up -= u.n;
+			f.name = nil;
+			if(u.n == 0)
+				f.name = nil;
+			else {
+				fn0 := utils->stralloc(u.n);
+				delta.read(up, fn0, 0, u.n);
+				f.name = fn0.s;
+				utils->strfree(fn0);
+				fn0 = nil;
+			}
+		* =>
+			error(sys->sprint("undo: 0x%ux", u.typex));
+			error("");
+		}
+		delta.delete(up, delta.nc);
+	}
+	utils->strfree(buf);
+	utils->strfree(a);
+	buf = nil;
+	if(isundo)
+		f.seq = 0;
+	return (q0, q1);
+}
+
+File.reset(f : self ref File)
+{
+	f.delta.reset();
+	f.epsilon.reset();
+	f.seq = 0;
+}
+
+File.close(f : self ref File)
+{
+	f.name = nil;
+	f.ntext = 0;
+	f.text = nil;
+	f.buf.close();
+	f.delta.close();
+	f.epsilon.close();
+	editlog->elogclose(f);
+	f = nil;
+}
+
+File.mark(f : self ref File)
+{
+	if(f.epsilon.nc)
+		f.epsilon.delete(0, f.epsilon.nc);
+	f.seq = dat->seq;
+}
+
+File.redoseq(f : self ref File): int
+{
+	u: Undo;
+	delta: ref Buffer;
+
+	delta = f.epsilon;
+	if(delta.nc == 0)
+		return ~0;
+	buf := utils->stralloc(Undosize);
+	delta.read(delta.nc-Undosize, buf, 0, Undosize);
+	u = strtoundo(buf.s);
+	utils->strfree(buf);
+	return u.seq;
+}
--- /dev/null
+++ b/appl/acme/file.m
@@ -1,0 +1,40 @@
+Filem : module {
+	PATH : con "/dis/acme/file.dis";
+
+	init : fn(mods : ref Dat->Mods);
+
+	File : adt {
+		buf : ref Bufferm->Buffer;		# the data
+		delta : ref Bufferm->Buffer;		# transcript of changes
+		epsilon : ref Bufferm->Buffer;	# inversion of delta for redo
+		elogbuf: ref Bufferm->Buffer;	# log of pending editor changes
+		elog: Editlog->Elog;			# current pending change
+		name : string;				# name of associated file
+		qidpath : big;				# of file when read
+		mtime : int;				# of file when read
+		dev : int;					# of file when read
+		unread : int;				# file has not been read from disk
+		editclean: int;				# mark clean after edit command
+		seq : int;					# if seq==0, File acts like Buffer
+		mod : int;
+		curtext : cyclic ref Textm->Text;	# most recently used associated text
+		text : cyclic array of ref Textm->Text;		# list of associated texts
+		ntext : int;
+		dumpid : int;				# used in dumping zeroxed windows
+
+		addtext : fn(f : self ref File, t : ref Textm->Text) : ref File;
+		deltext : fn(f : self ref File, t : ref Textm->Text);
+		insert : fn(f : self ref File, n : int, s : string, m : int);
+		delete : fn(f : self ref File, m : int, n : int);
+		loadx : fn(f : self ref File, p : int, fd : ref Sys->FD) : int;
+		setname : fn(f : self ref File, s : string, n : int);
+		undo : fn(f : self ref File, p : int, q : int, r : int) : (int, int);
+		mark : fn(f : self ref File);
+		reset : fn(f : self ref File);
+		close : fn(f : self ref File);
+		undelete : fn(f : self ref File, b : ref Bufferm->Buffer, m : int, n : int);
+		uninsert : fn(f : self ref File, b : ref Bufferm->Buffer, m : int, n : int);
+		unsetname : fn(f : self ref File, b : ref Bufferm->Buffer);
+		redoseq : fn(f: self ref File): int;
+	};
+};
--- /dev/null
+++ b/appl/acme/frame.b
@@ -1,0 +1,1189 @@
+implement Framem;
+
+include "common.m";
+
+sys : Sys;
+drawm : Draw;
+acme : Acme;
+gui : Gui;
+graph : Graph;
+utils : Utils;
+textm : Textm;
+
+sprint : import sys;
+Point, Rect, Font, Image, Pointer : import drawm;
+draw, berror, charwidth, strwidth : import graph;
+black, white : import gui;
+
+SLOP : con 25;
+
+noglyphs := array[4] of { 16rFFFD, 16r80, '?', ' ' };
+
+frame : ref Frame;
+
+init(mods : ref Dat->Mods)
+{
+	sys = mods.sys;
+	drawm = mods.draw;
+	acme = mods.acme;
+	gui = mods.gui;
+	graph = mods.graph;
+	utils = mods.utils;
+	textm = mods.textm;
+
+	frame = newframe();
+}
+
+nullframe : Frame;
+
+newframe() : ref Frame
+{
+	f := ref nullframe;
+	f.cols = array[NCOL] of ref Draw->Image;
+	return f;
+}
+
+frdump(f : ref Frame)
+{
+	utils->debug(sprint("nchars=%d\n", f.nchars));
+	for (i := 0; i < f.nbox; i++) {
+		utils->debug(sprint("box %d : ", i));
+		fb := f.box[i];
+		if (fb.nrune >= 0)
+			utils->debug(sprint("%d %d %s\n", fb.nrune, len fb.ptr, fb.ptr));
+		else
+			utils->debug(sprint("%d\n", fb.nrune));
+	}
+}
+
+# debugcheck(f : ref Frame, n : int)
+# {
+# 	if (f.nchars != xfrstrlen(f, 0)) {
+#		utils->debug(sprint("%d : bad frame nchars\n", n));
+#		frdump(f);
+#		berror("");
+#	}
+# }
+		
+xfraddbox(f : ref Frame, bn : int, n : int)		# add n boxes after bn, shift the rest up,
+									#  * box[bn+n]==box[bn]
+{
+	i : int;
+
+	if(bn > f.nbox)
+		berror("xfraddbox");
+	# bn = f.nbox has same effect as bn = f.nbox-1
+	if(f.nbox+n > f.nalloc)
+		xfrgrowbox(f, n+SLOP);
+	for (i=f.nbox; --i > bn; ) {
+		t := f.box[i+n];
+		f.box[i+n] = f.box[i];
+		f.box[i] = t;
+	}
+	if (bn < f.nbox)
+		*f.box[bn+n] = *f.box[bn];
+	f.nbox+=n;
+}
+
+xfrclosebox(f : ref Frame, n0 : int, n1 : int)	# inclusive
+{
+	i: int;
+
+	if(n0>=f.nbox || n1>=f.nbox || n1<n0)
+		berror("xfrclosebox");
+	n1++;
+	for(i=n1; i<f.nbox; i++) {
+		t := f.box[i-(n1-n0)];
+		f.box[i-(n1-n0)] = f.box[i];
+		f.box[i] = t;
+	}
+	f.nbox -= n1-n0;
+}
+
+xfrdelbox(f : ref Frame, n0 : int, n1 : int)		# inclusive
+{
+	if(n0>=f.nbox || n1>=f.nbox || n1<n0)
+		berror("xfrdelbox");
+	xfrfreebox(f, n0, n1);
+	xfrclosebox(f, n0, n1);
+}
+
+xfrfreebox(f : ref Frame, n0 : int, n1 : int)		# inclusive
+{
+	i : int;
+
+	if(n1<n0)
+		return;
+	if(n0>=f.nbox || n1>=f.nbox)
+		berror("xfrfreebox");
+	n1++;
+	for(i=n0; i<n1; i++)
+		if(f.box[i].nrune >= 0) {
+			f.box[i].nrune = 0;
+			f.box[i].ptr = nil;
+		}
+}
+
+nilfrbox : Frbox;
+
+xfrgrowbox(f : ref Frame, delta : int)
+{
+	ofb := f.box;
+	f.box = array[f.nalloc+delta] of ref Frbox;
+	if(f.box == nil)
+		berror("xfrgrowbox");
+	f.box[0:] = ofb[0:f.nalloc];
+	for (i := 0; i < delta; i++)
+		f.box[i+f.nalloc] = ref nilfrbox;
+	f.nalloc += delta;
+	ofb = nil;
+}
+
+dupbox(f : ref Frame, bn : int)
+{
+	if(f.box[bn].nrune < 0)
+		berror("dupbox");
+	xfraddbox(f, bn, 1);
+	if(f.box[bn].nrune >= 0) {
+		f.box[bn+1].nrune = f.box[bn].nrune;
+		f.box[bn+1].ptr = f.box[bn].ptr;
+	}
+}
+
+truncatebox(f : ref Frame, b : ref Frbox, n : int)	# drop last n chars; no allocation done
+{
+	if(b.nrune<0 || b.nrune<n)
+		berror("truncatebox");
+	b.nrune -= n;
+	b.ptr = b.ptr[0:b.nrune];
+	b.wid = strwidth(f.font, b.ptr);
+}
+
+chopbox(f : ref Frame, b : ref Frbox, n : int)	# drop first n chars; no allocation done
+{
+	if(b.nrune<0 || b.nrune<n)
+		berror("chopbox");
+	b.nrune -= n;
+	b.ptr = b.ptr[n:];
+	b.wid = strwidth(f.font, b.ptr);
+}
+
+xfrsplitbox(f : ref Frame, bn : int, n : int)
+{
+	dupbox(f, bn);
+	truncatebox(f, f.box[bn], f.box[bn].nrune-n);
+	chopbox(f, f.box[bn+1], n);
+}
+
+xfrmergebox(f : ref Frame, bn : int)		# merge bn and bn+1
+{
+	b0 := f.box[bn];
+	b1 := f.box[bn+1];
+	b0.ptr += b1.ptr;
+	b0.wid += b1.wid;
+	b0.nrune += b1.nrune;
+	xfrdelbox(f, bn+1, bn+1);
+}
+
+xfrfindbox(f : ref Frame, bn : int, p : int, q : int) : int	# find box containing q and put q on a box boundary
+{
+	nrune : int;
+
+	for( ; bn < f.nbox; bn++) {
+		nrune = 1;
+		b := f.box[bn];
+# if (b.nrune >= 0 && len b.ptr != b.nrune) {
+#	frdump(f);
+#	berror(sprint("findbox %d %d %d\n", bn, p, q));
+# }
+		if(b.nrune >= 0)
+			nrune = b.nrune;
+		if(p+nrune > q)
+			break; 
+		p += nrune;
+	}
+	if(p != q)
+		xfrsplitbox(f, bn++, q-p);
+	return bn;
+}
+
+frdelete(f : ref Frame, p0 : int, p1 : int) : int
+{
+	pt0, pt1, ppt0 : Point;
+	n0, n1, n, s : int;
+	r : Rect;
+	nn0 : int;
+	col : ref Image;
+
+	if(p0 >= f.nchars || p0 == p1 || f.b == nil)
+		return 0;
+	if(p1 > f.nchars)
+		p1 = f.nchars;
+	n0 = xfrfindbox(f, 0, 0, p0);
+	if(n0 == f.nbox)
+		berror("off end in frdelete");
+	n1 = xfrfindbox(f, n0, p0, p1);
+	pt0 = xfrptofcharnb(f, p0, n0);
+	pt1 = frptofchar(f, p1);
+	if(f.p0 == f.p1)
+		frtick(f, frptofchar(f, f.p0), 0);
+	nn0 = n0;
+	ppt0 = pt0;
+	xfrfreebox(f, n0, n1-1);
+	f.modified = 1;
+
+	#
+	# Invariants:
+	#  pt0 points to beginning, pt1 points to end
+	#  n0 is box containing beginning of stuff being deleted
+	#  n1, b are box containing beginning of stuff to be kept after deletion
+	# cn1 is char position of n1
+	# f.p0 and f.p1 are not adjusted until after all deletion is done
+	#  region between pt0 and pt1 is clear
+	#
+	cn1 := p1;
+	while(pt1.x!=pt0.x && n1<f.nbox){
+		b := f.box[n1];
+		pt0 = xfrcklinewrap0(f, pt0, b);
+		pt1 = xfrcklinewrap(f, pt1, b);
+		n = xfrcanfit(f, pt0, b);
+		if(n==0)
+			berror("xfrcanfit==0");
+		r.min = pt0;
+		r.max = pt0;
+		r.max.y += f.font.height;
+		if(b.nrune > 0){
+			if(n != b.nrune){
+				xfrsplitbox(f, n1, n);
+				b = f.box[n1];
+			}
+			r.max.x += b.wid;
+			draw(f.b, r, f.b, nil, pt1);
+			cn1 += b.nrune;
+		}
+		else{
+			r.max.x += xfrnewwid0(f, pt0, b);
+			if(r.max.x > f.r.max.x)
+				r.max.x = f.r.max.x;
+			col = f.cols[BACK];
+			if(f.p0<=cn1 && cn1<f.p1)
+				col = f.cols[HIGH];
+			draw(f.b, r, col, nil, pt0);
+			cn1++;
+		}
+		pt1 = xfradvance(f, pt1, b);
+		pt0.x += xfrnewwid(f, pt0, b);
+		*f.box[n0++] = *f.box[n1++];
+	}
+	if(n1==f.nbox && pt0.x!=pt1.x)	# deleting last thing in window; must clean up
+		frselectpaint(f, pt0, pt1, f.cols[BACK]);
+	if(pt1.y != pt0.y){
+		pt2 : Point;
+
+		pt2 = xfrptofcharptb(f, 32767, pt1, n1);
+		if(pt2.y > f.r.max.y)
+			berror("frptofchar in frdelete");
+		if(n1 < f.nbox){
+			q0, q1, q2 : int;
+
+			q0 = pt0.y+f.font.height;
+			q1 = pt1.y+f.font.height;
+			q2 = pt2.y+f.font.height;
+			# rob: before was just q2 = pt1.y+f.font.height;
+			# q2 = pt2.y;
+			if(q2 > f.r.max.y)
+				q2 = f.r.max.y;
+			draw(f.b, (pt0, (pt0.x+(f.r.max.x-pt1.x), q0)),
+				f.b, nil, pt1);
+			draw(f.b, ((f.r.min.x, q0), (f.r.max.x, q0+(q2-q1))),
+				f.b, nil, (f.r.min.x, q1));
+			frselectpaint(f, (pt2.x, pt2.y-(pt1.y-pt0.y)), pt2, f.cols[BACK]);
+		}else
+			frselectpaint(f, pt0, pt2, f.cols[BACK]);
+	}
+	xfrclosebox(f, n0, n1-1);
+	if(nn0>0 && f.box[nn0-1].nrune>=0 && ppt0.x-f.box[nn0-1].wid>=f.r.min.x){
+		--nn0;
+		ppt0.x -= f.box[nn0].wid;
+	}
+	s = n0;
+	if(n0 < f.nbox-1)
+		s++;
+	xfrclean(f, ppt0, nn0, s);
+	if(f.p1 > p1)
+		f.p1 -= p1-p0;
+	else if(f.p1 > p0)
+		f.p1 = p0;
+	if(f.p0 > p1)
+		f.p0 -= p1-p0;
+	else if(f.p0 > p0)
+		f.p0 = p0;
+	f.nchars -= p1-p0;
+	if(f.p0 == f.p1)
+		frtick(f, frptofchar(f, f.p0), 1);
+	pt0 = frptofchar(f, f.nchars);
+	n = f.nlines;
+	f.nlines = (pt0.y-f.r.min.y)/f.font.height+(pt0.x>f.r.min.x);
+	return n - f.nlines;
+}
+
+xfrredraw(f : ref Frame, pt : Point)
+{
+	nb : int;
+
+	for(nb = 0; nb < f.nbox; nb++) {
+		b := f.box[nb];
+		pt = xfrcklinewrap(f, pt, b);
+		if(b.nrune >= 0)
+			graph->stringx(f.b, pt, f.font, b.ptr, f.cols[TEXT]);
+		pt.x += b.wid;
+	}
+}
+
+frdrawsel(f : ref Frame, pt : Point, p0 : int, p1 : int, issel : int)
+{
+	back, text : ref Image;
+
+	if(f.ticked)
+		frtick(f, frptofchar(f, f.p0), 0);
+	if(p0 == p1){
+		frtick(f, pt, issel);
+		return;
+	}
+	if(issel){
+		back = f.cols[HIGH];
+		text = f.cols[HTEXT];
+	}else{
+		back = f.cols[BACK];
+		text = f.cols[TEXT];
+	}
+	frdrawsel0(f, pt, p0, p1, back, text);
+}
+
+frdrawsel0(f : ref Frame, pt : Point, p0 : int, p1 : int, back : ref Image, text : ref Image)
+{
+	b : ref Frbox;
+	nb, nr, w, x, trim : int;
+	qt : Point;
+	p : int;
+	ptr : string;
+
+	p = 0;
+	trim = 0;
+	for(nb=0; nb<f.nbox && p<p1; nb++){
+		b = f.box[nb];
+		nr = b.nrune;
+		if(nr < 0)
+			nr = 1;
+		if(p+nr <= p0){
+			p += nr;
+			continue;
+		}
+		if(p >= p0){
+			qt = pt;
+			pt = xfrcklinewrap(f, pt, b);
+			if(pt.y > qt.y)
+				draw(f.b, (qt, (f.r.max.x, pt.y)), back, nil, qt);
+		}
+		ptr = b.ptr;
+		if(p < p0){		# beginning of region: advance into box
+			ptr = ptr[p0-p:];
+			nr -= (p0-p);
+			p = p0;
+		}
+		trim = 0;
+		if(p+nr > p1){	# end of region: trim box
+			nr -= (p+nr)-p1;
+			trim = 1;
+		}
+		if(b.nrune<0 || nr==b.nrune)
+			w = b.wid;
+		else
+			w = strwidth(f.font, ptr[0:nr]);
+		x = pt.x+w;
+		if(x > f.r.max.x)
+			x = f.r.max.x;
+		draw(f.b, (pt, (x, pt.y+f.font.height)), back, nil, pt);
+		if(b.nrune >= 0)
+			graph->stringx(f.b, pt, f.font, ptr[0:nr], text);
+		pt.x += w;
+		p += nr;
+	}
+	# if this is end of last plain text box on wrapped line, fill to end of line
+	if(p1>p0 &&  nb>0 && nb<f.nbox && f.box[nb-1].nrune>0 && !trim){
+		qt = pt;
+		pt = xfrcklinewrap(f, pt, f.box[nb]);
+		if(pt.y > qt.y)
+			draw(f.b, (qt, (f.r.max.x, pt.y)), back, nil, qt);
+	}
+}
+
+frtick(f : ref Frame, pt : Point, ticked : int)
+{
+	r : Rect;
+
+	if(f.ticked==ticked || f.tick==nil || !pt.in(f.r))
+		return;
+	pt.x--;	# looks best just left of where requested
+	r = (pt, (pt.x+FRTICKW, pt.y+f.font.height));
+	if(ticked){
+		draw(f.tickback, f.tickback.r, f.b, nil, pt);
+		draw(f.b, r, f.tick, nil, (0, 0));
+	}else
+		draw(f.b, r, f.tickback, nil, (0, 0));
+	f.ticked = ticked;
+}
+
+xfrdraw(f : ref Frame, pt : Point) : Point
+{
+	nb, n : int;
+
+	for(nb=0; nb < f.nbox; nb++){
+		b := f.box[nb];
+		pt = xfrcklinewrap0(f, pt, b);
+		if(pt.y == f.r.max.y){
+			f.nchars -= xfrstrlen(f, nb);
+			xfrdelbox(f, nb, f.nbox-1);
+			break;
+		}
+		if(b.nrune > 0){
+			n = xfrcanfit(f, pt, b);
+			if(n == 0)
+				berror("draw: xfrcanfit==0");
+			if(n != b.nrune){
+				xfrsplitbox(f, nb, n);
+				b = f.box[nb];
+			}
+			pt.x += b.wid;
+		}else{
+			if(b.bc == '\n') {
+				pt.x = f.r.min.x;
+				pt.y += f.font.height;
+			}
+			else
+				pt.x += xfrnewwid(f, pt, b);
+		}
+	}
+	return pt;
+}
+
+xfrstrlen(f : ref Frame, nb : int) : int
+{
+	n, nrune : int;
+
+	for(n=0; nb<f.nbox; nb++) {
+		nrune = f.box[nb].nrune;
+		if(nrune < 0)
+			nrune = 1;
+		n += nrune;
+	}
+	return n;
+}
+
+frinit(f : ref Frame, r : Rect, ft : ref Font, b : ref Image, cols : array of ref Draw->Image)
+{
+	f.font = ft;
+	f.scroll = 0;
+	f.maxtab = 8*charwidth(ft, '0');
+	f.nbox = 0;
+	f.nalloc = 0;
+	f.nchars = 0;
+	f.nlines = 0;
+	f.p0 = 0;
+	f.p1 = 0;
+	f.box = nil;
+	f.lastlinefull = 0;
+	if(cols != nil)
+		for(i := 0; i < NCOL; i++)
+			f.cols[i] = cols[i];
+	for (i = 0; i < len noglyphs; i++) {
+		if (charwidth(ft, noglyphs[i]) != 0) {
+			f.noglyph = noglyphs[i];
+			break;
+		}
+	}
+	frsetrects(f, r, b);
+	if (f.tick==nil && f.cols[BACK] != nil)
+		frinittick(f);
+}
+
+frinittick(f : ref Frame)
+{
+	ft : ref Font;
+
+	ft = f.font;
+	f.tick = nil;
+	f.tick = graph->balloc(((0, 0), (FRTICKW, ft.height)), (gui->mainwin).chans, Draw->White);
+	if(f.tick == nil)
+		return;
+	f.tickback = graph->balloc(f.tick.r, (gui->mainwin).chans, Draw->White);
+	if(f.tickback == nil){
+		f.tick = nil;
+		return;
+	}
+	# background color
+	draw(f.tick, f.tick.r, f.cols[BACK], nil, (0, 0));
+	# vertical line
+	draw(f.tick, ((FRTICKW/2, 0), (FRTICKW/2+1, ft.height)), black, nil, (0, 0));
+	# box on each end
+	# draw(f->tick, Rect(0, 0, FRTICKW, FRTICKW), f->cols[TEXT], nil, ZP);
+	# draw(f->tick, Rect(0, ft->height-FRTICKW, FRTICKW, ft->height), f->cols[TEXT], nil, ZP);
+}
+
+frsetrects(f : ref Frame, r : Rect, b : ref Image)
+{
+	f.b = b;
+	f.entire = r;
+	f.r = r;
+	f.r.max.y -= (r.max.y-r.min.y)%f.font.height;
+	f.maxlines = (r.max.y-r.min.y)/f.font.height;
+}
+
+frclear(f : ref Frame, freeall : int)
+{
+	if(f.nbox)
+		xfrdelbox(f, 0, f.nbox-1);
+	for (i := 0; i < f.nalloc; i++)
+		f.box[i] = nil;
+	if(freeall)
+		f.tick = f.tickback = nil;
+	f.box = nil;
+	f.ticked = 0;
+}
+
+DELTA : con 25;
+TMPSIZE : con 256;
+
+Plist : adt {
+	pt0 : Point;
+	pt1 : Point;
+};
+
+nalloc : int = 0;
+pts : array of Plist;
+
+bxscan(f : ref Frame, rp : string, l : int, ppt : Point) : (Point, Point)
+{
+	w, c, nb, delta, nl, nr : int;
+	sp : int = 0;
+
+	frame.r = f.r;
+	frame.b = f.b;
+	frame.font = f.font;
+	frame.maxtab = f.maxtab;
+	frame.nbox = 0;
+	frame.nchars = 0;
+	for(i := 0; i < NCOL; i++)
+		frame.cols[i] = f.cols[i];
+	frame.noglyph = f.noglyph;
+	delta = DELTA;
+	nl = 0;
+	for(nb=0; sp<l && nl <= f.maxlines; nb++){
+		if(nb == frame.nalloc){
+			xfrgrowbox(frame, delta);
+			if(delta < 10000)
+				delta *= 2;
+		}
+		b := frame.box[nb];
+		c = rp[sp];
+		if(c=='\t' || c=='\n'){
+			b.bc = c;
+			b.wid = 5000;
+			if(c == '\n')
+				b.minwid = 0;
+			else
+				b.minwid = charwidth(frame.font, ' ');
+			b.nrune = -1;
+			if(c=='\n')
+				nl++;
+			frame.nchars++;
+			sp++;
+		}else{
+			nr = 0;
+			w = 0;
+			ssp := sp;
+			nul := 0;
+			while(sp < l){
+				c = rp[sp];
+				if(c=='\t' || c=='\n')
+					break;
+				if(nr+1 >= TMPSIZE)
+					break;
+				if ((cw := charwidth(frame.font, c)) == 0) {	# used to be only for c == 0
+					c = frame.noglyph;
+					cw = charwidth(frame.font, c);
+					nul = 1;
+				}
+				w += cw;
+				sp++;
+				nr++;
+			}
+			b = frame.box[nb];
+			b.ptr = rp[ssp:sp];
+			b.wid = w;
+			b.nrune = nr;
+			frame.nchars += nr;
+			if (nul) {
+				for (i = 0; i < nr; i++)
+					if (charwidth(frame.font, b.ptr[i]) == 0)
+						b.ptr[i] = frame.noglyph;
+			}
+		}
+		frame.nbox++;
+	}
+	ppt = xfrcklinewrap0(f, ppt, frame.box[0]);
+	return (xfrdraw(frame, ppt), ppt);
+}
+
+chopframe(f : ref Frame, pt : Point, p : int, bn : int)
+{
+	nb, nrune : int;
+
+	for(nb = bn; ; nb++){
+		if(nb >= f.nbox)
+			berror("endofframe");
+		b := f.box[nb];
+		pt = xfrcklinewrap(f, pt, b);
+		if(pt.y >= f.r.max.y)
+			break;
+		nrune = b.nrune;
+		if(nrune < 0)
+			nrune = 1;
+		p += nrune;
+		pt = xfradvance(f, pt, b);
+	}
+	f.nchars = p;
+	f.nlines = f.maxlines;
+	if (nb < f.nbox)				# BUG
+		xfrdelbox(f, nb, f.nbox-1);
+}
+
+frinsert(f : ref Frame, rp : string, l : int, p0 : int)
+{
+	pt0, pt1, ppt0, ppt1, pt : Point;
+	s, n, n0, nn0, y : int;
+	r : Rect;
+	npts : int;
+	col : ref Image;
+
+	if(p0 > f.nchars || l == 0 || f.b == nil)
+		return;
+	n0 = xfrfindbox(f, 0, 0, p0);
+	cn0 := p0;
+	nn0 = n0;
+	pt0 = xfrptofcharnb(f, p0, n0);
+	ppt0 = pt0;
+	(pt1, ppt0) = bxscan(f, rp, l, ppt0);
+	ppt1 = pt1;
+	if(n0 < f.nbox){
+		b := f.box[n0];
+		pt0 = xfrcklinewrap(f, pt0, b);	# for frdrawsel()
+		ppt1 = xfrcklinewrap0(f, ppt1, b);
+	}
+	f.modified = 1;
+	#
+	# ppt0 and ppt1 are start and end of insertion as they will appear when
+	# insertion is complete. pt0 is current location of insertion position
+	# (p0); pt1 is terminal point (without line wrap) of insertion.
+	#
+	if(f.p0 == f.p1)
+		frtick(f, frptofchar(f, f.p0), 0);
+	
+	#
+	# Find point where old and new x's line up
+	# Invariants:
+	#	pt0 is where the next box (b, n0) is now
+	#	pt1 is where it will be after then insertion
+	# If pt1 goes off the rectangle, we can toss everything from there on
+	#
+
+	for(npts=0; pt1.x!= pt0.x && pt1.y!=f.r.max.y && n0<f.nbox; npts++){
+		b := f.box[n0];
+		pt0 = xfrcklinewrap(f, pt0, b);
+		pt1 = xfrcklinewrap0(f, pt1, b);
+		if(b.nrune > 0){
+			n = xfrcanfit(f, pt1, b);
+			if(n == 0)
+				berror("xfrcanfit==0");
+			if(n != b.nrune){
+				xfrsplitbox(f, n0, n);
+				b = f.box[n0];
+			}
+		}
+		if(npts == nalloc){
+			opts := pts;
+			pts = array[npts+DELTA] of Plist;
+			pts[0:] = opts[0:npts];
+			for (k := 0; k < DELTA; k++)
+				pts[k+npts].pt0 = pts[k+npts].pt1 = (0, 0);
+			opts = nil;
+			nalloc += DELTA;
+			b = f.box[n0];
+		}
+		pts[npts].pt0 = pt0;
+		pts[npts].pt1 = pt1;
+		# has a text box overflowed off the frame?
+		if(pt1.y == f.r.max.y)
+			break;
+		pt0 = xfradvance(f, pt0, b);
+		pt1.x += xfrnewwid(f, pt1, b);
+		n0++;
+		nrune := b.nrune;
+		if(nrune < 0)
+			nrune = 1;
+		cn0 += nrune;
+	}
+	if(pt1.y > f.r.max.y)
+		berror("frinsert pt1 too far");
+	if(pt1.y==f.r.max.y && n0<f.nbox){
+		f.nchars -= xfrstrlen(f, n0);
+		xfrdelbox(f, n0, f.nbox-1);
+	}
+	if(n0 == f.nbox)
+		f.nlines = (pt1.y-f.r.min.y)/f.font.height+(pt1.x>f.r.min.x);
+	else if(pt1.y!=pt0.y){
+		q0, q1 : int;
+
+		y = f.r.max.y;
+		q0 = pt0.y+f.font.height;
+		q1 = pt1.y+f.font.height;
+		f.nlines += (q1-q0)/f.font.height;
+		if(f.nlines > f.maxlines)
+			chopframe(f, ppt1, p0, nn0);
+		if(pt1.y < y){
+			r = f.r;
+			r.min.y = q1;
+			r.max.y = y;
+			if(q1 < y)
+				draw(f.b, r, f.b, nil, (f.r.min.x, q0));
+			r.min = pt1;
+			r.max.x = pt1.x+(f.r.max.x-pt0.x);
+			r.max.y = q1;
+			draw(f.b, r, f.b, nil, pt0);
+		}
+	}
+	#
+	# Move the old stuff down to make room.  The loop will move the stuff
+	# between the insertion and the point where the x's lined up.
+	# The draws above moved everything down after the point they lined up.
+	#
+	y = 0;
+	if(pt1.y == f.r.max.y)
+		y = pt1.y;
+	for(j := n0-1; --npts >= 0; --j){
+		pt = pts[npts].pt1;
+		b := f.box[j];
+		if(b.nrune > 0){
+			r.min = pt;
+			r.max = r.min;
+			r.max.x += b.wid;
+			r.max.y += f.font.height;
+			draw(f.b, r, f.b, nil, pts[npts].pt0);
+			if(pt.y < y){	# clear bit hanging off right
+				r.min = pt;
+				r.max = pt;
+				r.min.x += b.wid;
+				r.max.x = f.r.max.x;
+				r.max.y += f.font.height;
+				if(f.p0<=cn0 && cn0<f.p1)	# b+1 is inside selection
+					col = f.cols[HIGH];
+				else
+					col = f.cols[BACK];
+				draw(f.b, r, col, nil, r.min);
+			}
+			y = pt.y;
+			cn0 -= b.nrune;
+		}else{
+			r.min = pt;
+			r.max = pt;
+			r.max.x += b.wid;
+			r.max.y += f.font.height;
+			if(r.max.x >= f.r.max.x)
+				r.max.x = f.r.max.x;
+			cn0--;
+			if(f.p0<=cn0 && cn0<f.p1)	# b is inside selection
+				col = f.cols[HIGH];
+			else
+				col = f.cols[BACK];
+			draw(f.b, r, col, nil, r.min);
+			y = 0;
+			if(pt.x == f.r.min.x)
+				y = pt.y;
+		}
+	}
+	# insertion can extend the selection, so the condition here is different 
+	if(f.p0<p0 && p0<=f.p1)
+		col = f.cols[HIGH];
+	else
+		col = f.cols[BACK];
+	frselectpaint(f, ppt0, ppt1, col);
+	xfrredraw(frame, ppt0);
+	xfraddbox(f, nn0, frame.nbox);
+	for(n=0; n<frame.nbox; n++)
+		*f.box[nn0+n] = *frame.box[n];
+	if(nn0>0 && f.box[nn0-1].nrune>=0 && ppt0.x-f.box[nn0-1].wid>=f.r.min.x){
+		--nn0;
+		ppt0.x -= f.box[nn0].wid;
+	}
+	n0 += frame.nbox;
+	s = n0;
+	if(n0 < f.nbox-1)
+		s++;
+	xfrclean(f, ppt0, nn0, s);
+	f.nchars += frame.nchars;
+	if(f.p0 >= p0)
+		f.p0 += frame.nchars;
+	if(f.p0 > f.nchars)
+		f.p0 = f.nchars;
+	if(f.p1 >= p0)
+		f.p1 += frame.nchars;
+	if(f.p1 > f.nchars)
+		f.p1 = f.nchars;
+	if(f.p0 == f.p1)
+		frtick(f, frptofchar(f, f.p0), 1);
+}
+
+xfrptofcharptb(f : ref Frame, p : int, pt : Point, bn : int) : Point
+{
+	s : int;
+	l : int;
+	r : int;
+
+	for( ; bn < f.nbox; bn++){
+		b := f.box[bn];
+		pt = xfrcklinewrap(f, pt, b);
+		l = b.nrune;
+		if(l < 0)
+			l = 1;
+		if(p < l){
+			if(b.nrune > 0)
+				for(s = 0; p > 0; s++){
+					r = b.ptr[s];
+					pt.x += charwidth(f.font, r);
+					if(r==0 || pt.x>f.r.max.x)
+						berror("frptofchar");
+					p--;
+				}
+			break;
+		}
+		p -= l;
+		pt = xfradvance(f, pt, b);
+	}
+	return pt;
+}
+
+frptofchar(f : ref Frame, p : int) : Point
+{
+	return xfrptofcharptb(f, p, f.r.min, 0);
+}
+
+xfrptofcharnb(f : ref Frame, p : int, nb : int) : Point	# doesn't do final xfradvance to next line
+{
+	pt : Point;
+	nbox : int;
+
+	nbox = f.nbox;
+	f.nbox = nb;
+	pt = xfrptofcharptb(f, p, f.r.min, 0);
+	f.nbox = nbox;
+	return pt;
+}
+
+xfrgrid(f : ref Frame, p: Point) : Point
+{
+	p.y -= f.r.min.y;
+	p.y -= p.y%f.font.height;
+	p.y += f.r.min.y;
+	if(p.x > f.r.max.x)
+		p.x = f.r.max.x;
+	return p;
+}
+
+frcharofpt(f : ref Frame, pt : Point) : int
+{
+	qt : Point;
+	bn, nrune : int;
+	s : int;
+	p : int;
+	r : int;
+
+	pt = xfrgrid(f, pt);
+	qt = f.r.min;
+
+	bn=0;
+	for(p=0; bn<f.nbox && qt.y<pt.y; bn++){
+		b := f.box[bn];
+		qt = xfrcklinewrap(f, qt, b);
+		if(qt.y >= pt.y)
+			break;
+		qt = xfradvance(f, qt, b);
+		nrune = b.nrune;
+		if(nrune < 0)
+			nrune = 1;
+		p += nrune;
+	}
+
+	for(; bn<f.nbox && qt.x<=pt.x; bn++){
+		b := f.box[bn];
+		qt = xfrcklinewrap(f, qt, b);
+		if(qt.y > pt.y)
+			break;
+		if(qt.x+b.wid > pt.x){
+			if(b.nrune < 0)
+				qt = xfradvance(f, qt, b);
+			else{
+				s = 0;
+				for(;;){
+					r = b.ptr[s++];
+					qt.x += charwidth(f.font, r);
+					if(qt.x > pt.x)
+						break;
+					p++;
+				}
+			}
+		}else{
+			nrune = b.nrune;
+			if(nrune < 0)
+				nrune = 1;
+			p += nrune;
+			qt = xfradvance(f, qt, b);
+		}
+	}
+	return p;
+}
+
+region(a, b : int) : int
+{
+	if(a < b)
+		return -1;
+	if(a == b)
+		return 0;
+	return 1;
+}
+
+frselect(f : ref Frame, m : ref Pointer)	# when called, button 1 is down
+{
+	p0, p1, q : int;
+	mp, pt0, pt1, qt : Point;
+	b, scrled, reg : int;
+
+	mp = m.xy;
+	b = m.buttons;
+
+	f.modified = 0;
+	frdrawsel(f, frptofchar(f, f.p0), f.p0, f.p1, 0);
+	p0 = p1 = frcharofpt(f, mp);
+	f.p0 = p0;
+	f.p1 = p1;
+	pt0 = frptofchar(f, p0);
+	pt1 = frptofchar(f, p1);
+	frdrawsel(f, pt0, p0, p1, 1);
+	do{
+		scrled = 0;
+		if(f.scroll){
+			if(m.xy.y < f.r.min.y){
+				textm->framescroll(f, -(f.r.min.y-m.xy.y)/f.font.height-1);
+				p0 = f.p1;
+				p1 = f.p0;
+				scrled = 1;
+			}else if(m.xy.y > f.r.max.y){
+				textm->framescroll(f, (m.xy.y-f.r.max.y)/f.font.height+1);
+				p0 = f.p0;
+				p1 = f.p1;
+				scrled = 1;
+			}
+			if(scrled){
+				pt0 = frptofchar(f, p0);
+				pt1 = frptofchar(f, p1);
+				reg = region(p1, p0);
+			}
+		}
+		q = frcharofpt(f, m.xy);
+		if(p1 != q){
+			if(reg != region(q, p0)){	# crossed starting point; reset
+				if(reg > 0)
+					frdrawsel(f, pt0, p0, p1, 0);
+				else if(reg < 0)
+					frdrawsel(f, pt1, p1, p0, 0);
+				p1 = p0;
+				pt1 = pt0;
+				reg = region(q, p0);
+				if(reg == 0)
+					frdrawsel(f, pt0, p0, p1, 1);
+			}
+			qt = frptofchar(f, q);
+			if(reg > 0){
+				if(q > p1)
+					frdrawsel(f, pt1, p1, q, 1);
+				else if(q < p1)
+					frdrawsel(f, qt, q, p1, 0);
+			}else if(reg < 0){
+				if(q > p1)
+					frdrawsel(f, pt1, p1, q, 0);
+				else
+					frdrawsel(f, qt, q, p1, 1);
+			}
+			p1 = q;
+			pt1 = qt;
+		}
+		f.modified = 0;
+		if(p0 < p1) {
+			f.p0 = p0;
+			f.p1 = p1;
+		}
+		else {
+			f.p0 = p1;
+			f.p1 = p0;
+		}
+		if(scrled)
+			textm->framescroll(f, 0);
+		graph->bflush();
+		if(!scrled)
+			acme->frgetmouse();
+	}while(m.buttons == b);
+}
+
+frselectpaint(f : ref Frame, p0 : Point, p1 : Point, col : ref Image)
+{
+	n : int;
+	q0, q1 : Point;
+
+	q0 = p0;
+	q1 = p1;
+	q0.y += f.font.height;
+	q1.y += f.font.height;
+	n = (p1.y-p0.y)/f.font.height;
+	if(f.b == nil)
+		berror("frselectpaint b==0");
+	if(p0.y == f.r.max.y)
+		return;
+	if(n == 0)
+		draw(f.b, (p0, q1), col, nil, (0, 0));
+	else{
+		if(p0.x >= f.r.max.x)
+			p0.x = f.r.max.x-1;
+		draw(f.b, ((p0.x, p0.y), (f.r.max.x, q0.y)), col, nil, (0, 0));
+		if(n > 1)
+			draw(f.b, ((f.r.min.x, q0.y), (f.r.max.x, p1.y)),
+				col, nil, (0, 0));
+		draw(f.b, ((f.r.min.x, p1.y), (q1.x, q1.y)),
+			col, nil, (0, 0));
+	}
+}
+
+xfrcanfit(f : ref Frame, pt : Point, b : ref Frbox) : int
+{
+	left, nr : int;
+	p : int;
+	r : int;
+
+	left = f.r.max.x-pt.x;
+	if(b.nrune < 0)
+		return b.minwid <= left;
+	if(left >= b.wid)
+		return b.nrune;
+	nr = 0;
+	for(p = 0; p < len b.ptr; p++){
+		r = b.ptr[p];
+		left -= charwidth(f.font, r);
+		if(left < 0)
+			return nr;
+		nr++;
+	}
+	berror("xfrcanfit can't");
+	return 0;
+}
+
+xfrcklinewrap(f : ref Frame, p : Point, b : ref Frbox) : Point
+{
+	wid : int;
+
+	if(b.nrune < 0)
+		wid = b.minwid;
+	else
+		wid = b.wid;
+
+	if(wid > f.r.max.x-p.x){
+		p.x = f.r.min.x;
+		p.y += f.font.height;
+	}
+	return p;
+}
+
+xfrcklinewrap0(f : ref Frame, p : Point, b : ref Frbox) : Point
+{
+	if(xfrcanfit(f, p, b) == 0){
+		p.x = f.r.min.x;
+		p.y += f.font.height;
+	}
+	return p;
+}
+
+xfrcklinewrap1(f : ref Frame, p : Point, wid : int) : Point
+{
+	if(wid > f.r.max.x-p.x){
+		p.x = f.r.min.x;
+		p.y += f.font.height;
+	}
+	return p;
+}
+
+xfradvance(f : ref Frame, p : Point, b : ref Frbox) : Point
+{
+	if(b.nrune<0 && b.bc=='\n'){
+		p.x = f.r.min.x;
+		p.y += f.font.height;
+	}else
+		p.x += b.wid;
+	return p;
+}
+
+xfrnewwid(f : ref Frame, pt : Point, b : ref Frbox) : int
+{
+	b.wid = xfrnewwid0(f, pt, b);
+	return b.wid;
+}
+
+xfrnewwid0(f : ref Frame, pt : Point, b : ref Frbox) : int
+{
+	c, x : int;
+
+	c = f.r.max.x;
+	x = pt.x;
+	if(b.nrune >= 0 || b.bc != '\t')
+		return b.wid;
+	if(x+b.minwid > c)
+		x = pt.x = f.r.min.x;
+	x += f.maxtab;
+	x -= (x-f.r.min.x)%f.maxtab;
+	if(x-pt.x<b.minwid || x>c)
+		x = pt.x+b.minwid;
+	return x-pt.x;
+}
+
+xfrclean(f : ref Frame, pt : Point, n0 : int, n1 : int)	# look for mergeable boxes
+{
+	nb, c : int;
+
+	c = f.r.max.x;
+	for(nb=n0; nb<n1-1; nb++){
+		b0 := f.box[nb];
+		b1 := f.box[nb+1];
+		pt = xfrcklinewrap(f, pt, b0);
+		while(b0.nrune>=0 && nb<n1-1 && b1.nrune>=0 && pt.x+b0.wid+b1.wid<c){
+			xfrmergebox(f, nb);
+			n1--;
+			b0 = f.box[nb];
+			b1 = f.box[nb+1];
+		}
+		pt = xfradvance(f, pt, f.box[nb]);
+	}
+	for(; nb<f.nbox; nb++){
+		b := f.box[nb];
+		pt = xfrcklinewrap(f, pt, b);
+		pt = xfradvance(f, pt, f.box[nb]);
+	}
+	f.lastlinefull = 0;
+	if(pt.y >= f.r.max.y)
+		f.lastlinefull = 1;
+}
--- /dev/null
+++ b/appl/acme/frame.m
@@ -1,0 +1,54 @@
+Framem : module {
+	PATH : con "/dis/acme/frame.dis";
+
+	BACK, HIGH, BORD, TEXT, HTEXT, NCOL : con iota;
+
+	FRTICKW : con 3;
+
+	init : fn(mods : ref Dat->Mods);
+
+	newframe : fn() : ref Frame;
+
+	Frbox : adt {
+		wid : int;					# in pixels
+		nrune : int;				# <0 ==> negate and treat as break char
+		ptr : string;
+		bc : int;		# break char
+		minwid : int;
+	};
+
+	Frame : adt {
+		font : ref Draw->Font;		# of chars in the frame
+		b : ref Draw->Image;		# on which frame appears
+		cols : array of ref Draw->Image;	# colours
+		r : Draw->Rect;				# in which text appears
+		entire : Draw->Rect;			# of full frame
+		box : array of ref Frbox;
+		scroll : int;				# call framescroll function
+		p0 : int;
+		p1 : int;					# selection
+		nbox, nalloc : int;
+		maxtab : int;				# max size of tab, in pixels
+		nchars : int;				# runes in frame
+		nlines : int;				# lines with text
+		maxlines : int;				# total # lines in frame
+		lastlinefull : int;				# last line fills frame
+		modified : int;				# changed since frselect()
+		noglyph : int;				# char to use when a char has 0 width glyph
+		tick : ref Draw->Image;		# typing tick
+		tickback : ref Draw->Image;	# saved image under tick
+		ticked : int;				# is tick on screen ?
+	};
+
+	frcharofpt : fn(f : ref Frame, p : Draw->Point) : int;
+	frptofchar : fn(f : ref Frame, c : int) : Draw->Point;
+	frdelete : fn(f : ref Frame, c1 : int, c2 : int) : int;
+	frinsert : fn(f : ref Frame, s : string, l : int, i : int);
+	frselect : fn(f : ref Frame, m : ref Draw->Pointer);
+	frinit : fn(f : ref Frame, r : Draw->Rect, f : ref Draw->Font, b : ref Draw->Image, cols : array of ref Draw->Image);
+	frsetrects : fn(f : ref Frame, r : Draw->Rect, b : ref Draw->Image);
+	frclear : fn(f : ref Frame, x : int);
+	frdrawsel : fn(f : ref Frame, p : Draw->Point, p0 : int, p1 : int, n : int);
+	frdrawsel0 : fn(f : ref Frame, p : Draw->Point, p0 : int, p1 : int, i1 : ref Draw->Image, i2 : ref Draw->Image);
+	frtick : fn(f : ref Frame, p : Draw->Point, n : int);
+};
--- /dev/null
+++ b/appl/acme/fsys.b
@@ -1,0 +1,866 @@
+implement Fsys;
+
+include "common.m";
+
+sys : Sys;
+styx : Styx;
+styxaux : Styxaux;
+acme : Acme;
+dat : Dat;
+utils : Utils;
+look : Look;
+windowm : Windowm;
+xfidm : Xfidm;
+
+QTDIR, QTFILE, QTAPPEND : import Sys;
+DMDIR, DMAPPEND, Qid, ORCLOSE, OTRUNC, OREAD, OWRITE, ORDWR, Dir : import Sys;
+sprint : import sys;
+MAXWELEM, Rerror : import Styx;
+Qdir,Qacme,Qcons,Qconsctl,Qdraw,Qeditout,Qindex,Qlabel,Qnew,QWaddr,QWbody,QWconsctl,QWctl,QWdata,QWeditout,QWevent,QWrdsel,QWwrsel,QWtag,QMAX : import Dat;
+TRUE, FALSE : import Dat;
+cxfidalloc, cerr : import dat;
+Mntdir, Fid, Dirtab, Lock, Ref, Smsg0 : import dat;
+Tmsg, Rmsg : import styx;
+msize, version, fid, uname, aname, newfid, name, mode, offset, count, setmode : import styxaux;
+Xfid : import xfidm;
+row : import dat;
+Column : import Columnm;
+Window : import windowm;
+lookid : import look;
+warning, error : import utils;
+
+init(mods : ref Dat->Mods)
+{
+	messagesize = Styx->MAXRPC;
+
+	sys = mods.sys;
+	styx = mods.styx;
+	styxaux = mods.styxaux;
+	acme = mods.acme;
+	dat = mods.dat;
+	utils = mods.utils;
+	look = mods.look;
+	windowm = mods.windowm;
+	xfidm = mods.xfidm;
+}
+
+sfd, cfd : ref Sys->FD;
+
+Nhash : con 16;
+DEBUG : con 0;
+
+fids := array[Nhash] of ref Fid;
+
+Eperm := "permission denied";
+Eexist := "file does not exist";
+Enotdir := "not a directory";
+
+dirtab := array[10] of {
+	Dirtab ( ".",		QTDIR,		Qdir,			8r500|DMDIR ),
+	Dirtab ( "acme",	QTDIR,		Qacme,		8r500|DMDIR ),
+	Dirtab ( "cons",		QTFILE,		Qcons,		8r600 ),
+	Dirtab ( "consctl",	QTFILE,		Qconsctl,		8r000 ),
+	Dirtab ( "draw",		QTDIR,		Qdraw,		8r000|DMDIR ),
+	Dirtab ( "editout",	QTFILE,		Qeditout,		8r200 ),
+	Dirtab ( "index",	QTFILE,		Qindex,		8r400 ),
+	Dirtab ( "label",		QTFILE,		Qlabel,		8r600 ),
+	Dirtab ( "new",		QTDIR,		Qnew,		8r500|DMDIR ),
+	Dirtab ( nil,		0,			0,			0 ),
+};
+
+dirtabw := array[12] of {
+	Dirtab ( ".",		QTDIR,		Qdir,			8r500|DMDIR ),
+	Dirtab ( "addr",		QTFILE,		QWaddr,		8r600 ),
+	Dirtab ( "body",		QTAPPEND,	QWbody,		8r600|DMAPPEND ),
+	Dirtab ( "ctl",		QTFILE,		QWctl,		8r600 ),
+	Dirtab ( "consctl",	QTFILE,		QWconsctl,	8r200 ),
+	Dirtab ( "data",		QTFILE,		QWdata,		8r600 ),
+	Dirtab ( "editout",	QTFILE,		QWeditout,	8r200 ),
+	Dirtab ( "event",	QTFILE,		QWevent,		8r600 ),
+	Dirtab ( "rdsel",		QTFILE,		QWrdsel,		8r400 ),
+	Dirtab ( "wrsel",	QTFILE,		QWwrsel,		8r200 ),
+	Dirtab ( "tag",		QTAPPEND,	QWtag,		8r600|DMAPPEND ),
+	Dirtab ( nil, 		0,			0,			0 ),
+};
+
+Mnt : adt {
+	qlock : ref Lock;
+	id : int;
+	md : ref Mntdir;
+};
+
+mnt : Mnt;
+user : string;
+clockfd : ref Sys->FD;
+closing := 0;
+
+fsysinit() 
+{
+	p :  array of ref Sys->FD;
+
+	p = array[2] of ref Sys->FD;
+	if(sys->pipe(p) < 0)
+		error("can't create pipe");
+	cfd = p[0];
+	sfd = p[1];
+	clockfd = sys->open("/dev/time", Sys->OREAD);
+	user = utils->getuser();
+	if (user == nil)
+		user = "Wile. E. Coyote";
+	mnt.qlock = Lock.init();
+	mnt.id = 0;
+	spawn fsysproc();
+}
+
+fsyscfd() : int
+{
+	return cfd.fd;
+}
+
+QID(w, q : int) : int
+{
+	return (w<<8)|q;
+}
+
+FILE(q : Qid) : int
+{
+	return int q.path & 16rFF;
+}
+
+WIN(q : Qid) : int
+{
+	return (int q.path>>8) & 16rFFFFFF;
+}
+
+# nullsmsg : Smsg;
+nullsmsg0 : Smsg0;
+
+fsysproc()
+{
+	n, ok : int;
+	x : ref Xfid;
+	f : ref Fid;
+	t : Smsg0;
+
+	acme->fsyspid = sys->pctl(0, nil);
+	x = nil;
+	for(;;){
+		if(x == nil){
+			cxfidalloc <-= nil;
+			x = <-cxfidalloc;
+		}
+		n = sys->read(sfd, x.buf, messagesize);
+		if(n <= 0) {
+			if (closing)
+				break;
+			error("i/o error on server channel");
+		}
+		(ok, x.fcall) = Tmsg.unpack(x.buf[0:n]);
+		if(ok < 0)
+			error("convert error in convM2S");
+		if(DEBUG)
+			utils->debug(sprint("%d:%s\n", x.tid, x.fcall.text()));
+		pick fc := x.fcall {
+			Version =>
+				f = nil;
+			Auth =>
+				f = nil;
+			* =>
+				f = allocfid(fid(x.fcall));
+		}
+		x.f = f;
+		pick fc := x.fcall {
+			Readerror =>	x = fsyserror();
+			Flush =>		x = fsysflush(x);
+			Version =>	x = fsysversion(x);
+			Auth =>		x = fsysauth(x);
+			Attach =>		x = fsysattach(x, f);
+			Walk =>		x = fsyswalk(x, f);
+			Open =>		x = fsysopen(x, f);
+			Create =>		x = fsyscreate(x);
+			Read =>		x = fsysread(x, f);
+			Write =>		x = fsyswrite(x);
+			Clunk =>		x = fsysclunk(x, f);
+			Remove =>	x = fsysremove(x);
+			Stat =>		x = fsysstat(x, f);
+			Wstat =>		x = fsyswstat(x);
+			# Clone =>	x = fsysclone(x, f);
+			* =>
+				x = respond(x, t, "bad fcall type");
+		}
+	}
+}
+
+fsysaddid(dir : string, ndir : int, incl : array of string, nincl : int) : ref Mntdir
+{
+	m : ref Mntdir;
+	id : int;
+
+	mnt.qlock.lock();
+	id = ++mnt.id;
+	m = ref Mntdir;
+	m.id = id;
+	m.dir =  dir;
+	m.refs = 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;
+	mnt.qlock.unlock();
+	return m;
+}
+
+fsysdelid(idm : ref Mntdir)
+{
+	m, prev : ref Mntdir;
+	i : int;
+	
+	if(idm == nil)
+		return;
+	mnt.qlock.lock();
+	if(--idm.refs > 0){
+		mnt.qlock.unlock();
+		return;
+	}
+	prev = nil;
+	for(m=mnt.md; m != nil; m=m.next){
+		if(m == idm){
+			if(prev != nil)
+				prev.next = m.next;
+			else
+				mnt.md = m.next;
+			for(i=0; i<m.nincl; i++)
+				m.incl[i] = nil;
+			m.incl = nil;
+			m.dir = nil;
+			m = nil;
+			mnt.qlock.unlock();
+			return;
+		}
+		prev = m;
+	}
+	mnt.qlock.unlock();
+	buf := sys->sprint("fsysdelid: can't find id %d\n", idm.id);
+	cerr <-= buf;
+}
+
+#
+# Called only in exec.l:run(), from a different FD group
+#
+fsysmount(dir : string, ndir : int, incl : array of string, nincl : int) : ref Mntdir
+{
+	m : ref Mntdir;
+
+	# close server side so don't hang if acme is half-exited
+	# sfd = nil;
+	m = fsysaddid(dir, ndir, incl, nincl);
+	buf := sys->sprint("%d", m.id);
+	if(sys->mount(cfd, nil, "/mnt/acme", Sys->MREPL, buf) < 0){
+		fsysdelid(m);
+		return nil;
+	}
+	# cfd = nil;
+	sys->bind("/mnt/acme", "/chan", Sys->MBEFORE);	# was MREPL
+	if(sys->bind("/mnt/acme", "/dev", Sys->MBEFORE) < 0){
+		fsysdelid(m);
+		return nil;
+	}
+	return m;
+}
+
+fsysclose()
+{
+	closing = 1;
+	# sfd = cfd = nil;
+}
+
+respond(x : ref Xfid, t0 : Smsg0, err : string) : ref Xfid
+{
+	t : ref Rmsg;
+
+	# t = nullsmsg;
+	tag := x.fcall.tag;
+	# fid := fid(x.fcall);
+	qid := t0.qid;
+	if(err != nil)
+		t = ref Rmsg.Error(tag, err);
+	else
+	pick fc := x.fcall {
+		Readerror =>	t = ref Rmsg.Error(tag, err);
+		Flush =>		t = ref Rmsg.Flush(tag);
+		Version =>	t = ref Rmsg.Version(tag, t0.msize, t0.version);
+		Auth =>		t = ref Rmsg.Auth(tag, qid);
+		# Clone =>	t = ref Rmsg.Clone(tag, fid);
+		Attach =>		t = ref Rmsg.Attach(tag, qid);
+		Walk =>		t = ref Rmsg.Walk(tag, t0.qids);
+		Open =>		t = ref Rmsg.Open(tag, qid, t0.iounit);
+		Create =>		t = ref Rmsg.Create(tag, qid, 0);
+		Read =>		if(t0.count == len t0.data)
+						t = ref Rmsg.Read(tag, t0.data);
+					else
+						t = ref Rmsg.Read(tag, t0.data[0: t0.count]);
+		Write =>		t = ref Rmsg.Write(tag, t0.count);
+		Clunk =>		t = ref Rmsg.Clunk(tag);
+		Remove =>	t = ref Rmsg.Remove(tag);
+		Stat =>		t = ref Rmsg.Stat(tag, t0.stat);
+		Wstat =>		t = ref Rmsg.Wstat(tag);
+		
+	}
+	# t.qid = t0.qid;
+	# t.count = t0.count;
+	# t.data = t0.data;
+	# t.stat = t0.stat;
+	# t.fid = x.fcall.fid;
+	# t.tag = x.fcall.tag;
+	buf := t.pack();
+	if(buf == nil)
+		error("convert error in convS2M");
+	if(sys->write(sfd, buf, len buf) != len buf)
+		error("write error in respond");
+	buf = nil;
+	if(DEBUG)
+		utils->debug(sprint("%d:r: %s\n", x.tid, t.text()));
+	return x;
+}
+
+# fsysnop(x : ref Xfid) : ref Xfid
+# {
+# 	t : Smsg0;
+# 
+# 	return respond(x, t, nil);
+# }
+
+fsyserror() : ref Xfid
+{
+	error("sys error : Terror");
+	return nil;
+}
+
+fsyssession(x : ref Xfid) : ref Xfid
+{
+	t : Smsg0;
+
+	# BUG: should shut everybody down ??
+	t = nullsmsg0;
+	return respond(x, t, nil);
+}
+
+fsysversion(x : ref Xfid) : ref Xfid
+{
+	t : Smsg0;
+
+	pick m := x.fcall {
+		Version =>
+			(t.msize, t.version) = styx->compatible(m, messagesize, nil);
+			messagesize = t.msize;
+			return respond(x, t, nil);
+	}
+	return respond(x, t, "acme: bad version");
+
+	# ms := msize(x.fcall);
+	# if(ms < 256)
+	# 	return respond(x, t, "version: message size too small");
+	# t.msize = messagesize = ms;
+	# v := version(x.fcall);
+	# if(len v < 6 || v[0: 6] != "9P2000")
+	# 	return respond(x, t, "unrecognized 9P version");
+	# t.version = "9P2000";
+	# return respond(x, t, nil);
+}
+
+fsysauth(x : ref Xfid) : ref Xfid
+{
+	t : Smsg0;
+
+	return respond(x, t, "acme: authentication not required");
+}
+
+fsysflush(x : ref Xfid) : ref Xfid
+{
+	x.c <-= Xfidm->Xflush;
+	return nil;
+}
+
+fsysattach(x : ref Xfid, f : ref Fid) : ref Xfid
+{
+	t : Smsg0;
+	id : int;
+	m : ref Mntdir;
+
+	if (uname(x.fcall) != user)
+		return respond(x, t, Eperm);
+	f.busy = TRUE;
+	f.open = FALSE;
+	f.qid = (Qid)(big Qdir, 0, QTDIR);
+	f.dir = dirtab;
+	f.nrpart = 0;
+	f.w = nil;
+	t.qid = f.qid;
+	f.mntdir = nil;
+	id = int aname(x.fcall);
+	mnt.qlock.lock();
+	for(m=mnt.md; m != nil; m=m.next)
+		if(m.id == id){
+			f.mntdir = m;
+			m.refs++;
+			break;
+		}
+	if(m == nil)
+		cerr <-= "unknown id in attach";
+	mnt.qlock.unlock();
+	return respond(x, t, nil);
+}
+
+fsyswalk(x : ref Xfid, f : ref Fid) : ref Xfid
+{
+	t : Smsg0;
+	c, i, j, id : int;
+	path, qtype : int;
+	d, dir : array of Dirtab;
+	w : ref Window;
+	nf : ref Fid;
+
+	if(f.open)
+		return respond(x, t, "walk of open file");
+	if(fid(x.fcall) != newfid(x.fcall)){
+		nf = allocfid(newfid(x.fcall));
+		if(nf.busy)
+			return respond(x, t, "newfid already in use");
+		nf.busy = TRUE;
+		nf.open = FALSE;
+		nf.mntdir = f.mntdir;
+		if(f.mntdir != nil)
+			f.mntdir.refs++;
+		nf.dir = f.dir;
+		nf.qid = f.qid;
+		nf.w = f.w;
+		nf.nrpart = 0;	# not open, so must be zero
+		if(nf.w != nil)
+			nf.w.refx.inc();
+		f = nf;	# walk f
+	}
+
+	qtype = QTFILE;
+	wqids: list of Qid;
+	err := string nil;
+	id = WIN(f.qid);
+	q := f.qid;
+	names := styxaux->names(x.fcall);
+	nwname := len names;
+
+	if(nwname > 0){
+		for(i = 0; i < nwname; i++){
+			if((q.qtype & QTDIR) == 0){
+				err = Enotdir;
+				break;
+			}
+
+			name := names[i];
+			if(name == ".."){
+				path = Qdir;
+				qtype = QTDIR;
+				id = 0;
+				if(w != nil){
+					w.close();
+					w = nil;
+				}
+				if(i == MAXWELEM){
+					err = "name too long";
+					break;
+				}
+				q.qtype = qtype;
+				q.vers = 0;
+				q.path = big QID(id, path);
+				wqids = q :: wqids;
+				continue;
+			}
+
+			# is it a numeric name?
+			regular := 0;
+			for(j=0; j < len name; j++) {
+				c = name[j];
+				if(c<'0' || '9'<c) {
+					regular = 1;
+					break;
+				}
+			}
+
+			if (!regular) {
+				# yes: it's a directory
+				if(w != nil)	# name has form 27/23; get out before losing w
+					break;
+				id = int name;
+				row.qlock.lock();
+				w = lookid(id, FALSE);
+				if(w == nil){
+					row.qlock.unlock();
+					break;
+				}
+				w.refx.inc();
+				path = Qdir;
+				qtype = QTDIR;
+				row.qlock.unlock();
+				dir = dirtabw;
+				if(i == MAXWELEM){
+					err = "name too long";
+					break;
+				}
+				q.qtype = qtype;
+				q.vers = 0;
+				q.path = big QID(id, path);
+				wqids = q :: wqids;
+				continue;
+			}
+			else {
+				# if(FILE(f.qid) == Qacme) 	# empty directory
+				#	break;
+				if(name == "new"){
+					if(w != nil)
+						error("w set in walk to new");
+					cw := chan of ref Window;
+					spawn x.walk(cw);
+					w = <- cw;
+					w.refx.inc();
+					path = QID(w.id, Qdir);
+					qtype = QTDIR;
+					id = w.id;
+					dir = dirtabw;
+					# x.c <-= Xfidm->Xwalk;
+					if(i == MAXWELEM){
+						err = "name too long";
+						break;
+					}
+					q.qtype = qtype;
+					q.vers = 0;
+					q.path = big QID(id, path);
+					wqids = q :: wqids;
+					continue;
+				}
+
+				if(id == 0)
+					d = dirtab;
+				else
+					d = dirtabw;
+				k := 1;	# skip '.'
+				found := 0;
+				for( ; d[k].name != nil; k++){
+					if(name == d[k].name){
+						path = d[k].qid;
+						qtype = d[k].qtype;
+						dir = d[k:];
+						if(i == MAXWELEM){
+							err = "name too long";
+							break;
+						}
+						q.qtype = qtype;
+						q.vers = 0;
+						q.path = big QID(id, path);
+						wqids = q :: wqids;
+						found = 1;
+						break;
+					}
+				}
+				if(found)
+					continue;
+				break;	# file not found
+			}
+		}
+
+		if(i == 0 && err == nil)
+			err = Eexist;
+	}
+
+	nwqid := len wqids;
+	if(nwqid > 0){
+		t.qids = array[nwqid] of Qid;
+		for(i = nwqid-1; i >= 0; i--){
+			t.qids[i] = hd wqids;
+			wqids = tl wqids;
+		}
+	}
+	if(err != nil || nwqid < nwname){
+		if(nf != nil){
+			nf.busy = FALSE;
+			fsysdelid(nf.mntdir);
+		}
+	}
+	else if(nwqid == nwname){
+		if(w != nil){
+			f.w = w;
+			w = nil;
+		}
+		if(dir != nil)
+			f.dir = dir;
+		f.qid = q;
+	}
+
+	if(w != nil)
+		w.close();
+
+	return respond(x, t, err);
+}
+
+fsysopen(x : ref Xfid, f : ref Fid) : ref Xfid
+{
+	t : Smsg0;
+	m : int;
+
+	# can't truncate anything, so just disregard
+	setmode(x.fcall, mode(x.fcall)&~OTRUNC);
+	# can't execute or remove anything
+	if(mode(x.fcall)&ORCLOSE)
+		return respond(x, t, Eperm);
+	case(mode(x.fcall)){
+	OREAD =>
+		m = 8r400;
+	OWRITE =>
+		m = 8r200;
+	ORDWR =>
+		m = 8r600;
+	* =>
+		return respond(x, t, Eperm);
+	}
+	if(((f.dir[0].perm&~(DMDIR|DMAPPEND))&m) != m)
+		return respond(x, t, Eperm);
+	x.c <-= Xfidm->Xopen;
+	return nil;
+}
+
+fsyscreate(x : ref Xfid) : ref Xfid
+{
+	t : Smsg0;
+
+	return respond(x, t, Eperm);
+}
+
+idcmp(a, b : int) : int
+{
+	return a-b;
+}
+
+qsort(a : array of int, n : int)
+{
+	i, j : int;
+	t : int;
+
+	while(n > 1) {
+		i = n>>1;
+		t = a[0]; a[0] = a[i]; a[i] = t;
+		i = 0;
+		j = n;
+		for(;;) {
+			do
+				i++;
+			while(i < n && idcmp(a[i], a[0]) < 0);
+			do
+				j--;
+			while(j > 0 && idcmp(a[j], a[0]) > 0);
+			if(j < i)
+				break;
+			t = a[i]; a[i] = a[j]; a[j] = t;
+		}
+		t = a[0]; a[0] = a[j]; a[j] = t;
+		n = n-j-1;
+		if(j >= n) {
+			qsort(a, j);
+			a = a[j+1:];
+		} else {
+			qsort(a[j+1:], n);
+			n = j;
+		}
+	}
+}
+
+fsysread(x : ref Xfid, f : ref Fid) : ref Xfid
+{
+	t : Smsg0;
+	b : array of byte;
+	i, id, n, o, e, j, k, nids : int;
+	ids : array of int;
+	d : array of Dirtab;
+	dt : Dirtab;
+	c : ref Column;
+	clock : int;
+
+	b = nil;
+	if(f.qid.qtype & QTDIR){
+		# if(int offset(x.fcall) % DIRLEN)
+		#	return respond(x, t, "illegal offset in directory");
+		if(FILE(f.qid) == Qacme){	# empty dir
+			t.data = nil;
+			t.count = 0;
+			respond(x, t, nil);
+			return x;
+		}
+		o = int offset(x.fcall);
+		e = int offset(x.fcall)+count(x.fcall);
+		clock = getclock();
+		b = array[messagesize] of byte;
+		id = WIN(f.qid);
+		n = 0;
+		if(id > 0)
+			d = dirtabw;
+		else
+			d = dirtab;
+		k = 1;	# first entry is '.' 
+		leng := 0;
+		for(i=0; d[k].name!=nil && i<e; i+=leng){
+			bb := styx->packdir(dostat(WIN(x.f.qid), d[k], clock));
+			leng = len bb;
+			for (kk := 0; kk < leng; kk++)
+				b[kk+n] = bb[kk];
+			bb = nil;
+			if(leng <= Styx->BIT16SZ)
+				break;
+			if(i >= o)
+				n += leng;
+			k++;
+		}
+		if(id == 0){
+			row.qlock.lock();
+			nids = 0;
+			ids = nil;
+			for(j=0; j<row.ncol; j++){
+				c = row.col[j];
+				for(k=0; k<c.nw; k++){
+					oids := ids;
+					ids = array[nids+1] of int;
+					ids[0:] = oids[0:nids];
+					oids = nil;
+					ids[nids++] = c.w[k].id;
+				}
+			}
+			row.qlock.unlock();
+			qsort(ids, nids);
+			j = 0;
+			for(; j<nids && i<e; i+=leng){
+				k = ids[j];
+				dt.name = sys->sprint("%d", k);
+				dt.qid = QID(k, 0);
+				dt.qtype = QTDIR;
+				dt.perm = DMDIR|8r700;
+				bb := styx->packdir(dostat(k, dt, clock));
+				leng = len bb;
+				for (kk := 0; kk < leng; kk++)
+					b[kk+n] = bb[kk];
+				bb = nil;
+				if(leng == 0)
+					break;
+				if(i >= o)
+					n += leng;
+				j++;
+			}
+			ids = nil;
+		}
+		t.data = b;
+		t.count = n;
+		respond(x, t, nil);
+		b = nil;
+		return x;
+	}
+	x.c <-= Xfidm->Xread;
+	return nil;
+}
+
+fsyswrite(x : ref Xfid) : ref Xfid
+{
+	x.c <-= Xfidm->Xwrite;
+	return nil;
+}
+
+fsysclunk(x : ref Xfid, f : ref Fid) : ref Xfid
+{
+	t : Smsg0;
+
+	fsysdelid(f.mntdir);
+	if(f.open){
+		f.busy = FALSE;
+		f.open = FALSE;
+		x.c <-= Xfidm->Xclose;
+		return nil;
+	}
+	if(f.w != nil)
+		f.w.close();
+	f.busy = FALSE;
+	f.open = FALSE;
+	return respond(x, t, nil);
+}
+
+fsysremove(x : ref Xfid) : ref Xfid
+{
+	t : Smsg0;
+
+	return respond(x, t, Eperm);
+}
+
+fsysstat(x : ref Xfid, f : ref Fid) : ref Xfid
+{
+	t : Smsg0;
+
+	t.stat = dostat(WIN(x.f.qid), f.dir[0], getclock());
+	return respond(x, t, nil);
+}
+
+fsyswstat(x : ref Xfid) : ref Xfid
+{
+	t : Smsg0;
+
+	return respond(x, t, Eperm);
+}
+
+allocfid(fid : int) : ref Fid
+{	
+	f, ff : ref Fid;
+	fh : int;
+
+	ff = nil;
+	fh = fid&(Nhash-1);
+	for(f=fids[fh]; f != nil; f=f.next)
+		if(f.fid == fid)
+			return f;
+		else if(ff==nil && f.busy==FALSE)
+			ff = f;
+	if(ff != nil){
+		ff.fid = fid;
+		return ff;
+	}
+	f = ref Fid;
+	f.busy = FALSE;
+	f.rpart = array[Sys->UTFmax] of byte;
+	f.nrpart = 0;
+	f.fid = fid;
+	f.next = fids[fh];
+	fids[fh] = f;
+	return f;
+}
+
+cbuf := array[32] of byte;
+
+getclock() : int
+{
+	sys->seek(clockfd, big 0, 0);
+	n := sys->read(clockfd, cbuf, len cbuf);
+	return int string cbuf[0:n];
+}
+
+dostat(id : int, dir : Dirtab, clock : int) : Sys->Dir
+{
+	d : Dir;
+
+	d.qid.path = big QID(id, dir.qid);
+	d.qid.vers = 0;
+	d.qid.qtype = dir.qtype;
+	d.mode = dir.perm;
+	d.length = big 0;	# would be nice to do better
+	d.name = dir.name;
+	d.uid = user;
+	d.gid = user;
+	d.atime = clock;
+	d.mtime = clock;
+	d.dtype = d.dev = 0;
+	return d;
+	# buf := styx->convD2M(d);
+	# d = nil;
+	# return buf;
+}
--- /dev/null
+++ b/appl/acme/fsys.m
@@ -1,0 +1,18 @@
+Fsys : module {
+	PATH : con "/dis/acme/fsys.dis";
+
+	init : fn(mods : ref Dat->Mods);
+
+	messagesize: int;
+
+	QID : fn(w, f : int) : int;
+	FILE : fn(q : Sys->Qid) : int;
+	WIN : fn(q : Sys->Qid) : int;
+
+	fsysinit : fn();
+	fsyscfd : fn() : int;
+	fsysmount: fn(dir : string, ndir : int, incl : array of string, nincl : int) : ref Dat->Mntdir;
+	fsysdelid : fn(idm : ref Dat->Mntdir);
+	fsysclose: fn();
+	respond : fn(x : ref Xfidm->Xfid, t : Dat->Smsg0, err : string) : ref Xfidm->Xfid;
+};
--- /dev/null
+++ b/appl/acme/graph.b
@@ -1,0 +1,82 @@
+implement Graph;
+
+include "common.m";
+
+sys : Sys;
+drawm : Draw;
+dat : Dat;
+gui : Gui;
+utils : Utils;
+
+Image, Point, Rect, Font, Display : import drawm;
+black, white, display : import gui;
+error : import utils;
+
+refp : ref Point;
+pixarr : array of byte;
+
+init(mods : ref Dat->Mods)
+{
+	sys = mods.sys;
+	drawm = mods.draw;
+	dat = mods.dat;
+	gui = mods.gui;
+	utils = mods.utils;
+
+	refp = ref Point;
+	refp.x = refp.y = 0;
+}
+
+charwidth(f : ref Font, c : int) : int
+{
+	s : string = "z";
+
+	s[0] = c;
+	return f.width(s);
+}
+
+strwidth(f : ref Font, s : string) : int
+{
+	return f.width(s);
+}
+
+balloc(r : Rect, c : Draw->Chans, col : int) : ref Image
+{
+	im := display.newimage(r, c, 0, col);
+	if (im == nil)
+		error("failed to get new image");
+	return im;
+}
+
+draw(d : ref Image, r : Rect, s : ref Image, m : ref Image, p : Point)
+{
+	d.draw(r, s, m, p);
+}
+
+stringx(d : ref Image, p : Point, f : ref Font, s : string, c : ref Image)
+{
+	d.text(p, c, (0, 0), f, s);
+}
+
+cursorset(p : Point)
+{
+	gui->cursorset(p);
+}
+
+cursorswitch(c : ref Dat->Cursor)
+{
+	gui->cursorswitch(c);
+}
+
+binit()
+{
+}
+
+bflush()
+{
+}
+
+berror(s : string)
+{
+	error(s);
+}
--- /dev/null
+++ b/appl/acme/graph.m
@@ -1,0 +1,18 @@
+Graph : module {
+	PATH : con "/dis/acme/graph.dis";
+
+	init : fn(mods : ref Dat->Mods);
+
+	balloc : fn(r : Draw->Rect, c : Draw->Chans, col : int) : ref Draw->Image;
+	draw : fn(d : ref Draw->Image, r : Draw->Rect, s : ref Draw->Image, m : ref Draw->Image, p : Draw->Point);
+	stringx : fn(d : ref Draw->Image, p : Draw->Point, f : ref Draw->Font, s : string, c : ref Draw->Image);
+	cursorset: fn(p : Draw->Point);
+	cursorswitch : fn(c : ref Dat->Cursor);
+	charwidth : fn(f : ref Draw->Font, c : int) : int;
+	strwidth : fn(f : ref Draw->Font, p : string) : int;
+	binit : fn();
+	bflush : fn();
+	berror : fn(s : string);
+
+	font : ref Draw->Font;
+};
--- /dev/null
+++ b/appl/acme/gui.b
@@ -1,0 +1,126 @@
+implement Gui;
+
+include "common.m";
+include "tk.m";
+include "wmclient.m";
+	wmclient: Wmclient;
+
+sys : Sys;
+draw : Draw;
+acme : Acme;
+dat : Dat;
+utils : Utils;
+
+Font, Point, Rect, Image, Context, Screen, Display, Pointer : import draw;
+keyboardpid, mousepid : import acme;
+ckeyboard, cmouse : import dat;
+mousefd: ref Sys->FD;
+error : import utils;
+
+win: ref Wmclient->Window;
+
+r2s(r: Rect): string
+{
+	return sys->sprint("%d %d %d %d", r.min.x, r.min.y, r.max.x, r.max.y);
+}
+
+init(mods : ref Dat->Mods)
+{
+	sys = mods.sys;
+	draw = mods.draw;
+	acme = mods.acme;
+	dat = mods.dat;
+	utils = mods.utils;
+	wmclient = load Wmclient Wmclient->PATH;
+	if(wmclient == nil)
+		error(sys->sprint("cannot load %s: %r", Wmclient->PATH));
+	wmclient->init();
+
+	if(acme->acmectxt == nil)
+		acme->acmectxt = wmclient->makedrawcontext();
+	display = (acme->acmectxt).display;
+	win = wmclient->window(acme->acmectxt, "Acme", Wmclient->Appl);
+	wmclient->win.reshape(((0, 0), (win.displayr.size().div(2))));
+	cmouse = chan of ref Draw->Pointer;
+	ckeyboard = win.ctxt.kbd;
+	wmclient->win.onscreen("place");
+	wmclient->win.startinput("kbd"::"ptr"::nil);
+	mainwin = win.image;
+	
+	yellow = display.color(Draw->Yellow);
+	green = display.color(Draw->Green);
+	red = display.color(Draw->Red);
+	blue = display.color(Draw->Blue);
+	black = display.color(Draw->Black);
+	white = display.color(Draw->White);
+}
+
+spawnprocs()
+{
+	spawn mouseproc();
+	spawn eventproc();
+}
+
+zpointer: Draw->Pointer;
+
+eventproc()
+{
+	for(;;) alt{
+	e := <-win.ctl or
+	e = <-win.ctxt.ctl =>
+		p := ref zpointer;
+		if(e == "exit"){
+			p.buttons = Acme->M_QUIT;
+			cmouse <-= p;
+		}else{
+			wmclient->win.wmctl(e);
+			if(win.image != mainwin){
+				mainwin = win.image;
+				p.buttons = Acme->M_RESIZE;
+				cmouse <-= p;
+			}
+		}
+	}
+}
+
+mouseproc()
+{
+	for(;;){
+		p := <-win.ctxt.ptr;
+		if(wmclient->win.pointer(*p) == 0){
+			p.buttons &= ~Acme->M_DOUBLE;
+			cmouse <-= p;
+		}
+	}
+}
+		
+
+# consctlfd : ref Sys->FD;
+
+cursorset(p: Point)
+{
+	wmclient->win.wmctl("ptr " + string p.x + " " + string p.y);
+}
+
+cursorswitch(cur: ref Dat->Cursor)
+{
+	s: string;
+	if(cur == nil)
+		s = "cursor";
+	else{
+		Hex: con "0123456789abcdef";
+		s = sys->sprint("cursor %d %d %d %d ", cur.hot.x, cur.hot.y, cur.size.x, cur.size.y);
+		buf := cur.bits;
+		for(i := 0; i < len buf; i++){
+			c := int buf[i];
+			s[len s] = Hex[c >> 4];
+			s[len s] = Hex[c & 16rf];
+	 	}
+	}
+	wmclient->win.wmctl(s);
+}
+
+killwins()
+{
+	wmclient->win.wmctl("exit");
+}
--- /dev/null
+++ b/appl/acme/gui.m
@@ -1,0 +1,15 @@
+Gui: module {
+	PATH: con "/dis/acme/gui.dis";
+	WMPATH: con "/dis/acme/guiwm.dis";
+
+	display : ref Draw->Display;
+	mainwin : ref Draw->Image;
+	yellow, green, red, blue, black, white : ref Draw->Image;
+
+	init : fn(mods : ref Dat->Mods);
+	spawnprocs : fn();
+	cursorset : fn(p : Draw->Point);
+	cursorswitch: fn(c : ref Dat->Cursor);
+
+	killwins : fn();
+};
--- /dev/null
+++ b/appl/acme/look.b
@@ -1,0 +1,743 @@
+implement Look;
+
+include "common.m";
+
+sys : Sys;
+draw : Draw;
+utils : Utils;
+dat : Dat;
+graph : Graph;
+acme : Acme;
+framem : Framem;
+regx : Regx;
+bufferm : Bufferm;
+textm : Textm;
+windowm : Windowm;
+columnm : Columnm;
+exec : Exec;
+scrl : Scroll;
+plumbmsg : Plumbmsg;
+
+sprint : import sys;
+Point : import draw;
+warning, isalnum, stralloc, strfree, strchr, tgetc : import utils;
+Range, TRUE, FALSE, XXX, BUFSIZE, Astring : import Dat;
+Expand, seltext, row : import dat;
+cursorset : import graph;
+frptofchar : import framem;
+isaddrc, isregexc, address : import regx;
+Buffer : import bufferm;
+Text : import textm;
+Window : import windowm;
+Column : import columnm;
+Msg : import plumbmsg;
+
+init(mods : ref Dat->Mods)
+{
+	sys = mods.sys;
+	draw = mods.draw;
+	utils = mods.utils;
+	graph = mods.graph;
+	acme = mods.acme;
+	framem = mods.framem;
+	regx = mods.regx;
+	dat = mods.dat;
+	bufferm = mods.bufferm;
+	textm = mods.textm;
+	windowm = mods.windowm;
+	columnm = mods.columnm;
+	exec = mods.exec;
+	scrl = mods.scroll;
+	plumbmsg = mods.plumbmsg;
+}
+
+nuntitled : int;
+
+look3(t : ref Text, q0 : int, q1 : int, external : int)
+{
+	n, c, f : int;
+	ct : ref Text;
+	e : Expand;
+	r : ref Astring;
+	expanded : int;
+
+	ct = seltext;
+	if(ct == nil)
+		seltext = t;
+	(expanded, e) = expand(t, q0, q1);
+	if(!external && t.w!=nil && t.w.nopen[Dat->QWevent]>byte 0){
+		if(!expanded)
+			return;
+		f = 0;
+		if((e.at!=nil && t.w!=nil) || (e.name!=nil && lookfile(e.name, len e.name)!=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.name != nil)
+			f |= 4;	# it's a file name 
+		c = 'l';
+		if(t.what == Textm->Body)
+			c = 'L';
+		n = q1-q0;
+		if(n <= Dat->EVENTSIZE){
+			r = stralloc(n);
+			t.file.buf.read(q0, r, 0, n);
+			t.w.event(sprint("%c%d %d %d %d %s\n", c, q0, q1, f, n, r.s[0:n]));
+			strfree(r);
+			r = nil;
+		}else
+			t.w.event(sprint("%c%d %d %d 0 \n", c, q0, q1, f));
+		if(q0==e.q0 && q1==e.q1)
+			return;
+		if(e.name != nil){
+			n = len e.name;
+			if(e.a1 > e.a0)
+				n += 1+(e.a1-e.a0);
+			r = stralloc(n);
+			for (i := 0; i < len e.name; i++)
+				r.s[i] = e.name[i];
+			if(e.a1 > e.a0){
+				r.s[len e.name] = ':';
+				e.at.file.buf.read(e.a0, r, len e.name+1, e.a1-e.a0);
+			}
+		}else{
+			n = e.q1 - e.q0;
+			r = stralloc(n);
+			t.file.buf.read(e.q0, r, 0, n);
+		}
+		f &= ~2;
+		if(n <= Dat->EVENTSIZE)
+			t.w.event(sprint("%c%d %d %d %d %s\n", c, e.q0, e.q1, f, n, r.s[0:n]));
+		else
+			t.w.event(sprint("%c%d %d %d 0 \n", c, e.q0, e.q1, f));
+		strfree(r);
+		r = nil;
+		return;
+	}
+	if(0 && dat->plumbed){	# don't do yet : 2 acmes running => only 1 receives msg
+		m := ref Msg;
+		m.src = "acme";
+		m.dst = nil;
+		(dir, nil) := dirname(t, nil, 0);
+		if(dir == ".")	# sigh
+			dir = nil;
+		if(dir == nil)
+			dir = acme->wdir;
+		m.dir = dir;
+		m.kind = "text";
+		m.attr = nil;
+		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.buf.nc && (c = tgetc(t, q1)) != ' ' && c != '\t' && c != '\n')
+					q1++;
+				if(q1 == q0)
+					return;
+				m.attr = "click=" + string (p-q0);
+			}
+		}
+		r = stralloc(q1-q0);
+		t.file.buf.read(q0, r, 0, q1-q0);
+		m.data = array of byte r.s;
+		strfree(r);
+		if(m.send() >= 0)
+			return;
+		# plumber failed to match : fall through
+	}
+	if(!expanded)
+		return;
+	if(e.name != nil || e.at != nil)
+		(nil, e) = openfile(t, e);
+	else{
+		if(t.w == nil)
+			return;
+		ct = t.w.body;
+		if(t.w != ct.w)
+			ct.w.lock('M');
+		if(t == ct)
+			ct.setselect(e.q1, e.q1);
+		n = e.q1 - e.q0;
+		r = stralloc(n);
+		t.file.buf.read(e.q0, r, 0, n);
+		if(search(ct, r.s, n) && e.jump)
+			cursorset(frptofchar(ct.frame, ct.frame.p0).add((4, ct.frame.font.height-4)));
+		if(t.w != ct.w)
+			ct.w.unlock();
+		strfree(r);
+		r = nil;
+	}
+	e.name = nil;
+	e.bname = nil;
+}
+
+plumblook(m : ref Msg)
+{
+	e : Expand;
+
+	if (len m.data > Dat->PLUMBSIZE) {
+		warning(nil, sys->sprint("plumb message too long : %s\n", string m.data));
+		return;
+	}
+	e.q0 = e.q1 = 0;
+	if (len m.data == 0)
+		return;
+	e.ar = nil;
+	e.name = string m.data;
+	if(e.name[0] != '/' && m.dir != nil)
+		e.name = m.dir + "/" + e.name;
+	(e.name, nil) = cleanname(e.name, len e.name);
+	e.bname = e.name;
+	e.jump = TRUE;
+	e.a0 = e.a1 = 0;
+	(found, addr) := plumbmsg->lookup(plumbmsg->string2attrs(m.attr), "addr");
+	if (found && addr != nil) {
+		e.ar = addr;
+		e.a1 = len addr;
+	}
+	openfile(nil, e);
+	e.at = nil;
+}
+
+plumbshow(m : ref Msg)
+{
+	w := utils->newwindow(nil);
+	(found, name) := plumbmsg->lookup(plumbmsg->string2attrs(m.attr), "filename");
+	if (!found || name == nil) {
+		nuntitled++;
+		name = "Untitled-" + string nuntitled;
+	}
+	if (name[0] != '/' && m.dir != nil)
+		name = m.dir + "/" + name;
+	(name, nil) = cleanname(name, len name);
+	w.setname(name, len name);
+	d := string m.data;
+	w.body.insert(0, d, len d, TRUE, FALSE);
+	w.body.file.mod = FALSE;
+	w.dirty = FALSE;
+	w.settag();
+	scrl->scrdraw(w.body);
+	w.tag.setselect(w.tag.file.buf.nc, w.tag.file.buf.nc);
+}
+
+search(ct : ref Text, r : string, n : int) : int
+{
+	q, nb, maxn : int;
+	around : int;
+	s : ref Astring;
+	b, c : int;
+
+	if(n==0 || n>ct.file.buf.nc)
+		return FALSE;
+	if(2*n > BUFSIZE){
+		warning(nil, "string too long\n");
+		return FALSE;
+	}
+	maxn = utils->max(2*n, BUFSIZE);
+	s = utils->stralloc(BUFSIZE);
+	b = nb = 0;
+	around = 0;
+	q = ct.q1;
+	for(;;){
+		if(q >= ct.file.buf.nc){
+			q = 0;
+			around = 1;
+			nb = 0;
+		}
+		if(nb > 0){
+			for (c = 0; c < nb; c++)
+				if (s.s[b+c] == r[0])
+					break;
+			if(c >= nb){
+				q += nb;
+				nb = 0;
+				if(around && q>=ct.q1)
+					break;
+				continue;
+			}
+			q += c;
+			nb -= c;
+			b += c;
+		}
+		# reload if buffer covers neither string nor rest of file 
+		if(nb<n && nb!=ct.file.buf.nc-q){
+			nb = ct.file.buf.nc-q;
+			if(nb >= maxn)
+				nb = maxn-1;
+			ct.file.buf.read(q, s, 0, nb);
+			b = 0;
+		}
+		if(n <= nb && s.s[b:b+n] == r[0:n]){
+			if(ct.w != nil){
+				ct.show(q, q+n, TRUE);
+				ct.w.settag();
+			}else{
+				ct.q0 = q;
+				ct.q1 = q+n;
+			}
+			seltext = ct;
+			utils->strfree(s);
+			s = nil;
+			return TRUE;
+		}
+		if(around && q>=ct.q1)
+			break;
+		--nb;
+		b++;
+		q++;
+	}
+	utils->strfree(s);
+	s = nil;
+	return FALSE;
+}
+
+isfilec(r : int) : int
+{
+	if(isalnum(r))
+		return TRUE;
+	if(strchr(".-+/:", r) >= 0)
+		return TRUE;
+	return FALSE;
+}
+
+cleanname(b : string, n : int) : (string, int)
+{
+	i, j, found : int;
+
+	b = b[0:n];
+	# compress multiple slashes 
+	for(i=0; i<n-1; i++)
+		if(b[i]=='/' && b[i+1]=='/'){
+			b = b[0:i] + b[i+1:];
+			--n;
+			--i;
+		}
+	#  eliminate ./ 
+	for(i=0; i<n-1; i++)
+		if(b[i]=='.' && b[i+1]=='/' && (i==0 || b[i-1]=='/')){
+			b = b[0:i] + b[i+2:];
+			n -= 2;
+			--i;
+		}
+	# eliminate trailing . 
+	if(n>=2 && b[n-2]=='/' && b[n-1]=='.') {
+		--n;
+		b = b[0:n];
+	}
+	do{
+		# compress xx/.. 
+		found = FALSE;
+		for(i=1; i<=n-3; i++)
+			if(b[i:i+3] == "/.."){
+				if(i==n-3 || b[i+3]=='/'){
+					found = TRUE;
+					break;
+				}
+			}
+		if(found)
+			for(j=i-1; j>=0; --j)
+				if(j==0 || b[j-1]=='/'){
+					i += 3;		# character beyond .. 
+					if(i<n && b[i]=='/')
+						++i;
+					b = b[0:j] + b[i:];
+					n -= (i-j);
+					break;
+				}
+	}while(found);
+	if(n == 0){
+		b = ".";
+		n = 1;
+	}
+	return (b, n);
+}
+
+includefile(dir : string, file : string, nfile : int) : (string, int)
+{
+	m, n : int;
+	a : string;
+
+	if (dir == ".") {
+		m = 0;
+		a = file;
+	}
+	else {
+		m = 1 + len dir;
+		a = dir + "/" + file;
+	}
+	n = utils->access(a);
+	if(n < 0) {
+		a = nil;
+		return (nil, 0);
+	}
+	file = nil;
+	return cleanname(a, m+nfile);
+}
+
+objdir : string;
+
+includename(t : ref Text , r : string, n : int) : (string, int)
+{
+	file : string;
+	i, nfile : int;
+	w : ref Window;
+
+	{
+		w = t.w;
+		if(n==0 || r[0]=='/' || w==nil)
+			raise "e";
+		if(n>2 && r[0]=='.' && r[1]=='/')
+			raise "e";
+		file = nil;
+		nfile = 0;
+		(file, nfile) = includefile(".", r, n);
+		if (file == nil) {
+			(dr, dn) := dirname(t, r, n);
+			(file, nfile) = includefile(".", dr, dn);
+		}
+		if (file == nil) {
+			for(i=0; i<w.nincl && file==nil; i++)
+				(file, nfile) = includefile(w.incl[i], r, n);
+		}
+		if(file == nil)
+			(file, nfile) = includefile("/module", r, n);
+		if(file == nil)
+			(file, nfile) = includefile("/include", r, n);
+		if(file==nil && objdir!=nil)
+			(file, nfile) = includefile(objdir, r, n);
+		if(file == nil)
+			raise "e";
+		return (file, nfile);
+	}
+	exception{
+		* =>
+			return (r, n);
+	}
+	return (nil, 0);
+}
+
+dirname(t : ref Text, r : string, n : int) : (string, int)
+{
+	b : ref Astring;
+	c : int;
+	m, nt : int;
+	slash : int;
+
+	{
+		b = nil;
+		if(t == nil || t.w == nil)
+			raise "e";
+		nt = t.w.tag.file.buf.nc;
+		if(nt == 0)
+			raise "e";
+		if(n>=1 &&  r[0]=='/')
+			raise "e";
+		b = stralloc(nt+n+1);
+		t.w.tag.file.buf.read(0, b, 0, nt);
+		slash = -1;
+		for(m=0; m<nt; m++){
+			c = b.s[m];
+			if(c == '/')
+				slash = m;
+			if(c==' ' || c=='\t')
+				break;
+		}
+		if(slash < 0)
+			raise "e";
+		for (i := 0; i < n; i++)
+			b.s[slash+1+i] = r[i];
+		r = nil;
+		return cleanname(b.s, slash+1+n);
+	}
+	exception{
+		* =>
+			b = nil;
+			if(r != nil)
+				return cleanname(r, n);
+			return (r, n);
+	}
+	return (nil, 0);
+}
+
+expandfile(t : ref Text, q0 : int, q1 : int, e : Expand) : (int, Expand)
+{
+	i, n, nname, colon : int;
+	amin, amax : int;
+	r : ref Astring;
+	c : int;
+	w : ref Window;
+
+	amax = q1;
+	if(q1 == q0){
+		colon = -1;
+		while(q1<t.file.buf.nc && isfilec(c=t.readc(q1))){
+			if(c == ':'){
+				colon = q1;
+				break;
+			}
+			q1++;
+		}
+		while(q0>0 && (isfilec(c=t.readc(q0-1)) || isaddrc(c) || isregexc(c))){
+			q0--;
+			if(colon==-1 && c==':')
+				colon = q0;
+		}
+		#
+		# if it looks like it might begin file: , consume address chars after :
+		# otherwise terminate expansion at :
+		#
+		
+		if(colon>=0 && colon<t.file.buf.nc-1 && isaddrc(t.readc(colon+1))){
+			q1 = colon+1;
+			while(q1<t.file.buf.nc-1 && isaddrc(t.readc(q1)))
+				q1++;
+		}else if(colon >= 0)
+			q1 = colon;
+		if(q1 > q0)
+			if(colon >= 0){	# stop at white space
+				for(amax=colon+1; amax<t.file.buf.nc; amax++)
+					if((c=t.readc(amax))==' ' || c=='\t' || c=='\n')
+						break;
+			}else
+				amax = t.file.buf.nc;
+	}
+	amin = amax;
+	e.q0 = q0;
+	e.q1 = q1;
+	n = q1-q0;
+	if(n == 0)
+		return (FALSE, e);
+	# see if it's a file name 
+	r = stralloc(n);
+	t.file.buf.read(q0, r, 0, n);
+	# first, does it have bad chars? 
+	nname = -1;
+	for(i=0; i<n; i++){
+		c = r.s[i];
+		if(c==':' && nname<0){
+			if(q0+i+1<t.file.buf.nc && (i==n-1 || isaddrc(t.readc(q0+i+1))))
+				amin = q0+i;
+			else {
+				strfree(r);
+				r = nil;
+				return (FALSE, e);
+			}
+			nname = i;
+		}
+	}
+	if(nname == -1)
+		nname = n;
+	for(i=0; i<nname; i++)
+		if(!isfilec(r.s[i])) {
+			strfree(r);
+			r = nil;
+			return (FALSE, e);
+		}
+	#
+	# 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.
+	#
+	 
+	isfile := 0;
+	if(q0>0 && t.readc(q0-1)=='<' && q1<t.file.buf.nc && t.readc(q1)=='>')
+		(r.s, nname) = includename(t, r.s, nname);
+	else if(q0>0 && t.readc(q0-1)=='"' && q1<t.file.buf.nc && t.readc(q1)=='"')
+		(r.s, nname) = includename(t, r.s, nname);
+	else if(amin == q0)
+		isfile = 1;
+	else
+		(r.s, nname) = dirname(t, r.s, nname);
+	if (!isfile) {
+		e.bname = r.s;
+		# if it's already a window name, it's a file 
+		w = lookfile(r.s, nname);
+		# if it's the name of a file, it's a file 
+		if(w == nil && utils->access(e.bname) < 0){
+			e.bname = nil;
+			strfree(r);
+			r = nil;
+			return (FALSE, e);
+		}
+	}
+
+	e.name = r.s[0:nname];
+	e.at = t;
+	e.a0 = amin+1;
+	(nil, e.a1, nil) = address(nil, nil, (Range)(-1,-1), (Range)(0, 0), t, nil, e.a0, amax, FALSE);
+	strfree(r);
+	r = nil;
+	return (TRUE, e);
+}
+
+expand(t : ref Text, q0 : int, q1 : int) : (int, Expand)
+{
+	e : Expand;
+	ok : int;
+
+	e.q0 = e.q1 = e.a0 = e.a1 = 0;
+	e.name = e.bname = nil;
+	e.at = nil;
+	# 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 == Textm->Tag)
+			e.jump = FALSE;
+	}
+
+	(ok, e) = expandfile(t, q0, q1, e);
+	if (ok)
+		return (TRUE, e);
+
+	if(q0 == q1){
+		while(q1<t.file.buf.nc && isalnum(t.readc(q1)))
+			q1++;
+		while(q0>0 && isalnum(t.readc(q0-1)))
+			q0--;
+	}
+	e.q0 = q0;
+	e.q1 = q1;
+	return (q1 > q0, e);
+}
+
+lookfile(s : string, n : int) : ref Window
+{
+	i, j, k : int;
+	w : ref Window;
+	c : ref Column;
+	t : ref Text;
+
+	# 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 = len t.file.name;
+			if(k>0 && t.file.name[k-1] == '/')
+				k--;
+			if(t.file.name[0:k] == s[0:n]){
+				w = w.body.file.curtext.w;
+				if(w.col != nil)	# protect against race deleting w
+					return w;
+			}
+		}
+	}
+	return nil;
+}
+
+lookid(id : int, dump : int) : ref Window
+{
+	i, j : int;
+	w : ref Window;
+	c : ref Column;
+
+	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;
+}
+
+openfile(t : ref Text, e : Expand) : (ref Window, Expand)
+{
+	r : Range;
+	w, ow : ref Window;
+	eval, i, n : int;
+
+	if(e.name == nil){
+		w = t.w;
+		if(w == nil)
+			return (nil, e);
+	}else
+		w = lookfile(e.name, len e.name);
+	if(w != nil){
+		t = w.body;
+		if(!t.col.safe && t.frame.maxlines==0) # window is obscured by full-column window
+			t.col.grow(t.col.w[0], 1, 1);
+	}
+	else{
+		ow = nil;
+		if(t != nil)
+			ow = t.w;
+		w = utils->newwindow(t);
+		t = w.body;
+		w.setname(e.name, len e.name);
+		t.loadx(0, e.bname, 1);
+		t.file.mod = FALSE;
+		t.w.dirty = FALSE;
+		t.w.settag();
+		t.w.tag.setselect(t.w.tag.file.buf.nc, t.w.tag.file.buf.nc);
+		if(ow != nil)
+			for(i=ow.nincl; --i>=0; ){
+				n = len ow.incl[i];
+				w.addincl(ow.incl[i], n);	# really do want to copy here
+			}
+	}
+	if(e.a1 == e.a0)
+		eval = FALSE;
+	else
+		(eval, nil, r) = address(nil, t, (Range)(-1, -1), (Range)(t.q0, t.q1), e.at, e.ar, e.a0, e.a1, TRUE);
+		# was (eval, nil, r) = address(nil, t, (Range)(-1, -1), (Range)(t.q0, t.q1), e.at, nil, e.a0, e.a1, TRUE);
+	if(eval == FALSE){
+		r.q0 = t.q0;
+		r.q1 = t.q1;
+	}
+	t.show(r.q0, r.q1, TRUE);
+	t.w.settag();
+	seltext = t;
+	if(e.jump)
+		cursorset(frptofchar(t.frame, t.frame.p0).add((4, t.frame.font.height-4)));
+	return (w, e);
+}
+
+new(et : ref Text, t : ref Text, argt : ref Text, flag1 : int, flag2 : int, arg : string, narg : int)
+{
+	ndone : int;
+	a, f : string;
+	na, nf : int;
+	e : Expand;
+
+	(nil, a, na) = exec->getarg(argt, FALSE, TRUE);
+	if(a != nil){
+		new(et, t, nil, flag1, flag2, a, na);
+		if(narg == 0)
+			return;
+	}
+	# loop condition: *arg is not a blank 
+	for(ndone=0; ; ndone++){
+		(a, na) = utils->findbl(arg, narg);
+		if(a == arg){
+			if(ndone==0 && et.col!=nil)
+				et.col.add(nil, nil, -1).settag();
+			break;
+		}
+		nf = narg-na;
+		f = arg[0:nf];	# want a copy
+		(f, nf) = dirname(et, f, nf);
+		e.q0 = e.q1 = e.a0 = e.a1 = 0;
+		e.at = nil;
+		e.name = f;
+		e.bname = f;
+		e.jump = TRUE;
+		(nil, e) = openfile(et, e);
+		f = nil;
+		e.bname = nil;
+		(arg, narg) = utils->skipbl(a, na);
+	}
+}
--- /dev/null
+++ b/appl/acme/look.m
@@ -1,0 +1,17 @@
+Look : module {
+	PATH : con "/dis/acme/look.dis";
+
+	init : fn(mods : ref Dat->Mods);
+
+	isfilec: fn(r : int) : int;
+	lookid : fn(n : int, b : int) : ref Windowm->Window;
+	lookfile : fn(s : string, n : int) : ref Windowm->Window;
+	dirname : fn(t : ref Textm->Text, r : string, n : int) : (string, int);
+	cleanname : fn(s : string, n : int) : (string, int);
+	new : fn(et, t, argt : ref Textm->Text, flag1, flag2 : int, arg : string, narg : int);
+	expand : fn(t : ref Textm->Text, q0, q1 : int) : (int, Dat->Expand);
+	search : fn(t : ref Textm->Text, r : string, n : int) : int;
+	look3 : fn(t : ref Textm->Text, q0, q1, external : int);
+	plumblook : fn(m : ref Plumbmsg->Msg);
+	plumbshow : fn(m : ref Plumbmsg->Msg);
+};
--- /dev/null
+++ b/appl/acme/mkfile
@@ -1,0 +1,90 @@
+<../../mkconfig
+
+DIRS=\
+	acme\
+
+TARG=\
+	acme.dis\
+	dat.dis\
+	buff.dis\
+	col.dis\
+	disk.dis\
+	exec.dis\
+	file.dis\
+	fsys.dis\
+	look.dis\
+	regx.dis\
+	row.dis\
+	scrl.dis\
+	text.dis\
+	time.dis\
+	util.dis\
+	wind.dis\
+	graph.dis\
+	xfid.dis\
+	gui.dis\
+	frame.dis\
+	edit.dis\
+	ecmd.dis\
+	elog.dis\
+	styxaux.dis\
+
+ICONS=\
+	abcde.bit\
+
+MODULES=\
+	acme.m\
+	buff.m\
+	col.m\
+	disk.m\
+	exec.m\
+	file.m\
+	fsys.m\
+	look.m\
+	regx.m\
+	row.m\
+	scrl.m\
+	text.m\
+	time.m\
+	util.m\
+	wind.m\
+	xfid.m\
+	common.m\
+	graph.m\
+	gui.m\
+	frame.m\
+	dat.m\
+	edit.m\
+	elog.m\
+	ecmd.m\
+	styxaux.m\
+
+SYSMODULES=\
+	bufio.m\
+	daytime.m\
+	debug.m\
+	draw.m\
+	sh.m\
+	string.m\
+	styx.m\
+	sys.m\
+	tk.m\
+	workdir.m\
+	wmclient.m\
+
+DISBIN=$ROOT/dis/acme
+
+all:V:	acme.dis
+
+<$ROOT/mkfiles/mkdis
+<$ROOT/mkfiles/mksubdirs
+
+install:V:	$ROOT/dis/acme.dis
+
+$ROOT/dis/acme.dis:	acme.dis
+	rm -f $target && cp acme.dis $target
+
+acme.dis:	$MODULES $SYS_MODULES
+
+nuke:V:
+	rm -f $ROOT/dis/acme.dis
--- /dev/null
+++ b/appl/acme/regx.b
@@ -1,0 +1,1050 @@
+implement Regx;
+
+include "common.m";
+
+sys : Sys;
+utils : Utils;
+textm : Textm;
+
+FALSE, TRUE, XXX : import Dat;
+NRange : import Dat;
+Range, Rangeset : import Dat;
+error, warning, tgetc, rgetc : import utils;
+Text : import textm;
+
+init(mods : ref Dat->Mods)
+{
+	sys = mods.sys;
+	utils = mods.utils;
+	textm = mods.textm;
+}
+
+None : con 0;
+Fore : con '+';
+Back : con '-';
+
+Char : con 0;
+Line : con 1;
+
+isaddrc(r : int) : int
+{
+	if (utils->strchr("0123456789+-/$.#", r) >= 0)
+		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
+#
+isregexc(r : int) : int
+{
+	if(r == 0)
+		return FALSE;
+	if(utils->isalnum(r))
+		return TRUE;
+	if(utils->strchr("^+-.*?#,;[]()$", r)>=0)
+		return TRUE;
+	return FALSE;
+}
+
+number(md: ref Dat->Mntdir, t : ref Text, r : Range, line : int, dir : int, size : int) : (int, Range)
+{
+	q0, q1 : int;
+
+	{
+		if(size == Char){
+			if(dir == Fore)
+				line = r.q1+line;	# was t.file.buf.nc+line;
+			else if(dir == Back){
+				if(r.q0==0 && line > 0)
+					r.q0 = t.file.buf.nc;
+				line = r.q0-line;	# was t.file.buf.nc - line;
+			}
+			if(line<0 || line>t.file.buf.nc)
+				raise "e";
+			return (TRUE, (line, line));
+		}
+		(q0, q1) = r;
+		case(dir){
+		None =>
+			q0 = 0;
+			q1 = 0;
+			while(line>0 && q1<t.file.buf.nc)
+				if(t.readc(q1++) == '\n')
+					if(--line > 0)
+						q0 = q1;
+			if(line==1 && t.readc(q1-1)!='\n')	# no newline at end - count it
+				;
+			else if(line > 0)
+				raise "e";
+		Fore =>
+			if(q1 > 0)
+				while(t.readc(q1-1) != '\n')
+					q1++;
+			q0 = q1;
+			while(line>0 && q1<t.file.buf.nc)
+				if(t.readc(q1++) == '\n')
+					if(--line > 0)
+						q0 = q1;
+			if(line > 0)
+				raise "e";
+		Back =>
+			if(q0 < t.file.buf.nc)
+				while(q0>0 && t.readc(q0-1)!='\n')
+					q0--;
+			q1 = q0;
+			while(line>0 && q0>0){
+				if(t.readc(q0-1) == '\n'){
+					if(--line >= 0)
+						q1 = q0;
+				}
+				--q0;
+			}
+			if(line > 0)
+				raise "e";
+			while(q0>0 && t.readc(q0-1)!='\n')
+				--q0;
+		}
+		return (TRUE, (q0, q1));
+	}
+	exception{
+		* =>
+			if(md != nil)
+				warning(nil, "address out of range\n");
+			return (FALSE, r);
+	}
+	return (FALSE, r);
+}
+
+regexp(md: ref Dat->Mntdir, t : ref Text, lim : Range, r : Range, pat : string, dir : int) : (int, Range)
+{
+	found : int;
+	sel : Rangeset;
+	q : int;
+
+	if(pat == nil && rxnull()){
+		warning(md, "no previous regular expression");
+		return (FALSE, r);
+	}
+	if(pat == nil || !rxcompile(pat))
+		return (FALSE, r);
+	if(dir == Back)
+		(found, sel) = rxbexecute(t, r.q0);
+	else{
+		if(lim.q0 < 0)
+			q = Dat->Infinity;
+		else
+			q = lim.q1;
+		(found, sel) = rxexecute(t, nil, r.q1, q);
+	}
+	if(!found && md == nil)
+		warning(nil, "no match for regexp\n");
+	return (found, sel[0]);
+}
+
+xgetc(a0 : ref Text, a1 : string, n : int) : int
+{
+	if (a0 == nil)
+		return rgetc(a1, n);
+	return tgetc(a0, n);
+}
+
+address(md: ref Dat->Mntdir, t : ref Text, lim : Range, ar : Range, a0 : ref Text, a1 : string, q0 : int, q1 : int,  eval : int) : (int, int, Range)
+{
+	dir, size : int;
+	prevc, c, n : int;
+	q : int;
+	pat : string;
+	r, nr : Range;
+
+	r = ar;
+	q = q0;
+	dir = None;
+	size = Line;
+	c = 0;
+	while(q < q1){
+		prevc = c;
+		c = xgetc(a0, a1, q++);
+		case(c){
+		';' =>
+			ar = r;
+			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.buf.nc;
+			else{
+				(eval, q, nr) = address(md, t, lim, ar, a0, a1, q, q1, eval);
+				r.q1 = nr.q1;
+			}
+			return (eval, q, r);
+		',' =>
+			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.buf.nc;
+			else{
+				(eval, q, nr) = address(md, t, lim, ar, a0, a1, q, q1, eval);
+				r.q1 = nr.q1;
+			}
+			return (eval, q, r);
+		'+'  or '-' =>
+			if(eval && (prevc=='+' || prevc=='-')){
+				if((nc := xgetc(a0, a1, q)) != '#' && nc != '/' && nc != '?')
+					(eval, r) = number(md, t, r, 1, prevc, Line);	# do previous one
+			}
+			dir = c;
+		'.' or '$' =>
+			if(q != q0+1)
+				return (eval, q-1, r);
+			if(eval)
+				if(c == '.')
+					r = ar;
+				else
+					r = (t.file.buf.nc, t.file.buf.nc);
+			if(q < q1)
+				dir = Fore;
+			else
+				dir = None;
+		'#' =>
+			if(q==q1 || (c=xgetc(a0, a1, q++))<'0' || '9'<c)
+				return (eval, q-1, r);
+			size = Char;
+			n = c -'0';
+			while(q<q1){
+				c = xgetc(a0, a1, q++);
+				if(c<'0' || '9'<c){
+					q--;
+					break;
+				}
+				n = n*10+(c-'0');
+			}
+			if(eval)
+				(eval, r) = number(md, t, r, n, dir, size);
+			dir = None;
+			size = Line;
+		'0' to '9' =>
+			n = c -'0';
+			while(q<q1){
+				c = xgetc(a0, a1, q++);
+				if(c<'0' || '9'<c){
+					q--;
+					break;
+				}
+				n = n*10+(c-'0');
+			}
+			if(eval)
+				(eval, r) = number(md, t, r, n, dir, size);
+			dir = None;
+			size = Line;
+		'/' =>
+			pat = nil;
+			break2 := 0; # Ow !
+			while(q<q1){
+				c = xgetc(a0, a1, q++);
+				case(c){
+				'\n' =>
+					--q;
+					break2 = 1;
+				'\\' =>
+					pat[len pat] = c;
+					if(q == q1)
+						break2 = 1;
+					else
+						c = xgetc(a0, a1, q++);
+				'/' =>
+					break2 = 1;
+				}
+				if (break2)
+					break;
+				pat[len pat] = c;
+			}
+			if(eval)
+				(eval, r) = regexp(md, t, lim, r, pat, dir);
+			pat = nil;
+			dir = None;
+			size = Line;
+		* =>
+			return (eval, q-1, r);
+		}
+	}
+	if(eval && dir != None)
+		(eval, r) = number(md, t, r, 1, dir, Line);	# do previous one
+	return (eval, q, r);
+}
+
+sel : Rangeset = array[NRange] of Range;
+lastregexp : string;
+
+# Machine Information
+ 
+Inst : adt {
+	typex : int;		# < 16r10000 ==> literal, otherwise action 
+	# sid : int;
+	subid : int;
+	class : int;
+	# other : cyclic ref Inst;
+	right : cyclic ref Inst;
+	# left : cyclic ref Inst;
+	next : cyclic ref Inst;
+};
+
+NPROG : con	1024;
+program := array[NPROG] of ref Inst;
+progp : int;
+startinst : ref Inst;		# First inst. of program; might not be program[0] 
+bstartinst : ref Inst;		# same for backwards machine 
+
+Ilist : adt {
+	inst : ref Inst;			# Instruction of the thread 
+	se : Rangeset;
+	startp : int;		# first char of match 
+};
+
+NLIST : con	128;
+
+thl, nl : array of Ilist;			# This list, next list 
+listx := array[2] of array of Ilist;
+sempty : Rangeset = array[NRange] of Range;
+
+#
+# Actions and Tokens
+#
+#	0x100xx are operators, value == precedence
+#	0x200xx are tokens, i.e. operands for operators
+#
+
+OPERATOR : con		16r10000;	# Bitmask of all operators 
+START	  : con		16r10000;	# Start, used for marker on stack 
+RBRA	  : con		16r10001;	# Right bracket, ) 
+LBRA	  : con		16r10002;	# Left bracket, ( 
+OR		  : con		16r10003;	# Alternation, | 
+CAT		  : con		16r10004;	# Concatentation, implicit operator 
+STAR	  : con		16r10005;	# Closure, * 
+PLUS		  : con		16r10006;	# a+ == aa* 
+QUEST	  : con		16r10007;	# a? == a|nothing, i.e. 0 or 1 a's 
+ANY		  : con		16r20000;	# Any character but newline, . 
+NOP		  : con		16r20001;	# No operation, internal use only 
+BOL		  : con		16r20002;	# Beginning of line, ^ 
+EOL		  : con		16r20003;	# End of line, $ 
+CCLASS	  : con		16r20004;	# Character class, [] 
+NCCLASS	  : con		16r20005;	# Negated character class, [^] 
+END		  : con		16r20077;	# Terminate: match found 
+
+ISATOR	  : con		16r10000;
+ISAND	  : con		16r20000;
+
+# Parser Information
+ 
+Node : adt {
+	first : ref Inst;
+	last : ref Inst;
+};
+
+NSTACK : con	20;
+andstack := array[NSTACK] of ref Node;
+andp : int;
+atorstack := array[NSTACK] of int;
+atorp : int;
+lastwasand : int;	# Last token was operand 
+cursubid : int;
+subidstack := array[NSTACK] of int;
+subidp : int;
+backwards : int;
+nbra : int;
+exprs : string;
+exprp : int;		# pointer to next character in source expression 
+DCLASS : con	10;	# allocation increment 
+nclass : int;		# number active 
+Nclass : int = 0;		# high water mark 
+class : array of string;
+negateclass : int;
+
+nilnode : Node;
+nilinst : Inst;
+
+rxinit()
+{
+	lastregexp = nil;
+	for (k := 0; k < NPROG; k++)
+		program[k] = ref nilinst;
+	for (k = 0; k < NSTACK; k++)
+		andstack[k] = ref nilnode;
+	for (k = 0; k < 2; k++) {
+		listx[k] = array[NLIST] of Ilist;
+		for (i := 0; i < NLIST; i++) {
+			listx[k][i].inst = nil;
+			listx[k][i].startp = 0;
+			listx[k][i].se = array[NRange] of Range;
+			for (j := 0; j < NRange; j++)
+				listx[k][i].se[j].q0 = listx[k][i].se[j].q1 = 0;
+		}
+	}
+}
+
+regerror(e : string)
+{
+	lastregexp = nil;
+	buf := sys->sprint("regexp: %s\n", e);
+	warning(nil, buf);
+	raise "regerror";
+}
+
+newinst(t : int) : ref Inst
+{
+	if(progp >= NPROG)
+		regerror("expression too long");
+	program[progp].typex = t;
+	program[progp].next = nil;	# next was left
+	program[progp].right = nil;
+	return program[progp++];
+}
+
+realcompile(s : string) : ref Inst
+{
+	token : int;
+
+	{
+		startlex(s);
+		atorp = 0;
+		andp = 0;
+		subidp = 0;
+		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 
+		return andstack[andp].first;
+	}
+	exception{
+		"regerror" =>
+			return nil;
+	}
+	return nil;
+}
+
+rxcompile(r : string) : int
+{
+	oprogp : int;
+
+	if(lastregexp == r)
+		return TRUE;
+	lastregexp = nil;
+	for (i := 0; i < nclass; i++)
+		class[i] = nil;
+	nclass = 0;
+	progp = 0;
+	backwards = FALSE;
+	bstartinst = nil;
+	startinst = realcompile(r);
+	if(startinst == nil)
+		return FALSE;
+	optimize(0);
+	oprogp = progp;
+	backwards = TRUE;
+	bstartinst = realcompile(r);
+	if(bstartinst == nil)
+		return FALSE;
+	optimize(oprogp);
+	lastregexp = r;
+	return TRUE;
+}
+
+operand(t : int)
+{
+	i : ref Inst;
+
+	if(lastwasand)
+		operator(CAT);	# catenate is implicit 
+	i = newinst(t);
+	if(t == CCLASS){
+		if(negateclass)
+			i.typex = NCCLASS;	# UGH 
+		i.class = nclass-1;		# UGH 
+	}
+	pushand(i, i);
+	lastwasand = TRUE;
+}
+
+operator(t : int)
+{
+	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 
+}
+
+pushand(f : ref Inst, l : ref Inst)
+{
+	if(andp >= NSTACK)
+		error("operand stack overflow");
+	andstack[andp].first = f;
+	andstack[andp].last = l;
+	andp++;
+}
+
+pushator(t : int)
+{
+	if(atorp >= NSTACK)
+		error("operator stack overflow");
+	atorstack[atorp++]=t;
+	if(cursubid >= NRange)
+		subidstack[subidp++]= -1;
+	else
+		subidstack[subidp++]=cursubid;
+}
+
+popand(op : int) : ref Node
+{
+	if(andp <= 0)
+		if(op){
+			buf := sys->sprint("missing operand for %c", op);
+			regerror(buf);
+		}else
+			regerror("malformed regexp");
+	return andstack[--andp];
+}
+
+popator() : int
+{
+	if(atorp <= 0)
+		error("operator stack underflow");
+	--subidp;
+	return atorstack[--atorp];
+}
+
+evaluntil(pri : int)
+{
+	op1, op2 : ref Node;
+	inst1, inst2 : ref Inst;
+
+	while(pri==RBRA || atorstack[atorp-1]>=pri){
+		case(popator()){
+		LBRA =>
+			op1 = popand('(');
+			inst2 = newinst(RBRA);
+			inst2.subid = subidstack[subidp];
+			op1.last.next = inst2;
+			inst1 = newinst(LBRA);
+			inst1.subid = subidstack[subidp];
+			inst1.next = op1.first;
+			pushand(inst1, inst2);
+			return;		# must have been RBRA 
+		OR =>
+			op2 = popand('|');
+			op1 = popand('|');
+			inst2 = newinst(NOP);
+			op2.last.next = inst2;
+			op1.last.next = inst2;
+			inst1 = newinst(OR);
+			inst1.right = op1.first;
+			inst1.next = op2.first;	# next was left
+			pushand(inst1, inst2);
+		CAT =>
+			op2 = popand(0);
+			op1 = popand(0);
+			if(backwards && op2.first.typex!=END)
+				(op1, op2) = (op2, op1);
+			op1.last.next = op2.first;
+			pushand(op1.first, op2.last);
+		STAR =>
+			op2 = popand('*');
+			inst1 = newinst(OR);
+			op2.last.next = inst1;
+			inst1.right = op2.first;
+			pushand(inst1, inst1);
+		PLUS =>
+			op2 = popand('+');
+			inst1 = newinst(OR);
+			op2.last.next = inst1;
+			inst1.right = op2.first;
+			pushand(op2.first, inst1);
+		QUEST =>
+			op2 = popand('?');
+			inst1 = newinst(OR);
+			inst2 = newinst(NOP);
+			inst1.next = inst2;	# next was left
+			inst1.right = op2.first;
+			op2.last.next = inst2;
+			pushand(inst1, inst2);
+		* =>
+			error("unknown regexp operator");
+		}
+	}
+}
+
+optimize(start : int)
+{
+	inst : int;
+	target : ref Inst;
+
+	for(inst=start; program[inst].typex!=END; inst++){
+		target = program[inst].next;
+		while(target.typex == NOP)
+			target = target.next;
+		program[inst].next = target;
+	}
+}
+
+startlex(s : string)
+{
+	exprs = s;
+	exprp = 0;
+	nbra = 0;
+}
+
+lex() : int
+{
+	c : int;
+
+	if (exprp == len exprs)
+		return END;
+	c = exprs[exprp++];
+	case(c){
+	'\\' =>
+		if(exprp < len exprs)
+			if((c= exprs[exprp++])=='n')
+				c='\n';
+	'*' =>
+		c = STAR;
+	'?' =>
+		c = QUEST;
+	'+' =>
+		c = PLUS;
+	 '|' =>
+		c = OR;
+	'.' =>
+		c = ANY;
+	'(' =>
+		c = LBRA;
+	')' =>
+		c = RBRA;
+	'^' =>
+		c = BOL;
+	'$' =>
+		c = EOL;
+	'[' =>
+		c = CCLASS;
+		bldcclass();
+	}
+	return c;
+}
+
+nextrec() : int
+{
+	if(exprp == len exprs || (exprp == len exprs-1 && exprs[exprp]=='\\'))
+		regerror("malformed `[]'");
+	if(exprs[exprp] == '\\'){
+		exprp++;
+		if(exprs[exprp]=='n'){
+			exprp++;
+			return '\n';
+		}
+		return exprs[exprp++] | 16r10000;
+	} 
+	return exprs[exprp++];
+}
+
+bldcclass()
+{
+	c1, c2 : int;
+	classp : string;
+
+	# we have already seen the '[' 
+	if(exprp < len exprs && exprs[exprp] == '^'){
+		classp[len classp] = '\n';	# don't match newline in negate case 
+		negateclass = TRUE;
+		exprp++;
+	}else
+		negateclass = FALSE;
+	while((c1 = nextrec()) != ']'){
+		if(c1 == '-'){
+			classp = nil;
+			regerror("malformed `[]'");
+		}
+		if(exprp < len exprs && exprs[exprp] == '-'){
+			exprp++;	# eat '-' 
+			if((c2 = nextrec()) == ']') {
+				classp = nil;
+				regerror("malformed '[]'");
+			}
+			classp[len classp] = 16rFFFF;
+			classp[len classp] = c1;
+			classp[len classp] = c2;
+		}else
+			classp[len classp] = c1;
+	}
+	if(nclass == Nclass){
+		Nclass += DCLASS;
+		oc := class;
+		class = array[Nclass] of string;
+		if (oc != nil) {
+			class[0:] = oc[0:Nclass-DCLASS];
+			oc = nil;
+		}
+	}
+	class[nclass++] = classp;
+}
+
+classmatch(classno : int, c : int, negate : int) : int
+{
+	p : string;
+
+	p = class[classno];
+	for (i := 0; i < len p; ) {
+		if(p[i] == 16rFFFF){
+			if(p[i+1]<=c && c<=p[i+2])
+				return !negate;
+			i += 3;
+		}else if(p[i++] == 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.
+#
+addinst(l : array of Ilist, inst : ref Inst, sep : Rangeset)
+{
+	p : int;
+
+	for(p = 0; l[p].inst != nil; p++){
+		if(l[p].inst==inst){
+			if(sep[0].q0 < l[p].se[0].q0)
+				l[p].se[0:] = sep[0:NRange]; # this would be bug 
+			return;	# It's already there 
+		}
+	}
+	l[p].inst = inst;
+	l[p].se[0:]= sep[0:NRange];
+	l[p+1].inst = nil;
+}
+
+rxnull() : int
+{
+	return startinst==nil || bstartinst==nil;
+}
+
+OVERFLOW : con "overflow";
+
+# either t!=nil or r!=nil, and we match the string in the appropriate place
+rxexecute(t : ref Text, r: string, startp : int, eof : int) : (int, Rangeset)
+{
+	flag : int;
+	inst : ref Inst;
+	tlp : int;
+	p : int;
+	nnl, ntl : int;
+	nc, c : int;
+	wrapped : int;
+	startchar : int;
+
+	flag = 0;
+	p = startp;
+	startchar = 0;
+	wrapped = 0;
+	nnl = 0;
+	if(startinst.typex<OPERATOR)
+		startchar = startinst.typex;
+	listx[0][0].inst = listx[1][0].inst = nil;
+	sel[0].q0 = -1;
+	
+	{
+		if(t != nil)
+			nc = t.file.buf.nc;
+		else
+			nc = len r;
+		# Execute machine once for each character 
+		for(;;p++){
+			if(p>=eof || p>=nc){
+				case(wrapped++){
+				0 or 2 =>		# let loop run one more click 
+					;
+				1 =>		# expired; wrap to beginning 
+					if(sel[0].q0>=0 || eof!=Dat->Infinity)
+						return (sel[0].q0>=0, sel);
+					listx[0][0].inst = listx[1][0].inst = nil;
+					p = -1;
+					continue;
+				* =>
+					return (sel[0].q0>=0, sel);
+				}
+				c = 0;
+			}else{
+				if(((wrapped && p>=startp) || sel[0].q0>0) && nnl==0)
+					break;
+				if(t != nil)
+					c = t.readc(p);
+				else
+					c = r[p];
+			}
+			# fast check for first char 
+			if(startchar && nnl==0 && c!=startchar)
+				continue;
+			thl = listx[flag];
+			nl = listx[flag^=1];
+			nl[0].inst = nil;
+			ntl = nnl;
+			nnl = 0;
+			if(sel[0].q0<0 && (!wrapped || p<startp || startp==eof)){
+				# Add first instruction to this list 
+				if(++ntl >= NLIST)
+					raise OVERFLOW;
+				sempty[0].q0 = p;
+				addinst(thl, startinst, sempty);
+			}
+			# Execute machine until this list is empty 
+			tlp = 0;
+			inst = thl[0].inst;
+			while(inst  != nil){	# assignment = 
+				case(inst.typex){
+				LBRA =>
+					if(inst.subid>=0)
+						thl[tlp].se[inst.subid].q0 = p;
+					inst = inst.next;
+					continue;
+				RBRA =>
+					if(inst.subid>=0)
+						thl[tlp].se[inst.subid].q1 = p;
+					inst = inst.next;
+					continue;
+				ANY =>
+					if(c!='\n') {
+						if(++nnl >= NLIST)
+							raise OVERFLOW;
+						addinst(nl, inst.next, thl[tlp].se);
+					}
+				BOL =>
+					if(p==0 || (t != nil && t.readc(p-1)=='\n') || (r != nil && r[p-1] == '\n')){
+						inst = inst.next;
+						continue;
+					}
+				EOL =>
+					if(c == '\n') {
+						inst = inst.next;
+						continue;
+					}
+				CCLASS =>
+					if(c>=0 && classmatch(inst.class, c, 0)) {
+						if(++nnl >= NLIST)
+							raise OVERFLOW;
+						addinst(nl, inst.next, thl[tlp].se);
+					}
+				NCCLASS =>
+					if(c>=0 && classmatch(inst.class, c, 1)) {
+						if(++nnl >= NLIST)
+							raise OVERFLOW;
+						addinst(nl, inst.next, thl[tlp].se);
+					}
+				OR =>
+					# evaluate right choice later 
+					if(++ntl >= NLIST)
+						raise OVERFLOW;
+					addinst(thl[tlp:], inst.right, thl[tlp].se);
+					# efficiency: advance and re-evaluate 
+					inst = inst.next;	# next was left
+					continue;
+				END =>		# Match! 
+					thl[tlp].se[0].q1 = p;
+					newmatch(thl[tlp].se);
+				* =>		# regular character 
+					if(inst.typex==c){
+						if(++nnl >= NLIST)
+							raise OVERFLOW;
+						addinst(nl, inst.next, thl[tlp].se);
+					}
+				}
+				tlp++;
+				inst = thl[tlp].inst;
+			}
+		}
+		return (sel[0].q0>=0, sel);
+	}
+	exception{
+		OVERFLOW =>
+			error("regexp list overflow");
+			sel[0].q0 = -1;
+			return (0, sel);
+	}
+	return (0, sel);
+}
+
+newmatch(sp : Rangeset)
+{
+	if(sel[0].q0<0 || sp[0].q0<sel[0].q0 ||
+	   (sp[0].q0==sel[0].q0 && sp[0].q1>sel[0].q1))
+		sel[0:] = sp[0:NRange];
+}
+
+rxbexecute(t : ref Text, startp : int) : (int, Rangeset)
+{
+	flag : int;
+	inst : ref Inst;
+	tlp : int;
+	p : int;
+	nnl, ntl : int;
+	c : int;
+	wrapped : int;
+	startchar : int;
+
+	flag = 0;
+	nnl = 0;
+	wrapped = 0;
+	p = startp;
+	startchar = 0;
+	if(bstartinst.typex<OPERATOR)
+		startchar = bstartinst.typex;
+	listx[0][0].inst = listx[1][0].inst = nil;
+	sel[0].q0= -1;
+	
+	{
+		# Execute machine once for each character, including terminal NUL 
+		for(;;--p){
+			if(p <= 0){
+				case(wrapped++){
+				0 or 2 =>		# let loop run one more click 
+					;
+				1 =>			# expired; wrap to end 
+					if(sel[0].q0>=0)
+						return (sel[0].q0>=0, sel);
+					listx[0][0].inst = listx[1][0].inst = nil;
+					p = t.file.buf.nc+1;
+					continue;
+				3 or * =>
+					return (sel[0].q0>=0, sel);
+				}
+				c = 0;
+			}else{
+				if(((wrapped && p<=startp) || sel[0].q0>0) && nnl==0)
+					break;
+				c = t.readc(p-1);
+			}
+			# fast check for first char 
+			if(startchar && nnl==0 && c!=startchar)
+				continue;
+			thl = listx[flag];
+			nl = listx[flag^=1];
+			nl[0].inst = nil;
+			ntl = nnl;
+			nnl = 0;
+			if(sel[0].q0<0 && (!wrapped || p>startp)){
+				# Add first instruction to this list 
+				if(++ntl >= NLIST)
+					raise OVERFLOW;
+				# the minus is so the optimizations in addinst work 
+				sempty[0].q0 = -p;
+				addinst(thl, bstartinst, sempty);
+			}
+			# Execute machine until this list is empty 
+			tlp = 0;
+			inst = thl[0].inst;
+			while(inst != nil){	# assignment = 
+				case(inst.typex){
+				LBRA =>
+					if(inst.subid>=0)
+						thl[tlp].se[inst.subid].q0 = p;
+					inst = inst.next;
+					continue;
+				RBRA =>
+					if(inst.subid >= 0)
+						thl[tlp].se[inst.subid].q1 = p;
+					inst = inst.next;
+					continue;
+				ANY =>
+					if(c != '\n') {
+						if(++nnl >= NLIST)
+							raise OVERFLOW;
+						addinst(nl, inst.next, thl[tlp].se);
+					}
+				BOL =>
+					if(c=='\n' || p==0){
+						inst = inst.next;
+						continue;
+					}
+				EOL =>
+					if(p<t.file.buf.nc && t.readc(p)=='\n') {
+						inst = inst.next;
+						continue;
+					}
+				CCLASS =>
+					if(c>0 && classmatch(inst.class, c, 0)) {
+						if(++nnl >= NLIST)
+							raise OVERFLOW;
+						addinst(nl, inst.next, thl[tlp].se);
+					}
+				NCCLASS =>
+					if(c>0 && classmatch(inst.class, c, 1)) {
+						if(++nnl >= NLIST)
+							raise OVERFLOW;
+						addinst(nl, inst.next, thl[tlp].se);
+					}
+				OR =>
+					# evaluate right choice later 
+					if(++ntl >= NLIST)
+						raise OVERFLOW;
+					addinst(thl[tlp:], inst.right, thl[tlp].se);
+					# efficiency: advance and re-evaluate 
+					inst = inst.next;	# next was left
+					continue;
+				END =>		# Match! 
+					thl[tlp].se[0].q0 = -thl[tlp].se[0].q0; # minus sign 
+					thl[tlp].se[0].q1 = p;
+					bnewmatch(thl[tlp].se);
+				* =>	# regular character 
+					if(inst.typex == c){
+						if(++nnl >= NLIST)
+							raise OVERFLOW;
+						addinst(nl, inst.next, thl[tlp].se);
+					}
+				}
+				tlp++;
+				inst = thl[tlp].inst;
+			}
+		}
+		return (sel[0].q0>=0, sel);
+	}
+	exception{
+		OVERFLOW =>
+			error("regexp list overflow");
+			sel[0].q0 = -1;
+			return (0, sel);
+	}
+	return (0, sel);
+}
+
+bnewmatch(sp : Rangeset)
+{
+        i : int;
+
+        if(sel[0].q0<0 || sp[0].q0>sel[0].q1 || (sp[0].q0==sel[0].q1 && sp[0].q1<sel[0].q0))
+                for(i = 0; i<NRange; i++){       # note the reversal; q0<=q1 
+                        sel[i].q0 = sp[i].q1;
+                        sel[i].q1 = sp[i].q0;
+                }
+}
--- /dev/null
+++ b/appl/acme/regx.m
@@ -1,0 +1,13 @@
+Regx : module {
+	PATH : con "/dis/acme/regx.dis";
+
+	init : fn(mods : ref Dat->Mods);
+
+	rxinit : fn();
+	rxcompile: fn(r : string) : int;
+	rxexecute: fn(t : ref Textm->Text, r: string, startp : int, eof : int) : (int, Dat->Rangeset);
+	rxbexecute: fn(t : ref Textm->Text, startp : int) : (int, Dat->Rangeset);
+	isaddrc : fn(r : int) : int;
+	isregexc : fn(r : int) : int;
+	address : fn(md: ref Dat->Mntdir, t : ref Textm->Text, lim : Dat->Range, ar : Dat->Range, a0 : ref Textm->Text, a1 : string, q0 : int, q1 : int, eval : int) : (int, int, Dat->Range);
+};
--- /dev/null
+++ b/appl/acme/row.b
@@ -1,0 +1,767 @@
+implement Rowm;
+
+include "common.m";
+
+sys : Sys;
+bufio : Bufio;
+utils : Utils;
+drawm : Draw;
+acme : Acme;
+graph : Graph;
+gui : Gui;
+dat : Dat;
+bufferm : Bufferm;
+textm : Textm;
+filem : Filem;
+windowm : Windowm;
+columnm : Columnm;
+exec : Exec;
+look : Look;
+edit : Edit;
+ecmd : Editcmd;
+
+ALLLOOPER, ALLTOFILE, ALLMATCHFILE, ALLFILECHECK, ALLELOGTERM, ALLEDITINIT, ALLUPDATE: import Edit;
+sprint : import sys;
+FALSE, TRUE, XXX : import Dat;
+Border, BUFSIZE, Astring : import Dat;
+Reffont, reffont, Lock, Ref : import dat;
+row, home, mouse : import dat;
+fontnames : import acme;
+font, draw : import graph;
+Point, Rect, Image : import drawm;
+min, max, abs, error, warning, clearmouse, stralloc, strfree : import utils;
+black, white, mainwin : import gui;
+Buffer : import bufferm;
+Tag, Rowtag, Text : import textm;
+Window : import windowm;
+File : import filem;
+Column : import columnm;
+Iobuf : import bufio;
+
+init(mods : ref Dat->Mods)
+{
+	sys = mods.sys;
+	bufio = mods.bufio;
+	dat = mods.dat;
+	utils = mods.utils;
+	drawm = mods.draw;
+	acme = mods.acme;
+	graph = mods.graph;
+	gui = mods.gui;
+	bufferm = mods.bufferm;
+	textm = mods.textm;
+	filem = mods.filem;
+	windowm = mods.windowm;
+	columnm = mods.columnm;
+	exec = mods.exec;
+	look = mods.look;
+	edit = mods.edit;
+	ecmd = mods.editcmd;
+}
+
+newrow() : ref Row
+{
+	r := ref Row;
+	r.qlock = Lock.init();
+	r.r = ((0, 0), (0, 0));
+	r.tag = nil;
+	r.col = nil;
+	r.ncol = 0;
+	return r;
+}
+
+Row.init(row : self ref Row, r : Rect)
+{
+	r1 : Rect;
+	t : ref Text;
+	dummy : ref File = nil;
+
+	draw(mainwin, r, white, nil, (0, 0));
+	row.r = r;
+	row.col = nil;
+	row.ncol = 0;
+	r1 = r;
+	r1.max.y = r1.min.y + font.height;
+	row.tag = textm->newtext();
+	t = row.tag;
+	t.init(dummy.addtext(t), r1, Reffont.get(FALSE, FALSE, FALSE, nil), acme->tagcols);
+	t.what = Rowtag;
+	t.row = row;
+	t.w = nil;
+	t.col = nil;
+	r1.min.y = r1.max.y;
+	r1.max.y += Border;
+	draw(mainwin, r1, black, nil, (0, 0));
+	t.insert(0, "Newcol Kill Putall Dump Exit ", 29, TRUE, 0);
+	t.setselect(t.file.buf.nc, t.file.buf.nc);
+}
+
+Row.add(row : self ref Row, c : ref Column, x : int) : ref Column
+{
+	r, r1 : Rect;
+	d : ref Column;
+	i : int;
+
+	d = nil;
+	r = row.r;
+	r.min.y = row.tag.frame.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*d.r.dx()/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(r.dx() < 100)
+			return nil;
+		draw(mainwin, r, white, nil, (0, 0));
+		r1 = r;
+		r1.max.x = min(x, r.max.x-50);
+		if(r1.dx() < 50)
+			r1.max.x = r1.min.x+50;
+		d.reshape(r1);
+		r1.min.x = r1.max.x;
+		r1.max.x = r1.min.x+Border;
+		draw(mainwin, r1, black, nil, (0, 0));
+		r.min.x = r1.max.x;
+	}
+	if(c == nil){
+		c = ref Column;
+		c.init(r);
+		reffont.r.inc();
+	}else
+		c.reshape(r);
+	c.row = row;
+	c.tag.row = row;
+	orc := row.col;
+	row.col = array[row.ncol+1] of ref Column;
+	row.col[0:] = orc[0:i];
+	row.col[i+1:] = orc[i:row.ncol];
+	orc = nil;
+	row.col[i] = c;
+	row.ncol++;
+	clearmouse();
+	return c;
+}
+
+Row.reshape(row : self ref Row, r : Rect)
+{
+	i, dx, odx : int;
+	r1, r2 : Rect;
+	c : ref Column;
+
+	dx = r.dx();
+	odx = row.r.dx();
+	row.r = r;
+	r1 = r;
+	r1.max.y = r1.min.y + font.height;
+	row.tag.reshape(r1);
+	r1.min.y = r1.max.y;
+	r1.max.y += Border;
+	draw(mainwin, r1, black, nil, (0, 0));
+	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+c.r.dx()*dx/odx;
+		r2 = r1;
+		r2.max.x = r2.min.x+Border;
+		draw(mainwin, r2, black, nil, (0, 0));
+		r1.min.x = r2.max.x;
+		c.reshape(r1);
+	}
+}
+
+Row.dragcol(row : self ref Row, c : ref Column)
+{
+	r : Rect;
+	i, b, x : int;
+	p, op : Point;
+	d : ref Column;
+
+	clearmouse();
+	graph->cursorswitch(dat->boxcursor);
+	b = mouse.buttons;
+	op = mouse.xy;
+	while(mouse.buttons == b)
+		acme->frgetmouse();
+	graph->cursorswitch(dat->arrowcursor);
+	if(mouse.buttons){
+		while(mouse.buttons)
+			acme->frgetmouse();
+		return;
+	}
+
+	for(i=0; i<row.ncol; i++)
+		if(row.col[i] == c)
+			break;
+	if (i == row.ncol)
+		error("can't find column");
+
+	if(i == 0)
+		return;
+	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;
+		row.close(c, FALSE);
+		if(row.add(c, p.x) == nil)	# whoops! 
+		if(row.add(c, x) == nil)		# WHOOPS! 
+		if(row.add(c, -1)==nil){		# shit! 
+			row.close(c, TRUE);
+			return;
+		}
+		c.mousebut();
+		return;
+	}
+	d = row.col[i-1];
+	if(p.x < d.r.min.x+80+Dat->Scrollwid)
+		p.x = d.r.min.x+80+Dat->Scrollwid;
+	if(p.x > c.r.max.x-80-Dat->Scrollwid)
+		p.x = c.r.max.x-80-Dat->Scrollwid;
+	r = d.r;
+	r.max.x = c.r.max.x;
+	draw(mainwin, r, white, nil, (0, 0));
+	r.max.x = p.x;
+	d.reshape(r);
+	r = c.r;
+	r.min.x = p.x;
+	r.max.x = r.min.x;
+	r.max.x += Border;
+	draw(mainwin, r, black, nil, (0, 0));
+	r.min.x = r.max.x;
+	r.max.x = c.r.max.x;
+	c.reshape(r);
+	c.mousebut();
+}
+
+Row.close(row : self ref Row, c : ref Column, dofree : int)
+{
+	r : Rect;
+	i : int;
+
+	for(i=0; i<row.ncol; i++)
+		if(row.col[i] == c)
+			break;
+	if (i == row.ncol)
+		error("can't find column");
+
+	r = c.r;
+	if(dofree)
+		c.closeall();
+	orc := row.col;
+	row.col = array[row.ncol-1] of ref Column;
+	row.col[0:] = orc[0:i];
+	row.col[i:] = orc[i+1:row.ncol];
+	orc = nil;
+	row.ncol--;
+	if(row.ncol == 0){
+		draw(mainwin, r, white, nil, (0, 0));
+		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(mainwin, r, white, nil, (0, 0));
+	c.reshape(r);
+}
+
+Row.whichcol(row : self ref Row, p : Point) : ref Column
+{
+	i : int;
+	c : ref Column;
+
+	for(i=0; i<row.ncol; i++){
+		c = row.col[i];
+		if(p.in(c.r))
+			return c;
+	}
+	return nil;
+}
+
+Row.which(row : self ref Row, p : Point) : ref Text
+{
+	c : ref Column;
+
+	if(p.in(row.tag.all))
+		return row.tag;
+	c = row.whichcol(p);
+	if(c != nil)
+		return c.which(p);
+	return nil;
+}
+
+Row.typex(row : self ref Row, r : int, p : Point) : ref Text
+{
+	w : ref Window;
+	t : ref Text;
+
+	clearmouse();
+	row.qlock.lock();
+	if(dat->bartflag)
+		t = dat->barttext;
+	else
+		t = row.which(p);
+	if(t!=nil && !(t.what==Tag && p.in(t.scrollr))){
+		w = t.w;
+		if(w == nil)
+			t.typex(r, 0);
+		else{
+			w.lock('K');
+			w.typex(t, r);
+			w.unlock();
+		}
+	}
+	row.qlock.unlock();
+	return t;
+}
+
+Row.clean(row : self ref Row, exiting : int) : int
+{
+	clean : int;
+	i : int;
+
+	clean = TRUE;
+	for(i=0; i<row.ncol; i++)
+		clean &= row.col[i].clean(exiting);
+	return clean;
+}
+
+Row.dump(row : self ref Row, file : string)
+{
+	i, j, m, n, dumped : int;
+	q0, q1 : int;
+	b : ref Iobuf;
+	buf, fontname, a : string;
+	r : ref Astring;
+	c : ref Column;
+	w, w1 : ref Window;
+	t : ref Text;
+
+	if(row.ncol == 0)
+		return;
+	
+	{
+		if(file == nil){
+			if(home == nil){
+				warning(nil, "can't find file for dump: $home not defined\n");
+				raise "e";
+			}
+			buf = sprint("%s/acme.dump", home);
+			file = buf;
+		}
+		b = bufio->create(file, Bufio->OWRITE, 8r600);
+		if(b == nil){
+			warning(nil, sprint("can't open %s: %r\n", file));
+			raise "e";
+		}
+		r = stralloc(BUFSIZE);
+		b.puts(acme->wdir); b.putc('\n');
+		b.puts(fontnames[0]); b.putc('\n');
+		b.puts(fontnames[1]); b.putc('\n');
+		for(i=0; i<row.ncol; i++){
+			c = row.col[i];
+			b.puts(sprint("%11d", 100*(c.r.min.x-row.r.min.x)/row.r.dx()));
+			if(i == row.ncol-1)
+				b.putc('\n');
+			else
+				b.putc(' ');
+		}
+		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];
+				w.commit(w.tag);
+				t = w.body;
+				# windows owned by others get special treatment 
+				if(w.nopen[Dat->QWevent] > byte 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[Dat->QWevent] != byte 0) {
+							j = c.nw;
+							continue;
+						}
+					}
+				fontname = "";
+				if(t.reffont.f != font)
+					fontname = t.reffont.f.name;
+				a = t.file.name;
+				if(t.file.dumpid){
+					dumped = FALSE;
+					b.puts(sprint("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)/c.r.dy(),
+						fontname));
+				}else if(w.dumpstr != nil){
+					dumped = FALSE;
+					b.puts(sprint("e%11d %11d %11d %11d %11d %s\n", i, t.file.dumpid,
+						0, 0,
+						100*(w.r.min.y-c.r.min.y)/c.r.dy(),
+						fontname));
+				}else if(len a == 0){	# don't save unnamed windows 
+					continue;
+				}else if((!w.dirty && utils->access(a)==0) || w.isdir){
+					dumped = FALSE;
+					t.file.dumpid = w.id;
+					b.puts(sprint("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)/c.r.dy(),
+						fontname));
+				}else{
+					dumped = TRUE;
+					t.file.dumpid = w.id;
+					b.puts(sprint("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)/c.r.dy(),
+						w.body.file.buf.nc, fontname));
+				}
+				a = nil;
+				buf = w.ctlprint(0);
+				b.puts(buf);
+				m = min(BUFSIZE, w.tag.file.buf.nc);
+				w.tag.file.buf.read(0, r, 0, m);
+				n = 0;
+				while(n<m && r.s[n]!='\n')
+					n++;
+				r.s[n++] = '\n';
+				b.puts(r.s[0:n]);
+				if(dumped){
+					q0 = 0;
+					q1 = t.file.buf.nc;
+					while(q0 < q1){
+						n = q1 - q0;
+						if(n > Dat->BUFSIZE)
+							n = Dat->BUFSIZE;
+						t.file.buf.read(q0, r, 0, n);
+						b.puts(r.s[0:n]);
+						q0 += n;
+					}
+				}
+				if(w.dumpstr != nil){
+					if(w.dumpdir != nil)
+						b.puts(sprint("%s\n%s\n", w.dumpdir, w.dumpstr));
+					else
+						b.puts(sprint("\n%s\n", w.dumpstr));
+				}
+			}
+		}
+		b.close();
+		b = nil;
+		strfree(r);
+		r = nil;
+	}
+	exception{
+		* =>
+			return;
+	}
+}
+
+rdline(b : ref Iobuf, line : int) : (int, string)
+{
+	l : string;
+
+	l = b.gets('\n');
+	if(l != nil)
+		line++;
+	return (line, l);
+}
+
+Row.loadx(row : self ref Row, file : string, initing : int)
+{
+	i, j, line, percent, y, nr, nfontr, n, ns, ndumped, dumpid, x : int;
+	b, bout : ref Iobuf;
+	fontname : string;
+	l, buf, t : string;
+	rune : int;
+	r, fontr : string;
+	c, c1, c2 : ref Column;
+	q0, q1 : int;
+	r1, r2 : Rect;
+	w : ref Window;
+
+	{
+		if(file == nil){
+			if(home == nil){
+				warning(nil, "can't find file for load: $home not defined\n");
+				raise "e";
+			}
+			buf = sprint("%s/acme.dump", home);
+			file = buf;
+		}
+		b = bufio->open(file, Bufio->OREAD);
+		if(b == nil){
+			warning(nil, sprint("can't open load file %s: %r\n", file));
+			raise "e";
+		}
+		
+		{
+			# current directory 
+			(line, l) = rdline(b, 0);
+			if(l == nil)
+				raise "e";
+			l = l[0:len l - 1];
+			if(sys->chdir(l) < 0){
+				warning(nil, sprint("can't chdir %s\n", l));
+				b.close();
+				return;
+			}
+			# global fonts 
+			for(i=0; i<2; i++){
+				(line, l) = rdline(b, line);
+				if(l == nil)
+					raise "e";
+				l = l[0:len l -1];
+				if(l != nil && l != fontnames[i])
+					Reffont.get(i, TRUE, i==0 && initing, l);
+			}
+			if(initing && row.ncol==0)
+				row.init(mainwin.clipr);
+			(line, l) = rdline(b, line);
+			if(l == nil)
+				raise "e";
+			j = len l/12;
+			if(j<=0 || j>10)
+				raise "e";
+			for(i=0; i<j; i++){
+				percent = int l[12*i:12*i+11];
+				if(percent<0 || percent>=100)
+					raise "e";
+				x = row.r.min.x+percent*row.r.dx()/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(r1.dx() < 50 || r2.dx() < 50)
+						continue;
+					draw(mainwin, (r1.min, r2.max), white, nil, (0, 0));
+					c1.reshape(r1);
+					c2.reshape(r2);
+					r2.min.x = x;
+					r2.max.x = x+Border;
+					draw(mainwin, r2, black, nil, (0, 0));
+				}
+				if(i >= row.ncol)
+					row.add(nil, x);
+			}
+			for(;;){
+				(line, l) = rdline(b, line);
+				if(l == nil)
+					break;
+				dumpid = 0;
+				case(l[0]){
+				'e' =>
+					if(len l < 1+5*12+1)
+						raise "e";
+					(line, l) = rdline(b, line);	# ctl line; ignored 
+					if(l == nil)
+						raise "e";
+					(line, l) = rdline(b, line);	# directory 
+					if(l == nil)
+						raise "e";
+					l = l[0:len l -1];
+					if(len l != 0)
+						r = l;
+					else{
+						if(home == nil)
+							r = "./";
+						else
+							r = home+"/";
+					}
+					nr = len r;
+					(line, l) = rdline(b, line);	# command 
+					if(l == nil)
+						raise "e";
+					t = l[0:len l -1];
+					spawn exec->run(nil, t, r, nr, TRUE, nil, nil, FALSE);
+					# r is freed in run() 
+					continue;
+				'f' =>
+					if(len l < 1+5*12+1)
+						raise "e";
+					fontname = l[1+5*12:len l - 1];
+					ndumped = -1;
+				'F' =>
+					if(len l < 1+6*12+1)
+						raise "e";
+					fontname = l[1+6*12:len l - 1];
+					ndumped = int l[1+5*12:1+5*12+11];
+				'x' =>
+					if(len l < 1+5*12+1)
+						raise "e";
+					fontname = l[1+5*12: len l - 1];
+					ndumped = -1;
+					dumpid = int l[1+1*12:1+1*12+11];
+				* =>
+					raise "e";
+				}
+				l = l[0:len l -1];
+				if(len fontname != 0) {
+					fontr = fontname;
+					nfontr = len fontname;
+				}
+				else
+					(fontr, nfontr) = (nil, 0);
+				i = int l[1+0*12:1+0*12+11];
+				j = int l[1+1*12:1+1*12+11];
+				q0 = int l[1+2*12:1+2*12+11];
+				q1 = int l[1+3*12:1+3*12+11];
+				percent = int l[1+4*12:1+4*12+11];
+				if(i<0 || i>10)
+					raise "e";
+				if(i > row.ncol)
+					i = row.ncol;
+				c = row.col[i];
+				y = c.r.min.y+(percent*c.r.dy())/100;
+				if(y<c.r.min.y || y>=c.r.max.y)
+					y = -1;
+				if(dumpid == 0)
+					w = c.add(nil, nil, y);
+				else
+					w = c.add(nil, look->lookid(dumpid, TRUE), y);
+				if(w == nil)
+					continue;
+				w.dumpid = j;
+				(line, l) = rdline(b, line);
+				if(l == nil)
+					raise "e";
+				l = l[0:len l - 1];
+				r = l[5*12:len l];
+				nr = len r;
+				ns = -1;
+				for(n=0; n<nr; n++){
+					if(r[n] == '/')
+						ns = n;
+					if(r[n] == ' ')
+						break;
+				}
+				if(dumpid == 0)
+					w.setname(r, n);
+				for(; n<nr; n++)
+					if(r[n] == '|')
+						break;
+				w.cleartag();
+				w.tag.insert(w.tag.file.buf.nc, r[n+1:len r], nr-(n+1), TRUE, 0);
+				if(ndumped >= 0){
+					# simplest thing is to put it in a file and load that 
+					buf = sprint("/tmp/d%d.%.4sacme", sys->pctl(0, nil), utils->getuser());
+					bout = bufio->create(buf, Bufio->OWRITE, 8r600);
+					if(bout == nil){
+						warning(nil, "can't create temp file: %r\n");
+						b.close();
+						return;
+					}
+					for(n=0; n<ndumped; n++){
+						rune = b.getc();
+						if(rune == '\n')
+							line++;
+						if(rune == Bufio->EOF){
+							bout.close();
+							bout = nil;
+							raise "e";
+						}
+						bout.putc(rune);
+					}
+					bout.close();
+					bout = nil;
+					w.body.loadx(0, buf, 1);
+					w.body.file.mod = TRUE;
+					for(n=0; n<w.body.file.ntext; n++)
+						w.body.file.text[n].w.dirty = TRUE;
+					w.settag();
+					sys->remove(buf);
+					buf = nil;
+				}else if(dumpid==0 && r[ns+1]!='+' && r[ns+1]!='-')
+					exec->get(w.body, nil, nil, FALSE, nil, 0);
+				l = r = nil;
+				if(fontr != nil){
+					exec->fontx(w.body, nil, nil, fontr, nfontr);
+					fontr = nil;
+				}
+				if(q0>w.body.file.buf.nc || q1>w.body.file.buf.nc || q0>q1)
+					q0 = q1 = 0;
+				w.body.show(q0, q1, TRUE);
+				w.maxlines = min(w.body.frame.nlines, max(w.maxlines, w.body.frame.maxlines));
+			}
+			b.close();
+		}
+		exception{
+			* =>
+			 	warning(nil, sprint("bad load file %s:%d\n", file, line));
+				b.close();
+				raise "e";
+		}
+	}
+	exception{
+		* =>
+			return;
+	}
+}
+
+allwindows(o: int, aw: ref  Dat->Allwin)
+{
+	for(i:=0; i<row.ncol; i++){
+		c := row.col[i];
+		for(j:=0; j<c.nw; j++){
+			w := c.w[j];
+			case (o){
+			ALLLOOPER =>
+				pick k := aw{
+					LP => ecmd->alllooper(w, k.lp);
+				}
+			ALLTOFILE =>
+				pick k := aw{
+					FF => ecmd->alltofile(w, k.ff);
+				}
+			ALLMATCHFILE =>
+				pick k := aw{
+					FF => ecmd->allmatchfile(w, k.ff);
+				}
+			ALLFILECHECK =>
+				pick k := aw{
+					FC => ecmd->allfilecheck(w, k.fc);
+				}
+			ALLELOGTERM =>
+				edit->allelogterm(w);
+			ALLEDITINIT =>
+				edit->alleditinit(w);
+			ALLUPDATE =>
+				edit->allupdate(w);
+			}
+		}
+	}
+}
--- /dev/null
+++ b/appl/acme/row.m
@@ -1,0 +1,29 @@
+Rowm : module {
+	PATH : con "/dis/acme/row.dis";
+
+	init : fn(mods : ref Dat->Mods);
+
+	newrow : fn() : ref Row;
+
+	Row : adt {
+		qlock : ref Dat->Lock;
+		r : Draw->Rect;
+		tag : cyclic ref Textm->Text;
+		col : cyclic array of ref Columnm->Column;
+		ncol : int;
+
+		init : fn(r : self ref Row, re : Draw->Rect);
+		add : fn(r : self ref Row, c : ref Columnm->Column, n : int) : ref Columnm->Column;
+		close : fn(r : self ref Row, c : ref Columnm->Column, n : int);
+		which : fn(r : self ref Row, p : Draw->Point) : ref Textm->Text;
+		whichcol : fn(r : self ref Row, p : Draw->Point) : ref Columnm->Column;
+		reshape : fn(r : self ref Row, re : Draw->Rect);
+		typex : fn(r : self ref Row, ru : int, p : Draw->Point) : ref Textm->Text;
+		dragcol : fn(r : self ref Row, c : ref Columnm->Column);
+		clean : fn(r : self ref Row, exiting : int) : int;
+		dump : fn(r : self ref Row, b : string);
+		loadx : fn(r : self ref Row, b : string, n : int);
+	};
+
+	allwindows: fn(a0: int, aw: ref Dat->Allwin);
+};
--- /dev/null
+++ b/appl/acme/scrl.b
@@ -1,0 +1,187 @@
+implement Scroll;
+
+include "common.m";
+
+sys : Sys;
+drawm : Draw;
+acme : Acme;
+graph : Graph;
+utils : Utils;
+gui : Gui;
+dat : Dat;
+framem : Framem;
+textm : Textm;
+timerm : Timerm;
+
+BORD, BACK : import Framem;
+FALSE, TRUE, XXX, Maxblock : import Dat;
+error, warning : import utils;
+Point, Rect, Image, Display : import drawm;
+draw : import graph;
+black, white, display : import gui;
+mouse, cmouse : import dat;
+Frame : import framem;
+Timer : import Dat;
+Text : import textm;
+frgetmouse : import acme;
+mainwin : import gui;
+
+init(mods : ref Dat->Mods)
+{
+	sys = mods.sys;
+	drawm = mods.draw;
+	acme = mods.acme;
+	graph = mods.graph;
+	utils = mods.utils;
+	gui = mods.gui;
+	dat = mods.dat;
+	framem = mods.framem;
+	textm = mods.textm;
+	timerm = mods.timerm;
+}
+
+scrpos(r : Rect, p0 : int, p1 : int, tot : int) : Rect
+{
+	h : int;
+	q : Rect;
+
+	q = r;
+	# q = r.inset(1);
+	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;
+}
+
+scrx : ref Image;
+
+scrresize()
+{
+	scrx = nil;
+	h := 1024;
+	if (display != nil)
+		h = display.image.r.dy();
+	rr := Rect((0, 0), (32, h));
+	scrx = graph->balloc(rr, mainwin.chans, Draw->White);
+	if(scrx == nil)
+		error("scroll balloc");
+}
+
+scrdraw(t : ref Text)
+{
+	r, r1, r2 : Rect;
+
+	if(t.w==nil || t.what!=Textm->Body || t != t.w.body)
+		return;
+	if(scrx == nil)
+		scrresize();
+	r = t.scrollr;
+	b := scrx;
+	r1 = r;
+	# r.min.x += 1;	# border between margin and bar 
+	r1.min.x = 0;
+	r1.max.x = r.dx();
+	r2 = scrpos(r1, t.org, t.org+t.frame.nchars, t.file.buf.nc);
+	if(!r2.eq(t.lastsr)){
+		t.lastsr = r2;
+		draw(b, r1, t.frame.cols[BORD], nil, (0, 0));
+		draw(b, r2, t.frame.cols[BACK], nil, (0, 0));
+		r2.min.x = r2.max.x-1;
+		draw(b, r2, t.frame.cols[BORD], nil, (0, 0));
+		draw(t.frame.b, r, b, nil, (0, r1.min.y));
+		# bflush();
+	}
+}
+
+scrsleep(dt : int)
+{
+	timer : ref Timer;
+
+	timer = timerm->timerstart(dt);
+	graph->bflush();
+	# only run from mouse task, so safe to use cmouse 
+	alt{
+	<-(timer.c) =>
+		timerm->timerstop(timer);
+	*mouse = *<-cmouse =>
+		spawn timerm->timerwaittask(timer);
+	}
+}
+
+scroll(t : ref Text, but : int)
+{
+	p0, oldp0 : int;
+	s : Rect;
+	x, y, my, h, first : int;
+
+	s = t.scrollr.inset(1);
+	h = s.max.y-s.min.y;
+	x = (s.min.x+s.max.x)/2;
+	oldp0 = ~0;
+	first = TRUE;
+	do{
+		graph->bflush();
+		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;
+			if(y > s.max.y-2)
+				y = s.max.y-2;
+			if(t.file.buf.nc > 1024*1024)
+				p0 = ((t.file.buf.nc>>10)*(y-s.min.y)/h)<<10;
+			else
+				p0 = t.file.buf.nc*(y-s.min.y)/h;
+			if(oldp0 != p0)
+				t.setorigin(p0, FALSE);
+			oldp0 = p0;
+			frgetmouse();
+			continue;
+		}
+		if(but == 1) {
+			p0 = t.backnl(t.org, (my-s.min.y)/t.frame.font.height);
+			if(p0 == t.org)
+				p0 = t.backnl(t.org, 1);
+		}
+		else {
+			p0 = t.org+framem->frcharofpt(t.frame, (s.max.x, my));
+			if(p0 == t.org)
+				p0 = t.forwnl(t.org, 1);
+		}
+		if(oldp0 != p0)
+			t.setorigin(p0, TRUE);	
+		oldp0 = p0;
+		# debounce 
+		if(first){
+			graph->bflush();
+			sys->sleep(200);
+			alt {
+			*mouse = *<-cmouse =>
+				;
+			* =>
+				;
+			}
+			first = FALSE;
+		}
+		scrsleep(80);
+	}while(mouse.buttons & (1<<(but-1)));
+	while(mouse.buttons)
+		frgetmouse();
+}
--- /dev/null
+++ b/appl/acme/scrl.m
@@ -1,0 +1,9 @@
+Scroll : module {
+	PATH : con "/dis/acme/scrl.dis";
+
+	init : fn(mods : ref Dat->Mods);
+	scrsleep : fn(n : int);
+	scrdraw : fn(t : ref Textm->Text);
+	scrresize : fn();
+	scroll : fn(t : ref Textm->Text, but : int);
+};
--- /dev/null
+++ b/appl/acme/styxaux.b
@@ -1,0 +1,174 @@
+implement Styxaux;
+
+include "sys.m";
+	sys: Sys;
+include "styx.m";
+include "styxaux.m";
+
+Tmsg : import Styx;
+
+init()
+{
+}
+
+msize(m: ref Tmsg): int
+{
+	pick fc := m {
+		Version =>	return fc.msize;
+	}
+	error("bad styx msize", m);
+	return 0;
+}
+
+version(m: ref Tmsg): string
+{
+	pick fc := m {
+		Version =>	return fc.version;
+	}
+	error("bad styx version", m);
+	return nil;
+}
+
+fid(m: ref Tmsg): int
+{
+	pick fc := m {
+		Readerror =>	return 0;
+		Version =>	return 0;
+		Flush =>		return 0;
+		Walk =>		return fc.fid;
+		Open =>		return fc.fid;
+		Create =>		return fc.fid;
+		Read =>		return fc.fid;
+		Write =>		return fc.fid;
+		Clunk =>		return fc.fid;
+		Remove =>	return fc.fid;
+		Stat =>		return fc.fid;
+		Wstat =>		return fc.fid;
+		Attach =>		return fc.fid;
+	}
+	error("bad styx fid", m);
+	return 0;
+}
+
+uname(m: ref Tmsg): string
+{
+	pick fc := m {
+		Attach =>		return fc.uname;
+	}
+	error("bad styx uname", m);
+	return nil;
+}
+
+aname(m: ref Tmsg): string
+{
+	pick fc := m {
+		Attach =>		return fc.aname;
+	}
+	error("bad styx aname", m);
+	return nil;
+}
+
+newfid(m: ref Tmsg): int
+{
+	pick fc := m {
+		Walk =>		return fc.newfid;
+	}
+	error("bad styx newfd", m);
+	return 0;
+}
+
+name(m: ref Tmsg): string
+{
+	pick fc := m {
+		Create =>		return fc.name;
+	}
+	error("bad styx name", m);
+	return nil;
+}
+
+names(m: ref Tmsg): array of string
+{
+	pick fc := m {
+		Walk =>		return fc.names;
+	}
+	error("bad styx names", m);
+	return nil;
+}
+
+mode(m: ref Tmsg): int
+{
+	pick fc := m {
+		Open =>		return fc.mode;
+	}
+	error("bad styx mode", m);
+	return 0;
+}
+
+setmode(m: ref Tmsg, mode: int)
+{
+	pick fc := m {
+		Open =>		fc.mode = mode;
+		* =>			error("bad styx setmode", m);
+	}
+}
+
+offset(m: ref Tmsg): big
+{
+	pick fc := m {
+		Read =>		return fc.offset;
+		Write =>		return fc.offset;
+	}
+	error("bad styx offset", m);
+	return big 0;
+}
+
+count(m: ref Tmsg): int
+{
+	pick fc := m {
+		Read =>		return fc.count;
+		Write =>		return len fc.data;
+	}
+	error("bad styx count", m);
+	return 0;
+}
+
+setcount(m: ref Tmsg, count: int)
+{
+	pick fc := m {
+		Read =>		fc.count = count;
+		* =>			error("bad styx setcount", m);
+	}
+}
+
+oldtag(m: ref Tmsg): int
+{
+	pick fc := m {
+		Flush =>		return fc.oldtag;
+	}
+	error("bad styx oldtag", m);
+	return 0;
+}
+
+data(m: ref Tmsg): array of byte
+{
+	pick fc := m {
+		Write =>		return fc.data;
+	}
+	error("bad styx data", m);
+	return nil;
+}
+
+setdata(m: ref Tmsg, data: array of byte)
+{
+	pick fc := m {
+		Write =>		fc.data = data;
+		* =>			error("bad styx setdata", m);
+	}
+}
+
+error(s: string, m: ref Tmsg)
+{
+	if(sys == nil)
+		sys = load Sys Sys->PATH;
+	sys->fprint(sys->fildes(2), "%s %d\n", s, tagof m);
+}
--- /dev/null
+++ b/appl/acme/styxaux.m
@@ -1,0 +1,24 @@
+Styxaux : module {
+	PATH : con "/dis/acme/styxaux.dis";
+
+	init : fn();
+
+	msize: fn(m: ref Styx->Tmsg): int;
+	version: fn(m: ref Styx->Tmsg): string;
+	fid: fn(m: ref Styx->Tmsg): int;
+	uname: fn(m: ref Styx->Tmsg): string;
+	aname: fn(m: ref Styx->Tmsg): string;
+	newfid: fn(m: ref Styx->Tmsg): int;
+	name: fn(m: ref Styx->Tmsg): string;
+	names: fn(m: ref Styx->Tmsg): array of string;
+	mode: fn(m: ref Styx->Tmsg): int;
+	offset: fn(m: ref Styx->Tmsg): big;
+	count: fn(m: ref Styx->Tmsg): int;
+	oldtag: fn(m: ref Styx->Tmsg): int;
+	data: fn(m: ref Styx->Tmsg): array of byte;
+
+	setmode: fn(m: ref Styx->Tmsg, mode: int);
+	setcount: fn(m: ref Styx->Tmsg, count: int);
+	setdata: fn(m: ref Styx->Tmsg, data: array of byte);
+
+};
--- /dev/null
+++ b/appl/acme/text.b
@@ -1,0 +1,1455 @@
+implement Textm;
+
+include "common.m";
+include "keyboard.m";
+
+sys : Sys;
+utils : Utils;
+framem : Framem;
+drawm : Draw;
+acme : Acme;
+graph : Graph;
+gui : Gui;
+dat : Dat;
+scrl : Scroll;
+bufferm : Bufferm;
+filem : Filem;
+columnm : Columnm;
+windowm : Windowm;
+exec : Exec;
+
+Dir, sprint : import sys;
+frgetmouse : import acme;
+min, warning, error, stralloc, strfree, isalnum : import utils;
+Frame, frinsert, frdelete, frptofchar, frcharofpt, frselect, frdrawsel, frdrawsel0, frtick : import framem;
+BUFSIZE, Astring, SZINT, TRUE, FALSE, XXX, Reffont, Dirlist,Scrollwid, Scrollgap, seq, mouse : import dat;
+EM_NORMAL, EM_RAW, EM_MASK : import dat;
+ALPHA_LATIN, ALPHA_GREEK, ALPHA_CYRILLIC: import Dat;
+BACK, TEXT, HIGH, HTEXT : import Framem;
+Flushon, Flushoff : import Draw;
+Point, Display, Rect, Image : import drawm;
+charwidth, bflush, draw : import graph;
+black, white, mainwin, display : import gui;
+Buffer : import bufferm;
+File : import filem;
+Column : import columnm;
+Window : import windowm;
+scrdraw : import scrl;
+
+cvlist: adt {
+	ld: int;
+	nm: string;
+	si: string;
+	so: string;
+};
+
+#	"@@",  "'EKSTYZekstyz   ",	"ьЕКСТЫЗекстызъЁё",
+
+latintab := array[] of {
+	cvlist(
+		ALPHA_LATIN,
+		"latin",
+		nil,
+		nil
+	),
+	cvlist(
+		ALPHA_GREEK,
+		"greek",
+		"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
+		"ΑΒΞΔΕΦΓΘΙΪΚΛΜΝΟΠΨΡΣΤΥΫΩΧΗΖαβξδεφγθιϊκλμνοπψρστυϋωχηζ"
+	),
+	cvlist(
+		ALPHA_CYRILLIC,
+		"cyrillic",
+		"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
+		"АБЧДЭФГШИЙХЛМНОПЕРЩЦУВЮХЯЖабчдэфгшийхлмноперщцувюхяж"
+	),
+	cvlist(-1, nil, nil, nil)
+};
+
+alphabet := ALPHA_LATIN;	# per window perhaps
+
+setalphabet(s: string)
+{
+	for(a := 0; latintab[a].ld != -1; a++){
+		k := latintab[a].ld;
+		for(i := 0; latintab[i].ld != -1; i++){
+			if(s == transs(latintab[i].nm, k)){
+				alphabet = latintab[i].ld;
+				return;
+			}
+		}
+	}
+}
+
+transc(c: int, k: int): int
+{
+	for(i := 0; latintab[i].ld != -1; i++){
+		if(k == latintab[i].ld){
+			si := latintab[i].si;
+			so := latintab[i].so;
+			ln := len si;
+			for(j := 0; j < ln; j++)
+				if(c == si[j])
+					return so[j];
+		}
+	}
+	return c;
+}
+
+transs(s: string, k: int): string
+{
+	ln := len s;
+	for(i := 0; i < ln; i++)
+		s[i] = transc(s[i], k);
+	return s;
+}
+
+init(mods : ref Dat->Mods)
+{
+	sys = mods.sys;
+	framem = mods.framem;
+	dat = mods.dat;
+	utils = mods.utils;
+	drawm = mods.draw;
+	acme = mods.acme;
+	graph = mods.graph;
+	gui = mods.gui;
+	scrl = mods.scroll;
+	bufferm = mods.bufferm;
+	filem = mods.filem;
+	columnm = mods.columnm;
+	windowm = mods.windowm;
+	exec = mods.exec;
+}
+
+TABDIR : con 3;	# width of tabs in directory windows
+
+# remove eventually
+KF : con 16rF000;
+Kup : con KF | 16r0E;
+Kleft : con KF | 16r11;
+Kright : con KF | 16r12;
+Kend : con KF | 16r18;
+Kdown : con 16r80;
+
+
+nulltext : Text;
+
+newtext() : ref Text
+{
+	t := ref nulltext;
+	t.frame = framem->newframe();
+	return t;
+}
+
+Text.init(t : self ref Text, f : ref File, r : Rect, rf : ref Dat->Reffont, cols : array of ref Image)
+{
+	t.file = f;
+	t.all = r;
+	t.scrollr = r;
+	t.scrollr.max.x = r.min.x+Scrollwid;
+	t.lastsr = dat->nullrect;
+	r.min.x += Scrollwid+Scrollgap;
+	t.eq0 = ~0;
+	t.ncache = 0;
+	t.reffont = rf;
+	t.tabstop = dat->maxtab;
+	for(i:=0; i<Framem->NCOL; i++)
+		t.frame.cols[i] = cols[i];
+	t.redraw(r, rf.f, mainwin, -1);
+}
+
+Text.redraw(t : self ref Text, r : Rect, f : ref Draw->Font, b : ref Image, odx : int)
+{
+	framem->frinit(t.frame, r, f, b, t.frame.cols);
+	rr := t.frame.r;
+	rr.min.x -= Scrollwid;	# back fill to scroll bar
+	draw(t.frame.b, rr, t.frame.cols[Framem->BACK], nil, (0, 0));
+	# use no wider than 3-space tabs in a directory
+	maxt := dat->maxtab;
+	if(t.what == Body){
+		if(t.w != nil && t.w.isdir)
+			maxt = min(TABDIR, dat->maxtab);
+		else
+			maxt = t.tabstop;
+	}
+	t.frame.maxtab = maxt*charwidth(f, '0');
+	# c = '0';
+	# if(t.what==Body && t.w!=nil && t.w.isdir)
+	#	c = ' ';
+	# t.frame.maxtab = Dat->Maxtab*charwidth(f, c);
+	if(t.what==Body && t.w.isdir && odx!=t.all.dx()){
+		if(t.frame.maxlines > 0){
+			t.reset();
+			t.columnate(t.w.dlp,  t.w.ndl);
+			t.show(0, 0, TRUE);
+		}
+	}else{
+		t.fill();
+		t.setselect(t.q0, t.q1);
+	}
+}
+
+Text.reshape(t : self ref Text, r : Rect) : int
+{
+	odx : int;
+
+	if(r.dy() > 0)
+		r.max.y -= r.dy()%t.frame.font.height;
+	else
+		r.max.y = r.min.y;
+	odx = t.all.dx();
+	t.all = r;
+	t.scrollr = r;
+	t.scrollr.max.x = r.min.x+Scrollwid;
+	t.lastsr = dat->nullrect;
+	r.min.x += Scrollwid+Scrollgap;
+	framem->frclear(t.frame, 0);
+	# t.redraw(r, t.frame.font, t.frame.b, odx);
+	t.redraw(r, t.frame.font, mainwin, odx);
+	return r.max.y;
+}
+
+Text.close(t : self ref Text)
+{
+	t.cache = nil;
+	framem->frclear(t.frame, 1);
+	t.file.deltext(t);
+	t.file = nil;
+	t.reffont.close();
+	if(dat->argtext == t)
+		dat->argtext = nil;
+	if(dat->typetext == t)
+		dat->typetext = nil;
+	if(dat->seltext == t)
+		dat->seltext = nil;
+	if(dat->mousetext == t)
+		dat->mousetext = nil;
+	if(dat->barttext == t)
+		dat->barttext = nil;
+}
+
+dircmp(da : ref Dirlist, db : ref Dirlist) : int
+{
+	if (da.r < db.r)
+		return -1;
+	if (da.r > db.r)
+		return 1;
+	return 0;
+}
+
+qsort(a : array of ref Dirlist, n : int)
+{
+	i, j : int;
+	t : ref Dirlist;
+
+	while(n > 1) {
+		i = n>>1;
+		t = a[0]; a[0] = a[i]; a[i] = t;
+		i = 0;
+		j = n;
+		for(;;) {
+			do
+				i++;
+			while(i < n && dircmp(a[i], a[0]) < 0);
+			do
+				j--;
+			while(j > 0 && dircmp(a[j], a[0]) > 0);
+			if(j < i)
+				break;
+			t = a[i]; a[i] = a[j]; a[j] = t;
+		}
+		t = a[0]; a[0] = a[j]; a[j] = t;
+		n = n-j-1;
+		if(j >= n) {
+			qsort(a, j);
+			a = a[j+1:];
+		} else {
+			qsort(a[j+1:], n);
+			n = j;
+		}
+	}
+}
+
+Text.columnate(t : self ref Text, dlp : array of ref Dirlist, ndl : int)
+{
+	i, j, w, colw, mint, maxt, ncol, nrow : int;
+	dl : ref Dirlist;
+	q1 : int;
+
+	if(t.file.ntext > 1)
+		return;
+	mint = charwidth(t.frame.font, '0');
+	# go for narrower tabs if set more than 3 wide
+	t.frame.maxtab = min(dat->maxtab, TABDIR)*mint;
+	maxt = t.frame.maxtab;
+	colw = 0;
+	for(i=0; i<ndl; i++){
+		dl = dlp[i];
+		w = dl.wid;
+		if(maxt-w%maxt < mint)
+			w += mint;
+		if(w % maxt)
+			w += maxt-(w%maxt);
+		if(w > colw)
+			colw = w;
+	}
+	if(colw == 0)
+		ncol = 1;
+	else
+		ncol = utils->max(1, t.frame.r.dx()/colw);
+	nrow = (ndl+ncol-1)/ncol;
+
+	q1 = 0;
+	for(i=0; i<nrow; i++){
+		for(j=i; j<ndl; j+=nrow){
+			dl = dlp[j];
+			t.file.insert(q1, dl.r, len dl.r);
+			q1 += len dl.r;
+			if(j+nrow >= ndl)
+				break;
+			w = dl.wid;
+			if(maxt-w%maxt < mint){
+				t.file.insert(q1, "\t", 1);
+				q1++;
+				w += mint;
+			}
+			do{
+				t.file.insert(q1, "\t", 1);
+				q1++;
+				w += maxt-(w%maxt);
+			}while(w < colw);
+		}
+		t.file.insert(q1, "\n", 1);
+		q1++;
+	}
+}
+
+Text.loadx(t : self ref Text, q0 : int, file : string, setqid : int) : int
+{
+	rp : ref Astring;
+	dl : ref Dirlist;
+	dlp : array of ref Dirlist;
+	i, n, ndl : int;
+	fd : ref Sys->FD;
+	q, q1 : int;
+	d : Dir;
+	u : ref Text;
+	ok : int;
+
+	if(t.ncache!=0 || t.file.buf.nc || t.w==nil || t!=t.w.body || (t.w.isdir && t.file.name==nil))
+		error("text.load");
+
+	{
+		fd = sys->open(file, Sys->OREAD);
+		if(fd == nil){
+			warning(nil, sprint("can't open %s: %r\n", file));
+			raise "e";
+		}
+		(ok, d) = sys->fstat(fd);
+		if(ok){
+			warning(nil, sprint("can't fstat %s: %r\n", file));
+			raise "e";
+		}
+		if(d.qid.qtype & Sys->QTDIR){
+			# this is checked in get() but it's possible the file changed underfoot 
+			if(t.file.ntext > 1){
+				warning(nil, sprint("%s is a directory; can't read with multiple windows on it\n", file));
+				raise "e";
+			}
+			t.w.isdir = TRUE;
+			t.w.filemenu = FALSE;
+			if(t.file.name[len t.file.name-1] != '/')
+				t.w.setname(t.file.name + "/", len t.file.name+1);
+			dlp = nil;
+			ndl = 0;
+			for(;;){
+				(nd, dbuf) := sys->dirread(fd);
+				if(nd <= 0)
+					break;
+				for(i=0; i<nd; i++){
+					dl = ref Dirlist;
+					dl.r = dbuf[i].name;
+					if(dbuf[i].mode & Sys->DMDIR)
+						dl.r = dl.r + "/";
+					dl.wid = graph->strwidth(t.frame.font, dl.r);
+					ndl++;
+					odlp := dlp;
+					dlp = array[ndl] of ref Dirlist;
+					dlp[0:] = odlp[0:ndl-1];
+					odlp = nil;
+					dlp[ndl-1] = dl;
+				}
+			}
+			qsort(dlp, ndl);
+			t.w.dlp = dlp;
+			t.w.ndl = ndl;
+			t.columnate(dlp, ndl);
+			q1 = t.file.buf.nc;
+		}else{
+			tmp : int;
+	
+			t.w.isdir = FALSE;
+			t.w.filemenu = TRUE;
+			tmp = t.file.loadx(q0, fd);
+			q1 = q0 + tmp;
+		}
+		fd = nil;
+		if(setqid){
+			t.file.dev = d.dev;
+			t.file.mtime = d.mtime;
+			t.file.qidpath = d.qid.path;
+		}
+		rp = stralloc(BUFSIZE);
+		for(q=q0; q<q1; q+=n){
+			n = q1-q;
+			if(n > Dat->BUFSIZE)
+				n = Dat->BUFSIZE;
+			t.file.buf.read(q, rp, 0, n);
+			if(q < t.org)
+				t.org += n;
+			else if(q <= t.org+t.frame.nchars)
+				frinsert(t.frame, rp.s, n, q-t.org);
+			if(t.frame.lastlinefull)
+				break;
+		}
+		strfree(rp);
+		rp = nil;
+		for(i=0; i<t.file.ntext; i++){
+			u = t.file.text[i];
+			if(u != t){
+				if(u.org > u.file.buf.nc)	# will be 0 because of reset(), but safety first 
+					u.org = 0;
+				u.reshape(u.all);
+				u.backnl(u.org, 0);	# go to beginning of line 
+			}
+			u.setselect(q0, q0);
+		}
+		return q1-q0;
+	}
+	exception{
+		* =>
+			fd = nil;
+			return 0;
+	}
+	return 0;
+}
+
+Text.bsinsert(t : self ref Text, q0 : int, r : string, n : int, tofile : int) : (int, int)
+{
+	tp : ref Astring;
+	bp, up : int;
+	i, initial : int;
+
+	{
+		if(t.what == Tag)	# can't happen but safety first: mustn't backspace over file name 
+			raise "e";
+		bp = 0;
+		for(i=0; i<n; i++)
+			if(r[bp++] == '\b'){
+				--bp;
+				initial = 0;
+				tp = utils->stralloc(n);
+				for (k := 0; k < i; k++)
+					tp.s[k] = r[k];
+				up = i;
+				for(; i<n; i++){
+					tp.s[up] = r[bp++];
+					if(tp.s[up] == '\b')
+						if(up == 0)
+							initial++;
+						else
+							--up;
+					else
+						up++;
+				}
+				if(initial){
+					if(initial > q0)
+						initial = q0;
+					q0 -= initial;
+					t.delete(q0, q0+initial, tofile);
+				}
+				n = up;
+				t.insert(q0, tp.s, n, tofile, 0);
+				strfree(tp);
+				tp = nil;
+				return (q0, n);
+			}
+		raise "e";
+		return(0, 0);
+	}
+	exception{
+		* =>
+			t.insert(q0, r, n, tofile, 0);
+			return (q0, n);
+	}
+	return (0, 0);
+}
+
+Text.insert(t : self ref Text, q0 : int, r : string, n : int, tofile : int, echomode : int)
+{
+	c, i : int;
+	u : ref Text;
+
+	if(tofile && t.ncache != 0)
+		error("text.insert");
+	if(n == 0)
+		return;
+	if(tofile){
+		t.file.insert(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 
+					u.insert(q0, r, n, FALSE, echomode);
+					u.setselect(u.q0, u.q1);
+					scrdraw(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.frame.nchars) {
+		if (echomode == EM_MASK && len r == 1 && r[0] != '\n')
+			frinsert(t.frame, "*", n, q0-t.org);
+		else
+			frinsert(t.frame, r, n, q0-t.org);
+	}
+	if(t.w != nil){
+		c = 'i';
+		if(t.what == Body)
+			c = 'I';
+		if(n <= Dat->EVENTSIZE)
+			t.w.event(sprint("%c%d %d 0 %d %s\n", c, q0, q0+n, n, r[0:n]));
+		else
+			t.w.event(sprint("%c%d %d 0 0 \n", c, q0, q0+n));
+	}
+}
+
+Text.fill(t : self ref Text)
+{
+	rp : ref Astring;
+	i, n, m, nl : int;
+
+	if(t.frame.lastlinefull || t.nofill)
+		return;
+	if(t.ncache > 0){
+		if(t.w != nil)
+			t.w.commit(t);
+		else
+			t.commit(TRUE);
+	}
+	rp = stralloc(BUFSIZE);
+	do{
+		n = t.file.buf.nc-(t.org+t.frame.nchars);
+		if(n == 0)
+			break;
+		if(n > 2000)	# educated guess at reasonable amount 
+			n = 2000;
+		t.file.buf.read(t.org+t.frame.nchars, rp, 0, n);
+		#
+		# it's expensive to frinsert more than we need, so
+		# count newlines.
+		#
+		 
+		nl = t.frame.maxlines-t.frame.nlines;
+		m = 0;
+		for(i=0; i<n; ){
+			if(rp.s[i++] == '\n'){
+				m++;
+				if(m >= nl)
+					break;
+			}
+		}
+		frinsert(t.frame, rp.s, i, t.frame.nchars);
+	}while(t.frame.lastlinefull == FALSE);
+	strfree(rp);
+	rp = nil;
+}
+
+Text.delete(t : self ref Text, q0 : int, q1 : int, tofile : int)
+{
+	n, p0, p1 : int;
+	i, c : int;
+	u : ref Text;
+
+	if(tofile && t.ncache != 0)
+		error("text.delete");
+	n = q1-q0;
+	if(n == 0)
+		return;
+	if(tofile){
+		t.file.delete(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 
+					u.delete(q0, q1, FALSE);
+					u.setselect(u.q0, u.q1);
+					scrdraw(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.frame.nchars){
+		p1 = q1 - t.org;
+		if(p1 > t.frame.nchars)
+			p1 = t.frame.nchars;
+		if(q0 < t.org){
+			t.org = q0;
+			p0 = 0;
+		}else
+			p0 = q0 - t.org;
+		frdelete(t.frame, p0, p1);
+		t.fill();
+	}
+	if(t.w != nil){
+		c = 'd';
+		if(t.what == Body)
+			c = 'D';
+		t.w.event(sprint("%c%d %d 0 0 \n", c, q0, q1));
+	}
+}
+
+onechar : ref Astring;
+
+Text.readc(t : self ref Text, q : int) : int
+{
+	if(t.cq0<=q && q<t.cq0+t.ncache)
+		return t.cache[q-t.cq0];
+	if (onechar == nil)
+		onechar = stralloc(1);
+	t.file.buf.read(q, onechar, 0, 1);
+	return onechar.s[0];
+}
+
+Text.bswidth(t : self ref Text, c : int) : int
+{
+	q, eq : int;
+	r : int;
+	skipping : int;
+
+	# there is known to be at least one character to erase 
+	if(c == 16r08)	# ^H: erase character 
+		return 1;
+	q = t.q0;
+	skipping = TRUE;
+	while(q > 0){
+		r = t.readc(q-1);
+		if(r == '\n'){		# eat at most one more character 
+			if(q == t.q0)	# eat the newline 
+				--q;
+			break; 
+		}
+		if(c == 16r17){
+			eq = isalnum(r);
+			if(eq && skipping)	# found one; stop skipping 
+				skipping = FALSE;
+			else if(!eq && !skipping)
+				break;
+		}
+		--q;
+	}
+	return t.q0-q;
+}
+
+Text.typex(t : self ref Text, r : int, echomode : int)
+{
+	q0, q1 : int;
+	nnb, nb, n, i : int;
+	u : ref Text;
+
+	if(alphabet != ALPHA_LATIN)
+		r = transc(r, alphabet);
+	if (echomode == EM_RAW && t.what == Body) {
+		if (t.w != nil) {
+			s := "a";
+			s[0] = r;
+			t.w.event(sprint("R0 0 0 1 %s\n", s));
+		}
+		return;
+	}
+	if(t.what!=Body && r=='\n')
+		return;
+	case r {
+		Dat->Kscrolldown=>
+			if(t.what == Body){
+				q0 = t.org+frcharofpt(t.frame, (t.frame.r.min.x, t.frame.r.min.y+2*t.frame.font.height));
+				t.setorigin(q0, FALSE);
+			}
+			return;
+		Dat->Kscrollup=>
+			if(t.what == Body){
+				q0 = t.backnl(t.org, 4);
+				t.setorigin(q0, FALSE);
+			}
+			return;		
+		Kdown or Keyboard->Down =>
+			n = t.frame.maxlines/3;
+			q0 = t.org+frcharofpt(t.frame, (t.frame.r.min.x, t.frame.r.min.y+n*t.frame.font.height));
+			t.setorigin(q0, FALSE);
+			return;
+		Keyboard->Pgdown =>
+			n = 2*t.frame.maxlines/3;
+			q0 = t.org+frcharofpt(t.frame, (t.frame.r.min.x, t.frame.r.min.y+n*t.frame.font.height));
+			t.setorigin(q0, FALSE);
+			return;
+		Kup or Keyboard->Up =>
+			n = t.frame.maxlines/3;
+			q0 = t.backnl(t.org, n);
+			t.setorigin(q0, FALSE);
+			return;
+		Keyboard->Pgup =>
+			n = 2*t.frame.maxlines/3;
+			q0 = t.backnl(t.org, n);
+			t.setorigin(q0, FALSE);
+			return;
+		Keyboard->Home =>
+			t.commit(TRUE);
+			t.show(0, 0, FALSE);
+			return;
+		Kend or Keyboard->End =>
+			t.commit(TRUE);
+			t.show(t.file.buf.nc, t.file.buf.nc, FALSE);
+			return;
+		Kleft or Keyboard->Left =>
+			t.commit(TRUE);
+			if(t.q0 != t.q1)
+				t.show(t.q0, t.q0, TRUE);
+			else if(t.q0 != 0)
+				t.show(t.q0-1, t.q0-1, TRUE);
+			return;
+		Kright or Keyboard->Right =>
+			t.commit(TRUE);
+			if(t.q0 != t.q1)
+				t.show(t.q1, t.q1, TRUE);
+			else if(t.q1 != t.file.buf.nc)
+				t.show(t.q1+1, t.q1+1, TRUE);
+			return;
+		1 =>  	# ^A: beginning of line
+			t.commit(TRUE);
+			# go to where ^U would erase, if not already at BOL
+			nnb = 0;
+			if(t.q0>0 && t.readc(t.q0-1)!='\n')
+				nnb = t.bswidth(16r15);
+			t.show(t.q0-nnb, t.q0-nnb, TRUE);
+			return;
+		5 =>  	# ^E: end of line
+			t.commit(TRUE);
+			q0 = t.q0;
+			while(q0<t.file.buf.nc && t.readc(q0)!='\n')
+				q0++;
+			t.show(q0, q0, TRUE);
+			return;
+	}
+	if(t.what == Body){
+		seq++;
+		t.file.mark();
+	}
+	if(t.q1 > t.q0){
+		if(t.ncache != 0)
+			error("text.type");
+		exec->cut(t, t, TRUE, TRUE);
+		t.eq0 = ~0;
+		if (r == 16r08 || r == 16r7f){	# erase character : odd if a char then erased
+			t.show(t.q0, t.q0,TRUE);
+			return;
+		}
+	}
+	t.show(t.q0, t.q0, TRUE);
+	case(r){
+	16r1B =>
+		if(t.eq0 != ~0)
+			t.setselect(t.eq0, t.q0);
+		if(t.ncache > 0){
+			if(t.w != nil)
+				t.w.commit(t);
+			else
+				t.commit(TRUE);
+		}
+		return;
+	16r08 or 16r15 or 16r17 =>
+		# ^H: erase character or ^U: erase line or ^W: erase word 
+		if(t.q0 == 0)
+			return;
+if(0)	# DEBUGGING 
+	for(i=0; i<t.file.ntext; i++){
+		u = t.file.text[i];
+		if(u.cq0!=t.cq0 && (u.ncache!=t.ncache || t.ncache!=0))
+			error("text.type inconsistent caches");
+	}
+		nnb = t.bswidth(r);
+		q1 = t.q0;
+		q0 = q1-nnb;
+		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;
+				u.delete(q1-n, q1, FALSE);
+				nb -= n;
+			}
+			if(u.eq0==q1 || u.eq0==~0)
+				u.eq0 = q0;
+			if(nb && u==t)
+				u.delete(q0, q0+nb, TRUE);
+			if(u != t)
+				u.setselect(u.q0, u.q1);
+			else
+				t.setselect(q0, q0);
+			u.nofill = FALSE;
+		}
+		for(i=0; i<t.file.ntext; i++)
+			t.file.text[i].fill();
+		return;
+	16r7f or Keyboard->Del =>
+		# Delete character - forward delete
+		t.commit(TRUE);
+		if(t.q0 >= t.file.buf.nc)
+			return;
+		nnb = 1;
+		q0 = t.q0;
+		q1 = q0+nnb;
+		for(i=0; i<t.file.ntext; i++){
+			u = t.file.text[i];
+			if (u!=t)
+				u.commit(FALSE);
+			u.nofill = TRUE;
+			if(u.eq0==q1 || u.eq0==~0)
+				u.eq0 = q0;
+			if(u==t)
+				u.delete(q0, q1, TRUE);
+			if(u != t)
+				u.setselect(u.q0, u.q1);
+			else
+				t.setselect(q0, q0);
+			u.nofill = FALSE;
+		}
+		for(i=0; i<t.file.ntext; i++)
+			t.file.text[i].fill();
+		return;
+	}
+	# otherwise ordinary character; just insert, typically in caches of all texts 
+if(0)	# DEBUGGING 
+	for(i=0; i<t.file.ntext; i++){
+		u = t.file.text[i];
+		if(u.cq0!=t.cq0 && (u.ncache!=t.ncache || t.ncache!=0))
+			error("text.type inconsistent caches");
+	}
+	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");
+		str := "Z";
+		str[0] = r;
+		u.insert(t.q0, str, 1, FALSE, echomode);
+		str = nil;
+		if(u != t)
+			u.setselect(u.q0, u.q1);
+		if(u.ncache == u.ncachealloc){
+			u.ncachealloc += 10;
+			u.cache += "1234567890";
+		}
+		u.cache[u.ncache++] = r;
+	}
+	t.setselect(t.q0+1, t.q0+1);
+	if(r=='\n' && t.w!=nil)
+		t.w.commit(t);
+}
+
+Text.commit(t : self ref Text, tofile : int)
+{
+	if(t.ncache == 0)
+		return;
+	if(tofile)
+		t.file.insert(t.cq0, t.cache, t.ncache);
+	if(t.what == Body){
+		t.w.dirty = TRUE;
+		t.w.utflastqid = -1;
+	}
+	t.ncache = 0;
+}
+
+clicktext : ref Text;
+clickmsec : int = 0;
+selecttext : ref Text;
+selectq : int = 0;
+
+#
+# called from frame library
+#
+ 
+framescroll(f : ref Frame, dl : int)
+{
+	if(f != selecttext.frame)
+		error("frameselect not right frame");
+	selecttext.framescroll(dl);
+}
+
+Text.framescroll(t : self ref Text, dl : int)
+{
+	q0 : int;
+
+	if(dl == 0){
+		scrl->scrsleep(100);
+		return;
+	}
+	if(dl < 0){
+		q0 = t.backnl(t.org, -dl);
+		if(selectq > t.org+t.frame.p0)
+			t.setselect0(t.org+t.frame.p0, selectq);
+		else
+			t.setselect0(selectq, t.org+t.frame.p0);
+	}else{
+		if(t.org+t.frame.nchars == t.file.buf.nc)
+			return;
+		q0 = t.org+frcharofpt(t.frame, (t.frame.r.min.x, t.frame.r.min.y+dl*t.frame.font.height));
+		if(selectq > t.org+t.frame.p1)
+			t.setselect0(t.org+t.frame.p1, selectq);
+		else
+			t.setselect0(selectq, t.org+t.frame.p1);
+	}
+	t.setorigin(q0, TRUE);
+}
+
+
+Text.select(t : self ref Text, double : int)
+{
+	q0, q1 : int;
+	b, x, y : int;
+	state : int;
+
+	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;
+	selectq = t.org+frcharofpt(t.frame, mouse.xy);
+	if(double || (clicktext==t && mouse.msec-clickmsec<500))
+	if(q0==q1 && selectq==q0){
+		(q0, q1) = t.doubleclick(q0, q1);
+		t.setselect(q0, q1);
+		bflush();
+		x = mouse.xy.x;
+		y = mouse.xy.y;
+		# stay here until something interesting happens 
+		do
+			frgetmouse();
+		while(mouse.buttons==b && utils->abs(mouse.xy.x-x)<3 && utils->abs(mouse.xy.y-y)<3);
+		mouse.xy.x = x;	# in case we're calling frselect 
+		mouse.xy.y = y;
+		q0 = t.q0;	# may have changed 
+		q1 = t.q1;
+		selectq = q0;
+	}
+	if(mouse.buttons == b){
+		t.frame.scroll = 1;
+		frselect(t.frame, mouse);
+		# horrible botch: while asleep, may have lost selection altogether 
+		if(selectq > t.file.buf.nc)
+			selectq = t.org + t.frame.p0;
+		t.frame.scroll = 0;
+		if(selectq < t.org)
+			q0 = selectq;
+		else
+			q0 = t.org + t.frame.p0;
+		if(selectq > t.org+t.frame.nchars)
+			q1 = selectq;
+		else
+			q1 = t.org+t.frame.p1;
+	}
+	if(q0 == q1){
+		if(q0==t.q0 && (double || clicktext==t && mouse.msec-clickmsec<500)){
+			(q0, q1) = t.doubleclick(q0, q1);
+			clicktext = nil;
+		}else{
+			clicktext = t;
+			clickmsec = mouse.msec;
+		}
+	}else
+		clicktext = nil;
+	t.setselect(q0, q1);
+	bflush();
+	state = 0;	# undo when possible; +1 for cut, -1 for paste 
+	while(mouse.buttons){
+		mouse.msec = 0;
+		b = mouse.buttons;
+		if(b & 6){
+			if(state==0 && t.what==Body){
+				seq++;
+				t.w.body.file.mark();
+			}
+			if(b & 2){
+				if(state==-1 && t.what==Body){
+					t.w.undo(TRUE);
+					t.setselect(q0, t.q0);
+					state = 0;
+				}else if(state != 1){
+					exec->cut(t, t, TRUE, TRUE);
+					state = 1;
+				}
+			}else{
+				if(state==1 && t.what==Body){
+					t.w.undo(TRUE);
+					t.setselect(q0, t.q1);
+					state = 0;
+				}else if(state != -1){
+					exec->paste(t, t, TRUE, FALSE);
+					state = -1;
+				}
+			}
+			scrdraw(t);
+			utils->clearmouse();
+		}
+		bflush();
+		while(mouse.buttons == b)
+			frgetmouse();
+		clicktext = nil;
+	}
+}
+
+Text.show(t : self ref Text, q0 : int, q1 : int, doselect : int)
+{
+	qe : int;
+	nl : int;
+	q : int;
+
+	if(t.what != Body){
+		if(doselect)
+			t.setselect(q0, q1);
+		return;
+	}
+	if(t.w!=nil && t.frame.maxlines==0)
+		t.col.grow(t.w, 1, 0);
+	if(doselect)
+		t.setselect(q0, q1);
+	qe = t.org+t.frame.nchars;
+	if(t.org<=q0 && (q0<qe || (q0==qe && qe==t.file.buf.nc+t.ncache)))
+		scrdraw(t);
+	else{
+		if(t.w.nopen[Dat->QWevent]>byte 0)
+			nl = 3*t.frame.maxlines/4;
+		else
+			nl = t.frame.maxlines/4;
+		q = t.backnl(q0, nl);
+		# avoid going backwards if trying to go forwards - long lines! 
+		if(!(q0>t.org && q<t.org))
+			t.setorigin(q, TRUE);
+		while(q0 > t.org+t.frame.nchars)
+			t.setorigin(t.org+1, FALSE);
+	}
+}
+
+region(a, b : int) : int
+{
+	if(a < b)
+		return -1;
+	if(a == b)
+		return 0;
+	return 1;
+}
+
+selrestore(f : ref Frame, pt0 : Point, p0 : int, p1 : int)
+{
+	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]);
+}
+
+Text.setselect(t : self ref Text, q0 : int, q1 : int)
+{
+	p0, p1 : int;
+
+	# 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.frame.nchars)
+		p0 = t.frame.nchars;
+	if(p1 > t.frame.nchars)
+		p1 = t.frame.nchars;
+	if(p0==t.frame.p0 && p1==t.frame.p1)
+		return;
+	# screen disagrees with desired selection
+	if(t.frame.p1<=p0 || p1<=t.frame.p0 || p0==p1 || t.frame.p1==t.frame.p0){
+		# no overlap or too easy to bother trying
+		frdrawsel(t.frame, frptofchar(t.frame, t.frame.p0), t.frame.p0, t.frame.p1, 0);
+		frdrawsel(t.frame, frptofchar(t.frame, p0), p0, p1, 1);
+		t.frame.p0 = p0;
+		t.frame.p1 = p1;
+		return;
+	}
+	# overlap; avoid unnecessary painting
+	if(p0 < t.frame.p0){
+		# extend selection backwards
+		frdrawsel(t.frame, frptofchar(t.frame, p0), p0, t.frame.p0, 1);
+	}else if(p0 > t.frame.p0){
+		# trim first part of selection
+		frdrawsel(t.frame, frptofchar(t.frame, t.frame.p0), t.frame.p0, p0, 0);
+	}
+	if(p1 > t.frame.p1){
+		# extend selection forwards
+		frdrawsel(t.frame, frptofchar(t.frame, t.frame.p1), t.frame.p1, p1, 1);
+	}else if(p1 < t.frame.p1){
+		# trim last part of selection
+		frdrawsel(t.frame, frptofchar(t.frame, p1), p1, t.frame.p1, 0);
+	}
+	t.frame.p0 = p0;
+	t.frame.p1 = p1;
+}
+
+Text.setselect0(t : self ref Text, q0 : int, q1 : int)
+{
+	t.q0 = q0;
+	t.q1 = q1;
+}
+
+xselect(f : ref Frame, mc : ref Draw->Pointer, col, colt : ref Image) : (int, int)
+{
+	p0, p1, q, tmp : int;
+	mp, pt0, pt1, qt : Point;
+	reg, b : int;
+
+	# when called button 1 is down
+	mp = mc.xy;
+	b = mc.buttons;
+
+	# 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, colt);
+			}
+			qt = frptofchar(f, q);
+			if(reg > 0){
+				if(q > p1)
+					frdrawsel0(f, pt1, p1, q, col, colt);
+				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, colt);
+			}
+			p1 = q;
+			pt1 = qt;
+		}
+		if(p0 == p1)
+			frtick(f, pt0, 1);
+		bflush();
+		frgetmouse();
+	}while(mc.buttons == b);
+	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);
+	bflush();
+	return (p0, p1);
+}
+
+Text.select23(t : self ref Text, q0 : int, q1 : int, high, low : ref Image, mask : int) : (int, int, int)
+{
+	p0, p1 : int;
+	buts : int;
+
+	(p0, p1) = xselect(t.frame, mouse, high, low);
+	buts = mouse.buttons;
+	if((buts & mask) == 0){
+		q0 = p0+t.org;
+		q1 = p1+t.org;
+	}
+	while(mouse.buttons)
+		frgetmouse();
+	return (buts, q0, q1);
+}
+
+Text.select2(t : self ref Text, q0 : int, q1 : int) : (int, ref Text, int, int)
+{
+	buts : int;
+	
+	(buts, q0, q1) = t.select23(q0, q1, acme->but2col, acme->but2colt, 4);
+	if(buts & 4)
+		return (0, nil, q0, q1);
+	if(buts & 1)	# pick up argument 
+		return (1, dat->argtext, q0, q1);
+	return (1, nil, q0, q1);
+}
+
+Text.select3(t : self ref Text, q0 : int, q1 : int) : (int, int, int)
+{
+	buts : int;
+	
+	(buts, q0, q1) = t.select23(q0, q1, acme->but3col, acme->but3colt, 1|2);
+	return (buts == 0, q0, q1);
+}
+
+left := array[4] of {
+	"{[(<«",
+	"\n",
+	"'\"`",
+	nil
+};
+right := array[4] of {
+	"}])>»",
+	"\n",
+	"'\"`",
+	nil
+};
+
+Text.doubleclick(t : self ref Text, q0 : int, q1 : int) : (int, int)
+{
+	c, i : int;
+	r, l : string;
+	p : int;
+	q : int;
+	res : int;
+
+	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 = t.readc(q-1);
+		p = utils->strchr(l, c);
+		if(p >= 0){
+			(res, q) = t.clickmatch(c, r[p], 1, q);
+			if (res)
+				q1 = q-(c!='\n');
+			return (q0, q1);
+		}
+		# try matching character to right, looking left 
+		if(q == t.file.buf.nc)
+			c = '\n';
+		else
+			c = t.readc(q);
+		p = utils->strchr(r, c);
+		if(p >= 0){
+			(res, q) = t.clickmatch(c, l[p], -1, q);
+			if (res){
+				q1 = q0+(q0<t.file.buf.nc && c=='\n');
+				q0 = q;
+				if(c!='\n' || q!=0 || t.readc(0)=='\n')
+					q0++;
+			}
+			return (q0, q1);
+		}
+	}
+	# try filling out word to right 
+	while(q1<t.file.buf.nc && isalnum(t.readc(q1)))
+		q1++;
+	# try filling out word to left 
+	while(q0>0 && isalnum(t.readc(q0-1)))
+		q0--;
+	return (q0, q1);
+}
+
+Text.clickmatch(t : self ref Text, cl : int, cr : int, dir : int, q : int) : (int, int)
+{
+	c : int;
+	nest : int;
+
+	nest = 1;
+	for(;;){
+		if(dir > 0){
+			if(q == t.file.buf.nc)
+				break;
+			c = t.readc(q);
+			q++;
+		}else{
+			if(q == 0)
+				break;
+			q--;
+			c = t.readc(q);
+		}
+		if(c == cr){
+			if(--nest==0)
+				return (1, q);
+		}else if(c == cl)
+			nest++;
+	}
+	return (cl=='\n' && nest==1, q);
+}
+
+Text.forwnl(t : self ref Text, p : int, n : int) : int
+{
+	i, j : int;
+
+	e := t.file.buf.nc-1;
+	i = n;
+	while(i-- > 0 && p<e){
+		++p;
+		if(p == e)
+			break;
+		for(j=128; --j>0 && p<e; p++)
+			if(t.readc(p)=='\n')
+				break;
+	}
+	return p;
+}
+
+Text.backnl(t : self ref Text, p : int, n : int) : int
+{
+	i, j : int;
+
+	# look for start of this line if n==0 
+	if(n==0 && p>0 && t.readc(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(t.readc(p-1)=='\n')
+				break;
+	}
+	return p;
+}
+
+Text.setorigin(t : self ref Text, org : int, exact : int)
+{
+	i, a : int;
+	r : ref Astring;
+	n : int;
+
+	t.frame.b.flush(Flushoff);
+	if(org>0 && !exact){
+		# org is an estimate of the char posn; find a newline 
+		# don't try harder than 256 chars 
+		for(i=0; i<256 && org<t.file.buf.nc; i++){
+			if(t.readc(org) == '\n'){
+				org++;
+				break;
+			}
+			org++;
+		}
+	}
+	a = org-t.org;
+	fixup := 0;
+	if(a>=0 && a<t.frame.nchars){
+		frdelete(t.frame, 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.frame.nchars){
+		n = t.org - org;
+		r = utils->stralloc(n);
+		t.file.buf.read(org, r, 0, n);
+		frinsert(t.frame, r.s, n, 0);
+		utils->strfree(r);
+		r = nil;
+	}else
+		frdelete(t.frame, 0, t.frame.nchars);
+	t.org = org;
+	t.fill();
+	scrdraw(t);
+	t.setselect(t.q0, t.q1);
+	if(fixup && t.frame.p1 > t.frame.p0)
+		frdrawsel(t.frame, frptofchar(t.frame, t.frame.p1-1), t.frame.p1-1, t.frame.p1, 1);
+	t.frame.b.flush(Flushon);
+}
+
+Text.reset(t : self ref Text)
+{
+	t.file.seq = 0;
+	t.eq0 = ~0;
+	# do t.delete(0, t.nc, TRUE) without building backup stuff 
+	t.setselect(t.org, t.org);
+	frdelete(t.frame, 0, t.frame.nchars);
+	t.org = 0;
+	t.q0 = 0;
+	t.q1 = 0;
+	t.file.reset();
+	t.file.buf.reset();
+}
--- /dev/null
+++ b/appl/acme/text.m
@@ -1,0 +1,65 @@
+Textm : module {
+	PATH : con "/dis/acme/text.dis";
+
+	init : fn(mods : ref Dat->Mods);
+
+	# Text.what
+	Columntag, Rowtag, Tag, Body : con iota;
+
+	newtext : fn() : ref Text;
+
+	Text : adt {
+		file : cyclic ref Filem->File;
+		frame : ref Framem->Frame;
+		reffont : ref Dat->Reffont;
+		org : int;
+		q0 : int;
+		q1 : int;
+		what : int;
+		tabstop : int;
+		w : cyclic ref Windowm->Window;
+		scrollr : Draw->Rect;
+		lastsr : Draw->Rect;
+		all : Draw->Rect;
+		row : cyclic ref Rowm->Row;
+		col : cyclic ref Columnm->Column;
+		eq0 : int;		# start of typing for ESC
+		cq0 : int;		# cache position
+		ncache : int;	# storage for insert
+		ncachealloc : int;
+		cache : string;
+		nofill : int;
+
+		init : fn(t : self ref Text, f : ref Filem->File, r : Draw->Rect, rf : ref Dat->Reffont, cols : array of ref Draw->Image);
+		redraw : fn(t : self ref Text, r : Draw->Rect, f : ref Draw->Font, b : ref Draw->Image, n : int);
+		insert : fn(t : self ref Text, n : int, s : string, p : int, q : int, r : int);
+		bsinsert : fn(t : self ref Text, n : int, s : string, p : int, q : int) : (int, int);
+		delete : fn(t : self ref Text, n : int, p : int, q : int);
+		loadx : fn(t : self ref Text, n : int, b : string, q : int) : int;
+		typex : fn(t : self ref Text, r : int, echomode : int);
+		select : fn(t : self ref Text, d : int);
+		select2 : fn(t : self ref Text, p : int, q : int) : (int, ref Text, int, int);
+		select3 : fn(t : self ref Text, p: int, q : int) : (int, int, int);
+		setselect : fn(t : self ref Text, p : int, q : int);
+		setselect0 : fn(t : self ref Text, p : int, q : int);
+		show : fn(t : self ref Text, p : int, q : int, dosel : int);
+		fill : fn(t : self ref Text);
+		commit : fn(t : self ref Text, n : int);
+		setorigin : fn(t : self ref Text, p : int, q : int);
+		readc : fn(t : self ref Text, n : int) : int;
+		reset : fn(t : self ref Text);
+		reshape : fn(t : self ref Text, r : Draw->Rect) : int;
+		close : fn(t : self ref Text);
+		framescroll : fn(t : self ref Text, n : int);
+		select23 : fn(t : self ref Text, p : int, q : int, i, it : ref Draw->Image, n : int) : (int, int, int);
+		forwnl : fn(t : self ref Text, p : int, q : int) : int;
+		backnl : fn(t : self ref Text, p : int, q : int) : int;
+		bswidth : fn(t : self ref Text, r : int) : int;
+		doubleclick : fn(t : self ref Text, p : int, q : int) : (int, int);
+		clickmatch : fn(t : self ref Text, p : int, q : int, r : int, n : int) : (int, int);
+		columnate : fn(t : self ref Text, d : array of ref Dat->Dirlist, n : int);
+	};
+
+	framescroll : fn(f : ref Framem->Frame, dl : int);
+	setalphabet: fn(s: string);
+};
--- /dev/null
+++ b/appl/acme/time.b
@@ -1,0 +1,129 @@
+implement Timerm;
+
+include "common.m";
+
+sys : Sys;
+acme : Acme;
+utils : Utils;
+dat : Dat;
+
+millisec : import sys;
+Timer : import dat;
+
+init(mods : ref Dat->Mods)
+{
+	sys = mods.sys;
+	acme = mods.acme;
+	utils = mods.utils;
+	dat = mods.dat;
+}
+
+ctimer : chan of ref Timer;
+
+timeproc()
+{
+	i, nt, na, dt : int;
+	x : ref Timer;
+	t : array of ref Timer;
+	old, new : int;
+
+	acme->timerpid = sys->pctl(0, nil);
+	sys->pctl(Sys->FORKFD, nil);
+	t = array[10] of ref Timer;
+	na = 10;
+	nt = 0;
+	old = millisec();
+	for(;;){
+		if (nt == 0) {	# don't waste cpu time
+			x = <-ctimer;
+			t[nt++] = x;
+			old = millisec();
+		}
+		sys->sleep(1);	# will sleep minimum incr 
+		new = millisec();
+		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;
+			if(x.dt <= 0){
+				#
+				# avoid possible deadlock if client is
+				# now sending on ctimer
+				#
+				 
+				alt {
+					x.c <-= 0 =>
+						t[i:] = t[i+1:nt];
+						t[nt-1] = nil;
+						nt--;
+						i--;
+					* =>
+						;
+				}
+			}
+		}
+		gotone := 1;
+		while (gotone) {
+			alt {
+				x = <-ctimer =>
+					if (nt == na) {
+						ot := t;
+						t = array[na+10] of ref Timer;
+						t[0:] = ot[0:na];
+						ot = nil;
+						na += 10;
+					}
+					t[nt++] = x;
+					old = millisec();
+				* =>
+					gotone = 0;
+			}
+		}
+	}
+}
+
+timerinit()
+{
+	ctimer = chan of ref Timer;
+	spawn timeproc();
+}
+
+#
+# timeralloc() and timerfree() don't lock, so can only be
+# called from the main proc.
+#
+ 
+
+timer : ref Timer;
+
+timerstart(dt : int) : ref Timer
+{
+	t : ref Timer;
+
+	t = timer;
+	if(t != nil)
+		timer = timer.next;
+	else{
+		t = ref Timer;
+		t.c = chan of int;
+	}
+	t.next = nil;
+	t.dt = dt;
+	ctimer <-= t;
+	return t;
+}
+
+timerstop(t : ref Timer)
+{
+	t.next = timer;
+	timer = t;
+}
+
+timerwaittask(timer : ref Timer)
+{
+	<-(timer.c);
+	timerstop(timer);
+}
--- /dev/null
+++ b/appl/acme/time.m
@@ -1,0 +1,10 @@
+Timerm : module {
+	PATH : con "/dis/acme/time.dis";
+
+	init : fn(mods : ref Dat->Mods);
+
+	timerinit: fn();
+	timerstart : fn(dt : int) : ref Dat->Timer;
+	timerstop : fn(t : ref Dat->Timer);
+	timerwaittask : fn(t : ref Dat->Timer);
+};
--- /dev/null
+++ b/appl/acme/util.b
@@ -1,0 +1,574 @@
+implement Utils;
+
+include "common.m";
+include "sh.m";
+include "env.m";
+
+sys : Sys;
+draw : Draw;
+gui : Gui;
+acme : Acme;
+dat : Dat;
+graph : Graph;
+textm : Textm;
+windowm : Windowm;
+columnm : Columnm;
+rowm : Rowm;
+scrl : Scroll;
+look : Look;
+
+RELEASECOPY : import acme;
+Point, Rect : import draw;
+Astring, TRUE, FALSE, Mntdir, Lock : import dat;
+mouse, activecol, seltext, row : import dat;
+cursorset : import graph;
+mainwin : import gui;
+Text : import textm;
+Window : import windowm;
+Column : import columnm;
+Row : import rowm;
+
+init(mods : ref Dat->Mods)
+{
+	sys = mods.sys;
+	draw = mods.draw;
+	gui = mods.gui;
+	acme = mods.acme;
+	dat = mods.dat;
+	graph = mods.graph;
+	textm = mods.textm;
+	windowm = mods.windowm;
+	columnm = mods.columnm;
+	rowm = mods.rowm;
+	scrl = mods.scroll;
+	look = mods.look;
+
+	stderr = sys->fildes(2);
+}
+
+min(x : int, y : int) : int
+{
+	if (x < y)
+		return x;
+	return y;
+}
+
+max(x : int, y : int) : int
+{
+	if (x > y)
+		return x;
+	return y;
+}
+
+abs(x : int) : int
+{
+	if (x < 0)
+		return -x;
+	return x;
+}
+
+isalnum(c : int) : int
+{
+	#
+	# Hard to get absolutely right.  Use what we know about ASCII
+	# and assume anything above the Latin control characters is
+	# potentially an alphanumeric.
+	#
+	if(c <= ' ')
+		return FALSE;
+	if(16r7F<=c && c<=16rA0)
+		return FALSE;
+	if(strchr("!\"#$%&'()*+,-./:;<=>?@[\\]^`{|}~", c) >= 0)
+		return FALSE;
+	return TRUE;
+	# return ('a' <= c && c <= 'z') || 
+	#	   ('A' <= c && c <= 'Z') ||
+	#	   ('0' <= c && c <= '9');
+}
+
+strchr(s : string, c : int) : int
+{
+	for (i := 0; i < len s; i++)
+		if (s[i] == c)
+			return i;
+	return -1;
+} 
+
+strrchr(s : string, c : int) : int
+{
+	for (i := len s - 1; i >= 0; i--)
+		if (s[i] == c)
+			return i;
+	return -1;
+}
+
+strncmp(s, t : string, n : int) : int
+{
+	if (len s > n)
+		s = s[0:n];
+	if (len t > n)
+		t = t[0:n];
+	if (s < t)
+		return -1;
+	if (s > t)
+		return 1;
+	return 0;
+}
+
+env : Env;
+
+getenv(s : string) : string
+{
+	if (env == nil)
+		env = load Env Env->PATH;
+	e := env->getenv(s);
+	if(e != nil && e[len e - 1] == '\n')	# shell bug
+		return e[0: len e -1];
+	return e;
+}
+
+setenv(s, t : string)
+{
+	if (env == nil)
+		env = load Env Env->PATH;
+	env->setenv(s, t);
+}
+
+stob(s : string, n : int) : array of byte
+{
+	b := array[2*n] of byte;
+	for (i := 0; i < n; i++) {
+		b[2*i] = byte (s[i]&16rff);
+		b[2*i+1] = byte ((s[i]>>8)&16rff);
+	}
+	return b;
+}
+
+btos(b : array of byte, s : ref Astring)
+{
+	n := (len b)/2;
+	for (i := 0; i < n; i++)
+		s.s[i] = int b[2*i] | ((int b[2*i+1])<<8);
+}
+
+reverse(ol : list of string) : list of string
+{
+	nl : list of string;
+
+	nl = nil;
+	while (ol != nil) {
+		nl = hd ol :: nl;
+		ol = tl ol;
+	}
+	return nl;
+}
+
+nextarg(p : ref Arg) : int
+{
+	bp : string;
+
+	if(p.av != nil){
+		bp = hd p.av;
+		if(bp != nil && bp[0] == '-'){
+			p.p = bp[1:];
+			p.av = tl p.av;
+			return 1;
+		}
+	}
+	p.p = nil;
+	return 0;
+}
+
+arginit(av : list of string) : ref Arg
+{
+	p : ref Arg;
+
+	p = ref Arg;
+	p.arg0 = hd av;
+	p.av = tl av;
+	nextarg(p);
+	return p;
+}
+
+argopt(p : ref Arg) : int
+{
+	r : int;
+
+	if(p.p == nil && nextarg(p) == 0)
+		return 0;
+	r = p.p[0];
+	p.p = p.p[1:];
+	return r;
+}
+
+argf(p : ref Arg) : string
+{
+	bp : string;
+
+	if(p.p != nil){
+		bp = p.p;
+		p.p = nil;
+	} else if(p.av != nil){
+		bp = hd p.av;
+		p.av = tl p.av;
+	} else
+		bp = nil;
+	return bp;
+}
+
+exec(cmd : string, argl : list of string)
+{
+	file := cmd;
+	if(len file<4 || file[len file-4:]!=".dis")
+		file += ".dis";
+
+	c := load Command file;
+	if(c == nil) {
+		err := sys->sprint("%r");
+		if(file[0]!='/' && file[0:2]!="./"){
+			c = load Command "/dis/"+file;
+			if(c == nil)
+				err = sys->sprint("%r");
+		}
+		if(c == nil){
+			# debug(sys->sprint("file %s not found\n", file));
+			sys->fprint(stderr, "%s: %s\n", cmd, err);
+			return;
+		}
+	}
+	c->init(acme->acmectxt, argl);
+}
+
+getuser() : string
+{
+  	fd := sys->open("/dev/user", sys->OREAD);
+  	if(fd == nil)
+    		return "";
+
+  	buf := array[128] of byte;
+  	n := sys->read(fd, buf, len buf);
+  	if(n < 0)
+    		return "";
+
+  	return string buf[0:n];	
+}
+
+gethome(usr : string) : string
+{
+	if (usr == nil)
+		usr = "tmp";
+	return "/usr/" + usr;
+}
+
+postnote(t : int, this : int, pid : int, note : string) : int
+{
+	if (pid == this || pid == 0)
+		return 0;
+	# fd := sys->open("/prog/" + string pid + "/ctl", sys->OWRITE);
+	fd := sys->open("#p/" + string pid + "/ctl", sys->OWRITE);
+	if (fd == nil)
+		return -1;
+	if (t == PNGROUP)
+		note += "grp";
+	sys->fprint(fd, "%s", note);
+	fd = nil;
+	return 0;
+}
+
+error(s : string)
+{
+	sys->fprint(stderr, "acme: %s: %r\n", s);
+	debug(sys->sprint("error %s : %r\n", s));
+	# s[-1] = 0;	# create broken process for debugging
+	acme->acmeexit("error");
+}
+
+dlock : ref Lock;
+dfd : ref Sys->FD;
+
+debuginit()
+{
+	if (RELEASECOPY)
+		return;
+	dfd = sys->create("./debug", Sys->OWRITE, 8r600);
+	# fd = nil;
+	dlock = Lock.init();
+}
+
+debugpr(s : string)
+{
+	if (RELEASECOPY)
+		return;
+	# fd := sys->open("./debug", Sys->OWRITE);
+	# sys->seek(fd, big 0, Sys->SEEKEND);
+	sys->fprint(dfd, "%s", s);
+	# fd = nil;
+}
+
+debug(s : string)
+{
+	if (RELEASECOPY)
+		return;
+	if (dfd == nil)
+		return;
+	dlock.lock();
+	debugpr(s);	
+	dlock.unlock();
+}
+
+memfd : ref Sys->FD;
+memb : array of byte;
+
+memdebug(s : string)
+{
+	if (RELEASECOPY)
+		return;
+	dlock.lock();
+	if (memfd == nil) {
+		sys->bind("#c", "/usr/jrf/mnt", Sys->MBEFORE);
+		memfd = sys->open("/usr/jrf/mnt/memory", Sys->OREAD);
+		memb = array[1024] of byte;
+	}
+	sys->seek(memfd, big 0, 0);
+	n := sys->read(memfd, memb, len memb);
+	if (n <= 0) {
+		dlock.unlock();
+		debug(sys->sprint("bad read %r\n"));
+		return;
+	}
+	s = s + " : " + string memb[0:n] + "\n";
+	dlock.unlock();
+	debug(s);
+	s = nil;
+}
+
+rgetc(s : string, n : int) : int
+{
+	if (n < 0 || n >= len s)
+		return 0;
+	return s[n];
+}
+
+tgetc(t : ref Text, n : int) : int
+{
+	if(n >= t.file.buf.nc)
+		return 0;
+	return t.readc(n);
+}
+
+skipbl(r : string, n : int) : (string, int)
+{
+	i : int = 0;
+
+	while(n>0 && (r[i]==' ' || r[i]=='\t' || r[i]=='\n')){
+		--n;
+		i++;
+	}
+	return (r[i:], n);
+}
+
+findbl(r : string, n : int) : (string, int)
+{
+	i : int = 0;
+
+	while(n>0 && r[i]!=' ' && r[i]!='\t' && r[i]!='\n'){
+		--n;
+		i++;
+	}
+	return (r[i:], n);
+}
+
+prevmouse : Point;
+mousew : ref Window;
+
+savemouse(w : ref Window)
+{
+	prevmouse = mouse.xy;
+	mousew = w;
+}
+
+restoremouse(w : ref Window)
+{
+	if(mousew!=nil && mousew==w)
+		cursorset(prevmouse);
+	mousew = nil;
+}
+
+clearmouse()
+{
+	mousew = nil;
+}
+
+#
+# Heuristic city.
+#
+newwindow(t : ref Text) : ref Window
+{
+	c : ref Column;
+	w, bigw, emptyw : ref Window;
+	emptyb : ref Text;
+	i, y, el : int;
+
+	if(activecol != nil)
+		c = activecol;
+	else if(seltext != nil && seltext.col != nil)
+		c = seltext.col;
+	else if(t != nil && t.col != nil)
+		c = t.col;
+	else{
+		if(row.ncol==0 && row.add(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 c.add(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.frame.maxlines >= bigw.body.frame.maxlines)
+			bigw = w;
+		if(w.body.frame.maxlines-w.body.frame.nlines >= emptyw.body.frame.maxlines-emptyw.body.frame.nlines)
+			emptyw = w;
+	}
+	emptyb = emptyw.body;
+	el = emptyb.frame.maxlines-emptyb.frame.nlines;
+	# if empty space is big, use it
+	if(el>15 || (el>3 && el>(bigw.body.frame.maxlines-1)/2))
+		y = emptyb.frame.r.min.y+emptyb.frame.nlines*(graph->font).height;
+	else{
+		# if this window is in column and isn't much smaller, split it
+		if(t.col==c && t.w.r.dy()>2*bigw.r.dy()/3)
+			bigw = t.w;
+		y = (bigw.r.min.y + bigw.r.max.y)/2;
+	}
+	w = c.add(nil, nil, y);
+	if(w.body.frame.maxlines < 2)
+		w.col.grow(w, 1, 1);
+	return w;
+}
+
+stralloc(n : int) : ref Astring
+{
+	r := ref Astring;
+	ab := array[n] of { * => byte 'z' };
+	r.s = string ab;
+	if (len r.s != n)
+		error("bad stralloc");
+	ab = nil;
+	return r;
+}
+
+strfree(s : ref Astring)
+{
+	s.s = nil;
+	s = nil;
+}
+
+access(s : string) : int
+{
+	fd := sys->open(s, 0);
+	if (fd == nil)
+		return -1;
+	fd = nil;
+	return 0;
+}
+
+errorwin(dir : string, ndir : int, incl : array of string, nincl : int) : ref Window
+{
+	w : ref Window;
+	r : string;
+	i, n : int;
+
+	n = ndir;
+	r = dir + "+Errors";
+	n += 7;
+	w = look->lookfile(r, n);
+	if(w == nil){
+		w = row.col[row.ncol-1].add(nil, nil, -1);
+		w.filemenu = FALSE;
+		w.setname(r, n);
+	}
+	r = nil;
+	for(i=nincl; --i>=0; )
+		w.addincl(incl[i], n);
+	return w;
+}
+
+warning(md : ref Mntdir, s : string)
+{
+	n, q0, owner : int;
+	w : ref Window;
+	t : ref Text;
+
+	debug(sys->sprint("warning %s\n", s));
+	if (row == nil) {
+		sys->fprint(sys->fildes(2), "warning: %s\n", s);
+		debug(s); 
+		debug("\n");
+		return;
+	}	
+	if(row.ncol == 0){	# really early error
+		row.init(mainwin.clipr);
+		row.add(nil, -1);
+		row.add(nil, -1);
+		if(row.ncol == 0)
+			error("initializing columns in warning()");
+	}
+	if(md != nil){
+		for(;;){
+			w = errorwin(md.dir, md.ndir, md.incl, md.nincl);
+			w.lock('E');
+			if (w.col != nil)
+				break;
+			# window was deleted too fast
+			w.unlock();
+		}
+	}else
+		w = errorwin(nil, 0, nil, 0);
+	t = w.body;
+	owner = w.owner;
+	if(owner == 0)
+		w.owner = 'E';
+	w.commit(t);
+	(q0, n) = t.bsinsert(t.file.buf.nc, s, len s, TRUE);
+	t.show(q0, q0+n, TRUE);
+	t.w.settag();
+	scrl->scrdraw(t);
+	w.owner = owner;
+	w.dirty = FALSE;
+	if(md != nil)
+		w.unlock();
+}
+
+getexc(): string
+{
+	f := "/prog/"+string sys->pctl(0, nil)+"/exception";
+	if((fd := sys->open(f, Sys->OREAD)) == nil)
+		return nil;
+	b := array[8192] of byte;
+	if((n := sys->read(fd, b, len b)) < 0)
+		return nil;
+	return string b[0: n];
+}
+
+# returns pc, module, exception
+readexc(): (int, string, string)
+{
+	s := getexc();
+	if(s == nil)
+		return (0, nil, nil);
+	(m, l) := sys->tokenize(s, " ");
+	if(m < 3)
+		return (0, nil, nil);
+	pc := int hd l;	l = tl l;
+	mod := hd l;	l = tl l;
+	exc := hd l;	l = tl l;
+	for( ; l != nil; l = tl l)
+		exc += " " + hd l;
+	return (pc, mod, exc);
+}
--- /dev/null
+++ b/appl/acme/util.m
@@ -1,0 +1,52 @@
+Utils : module {
+	PATH : con "/dis/acme/util.dis";
+
+	stderr : ref Sys->FD;
+
+	Arg : adt {
+		arg0 : string;
+		av : list of string;
+		p : string;
+	};
+
+	PNPROC, PNGROUP : con iota;
+
+	init : fn(mods : ref Dat->Mods);
+	arginit : fn(av : list of string) : ref Arg;
+	argopt : fn(p : ref Arg) : int;
+	argf : fn(p : ref Arg) : string;
+	min : fn(a : int, b : int) : int;
+	max : fn(a : int, b : int) : int;
+	abs : fn(x : int) : int;
+	error : fn(s : string);
+	warning : fn(md : ref Dat->Mntdir, t : string);
+	debuginit : fn();
+	debug : fn(s : string);
+	memdebug : fn(s : string);
+	postnote : fn(t : int, this : int, pid : int, note : string) : int;
+	exec: fn(c: string, args : list of string);
+	getuser : fn() : string;
+	gethome : fn(user : string) : string;
+	access : fn(s : string) : int;
+	isalnum : fn(c : int) : int;
+	savemouse : fn(w : ref Windowm->Window);
+	restoremouse : fn(w : ref Windowm->Window);
+	clearmouse : fn();
+	rgetc : fn(r : string, n : int) : int;
+	tgetc : fn(t : ref Textm->Text, n : int) : int;
+	reverse : fn(l : list of string) : list of string;
+	stralloc : fn(n : int) : ref Dat->Astring;
+	strfree :fn(s : ref Dat->Astring);
+	strchr : fn(s : string, c : int) : int;
+	strrchr: fn(s : string, c : int) : int;
+	strncmp : fn(s, t : string, n : int) : int;
+	getenv : fn(s : string) : string;
+	setenv : fn(s, t : string);
+	stob : fn(s : string, n : int) : array of byte;
+	btos : fn(b : array of byte, s : ref Dat->Astring);
+	findbl : fn(s : string, n : int) : (string, int);
+	skipbl : fn(s : string, n : int) : (string, int);
+	newwindow : fn(t : ref Textm->Text) : ref Windowm->Window;
+	getexc: fn(): string;
+	readexc : fn() : (int, string, string);
+};
--- /dev/null
+++ b/appl/acme/wind.b
@@ -1,0 +1,558 @@
+implement Windowm;
+
+include "common.m";
+
+sys : Sys;
+utils : Utils;
+drawm : Draw;
+graph : Graph;
+gui : Gui;
+dat : Dat;
+bufferm : Bufferm;
+textm : Textm;
+filem : Filem;
+look : Look;
+scrl : Scroll;
+acme : Acme;
+
+sprint : import sys;
+FALSE, TRUE, XXX, Astring : import Dat;
+Reffont, reffont, Lock, Ref, button, modbutton : import dat;
+Point, Rect, Image : import drawm;
+min, max, error, warning, stralloc, strfree : import utils;
+font, draw : import graph;
+black, white, mainwin : import gui;
+Buffer : import bufferm;
+Body, Text, Tag : import textm;
+File : import filem;
+Xfid : import Xfidm;
+scrdraw : import scrl;
+tagcols, textcols : import acme;
+BORD : import Framem;
+
+init(mods : ref Dat->Mods)
+{
+	sys = mods.sys;
+	dat = mods.dat;
+	utils = mods.utils;
+	drawm = mods.draw;
+	graph = mods.graph;
+	gui = mods.gui;
+	textm = mods.textm;
+	filem = mods.filem;
+	bufferm = mods.bufferm;
+	look = mods.look;
+	scrl = mods.scroll;
+	acme = mods.acme;
+}
+
+winid : int;
+nullwin : Window;
+
+Window.init(w : self ref Window, clone : ref Window, r : Rect)
+{
+	r1, br : Rect;
+	f : ref File;
+	rf : ref Reffont;
+	rp : ref Astring;
+	nc : int;
+	dummy : ref File = nil;
+
+	c := w.col;
+	*w = nullwin;
+	w.col = c;
+	w.nopen = array[Dat->QMAX] of byte;
+	for (i := 0; i < Dat->QMAX; i++)
+		w.nopen[i] = byte 0;
+	w.qlock = Lock.init();
+	w.ctllock = Lock.init();
+	w.refx = Ref.init();
+	w.tag = textm->newtext();
+	w.tag.w = w;
+	w.body = textm->newtext();
+	w.body.w = w;
+	w.id = ++winid;
+	w.refx.inc();
+	w.ctlfid = ~0;
+	w.utflastqid = -1;
+	r1 = r;
+	r1.max.y = r1.min.y + font.height;
+	reffont.r.inc();
+	f = dummy.addtext(w.tag);
+	w.tag.init(f, r1, reffont, tagcols);
+	w.tag.what = Tag;
+	# tag is a copy of the contents, not a tracked image 
+	if(clone != nil){
+		w.tag.delete(0, w.tag.file.buf.nc, TRUE);
+		nc = clone.tag.file.buf.nc;
+		rp = utils->stralloc(nc);
+		clone.tag.file.buf.read(0, rp, 0, nc);
+		w.tag.insert(0, rp.s, nc, TRUE, 0);
+		utils->strfree(rp);
+		rp = nil;
+		w.tag.file.reset();
+		w.tag.setselect(nc, nc);
+	}
+	r1 = r;
+	r1.min.y += font.height + 1;
+	if(r1.max.y < r1.min.y)
+		r1.max.y = r1.min.y;
+	f = nil;
+	if(clone != nil){
+		f = clone.body.file;
+		w.body.org = clone.body.org;
+		w.isscratch = clone.isscratch;
+		rf = Reffont.get(FALSE, FALSE, FALSE, clone.body.reffont.f.name);
+	}else
+		rf = Reffont.get(FALSE, FALSE, FALSE, nil);
+	f = f.addtext(w.body);
+	w.body.what = Body;
+	w.body.init(f, r1, rf, textcols);
+	r1.min.y -= 1;
+	r1.max.y = r1.min.y+1;
+	draw(mainwin, r1, tagcols[BORD], nil, (0, 0));
+	scrdraw(w.body);
+	w.r = r;
+	w.r.max.y = w.body.frame.r.max.y;
+	br.min = w.tag.scrollr.min;
+	br.max.x = br.min.x + button.r.dx();
+	br.max.y = br.min.y + button.r.dy();
+	draw(mainwin, br, button, nil, button.r.min);
+	w.filemenu = TRUE;
+	w.maxlines = w.body.frame.maxlines;
+	if(clone != nil){
+		w.dirty = clone.dirty;
+		w.body.setselect(clone.body.q0, clone.body.q1);
+		w.settag();
+	}
+}
+
+Window.reshape(w : self ref Window, r : Rect, safe : int) : int
+{
+	r1, br : Rect;
+	y : int;
+	b : ref Image;
+
+	r1 = r;
+	r1.max.y = r1.min.y + font.height;
+	y = r1.max.y;
+	if(!safe || !w.tag.frame.r.eq(r1)){
+		y = w.tag.reshape(r1);
+		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 + b.r.dx();
+		br.max.y = br.min.y + b.r.dy();
+		draw(mainwin, br, b, nil, b.r.min);
+	}
+	if(!safe || !w.body.frame.r.eq(r1)){
+		if(y+1+font.height > r.max.y){		# no body 
+			r1.min.y = y;
+			r1.max.y = y;
+			w.body.reshape(r1);
+			w.r = r;
+			w.r.max.y = y;
+			return y;
+		}
+		r1 = r;
+		r1.min.y = y;
+		r1.max.y = y + 1;
+		draw(mainwin, r1, tagcols[BORD], nil, (0, 0));
+		r1.min.y = y + 1;
+		r1.max.y = r.max.y;
+		y = w.body.reshape(r1);
+		w.r = r;
+		w.r.max.y = y;
+		scrdraw(w.body);
+	}
+	w.maxlines = min(w.body.frame.nlines, max(w.maxlines, w.body.frame.maxlines));
+	return w.r.max.y;
+}
+
+Window.lock1(w : self ref Window, owner : int)
+{
+	w.refx.inc();
+	w.qlock.lock();
+	w.owner = owner;
+}
+
+Window.lock(w : self ref Window, owner : int)
+{
+	i : int;
+	f : ref File;
+
+	f = w.body.file;
+	for(i=0; i<f.ntext; i++)
+		f.text[i].w.lock1(owner);
+}
+
+Window.unlock(w : self ref Window)
+{
+	f := w.body.file;
+	#
+	# subtle: loop runs backwards to avoid tripping over
+	# winclose indirectly editing f.text and freeing f
+	# on the last iteration of the loop
+	#
+	for(i:=f.ntext-1; i>=0; i--){
+		w = f.text[i].w;
+		w.owner = 0;
+		w.qlock.unlock();
+		w.close();
+	}
+}
+
+Window.mousebut(w : self ref Window)
+{
+	graph->cursorset(w.tag.scrollr.min.add(w.tag.scrollr.max).div(2));
+}
+
+Window.dirfree(w : self ref Window)
+{
+	i : int;
+	dl : ref Dat->Dirlist;
+
+	if(w.isdir){
+		for(i=0; i<w.ndl; i++){
+			dl = w.dlp[i];
+			dl.r = nil;
+			dl = nil;
+		}
+	}
+	w.dlp = nil;
+	w.ndl = 0;
+}
+
+Window.close(w : self ref Window)
+{
+	i : int;
+
+	if(w.refx.dec() == 0){
+		w.dirfree();
+		w.tag.close();
+		w.body.close();
+		if(dat->activewin == w)
+			dat->activewin = nil;
+		for(i=0; i<w.nincl; i++)
+			w.incl[i] = nil;
+		w.incl = nil;
+		w.events = nil;
+		w = nil;
+	}
+}
+
+Window.delete(w : self ref Window)
+{
+	x : ref Xfid;
+
+	x = w.eventx;
+	if(x != nil){
+		w.nevents = 0;
+		w.events = nil;
+		w.eventx = nil;
+		x.c <-= Xfidm->Xnil;
+	}
+}
+
+Window.undo(w : self ref Window, isundo : int)
+{
+	body : ref Text;
+	i : int;
+	f : ref File;
+	v : ref Window;
+
+	if(w==nil)
+		return;
+	w.utflastqid = -1;
+	body = w.body;
+	(body.q0, body.q1) = body.file.undo(isundo, body.q0, body.q1);
+	body.show(body.q0, body.q1, TRUE);
+	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.frame.p0+v.body.org;
+			v.body.q1 = v.body.frame.p1+v.body.org;
+		}
+	}
+	w.settag();
+}
+
+Window.setname(w : self ref Window, name : string, n : int)
+{
+	t : ref Text;
+	v : ref Window;
+	i : int;
+
+	t = w.body;
+	if(t.file.name == name)
+		return;
+	w.isscratch = FALSE;
+	if(n>=6 && name[n-6:n] == "/guide")
+		w.isscratch = TRUE;
+	else if(n>=7 && name[n-7:n] == "+Errors")
+		w.isscratch = TRUE;
+	t.file.setname(name, n);
+	for(i=0; i<t.file.ntext; i++){
+		v = t.file.text[i].w;
+		v.settag();
+		v.isscratch = w.isscratch;
+	}
+}
+
+Window.typex(w : self ref Window, t : ref Text, r : int)
+{
+	i : int;
+
+	t.typex(r, w.echomode);
+	if(t.what == Body)
+		for(i=0; i<t.file.ntext; i++)
+			scrdraw(t.file.text[i]);
+	w.settag();
+}
+
+Window.cleartag(w : self ref Window)
+{
+	i, n : int;
+	r : ref Astring;
+
+	# w must be committed 
+	n = w.tag.file.buf.nc;
+	r = utils->stralloc(n);
+	w.tag.file.buf.read(0, r, 0, n);
+	for(i=0; i<n; i++)
+		if(r.s[i]==' ' || r.s[i]=='\t')
+			break;
+	for(; i<n; i++)
+		if(r.s[i] == '|')
+			break;
+	if(i == n)
+		return;
+	i++;
+	w.tag.delete(i, n, TRUE);
+	utils->strfree(r);
+	r = nil;
+	w.tag.file.mod = FALSE;
+	if(w.tag.q0 > i)
+		w.tag.q0 = i;
+	if(w.tag.q1 > i)
+		w.tag.q1 = i;
+	w.tag.setselect(w.tag.q0, w.tag.q1);
+}
+
+Window.settag(w : self ref Window)
+{
+	i : int;
+	f : ref File;
+
+	f = w.body.file;
+	for(i=0; i<f.ntext; i++){
+		v := f.text[i].w;
+		if(v.col.safe || v.body.frame.maxlines>0)
+			v.settag1();
+	}
+}
+
+Window.settag1(w : self ref Window)
+{
+	ii, j, k, n, bar, dirty : int;
+	old : ref Astring;
+	new : string;
+	r : int;
+	b : ref Image;
+	q0, q1 : int;
+	br : Rect;
+
+	if(w.tag.ncache!=0 || w.tag.file.mod)
+		w.commit(w.tag);	# check file name; also can now modify tag
+	old = utils->stralloc(w.tag.file.buf.nc);
+	w.tag.file.buf.read(0, old, 0, w.tag.file.buf.nc);
+	for(ii=0; ii<w.tag.file.buf.nc; ii++)
+		if(old.s[ii]==' ' || old.s[ii]=='\t')
+			break;
+	if(old.s[0:ii] != w.body.file.name){
+		w.tag.delete(0, ii, TRUE);
+		w.tag.insert(0, w.body.file.name, len w.body.file.name, TRUE, 0);
+		strfree(old);
+		old = nil;
+		old = utils->stralloc(w.tag.file.buf.nc);
+		w.tag.file.buf.read(0, old, 0, w.tag.file.buf.nc);
+	}
+	new = w.body.file.name + " Del Snarf";
+	if(w.filemenu){
+		if(w.body.file.delta.nc>0 || w.body.ncache)
+			new += " Undo";
+		if(w.body.file.epsilon.nc > 0)
+			new += " Redo";
+		dirty = w.body.file.name != nil && (w.body.ncache || w.body.file.seq!=w.putseq);
+		if(!w.isdir && dirty)
+			new += " Put";
+	}
+	if(w.isdir)
+		new += " Get";
+	l := len w.body.file.name;
+	if(l >= 2 && w.body.file.name[l-2: ] == ".b")
+		new += " Limbo";
+	new += " |";
+	r = utils->strchr(old.s, '|');
+	if(r >= 0)
+		k = r+1;
+	else{
+		k = w.tag.file.buf.nc;
+		if(w.body.file.seq == 0)
+			new += " Look ";
+	}
+	if(new != old.s[0:k]){
+		n = k;
+		if(n > len new)
+			n = len new;
+		for(j=0; j<n; j++)
+			if(old.s[j] != new[j])
+				break;
+		q0 = w.tag.q0;
+		q1 = w.tag.q1;
+		w.tag.delete(j, k, TRUE);
+		w.tag.insert(j, new[j:], len new - j, TRUE, 0);
+		# try to preserve user selection 
+		r = utils->strchr(old.s, '|');
+		if(r >= 0){
+			bar = r;
+			if(q0 > bar){
+				bar = utils->strchr(new, '|')-bar;
+				w.tag.q0 = q0+bar;
+				w.tag.q1 = q1+bar;
+			}
+		}
+	}
+	strfree(old);
+	old = nil;
+	new = nil;
+	w.tag.file.mod = FALSE;
+	n = w.tag.file.buf.nc+w.tag.ncache;
+	if(w.tag.q0 > n)
+		w.tag.q0 = n;
+	if(w.tag.q1 > n)
+		w.tag.q1 = n;
+	w.tag.setselect(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 + b.r.dx();
+	br.max.y = br.min.y + b.r.dy();
+	draw(mainwin, br, b, nil, b.r.min);
+}
+
+Window.commit(w : self ref Window, t : ref Text)
+{
+	r : ref Astring;
+	i : int;
+	f : ref File;
+
+	t.commit(TRUE);
+	f = t.file;
+	if(f.ntext > 1)
+		for(i=0; i<f.ntext; i++)
+			f.text[i].commit(FALSE);	# no-op for t 
+	if(t.what == Body)
+		return;
+	r = utils->stralloc(w.tag.file.buf.nc);
+	w.tag.file.buf.read(0, r, 0, w.tag.file.buf.nc);
+	for(i=0; i<w.tag.file.buf.nc; i++)
+		if(r.s[i]==' ' || r.s[i]=='\t')
+			break;
+	if(r.s[0:i] != w.body.file.name){
+		dat->seq++;
+		w.body.file.mark();
+		w.body.file.mod = TRUE;
+		w.dirty = TRUE;
+		w.setname(r.s, i);
+		w.settag();
+	}
+	utils->strfree(r);
+	r = nil;
+}
+
+Window.addincl(w : self ref Window, r : string, n : int)
+{
+	{
+		(ok, d) := sys->stat(r);
+		if(ok < 0){
+			if(r[0] == '/')
+				raise "e";
+			(r, n) = look->dirname(w.body, r, n);
+			(ok, d) = sys->stat(r);
+			if(ok < 0)
+				raise "e";
+		}
+		if((d.mode&Sys->DMDIR) == 0){
+			warning(nil, sprint("%s: not a directory\n", r));
+			r = nil;
+			return;
+		}
+		w.nincl++;
+		owi := w.incl;
+		w.incl = array[w.nincl] of string;
+		w.incl[1:] = owi[0:w.nincl-1];
+		owi = nil;
+		w.incl[0] = r;
+		r = nil;
+	}
+	exception{
+		* =>
+			warning(nil, sprint("%s: %r\n", r));
+			r = nil;
+	}
+}
+
+Window.clean(w : self ref Window, conservative : int, exiting : int) : int	# as it stands, conservative is always TRUE 
+{
+	if(w.isscratch || w.isdir)	# don't whine if it's a guide file, error window, etc. 
+		return TRUE;
+	if((!conservative||exiting) && w.nopen[Dat->QWevent]>byte 0)
+		return TRUE;
+	if(w.dirty){
+		if(w.body.file.name != nil)
+			warning(nil, sprint("%s modified\n", w.body.file.name));
+		else{
+			if(w.body.file.buf.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;
+}
+
+Window.ctlprint(w : self ref Window, fonts : int) : string
+{
+	s := sprint("%11d %11d %11d %11d %11d ", w.id, w.tag.file.buf.nc,
+			w.body.file.buf.nc, w.isdir, w.dirty);
+	if(fonts)
+		return sprint("%s%11d %q %11d ", s, w.body.frame.r.dx(),
+			w.body.reffont.f.name, w.body.frame.maxtab);
+	return s;
+}
+
+Window.event(w : self ref Window, fmt : string)
+{
+	n : int;
+	x : ref Xfid;
+
+	if(w.nopen[Dat->QWevent] == byte 0)
+		return;
+	if(w.owner == 0)
+		error("no window owner");
+	n = len fmt;
+	w.events[len w.events] = w.owner;
+	w.events += fmt;
+	w.nevents += n+1;
+	x = w.eventx;
+	if(x != nil){
+		w.eventx = nil;
+		x.c <-= Xfidm->Xnil;
+	}
+}
--- /dev/null
+++ b/appl/acme/wind.m
@@ -1,0 +1,67 @@
+Windowm : module {
+	PATH : con "/dis/acme/wind.dis";
+
+	init : fn(mods : ref Dat->Mods);
+
+	Window : adt {
+		qlock : ref Dat->Lock;
+		refx : ref Dat->Ref;
+		tag : 	cyclic ref Textm->Text;
+		body : cyclic ref Textm->Text;
+		r : Draw->Rect;
+		isdir : int;
+		isscratch : int;
+		filemenu : int;
+		dirty : int;
+		id : int;
+		addr : Dat->Range;
+		limit : Dat->Range;
+		nopen : array of byte;
+		nomark : int;
+		noscroll : int;
+		echomode : int;
+		wrselrange : Dat->Range;
+		rdselfd : ref Sys->FD;
+		col : cyclic ref Columnm->Column;
+		eventx : cyclic ref Xfidm->Xfid;
+		events : string;
+		nevents : int;
+		owner : int;
+		maxlines :	int;
+		dlp : array of ref Dat->Dirlist;
+		ndl : int;
+		putseq : int;
+		nincl : int;
+		incl : array of string;
+		reffont : ref Dat->Reffont;
+		ctllock : ref Dat->Lock;
+		ctlfid : int;
+		dumpstr : string;
+		dumpdir : string;
+		dumpid : int;
+		utflastqid : int;
+		utflastboff : int;
+		utflastq : int;
+
+		init : fn(w : self ref Window, w0 : ref Window, r : Draw->Rect);
+		lock : fn(w : self ref Window, n : int);
+		lock1 : fn(w : self ref Window, n : int);
+		unlock : fn(w : self ref Window);
+		typex : fn(w : self ref Window, t : ref Textm->Text, r : int);
+		undo : fn(w : self ref Window, n : int);
+		setname : fn(w : self ref Window, r : string, n : int);
+		settag : fn(w : self ref Window);
+		settag1 : fn(w : self ref Window);
+		commit : fn(w : self ref Window, t : ref Textm->Text);
+		reshape : fn(w : self ref Window, r : Draw->Rect, n : int) : int;
+		close : fn(w : self ref Window);
+		delete : fn(w : self ref Window);
+		clean : fn(w : self ref Window, n : int, exiting : int) : int;
+		dirfree : fn(w : self ref Window);
+		event : fn(w : self ref Window, b : string);
+		mousebut : fn(w : self ref Window);
+		addincl : fn(w : self ref Window, r : string, n : int);
+		cleartag : fn(w : self ref Window);
+		ctlprint : fn(w : self ref Window, fonts : int) : string;
+	};
+};
--- /dev/null
+++ b/appl/acme/xfid.b
@@ -1,0 +1,1087 @@
+implement Xfidm;
+
+include "common.m";
+
+sys : Sys;
+dat : Dat;
+graph : Graph;
+utils : Utils;
+regx : Regx;
+bufferm : Bufferm;
+diskm : Diskm;
+filem : Filem;
+textm : Textm;
+columnm : Columnm;
+scrl : Scroll;
+look : Look;
+exec : Exec;
+windowm : Windowm;
+fsys : Fsys;
+editm: Edit;
+ecmd: Editcmd;
+styxaux: Styxaux;
+
+UTFmax : import Sys;
+sprint : import sys;
+Smsg0 : import Dat;
+TRUE, FALSE, XXX, BUFSIZE, MAXRPC : import Dat;
+EM_NORMAL, EM_RAW, EM_MASK : import Dat;
+Qdir, Qcons, Qlabel, Qindex, Qeditout : import Dat;
+QWaddr, QWdata, QWevent, QWconsctl, QWctl, QWbody, QWeditout, QWtag, QWrdsel, QWwrsel : import Dat;
+seq, cxfidfree, Lock, Ref, Range, Mntdir, Astring : import dat;
+error, warning, max, min, stralloc, strfree, strncmp : import utils;
+address : import regx;
+Buffer : import bufferm;
+File : import filem;
+Text : import textm;
+scrdraw : import scrl;
+Window : import windowm;
+bflush : import graph;
+Column : import columnm;
+row : import dat;
+FILE, QID, respond : import fsys;
+oldtag, name, offset, count, data, setcount, setdata : import styxaux;
+
+init(mods : ref Dat->Mods)
+{
+	sys = mods.sys;
+	dat = mods.dat;
+	graph = mods.graph;
+	utils = mods.utils;
+	regx = mods.regx;
+	filem = mods.filem;
+	bufferm = mods.bufferm;
+	diskm = mods.diskm;
+	textm = mods.textm;
+	columnm = mods.columnm;
+	scrl = mods.scroll;
+	look = mods.look;
+	exec = mods.exec;
+	windowm = mods.windowm;
+	fsys = mods.fsys;
+	editm = mods.edit;
+	ecmd = mods.editcmd;
+	styxaux = mods.styxaux;
+}
+
+nullxfid : Xfid;
+
+newxfid() : ref Xfid
+{
+	x := ref Xfid;
+	*x = nullxfid;
+	x.buf = array[fsys->messagesize+UTFmax] of byte;
+	return x;
+}
+
+Ctlsize : con 5*12;
+
+Edel		:= "deleted window";
+Ebadctl	:= "ill-formed control message";
+Ebadaddr	:= "bad address syntax";
+Eaddr	:= "address out of range";
+Einuse	:= "already in use";
+Ebadevent:= "bad event syntax";
+
+clampaddr(w : ref Window)
+{
+	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.buf.nc)
+		w.addr.q0 = w.body.file.buf.nc;
+	if(w.addr.q1 > w.body.file.buf.nc)
+		w.addr.q1 = w.body.file.buf.nc;
+}
+
+xfidtid : array of int;
+nxfidtid := 0;
+
+xfidkill()
+{
+	if (sys == nil)
+		return;
+	thispid := sys->pctl(0, nil);
+	for (i := 0; i < nxfidtid; i++)
+		utils->postnote(Utils->PNPROC, thispid, xfidtid[i], "kill");
+}
+
+Xfid.ctl(x : self ref Xfid)
+{
+	x.tid = sys->pctl(0, nil);
+	ox := xfidtid;
+	xfidtid = array[nxfidtid+1] of int;
+	xfidtid[0:] = ox[0:nxfidtid];
+	xfidtid[nxfidtid++] = x.tid;
+	ox = nil;
+	for (;;) {
+		f := <- x.c;
+		case (f) {
+			Xnil => ;
+			Xflush => x.flush();
+			Xwalk => x.walk(nil);
+			Xopen => x.open();
+			Xclose => x.close();
+			Xread => x.read();
+			Xwrite => x.write();
+			* =>		error("bad case in Xfid.ctl()");
+		}
+		bflush();
+		cxfidfree <-= x;
+	}
+}
+ 
+Xfid.flush(x : self ref Xfid)
+{
+	fc : Smsg0;
+	i, j : int;
+	w : ref Window;
+	c : ref Column;
+	wx : ref Xfid;
+
+	# search windows for matching tag
+	row.qlock.lock();
+loop:
+	for(j=0; j<row.ncol; j++){
+		c = row.col[j];
+		for(i=0; i<c.nw; i++){
+			w = c.w[i];
+			w.lock('E');
+			wx = w.eventx;
+			if(wx!=nil && wx.fcall.tag==oldtag(x.fcall)){
+				w.eventx = nil;
+				wx.flushed = TRUE;
+				wx.c <-= Xnil;
+				w.unlock();
+				break loop;
+			}
+			w.unlock();
+		}
+	}
+	row.qlock.unlock();
+	respond(x, fc, nil);
+}
+ 
+Xfid.walk(nil : self ref Xfid, cw: chan of ref Window)
+{
+	# fc : Smsg0;
+	w : ref Window;
+
+	# if(name(x.fcall) != "new")
+	#	error("unknown path in walk\n");
+	row.qlock.lock();	# tasks->procs now
+	w = utils->newwindow(nil);
+	w.settag();
+	# w.refx.inc();
+	# x.f.w = w;
+	# x.f.qid.path = big QID(w.id, Qdir);
+	# x.f.qid.qtype = Sys->QTDIR;
+	# fc.qid = x.f.qid;
+	row.qlock.unlock();
+	# respond(x, fc, nil);
+	cw <-= w;
+}
+ 
+Xfid.open(x : self ref Xfid)
+{
+	fc : Smsg0;
+	w : ref Window;
+	q : int;
+
+	fc.iounit = 0;
+	w = x.f.w;
+	if(w != nil){
+		t := w.body;
+		row.qlock.lock();	# tasks->procs now
+		w.lock('E');
+		q = FILE(x.f.qid);
+		case(q){
+		QWaddr or QWdata or QWevent =>
+			if(w.nopen[q]++ == byte 0){
+				if(q == QWaddr){
+					w.addr = (Range)(0,0);
+					w.limit = (Range)(-1,-1);
+				}
+				if(q==QWevent && !w.isdir && w.col!=nil){
+					w.filemenu = FALSE;
+					w.settag();
+				}
+			}
+		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 != nil){
+				w.unlock();
+				respond(x, fc, Einuse);
+				return;
+			}
+			w.rdselfd = diskm->tempfile();
+			if(w.rdselfd == nil){
+				w.unlock();
+				respond(x, fc, "can't create temp file");
+				return;
+			}
+			w.nopen[q]++;
+			q0 := t.q0;
+			q1 := t.q1;
+			r := utils->stralloc(BUFSIZE);
+			while(q0 < q1){
+				n := q1 - q0;
+				if(n > BUFSIZE)
+					n = BUFSIZE;
+				t.file.buf.read(q0, r, 0, n);
+				s := array of byte r.s[0:n];
+				m := len s;
+				if(sys->write(w.rdselfd, s, m) != m){
+					warning(nil, "can't write temp file for pipe command %r\n");
+					break;
+				}
+				s = nil;
+				q0 += n;
+			}
+			utils->strfree(r);
+		QWwrsel =>
+			w.nopen[q]++;
+			seq++;
+			t.file.mark();
+			exec->cut(t, t, FALSE, TRUE);
+			w.wrselrange = (Range)(t.q1, t.q1);
+			w.nomark = TRUE;
+		QWeditout =>
+			if(editm->editing == FALSE){
+				w.unlock();
+				respond(x, fc, "permission denied");
+				return;
+			}
+			w.wrselrange = (Range)(t.q1, t.q1);
+			break;
+		}
+		w.unlock();
+		row.qlock.unlock();
+	}
+	fc.qid = x.f.qid;
+	fc.iounit = fsys->messagesize-Styx->IOHDRSZ;
+	x.f.open = TRUE;
+	respond(x, fc, nil);
+}
+ 
+Xfid.close(x : self ref Xfid)
+{
+	fc : Smsg0;
+	w : ref Window;
+	q : int;
+
+	w = x.f.w;
+	# BUG in C version ? fsysclunk() has just set busy, open to FALSE
+	# x.f.busy = FALSE;
+	# if(!x.f.open){
+	#	if(w != nil)
+	#		w.close();
+	#	respond(x, fc, nil);
+	#	return;
+	# }
+	# x.f.open = FALSE;
+	if(w != nil){
+		row.qlock.lock();	# tasks->procs now 
+		w.lock('E');
+		q = FILE(x.f.qid);
+		case(q){
+		QWctl =>
+			if(w.ctlfid!=~0 && w.ctlfid==x.f.fid){
+				w.ctlfid = ~0;
+				w.ctllock.unlock();
+			}
+		QWdata or QWaddr or QWevent =>	
+			# BUG: do we need to shut down Xfid?
+			if (q == QWdata)
+				w.nomark = FALSE;
+			if(--w.nopen[q] == byte 0){
+				if(q == QWdata)
+					w.nomark = FALSE;
+				if(q==QWevent && !w.isdir && w.col!=nil){
+					w.filemenu = TRUE;
+					w.settag();
+				}
+				if(q == QWevent){
+					w.dumpstr = nil;
+					w.dumpdir = nil;
+				}
+			}
+		QWrdsel =>
+			w.rdselfd = nil;
+		QWwrsel =>
+			w.nomark = FALSE;
+			t :=w.body;
+			# before: only did this if !w->noscroll, but that didn't seem right in practice
+			t.show(min(w.wrselrange.q0, t.file.buf.nc),
+				    min(w.wrselrange.q1, t.file.buf.nc), TRUE);
+			scrdraw(t);
+		QWconsctl=>
+			w.echomode = EM_NORMAL;
+		}
+		w.close();
+		w.unlock();
+		row.qlock.unlock();
+	}
+	respond(x, fc, nil);
+}
+ 
+Xfid.read(x : self ref Xfid)
+{
+	fc : Smsg0;
+	n, q : int;
+	off : int;
+	sbuf : string;
+	buf : array of byte;
+	w : ref Window;
+
+	sbuf = nil;
+	q = FILE(x.f.qid);
+	w = x.f.w;
+	if(w == nil){
+		fc.count = 0;
+		case(q){
+		Qcons or Qlabel =>
+			;
+		Qindex =>
+			x.indexread();
+			return;
+		* =>
+			warning(nil, sprint("unknown qid %d\n", q));
+		}
+		respond(x, fc, nil);
+		return;
+	}
+	w.lock('F');
+	if(w.col == nil){
+		w.unlock();
+		respond(x, fc, Edel);
+		return;
+	}
+	off = int offset(x.fcall);	
+	case(q){
+	QWaddr =>
+		w.body.commit(TRUE);
+		clampaddr(w);
+		sbuf = sprint("%11d %11d ", w.addr.q0, w.addr.q1);
+	QWbody =>
+		x.utfread(w.body, 0, w.body.file.buf.nc, QWbody);
+	QWctl =>
+		sbuf = w.ctlprint(1);
+	QWevent =>
+		x.eventread(w);
+	QWdata =>
+		# BUG: what should happen if q1 > q0?
+		if(w.addr.q0 > w.body.file.buf.nc){
+			respond(x, fc, Eaddr);
+			break;
+		}
+		w.addr.q0 += x.runeread(w.body, w.addr.q0, w.body.file.buf.nc);
+		w.addr.q1 = w.addr.q0;
+	QWtag =>
+		x.utfread(w.tag, 0, w.tag.file.buf.nc, QWtag);
+	QWrdsel =>
+		sys->seek(w.rdselfd, big off, 0);
+		n = count(x.fcall);
+		if(n > BUFSIZE)
+			n = BUFSIZE;
+		b := array[n] of byte;
+		n = sys->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);
+		b = nil;
+	* =>
+		sbuf = sprint("unknown qid %d in read", q);
+		respond(x, fc, sbuf);
+		sbuf = nil;
+	}
+	if (sbuf != nil) {
+		buf = array of byte sbuf;
+		sbuf = nil;
+		n = len buf;
+		if(off > n)
+			off = n;
+		if(off+count(x.fcall) > n)
+			setcount(x.fcall, n-off);
+		fc.count = count(x.fcall);
+		fc.data = buf[off:];
+		respond(x, fc, nil);
+		buf = nil;
+	}
+	w.unlock();
+}
+ 
+Xfid.write(x : self ref Xfid)
+{
+	fc  : Smsg0;
+	c, cnt, qid, q, nb, nr, eval : int;
+	w : ref Window;
+	r : string;
+	a : Range;
+	t : ref Text;
+	q0, tq0, tq1 : int;
+	md : ref Mntdir;
+
+	qid = FILE(x.f.qid);
+	w = x.f.w;
+	row.qlock.lock();	# tasks->procs now
+	if(w != nil){
+		c = 'F';
+		if(qid==QWtag || qid==QWbody)
+			c = 'E';
+		w.lock(c);
+		if(w.col == nil){
+			w.unlock();
+			row.qlock.unlock();
+			respond(x, fc, Edel);
+			return;
+		}
+	}
+	bodytag := 0;
+	case(qid){
+	Qcons =>
+		md = x.f.mntdir;
+		warning(md, string data(x.fcall));
+		fc.count = count(x.fcall);
+		respond(x, fc, nil);
+	QWconsctl =>
+		if (w != nil) {
+			r = string data(x.fcall);
+			if (strncmp(r, "rawon", 5) == 0)
+				w.echomode = EM_RAW;
+			else if (strncmp(r, "rawoff", 6) == 0)
+				w.echomode = EM_NORMAL;
+		}
+		fc.count = count(x.fcall);
+		respond(x, fc, nil);
+	Qlabel =>
+		fc.count = count(x.fcall);
+		respond(x, fc, nil);
+	QWaddr =>
+		r = string data(x.fcall);
+		nr = len r;
+		t = w.body;
+		w.commit(t);
+		(eval, nb, a) = address(x.f.mntdir, t, w.limit, w.addr, nil, r, 0, nr, TRUE);
+		r = nil;
+		if(nb < nr){
+			respond(x, fc, Ebadaddr);
+			break;
+		}
+		if(!eval){
+			respond(x, fc, Eaddr);
+			break;
+		}
+		w.addr = a;
+		fc.count = count(x.fcall);
+		respond(x, fc, nil);
+	Qeditout or
+	QWeditout =>
+		r = string data(x.fcall);
+		nr = len r;
+		if(w!=nil)
+			err := ecmd->edittext(w.body.file, w.wrselrange.q1, r, nr);
+		else
+			err = ecmd->edittext(nil, 0, r, nr);
+		r = nil;
+		if(err != nil){
+			respond(x, fc, err);
+			break;
+		}
+		fc.count = count(x.fcall);
+		respond(x, fc, nil);
+		break;
+	QWbody or QWwrsel =>
+		t = w.body;
+		bodytag = 1;
+	QWctl =>
+		x.ctlwrite(w);
+	QWdata =>
+		t = w.body;
+		w.commit(t);
+		if(w.addr.q0>t.file.buf.nc || w.addr.q1>t.file.buf.nc){
+			respond(x, fc, Eaddr);
+			break;
+		}
+		nb = sys->utfbytes(data(x.fcall), count(x.fcall));
+		r = string data(x.fcall)[0:nb];
+		nr = len r;
+		if(w.nomark == FALSE){
+			seq++;
+			t.file.mark();
+		}
+		q0 = w.addr.q0;
+		if(w.addr.q1 > q0){
+			t.delete(q0, w.addr.q1, TRUE);
+			w.addr.q1 = q0;
+		}
+		tq0 = t.q0;
+		tq1 = t.q1;
+		t.insert(q0, r, nr, TRUE, 0);
+		if(tq0 >= q0)
+			tq0 += nr;
+		if(tq1 >= q0)
+			tq1 += nr;
+		if(!t.w.noscroll)
+			t.show(tq0, tq1, TRUE);
+		scrdraw(t);
+		w.settag();
+		r = nil;
+		w.addr.q0 += nr;
+		w.addr.q1 = w.addr.q0;
+		fc.count = count(x.fcall);
+		respond(x, fc, nil);
+	QWevent =>
+		x.eventwrite(w);
+	QWtag =>
+		t = w.tag;
+		bodytag = 1;
+	* =>
+		r = sprint("unknown qid %d in write", qid);
+		respond(x, fc, r);
+		r = nil;
+	}
+	if (bodytag) {
+		q = x.f.nrpart;
+		cnt = count(x.fcall);
+		if(q > 0){
+			nd := array[cnt+q] of byte;
+			nd[q:] = data(x.fcall)[0:cnt];
+			nd[0:] = x.f.rpart[0:q];
+			setdata(x.fcall, nd);
+			cnt += q;
+			x.f.nrpart = 0;
+		}
+		nb = sys->utfbytes(data(x.fcall), cnt);
+		r = string data(x.fcall)[0:nb];
+		nr = len r;
+		if(nb < cnt){
+			x.f.rpart = data(x.fcall)[nb:cnt];
+			x.f.nrpart = cnt-nb;
+		}
+		if(nr > 0){
+			t.w.commit(t);
+			if(qid == QWwrsel){
+				q0 = w.wrselrange.q1;
+				if(q0 > t.file.buf.nc)
+					q0 = t.file.buf.nc;
+			}else
+				q0 = t.file.buf.nc;
+			if(qid == QWbody || qid == QWwrsel){
+				if(!w.nomark){
+					seq++;
+					t.file.mark();
+				}
+				(q0, nr) = t.bsinsert(q0, r, nr, TRUE);
+				if(qid!=QWwrsel && !t.w.noscroll)
+					t.show(q0+nr, q0+nr, TRUE);
+				scrdraw(t);
+			}else
+				t.insert(q0, r, nr, TRUE, 0);
+			w.settag();
+			if(qid == QWwrsel)
+				w.wrselrange.q1 += nr;
+			r = nil;
+		}
+		fc.count = count(x.fcall);
+		respond(x, fc, nil);
+	}
+	if(w != nil)
+		w.unlock();
+	row.qlock.unlock();
+}
+
+Xfid.ctlwrite(x : self ref Xfid, w : ref Window)
+{
+	fc : Smsg0;
+	i, m, n, nb : int;
+	r, err, p, pp : string;
+	q : int;
+	scrdrw, settag : int;
+	t : ref Text;
+
+	err = nil;
+	scrdrw = FALSE;
+	settag = FALSE;
+	w.tag.commit(TRUE);
+	nb = sys->utfbytes(data(x.fcall), count(x.fcall));
+	r = string data(x.fcall)[0:nb];
+loop :
+	for(n=0; n<len r; n+=m){
+		p = r[n:];
+		if(strncmp(p, "lock", 4) == 0){	# make window exclusive use
+			w.ctllock.lock();
+			w.ctlfid = x.f.fid;
+			m = 4;
+		}else
+		if(strncmp(p, "unlock", 6) == 0){	# release exclusive use
+			w.ctlfid = ~0;
+			w.ctllock.unlock();
+			m = 6;
+		}else
+		if(strncmp(p, "clean", 5) == 0){	# mark window 'clean', seq=0
+			t = w.body;
+			t.eq0 = ~0;
+			t.file.reset();
+			t.file.mod = FALSE;
+			w.dirty = FALSE;
+			settag = TRUE;
+			m = 5;
+		}else
+		if(strncmp(p, "show", 4) == 0){	# show dot
+			t = w.body;
+			t.show(t.q0, t.q1, TRUE);
+			m = 4;
+		}else
+		if(strncmp(p, "name ", 5) == 0){	# set file name
+			pp = p[5:];
+			m = 5;
+			q = utils->strchr(pp, '\n');
+			if(q<=0){
+				err = Ebadctl;
+				break;
+			}
+			nm := pp[0:q];
+			for(i=0; i<len nm; i++)
+				if(nm[i] <= ' '){
+					err = "bad character in file name";
+					break loop;
+				}
+			seq++;
+			w.body.file.mark();
+			w.setname(nm, len nm);
+			m += (q+1);
+		}else
+		if(strncmp(p, "dump ", 5) == 0){	# set dump string
+			pp = p[5:];
+			m = 5;
+			q = utils->strchr(pp, '\n');
+			if(q<=0){
+				err = Ebadctl;
+				break;
+			}
+			nm := pp[0:q];
+			w.dumpstr = nm;
+			m += (q+1);
+		}else
+		if(strncmp(p, "dumpdir ", 8) == 0){	# set dump directory
+			pp = p[8:];
+			m = 8;
+			q = utils->strchr(pp, '\n');
+			if(q<=0){
+				err = Ebadctl;
+				break;
+			}
+			nm := pp[0:q];
+			w.dumpdir = nm;
+			m += (q+1);
+		}else
+		if(strncmp(p, "delete", 6) == 0){	# delete for sure
+			w.col.close(w, TRUE);
+			m = 6;
+		}else
+		if(strncmp(p, "del", 3) == 0){	# delete, but check dirty
+			if(!w.clean(TRUE, FALSE)){
+				err = "file dirty";
+				break;
+			}
+			w.col.close(w, TRUE);
+			m = 3;
+		}else
+		if(strncmp(p, "get", 3) == 0){	# get file
+			exec->get(w.body, nil, nil, FALSE, nil, 0);
+			m = 3;
+		}else
+		if(strncmp(p, "put", 3) == 0){	# put file
+			exec->put(w.body, nil, nil, 0);
+			m = 3;
+		}else
+		if(strncmp(p, "dot=addr", 8) == 0){	# set dot
+			w.body.commit(TRUE);
+			clampaddr(w);
+			w.body.q0 = w.addr.q0;
+			w.body.q1 = w.addr.q1;
+			w.body.setselect(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
+			w.body.commit(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++;
+			w.body.file.mark();
+			settag = 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
+			w.cleartag();
+			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, "noecho", 6) == 0){	# don't echo chars - mask them
+			w.echomode = EM_MASK;
+			m = 6;
+		}else
+		if (strncmp(p, "echo", 4) == 0){		# echo chars (normal state)
+			w.echomode = EM_NORMAL;
+			m = 4;
+		}else{
+			err = Ebadctl;
+			break;
+		}
+		while(m < len p && p[m] == '\n')
+			m++;
+	}
+	
+	ab := array of byte r[0:n];
+	n = len ab;
+	ab = nil;
+	r = nil;
+	if(err != nil)
+		n = 0;
+	fc.count = n;
+	respond(x, fc, err);
+	if(settag)
+		w.settag();
+	if(scrdrw)
+		scrdraw(w.body);
+}
+
+Xfid.eventwrite(x : self ref Xfid, w : ref Window)
+{
+	fc : Smsg0;
+	m, n, nb : int;
+	r, err : string;
+	p, q : int;
+	t : ref Text;
+	c : int;
+	q0, q1 : int;
+
+	err = nil;
+	nb = sys->utfbytes(data(x.fcall), count(x.fcall));
+	r = string data(x.fcall)[0:nb];
+loop :
+	for(n=0; n<len r; n+=m){
+		p = n;
+		w.owner = r[p++];	# disgusting
+		c = r[p++];
+		while(r[p] == ' ')
+			p++;
+		q0 = int r[p:];
+		q = p;
+		if (r[q] == '+' || r[q] == '-')
+			q++;
+		while (r[q] >= '0' && r[q] <= '9')
+			q++;
+		if(q == p) {
+			err = Ebadevent;
+			break;
+		}
+		p = q;
+		while(r[p] == ' ')
+			p++;
+		q1 = int r[p:];
+		q = p;
+		if (r[q] == '+' || r[q] == '-')
+			q++;
+		while (r[q] >= '0' && r[q] <= '9')
+			q++;
+		if(q == p) {
+			err = Ebadevent;
+			break;
+		}
+		p = q;
+		while(r[p] == ' ')
+			p++;
+		if(r[p++] != '\n') {
+			err = Ebadevent;
+			break;
+		}
+		m = p-n;
+		if('a'<=c && c<='z')
+			t = w.tag;
+		else if('A'<=c && c<='Z')
+			t = w.body;
+		else {
+			err = Ebadevent;
+			break;
+		}
+		if(q0>t.file.buf.nc || q1>t.file.buf.nc || q0>q1) {
+			err = Ebadevent;
+			break;
+		}
+		# row.qlock.lock();
+		case(c){
+		'x' or 'X' =>
+			exec->execute(t, q0, q1, TRUE, nil);
+		'l' or 'L' =>
+			look->look3(t, q0, q1, TRUE);
+		* =>
+			err = Ebadevent;
+			break loop;
+		}
+		# row.qlock.unlock();
+	}
+
+	ab := array of byte r[0:n];
+	n = len ab;
+	ab = nil;
+	r = nil;
+	if(err != nil)
+		n = 0;
+	fc.count = n;
+	respond(x, fc, err);
+}
+
+Xfid.utfread(x : self ref Xfid, t : ref Text, q0, q1 : int, qid : int)
+{
+	fc : Smsg0;
+	w : ref Window;
+	r : ref Astring;
+	b, b1 : array of byte;
+	q, off, boff : int;
+	m, n, nr, nb : int;
+
+	w = t.w;
+	w.commit(t);
+	off = int offset(x.fcall);
+	r = stralloc(BUFSIZE);
+	b1 = array[MAXRPC] of byte;
+	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 = q0;
+	}
+	w.utflastqid = qid;
+	while(q<q1 && n<count(x.fcall)){
+		w.utflastboff = boff;
+		w.utflastq = q;
+		nr = q1-q;
+		if(nr > BUFSIZE)
+			nr = BUFSIZE;
+		t.file.buf.read(q, r, 0, nr);
+		b = array of byte r.s[0:nr];
+		nb = len b;
+		if(boff >= off){
+			m = nb;
+			if(boff+m > off+count(x.fcall))
+				m = off+count(x.fcall) - boff;
+			b1[n:] = b[0:m];
+			n += m;
+		}else if(boff+nb > off){
+			if(n != 0)
+				error("bad count in utfrune");
+			m = nb - (off-boff);
+			if(m > count(x.fcall))
+				m = count(x.fcall);
+			b1[0:] = b[off-boff:off-boff+m];
+			n += m;
+		}
+		b = nil;
+		boff += nb;
+		q += nr;
+	}
+	strfree(r);
+	r = nil;
+	fc.count = n;
+	fc.data = b1;
+	respond(x, fc, nil);
+	b1 = nil;
+}
+
+Xfid.runeread(x : self ref Xfid, t : ref Text, q0, q1 : int) : int
+{
+	fc : Smsg0;
+	w : ref Window;
+	r : ref Astring;
+	junk, ok : int;
+	b, b1 : array of byte;
+	q, boff : int;
+	i, rw, m, n, nr, nb : int;
+
+	w = t.w;
+	w.commit(t);
+	r = stralloc(BUFSIZE);
+	b1 = array[MAXRPC] of byte;
+	n = 0;
+	q = q0;
+	boff = 0;
+	while(q<q1 && n<count(x.fcall)){
+		nr = q1-q;
+		if(nr > BUFSIZE)
+			nr = BUFSIZE;
+		t.file.buf.read(q, r, 0, nr);
+		b = array of byte r.s[0:nr];
+		nb = len b;
+		m = nb;
+		if(boff+m > count(x.fcall)){
+			i = count(x.fcall) - boff;
+			# copy whole runes only
+			m = 0;
+			nr = 0;
+			while(m < i){
+				(junk, rw, ok) = sys->byte2char(b, m);
+				if(m+rw > i)
+					break;
+				m += rw;
+				nr++;
+			}
+			if(m == 0)
+				break;
+		}
+		b1[n:] = b[0:m];
+		b = nil;
+		n += m;
+		boff += nb;
+		q += nr;
+	}
+	strfree(r);
+	r = nil;
+	fc.count = n;
+	fc.data = b1;
+	respond(x, fc, nil);
+	b1 = nil;
+	return q-q0;
+}
+
+Xfid.eventread(x : self ref Xfid, w : ref Window)
+{
+	fc : Smsg0;
+	b : string;
+	i, n : int;
+
+	i = 0;
+	x.flushed = FALSE;
+	while(w.nevents == 0){
+		if(i){
+			if(!x.flushed)
+				respond(x, fc, "window shut down");
+			return;
+		}
+		w.eventx = x;
+		w.unlock();
+		<- x.c;
+		w.lock('F');
+		i++;
+	}
+	eveb := array of byte w.events;
+	ne := len eveb;
+	n = w.nevents;
+	if(ne > count(x.fcall)) {
+		ne = count(x.fcall);
+		while (sys->utfbytes(eveb, ne) != ne)
+			--ne;
+		s := string eveb[0:ne];
+		n = len s;
+		s = nil;
+	}
+	fc.count = ne;
+	fc.data = eveb;
+	respond(x, fc, nil);
+	b = w.events;
+	w.events = w.events[n:];
+	b = nil;
+	w.nevents -= n;
+	eveb = nil;
+}
+
+Xfid.indexread(x : self ref Xfid)
+{
+	fc : Smsg0;
+	i, j, m, n, nmax, cnt, off : int;
+	w : ref Window;
+	b : array of byte;
+	r : ref Astring;
+	c : ref Column;
+
+	row.qlock.lock();
+	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.buf.nc*UTFmax + 1;
+		}
+	}
+	nmax++;
+	b = array[nmax] of byte;
+	r = stralloc(BUFSIZE);
+	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;
+			ctls := w.ctlprint(0);
+			ctlb := array of byte ctls;
+			if (len ctls != Ctlsize || len ctlb != Ctlsize)
+				error("bad length in indexread");
+			b[n:] = ctlb[0:];
+			n += Ctlsize;
+			ctls = nil;
+			ctlb = nil;
+			m = min(BUFSIZE, w.tag.file.buf.nc);
+			w.tag.file.buf.read(0, r, 0, m);
+			rb := array of byte r.s[0:m];
+			b[n:] = rb[0:len rb];
+			m = n+len rb;
+			rb = nil;
+			while(n<m && b[n]!=byte '\n')
+				n++;
+			b[n++] = byte '\n';
+		}
+	}
+	row.qlock.unlock();
+	off = int offset(x.fcall);
+	cnt = count(x.fcall);
+	if(off > n)
+		off = n;
+	if(off+cnt > n)
+		cnt = n-off;
+	fc.count = cnt;
+	fc.data = b[off:off+cnt];
+	respond(x, fc, nil);
+	b = nil;
+	strfree(r);
+	r = nil;
+}
--- /dev/null
+++ b/appl/acme/xfid.m
@@ -1,0 +1,34 @@
+Xfidm : module {
+	PATH : con "/dis/acme/xfid.dis";
+
+	Xnil, Xflush, Xwalk, Xopen, Xclose, Xread, Xwrite : con iota;
+
+	init : fn(mods : ref Dat->Mods);
+
+	newxfid : fn() : ref Xfid;
+	xfidkill : fn();
+
+	Xfid : adt {
+		tid : int;
+		fcall : ref Styx->Tmsg;
+		next : cyclic ref Xfid;
+		c : chan of int;
+		f : cyclic ref Dat->Fid;
+		buf : array of byte;
+		flushed : int; 
+
+		ctl : fn(x : self ref Xfid);
+		flush: fn(x : self ref Xfid);
+		walk: fn(x : self ref Xfid, c: chan of ref Windowm->Window);
+		open: fn(x : self ref Xfid);
+		close: fn(x : self ref Xfid);
+		read: fn(x : self ref Xfid);
+		write: fn(x : self ref Xfid);
+		ctlwrite: fn(x : self ref Xfid, w : ref Windowm->Window);
+		eventread: fn(x : self ref Xfid, w : ref Windowm->Window);
+		eventwrite: fn(x : self ref Xfid, w : ref Windowm->Window);
+		indexread: fn(x : self ref Xfid);
+		utfread: fn(x : self ref Xfid, t : ref Textm->Text, m : int, n : int, qid : int);
+		runeread: fn(x : self ref Xfid, t : ref Textm->Text, m : int, n : int) : int;
+	};
+};
--- /dev/null
+++ b/appl/alphabet/abc/abc.b
@@ -1,0 +1,53 @@
+implement Mkabc, Abcmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	reports: Reports;
+	report: import reports;
+include "alphabet.m";
+include "alphabet/abc.m";
+	abc: Abc;
+	Value: import abc;
+
+Mkabc: module {};
+types(): string
+{
+	return "A";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	reports = checkload(load Reports Reports->PATH, Reports->PATH);
+	abc = checkload(load Abc Abc->PATH, Abc->PATH);
+	abc->init();
+}
+
+quit()
+{
+}
+
+run(errorc: chan of string, nil: ref Reports->Report,
+		nil: list of (int, list of ref Value),
+		nil: list of ref Value
+	): ref Value
+{
+	alphabet := load Alphabet Alphabet->PATH;
+	if(alphabet == nil){
+		report(errorc, sys->sprint("abc: cannot load %q: %r", Alphabet->PATH));
+		return nil;
+	}
+	alphabet->init();
+	c := chan[1] of int;
+	c <-= 1;
+	return ref Value.VA((c, alphabet));
+}
+
+checkload[T](m: T, path: string): T
+{
+	if(m != nil)
+		return m;
+	raise sys->sprint("fail:cannot load %s: %r", path);
+}
--- /dev/null
+++ b/appl/alphabet/abc/autoconvert.b
@@ -1,0 +1,80 @@
+implement Autoconvert, Abcmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+	Cmd,
+	n_BLOCK, n_WORD, n_SEQ, n_LIST, n_ADJ, n_VAR: import Sh;
+include "alphabet/reports.m";
+	reports: Reports;
+	report: import reports;
+include "alphabet.m";
+include "alphabet/abc.m";
+	abc: Abc;
+	Value: import abc;
+
+Autoconvert: module {};
+types(): string
+{
+	return "AAssc";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	reports = checkload(load Reports Reports->PATH, Reports->PATH);
+	abc = checkload(load Abc Abc->PATH, Abc->PATH);
+	abc->init();
+}
+
+quit()
+{
+}
+
+run(errorc: chan of string, nil: ref Reports->Report,
+		nil: list of (int, list of ref Value),
+		args: list of ref Value
+	): ref Value
+{
+	a := (hd args).A().i.alphabet;
+	src := (hd tl args).s().i;
+	dst := (hd tl tl args).s().i;
+	c := (hd tl tl tl args).c().i;
+
+	# {word} -> {(src); word $1}
+	if(c.ntype == n_BLOCK && c.left.ntype == n_WORD){
+		c = mk(n_BLOCK,
+			mk(n_SEQ,
+				mk(n_LIST, mkw(src), nil),
+				mk(n_ADJ,
+					c.left,
+					mk(n_VAR, mkw("1"), nil)
+				)
+			),
+			nil
+		);
+	}
+			
+	err := a->autoconvert(src, dst, c, errorc);
+	if(err != nil){
+		report(errorc, "abcautoconvert: "+err);
+		return nil;
+	}
+	return (hd args).dup();
+}
+
+checkload[T](m: T, path: string): T
+{
+	if(m != nil)
+		return m;
+	raise sys->sprint("fail:cannot load %s: %r", path);
+}
+
+mk(ntype: int, left, right: ref Cmd): ref Cmd
+{
+	return ref Cmd(ntype, left, right, nil, nil);
+}
+mkw(w: string): ref Cmd
+{
+	return ref Cmd(n_WORD, nil, nil, w, nil);
+}
--- /dev/null
+++ b/appl/alphabet/abc/autodeclare.b
@@ -1,0 +1,42 @@
+implement Autoconvert, Abcmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+include "alphabet.m";
+include "alphabet/abc.m";
+	abc: Abc;
+	Value: import abc;
+
+Autoconvert: module {};
+types(): string
+{
+	return "AAs";
+}
+
+init()
+{
+	abc = checkload(load Abc Abc->PATH, Abc->PATH);
+	abc->init();
+}
+
+quit()
+{
+}
+
+run(nil: chan of string, nil: ref Reports->Report,
+		nil: list of (int, list of ref Value),
+		args: list of ref Value
+	): ref Value
+{
+	(hd args).A().i.alphabet->setautodeclare(int (hd tl args).s().i);
+	return (hd args).dup();
+}
+
+checkload[T](m: T, path: string): T
+{
+	if(m != nil)
+		return m;
+	raise sys->sprint("fail:cannot load %s: %r", path);
+}
--- /dev/null
+++ b/appl/alphabet/abc/declare.b
@@ -1,0 +1,70 @@
+implement Declare, Abcmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	reports: Reports;
+	report: import reports;
+include "alphabet.m";
+include "alphabet/abc.m";
+	abc: Abc;
+	Value: import abc;
+
+Declare: module {};
+types(): string
+{
+	return "AAss*-q-c";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	reports = checkload(load Reports Reports->PATH, Reports->PATH);
+	abc = checkload(load Abc Abc->PATH, Abc->PATH);
+	abc->init();
+}
+
+quit()
+{
+}
+
+run(errorc: chan of string, nil: ref Reports->Report,
+		opts: list of (int, list of ref Value),
+		args: list of ref Value
+	): ref Value
+{
+	flags := 0;
+	for(; opts != nil; opts = tl opts){
+		case (hd opts).t0 {
+		'q' =>
+			flags |= Alphabet->ONDEMAND;
+		'c' =>
+			flags |= Alphabet->CHECK;
+		}
+	}
+
+	n := len args;
+	if(n > 3){
+		report(errorc, "declare: maximum of two arguments allowed");
+		return nil;
+	}
+	a := (hd args).A().i.alphabet;
+	m := (hd tl args).s().i;
+	sig := "";
+	if(n > 2)
+		sig = (hd tl tl args).s().i;
+	e := a->declare(m, sig, flags);
+	if(e != nil){
+		report(errorc, "declare: "+e);
+		return nil;
+	}
+	return (hd args).dup();
+}
+
+checkload[T](m: T, path: string): T
+{
+	if(m != nil)
+		return m;
+	raise sys->sprint("fail:cannot load %s: %r", path);
+}
--- /dev/null
+++ b/appl/alphabet/abc/declares.b
@@ -1,0 +1,124 @@
+implement Declares, Abcmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+	n_BLOCK, n_ADJ, n_VAR, n_WORD: import Sh;
+include "alphabet/reports.m";
+	reports: Reports;
+	report, Report: import reports;
+include "alphabet.m";
+	alphabet: Alphabet;
+include "alphabet/abc.m";
+	abc: Abc;
+	Value: import abc;
+include "alphabet/abctypes.m";
+	abctypes: Abctypes;
+	Abccvt: import abctypes;
+
+cvt: ref Abccvt;
+
+types(): string
+{
+	return "AAc";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	reports = checkload(load Reports Reports->PATH, Reports->PATH);
+	abc = checkload(load Abc Abc->PATH, Abc->PATH);
+	abc->init();
+	alphabet = checkload(load Alphabet Alphabet->PATH, Alphabet->PATH);
+	alphabet->init();
+	abctypes = checkload(load Abctypes Abctypes->PATH, Abctypes->PATH);
+	(c, nil, abccvt) := abctypes->proxy0();
+	cvt = abccvt;
+	alphabet->loadtypeset("/abc", c, nil);
+	alphabet->importtype("/abc/abc");
+	alphabet->importtype("/string");
+	alphabet->importtype("/cmd");
+	c = nil;
+	# note: it's faster if we provide the signatures, as we don't
+	# have to load the module to find out its signature just to throw
+	# it away again. pity about the maintenance.
+
+	# Edit x s:(/abc/[a-z]+) (.*):declimport("\1", "\2");
+	declimport("/abc/autoconvert", "abc string string cmd -> abc");
+	declimport("/abc/autodeclare", "abc string -> abc");
+	declimport("/abc/declare", "[-qc] abc string [string...] -> abc");
+	declimport("/abc/define", "abc string cmd -> abc");
+	declimport("/abc/import", "abc string [string...] -> abc");
+	declimport("/abc/type", "abc string [string...] -> abc");
+	declimport("/abc/typeset", "abc string -> abc");
+	declimport("/abc/undeclare", "abc string [string...] -> abc");
+}
+
+quit()
+{
+	alphabet->quit();
+}
+
+run(errorc: chan of string, r: ref Reports->Report,
+		nil: list of (int, list of ref Value),
+		args: list of ref Value
+	): ref Value
+{
+	(av, err) := alphabet->importvalue(cvt.int2ext((hd args).dup()), "/abc/abc");
+	if(av == nil){
+		report(errorc, sys->sprint("declares: cannot import abc value: %s", err));
+		return nil;
+	}
+	vc := chan of ref Alphabet->Value;
+	spawn alphabet->eval0((hd tl args).c().i, "/abc/abc", nil, r, r.start("evaldecl"), av :: nil, vc);
+	av = <-vc;
+	if(av == nil)
+		return nil;
+	v := cvt.ext2int(av).dup();
+	alphabet->av.free(1);
+	return v;
+}
+
+declimport(m: string, sig: string)
+{
+	if((e := alphabet->declare(m, sig, Alphabet->ONDEMAND)) != nil)
+		raise sys->sprint("fail:cannot declare %s: %s", m, e);
+	alphabet->importmodule(m);
+}
+
+checkload[T](m: T, path: string): T
+{
+	if(m != nil)
+		return m;
+	raise sys->sprint("fail:cannot load %s: %r", path);
+}
+
+declares(a: Alphabet, decls: ref Sh->Cmd, errorc: chan of string, stopc: chan of int): string
+{
+	spawn reports->reportproc(reportc := chan of string, stopc, reply := chan of ref Report);
+	r := <-reply;
+	reply = nil;
+	spawn declaresproc(a, decls, r.start("declares"), r, vc := chan of ref Value);
+	r.enable();
+
+	v: ref Value;
+wait:
+	for(;;)alt{
+	v = <-vc =>
+		;
+	msg := <-reportc =>
+		if(msg == nil)
+			break wait;
+		errorc <-= sys->sprint("declares: %s", msg);
+	}
+	if(v == nil)
+		return "declarations failed";
+	return nil;
+}
+
+declaresproc(a: Alphabet, decls: ref Sh->Cmd, errorc: chan of string, r: ref Report, vc: chan of ref Value)
+{
+	novals: list of ref Value;
+	vc <-= run(errorc, r, nil, abc->mkabc(a).dup() :: ref Value.Vc(decls) :: novals);
+	errorc <-= nil;
+}
--- /dev/null
+++ b/appl/alphabet/abc/define.b
@@ -1,0 +1,52 @@
+implement Define, Abcmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	reports: Reports;
+	report: import reports;
+include "alphabet.m";
+include "alphabet/abc.m";
+	abc: Abc;
+	Value: import abc;
+
+Define: module {};
+types(): string
+{
+	return "AAsc";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	reports = checkload(load Reports Reports->PATH, Reports->PATH);
+	abc = checkload(load Abc Abc->PATH, Abc->PATH);
+	abc->init();
+}
+
+quit()
+{
+}
+
+run(errorc: chan of string, nil: ref Reports->Report,
+		nil: list of (int, list of ref Value),
+		args: list of ref V