shithub: fork

Download patch

ref: 19c9042e451e61f1d71f79eea86209e9369d1944
parent: 2500e3f06f9b8a9fc4806e0aab81a157ea36dceb
author: qwx <qwx@sciops.net>
date: Sun Aug 13 23:27:32 EDT 2023

add jam, jamterm

--- /dev/null
+++ b/sys/src/cmd/jam/acid.lib
@@ -1,0 +1,104 @@
+defn saminit()
+{
+	if !access("/sys/src/cmd/sam/cmd.acid") then
+		rc("cd /sys/src/cmd/sam && mk cmd.acid");
+	include("/sys/src/cmd/sam/cmd.acid");
+}
+
+defn printrunestring(addr, len)
+{
+	loop 1,len do {
+		print(*(addr\r));
+		addr = addr+2;
+	}
+}
+
+defn runestring(addr, len)
+{
+	local s;
+	
+	s = "";
+	loop 1,len do {
+		s = s + *(addr\r);
+		addr = addr+2;
+	}
+	return s;
+}
+
+defn string(s)
+{
+	complex String s;
+	return runestring(s.s, s.n);
+}
+
+defn filemenu(f)
+{
+	complex File f;
+	local s;
+	
+	s = "";
+	if f != *cmd then {
+		if f.mod then
+			s = s + "'";
+		else 
+			s = s + " ";
+		if f.rasp != 0 then
+			s = s + "+";
+		else
+			s = s + "-";
+		if f == *curfile then
+			s = s + ".";
+		else
+			s = s + " ";
+		s = s + " ";
+	}
+	s = s + string(f.name);
+	print("file(", f\X, ") // ", s, "\n");
+}
+
+defn files()
+{
+	local i, f, fx, fp;
+	
+	fx = (List)file;
+	if fx.nused == 0 then
+		filemenu(*cmd);
+	i = 0;
+	loop 1,fx.nused do {
+		f = (File)fx.g.filep[i];
+		filemenu(f);
+		i = i+1;
+	}
+}
+
+defn range(r)
+{
+	complex Range r;
+	
+	if r.p1 == r.p2 then
+		return itoa(r.p1, "#%d");
+	return itoa(r.p1, "#%d")+"-"+itoa(r.p2, "#%d");
+}
+
+defn file(f)
+{
+	complex File f;
+	
+	filemenu(f);
+	print("\t$=#", f.nc\D, " .=", range(f.dot.r), " buffer(", f\X, ")\n");
+	if f == *cmd then {
+		print("\tcmdpt=#", *cmdpt\D, " cmdptadv=#", *cmdptadv\D, "\n");
+	}
+}
+
+defn buffer(b)
+{
+	complex Buffer b;
+	
+	print("buffer(", b\X, "): c=", b.c\X, " nc=", b.nc\D, "\n");
+	print("cache: ==(");
+	print(runestring(b.c, b.nc));
+	print(")==\n");
+}
+
+print("/sys/lib/acid/sam");
--- /dev/null
+++ b/sys/src/cmd/jam/address.c
@@ -1,0 +1,243 @@
+#include "sam.h"
+#include "parse.h"
+
+Address	addr;
+String	lastpat;
+int	patset;
+File	*menu;
+
+File	*matchfile(String*);
+Address	charaddr(Posn, Address, int);
+
+Address
+address(Addr *ap, Address a, int sign)
+{
+	File *f = a.f;
+	Address a1, a2;
+
+	do{
+		switch(ap->type){
+		case 'l':
+		case '#':
+			a = (*(ap->type=='#'?charaddr:lineaddr))(ap->num, a, sign);
+			break;
+
+		case '.':
+			a = f->dot;
+			break;
+
+		case '$':
+			a.r.p1 = a.r.p2 = f->nc;
+			break;
+
+		case '\'':
+			a.r = f->mark;
+			break;
+
+		case '?':
+			sign = -sign;
+			if(sign == 0)
+				sign = -1;
+			/* fall through */
+		case '/':
+			nextmatch(f, ap->are, sign>=0? a.r.p2 : a.r.p1, sign);
+			a.r = sel.p[0];
+			break;
+
+		case '"':
+			a = matchfile(ap->are)->dot;
+			f = a.f;
+			if(f->unread)
+				load(f);
+			break;
+
+		case '*':
+			a.r.p1 = 0, a.r.p2 = f->nc;
+			return a;
+
+		case ',':
+		case ';':
+			if(ap->left)
+				a1 = address(ap->left, a, 0);
+			else
+				a1.f = a.f, a1.r.p1 = a1.r.p2 = 0;
+			if(ap->type == ';'){
+				f = a1.f;
+				a = a1;
+				f->dot = a1;
+			}
+			if(ap->next)
+				a2 = address(ap->next, a, 0);
+			else
+				a2.f = a.f, a2.r.p1 = a2.r.p2 = f->nc;
+			if(a1.f != a2.f)
+				error(Eorder);
+			a.f = a1.f, a.r.p1 = a1.r.p1, a.r.p2 = a2.r.p2;
+			if(a.r.p2 < a.r.p1)
+				error(Eorder);
+			return a;
+
+		case '+':
+		case '-':
+			sign = 1;
+			if(ap->type == '-')
+				sign = -1;
+			if(ap->next==0 || ap->next->type=='+' || ap->next->type=='-')
+				a = lineaddr(1L, a, sign);
+			break;
+		default:
+			panic("address");
+			return a;
+		}
+	}while(ap = ap->next);	/* assign = */
+	return a;
+}
+
+void
+nextmatch(File *f, String *r, Posn p, int sign)
+{
+	compile(r);
+	if(sign >= 0){
+		if(!execute(f, p, INFINITY))
+			error(Esearch);
+		if(sel.p[0].p1==sel.p[0].p2 && sel.p[0].p1==p){
+			if(++p>f->nc)
+				p = 0;
+			if(!execute(f, p, INFINITY))
+				panic("address");
+		}
+	}else{
+		if(!bexecute(f, p))
+			error(Esearch);
+		if(sel.p[0].p1==sel.p[0].p2 && sel.p[0].p2==p){
+			if(--p<0)
+				p = f->nc;
+			if(!bexecute(f, p))
+				panic("address");
+		}
+	}
+}
+
+File *
+matchfile(String *r)
+{
+	File *f;
+	File *match = 0;
+	int i;
+
+	for(i = 0; i<file.nused; i++){
+		f = file.filepptr[i];
+		if(f == cmd)
+			continue;
+		if(filematch(f, r)){
+			if(match)
+				error(Emanyfiles);
+			match = f;
+		}
+	}
+	if(!match)
+		error(Efsearch);
+	return match;
+}
+
+int
+filematch(File *f, String *r)
+{
+	char *c, *s;
+	String *t;
+
+	c = Strtoc(&f->name);
+	s = smprint("%c%c%c %s\n", " '"[f->mod],
+		"-+"[f->rasp!=0], " ."[f==curfile], c);
+	if(s == nil)
+		error(Etoolong);
+	free(c);
+	t = tmpcstr(s);
+	Strduplstr(&genstr, t);
+	freetmpstr(t);
+	/* A little dirty... */
+	if(menu == 0)
+		menu = fileopen();
+	bufreset(menu);
+	bufinsert(menu, 0, genstr.s, genstr.n);
+	compile(r);
+	free(s);
+	return execute(menu, 0, menu->nc);
+}
+
+Address
+charaddr(Posn l, Address addr, int sign)
+{
+	if(sign == 0)
+		addr.r.p1 = addr.r.p2 = l;
+	else if(sign < 0)
+		addr.r.p2 = addr.r.p1-=l;
+	else if(sign > 0)
+		addr.r.p1 = addr.r.p2+=l;
+	if(addr.r.p1<0 || addr.r.p2>addr.f->nc)
+		error(Erange);
+	return addr;
+}
+
+Address
+lineaddr(Posn l, Address addr, int sign)
+{
+	int n;
+	int c;
+	File *f = addr.f;
+	Address a;
+	Posn p;
+
+	a.f = f;
+	if(sign >= 0){
+		if(l == 0){
+			if(sign==0 || addr.r.p2==0){
+				a.r.p1 = a.r.p2 = 0;
+				return a;
+			}
+			a.r.p1 = addr.r.p2;
+			p = addr.r.p2-1;
+		}else{
+			if(sign==0 || addr.r.p2==0){
+				p = (Posn)0;
+				n = 1;
+			}else{
+				p = addr.r.p2-1;
+				n = filereadc(f, p++)=='\n';
+			}
+			while(n < l){
+				if(p >= f->nc)
+					error(Erange);
+				if(filereadc(f, p++) == '\n')
+					n++;
+			}
+			a.r.p1 = p;
+		}
+		while(p < f->nc && filereadc(f, p++)!='\n')
+			;
+		a.r.p2 = p;
+	}else{
+		p = addr.r.p1;
+		if(l == 0)
+			a.r.p2 = addr.r.p1;
+		else{
+			for(n = 0; n<l; ){	/* always runs once */
+				if(p == 0){
+					if(++n != l)
+						error(Erange);
+				}else{
+					c = filereadc(f, p-1);
+					if(c != '\n' || ++n != l)
+						p--;
+				}
+			}
+			a.r.p2 = p;
+			if(p > 0)
+				p--;
+		}
+		while(p > 0 && filereadc(f, p-1)!='\n')	/* lines start after a newline */
+			p--;
+		a.r.p1 = p;
+	}
+	return a;
+}
--- /dev/null
+++ b/sys/src/cmd/jam/buff.c
@@ -1,0 +1,302 @@
+#include "sam.h"
+
+enum
+{
+	Slop = 100,	/* room to grow with reallocation */
+};
+
+static
+void
+sizecache(Buffer *b, uint n)
+{
+	if(n <= b->cmax)
+		return;
+	b->cmax = n+Slop;
+	b->c = runerealloc(b->c, b->cmax);
+}
+
+static
+void
+addblock(Buffer *b, uint i, uint n)
+{
+	if(i > b->nbl)
+		panic("internal error: addblock");
+
+	b->bl = realloc(b->bl, (b->nbl+1)*sizeof b->bl[0]);
+	if(i < b->nbl)
+		memmove(b->bl+i+1, b->bl+i, (b->nbl-i)*sizeof(Block*));
+	b->bl[i] = disknewblock(disk, n);
+	b->nbl++;
+}
+
+
+static
+void
+delblock(Buffer *b, uint i)
+{
+	if(i >= b->nbl)
+		panic("internal error: delblock");
+
+	diskrelease(disk, b->bl[i]);
+	b->nbl--;
+	if(i < b->nbl)
+		memmove(b->bl+i, b->bl+i+1, (b->nbl-i)*sizeof(Block*));
+	b->bl = realloc(b->bl, b->nbl*sizeof b->bl[0]);
+}
+
+/*
+ * Move cache so b->cq <= q0 < b->cq+b->cnc.
+ * If at very end, q0 will fall on end of cache block.
+ */
+
+static
+void
+flush(Buffer *b)
+{
+	if(b->cdirty || b->cnc==0){
+		if(b->cnc == 0)
+			delblock(b, b->cbi);
+		else
+			diskwrite(disk, &b->bl[b->cbi], b->c, b->cnc);
+		b->cdirty = FALSE;
+	}
+}
+
+static
+void
+setcache(Buffer *b, uint q0)
+{
+	Block **blp, *bl;
+	uint i, q;
+
+	if(q0 > b->nc)
+		panic("internal error: setcache");
+	/*
+	 * flush and reload if q0 is not in cache.
+	 */
+	if(b->nc == 0 || (b->cq<=q0 && q0<b->cq+b->cnc))
+		return;
+	/*
+	 * if q0 is at end of file and end of cache, continue to grow this block
+	 */
+	if(q0==b->nc && q0==b->cq+b->cnc && b->cnc<=Maxblock)
+		return;
+	flush(b);
+	/* find block */
+	if(q0 < b->cq){
+		q = 0;
+		i = 0;
+	}else{
+		q = b->cq;
+		i = b->cbi;
+	}
+	blp = &b->bl[i];
+	while(q+(*blp)->n <= q0 && q+(*blp)->n < b->nc){
+		q += (*blp)->n;
+		i++;
+		blp++;
+		if(i >= b->nbl)
+			panic("block not found");
+	}
+	bl = *blp;
+	/* remember position */
+	b->cbi = i;
+	b->cq = q;
+	sizecache(b, bl->n);
+	b->cnc = bl->n;
+	/*read block*/
+	diskread(disk, bl, b->c, b->cnc);
+}
+
+void
+bufinsert(Buffer *b, uint q0, Rune *s, uint n)
+{
+	uint i, m, t, off;
+
+	if(q0 > b->nc)
+		panic("internal error: bufinsert");
+
+	while(n > 0){
+		setcache(b, q0);
+		off = q0-b->cq;
+		if(b->cnc+n <= Maxblock){
+			/* Everything fits in one block. */
+			t = b->cnc+n;
+			m = n;
+			if(b->bl == nil){	/* allocate */
+				if(b->cnc != 0)
+					panic("internal error: bufinsert1 cnc!=0");
+				addblock(b, 0, t);
+				b->cbi = 0;
+			}
+			sizecache(b, t);
+			runemove(b->c+off+m, b->c+off, b->cnc-off);
+			runemove(b->c+off, s, m);
+			b->cnc = t;
+			goto Tail;
+		}
+		/*
+		 * We must make a new block.  If q0 is at
+		 * the very beginning or end of this block,
+		 * just make a new block and fill it.
+		 */
+		if(q0==b->cq || q0==b->cq+b->cnc){
+			if(b->cdirty)
+				flush(b);
+			m = min(n, Maxblock);
+			if(b->bl == nil){	/* allocate */
+				if(b->cnc != 0)
+					panic("internal error: bufinsert2 cnc!=0");
+				i = 0;
+			}else{
+				i = b->cbi;
+				if(q0 > b->cq)
+					i++;
+			}
+			addblock(b, i, m);
+			sizecache(b, m);
+			runemove(b->c, s, m);
+			b->cq = q0;
+			b->cbi = i;
+			b->cnc = m;
+			goto Tail;
+		}
+		/*
+		 * Split the block; cut off the right side and
+		 * let go of it.
+		 */
+		m = b->cnc-off;
+		if(m > 0){
+			i = b->cbi+1;
+			addblock(b, i, m);
+			diskwrite(disk, &b->bl[i], b->c+off, m);
+			b->cnc -= m;
+		}
+		/*
+		 * Now at end of block.  Take as much input
+		 * as possible and tack it on end of block.
+		 */
+		m = min(n, Maxblock-b->cnc);
+		sizecache(b, b->cnc+m);
+		runemove(b->c+b->cnc, s, m);
+		b->cnc += m;
+  Tail:
+		b->nc += m;
+		q0 += m;
+		s += m;
+		n -= m;
+		b->cdirty = TRUE;
+	}
+}
+
+void
+bufdelete(Buffer *b, uint q0, uint q1)
+{
+	uint m, n, off;
+
+	if(!(q0<=q1 && q0<=b->nc && q1<=b->nc))
+		panic("internal error: bufdelete");
+	while(q1 > q0){
+		setcache(b, q0);
+		off = q0-b->cq;
+		if(q1 > b->cq+b->cnc)
+			n = b->cnc - off;
+		else
+			n = q1-q0;
+		m = b->cnc - (off+n);
+		if(m > 0)
+			runemove(b->c+off, b->c+off+n, m);
+		b->cnc -= n;
+		b->cdirty = TRUE;
+		q1 -= n;
+		b->nc -= n;
+	}
+}
+
+uint
+bufload(Buffer *b, uint q0, int fd, int *nulls)
+{
+	char *p;
+	Rune *r;
+	int l, m, n, nb, nr;
+	uint q1;
+
+	if(q0 > b->nc)
+		panic("internal error: bufload");
+	p = malloc((Maxblock+UTFmax+1)*sizeof p[0]);
+	if(p == nil)
+		panic("bufload: malloc failed");
+	r = runemalloc(Maxblock);
+	m = 0;
+	n = 1;
+	q1 = q0;
+	/*
+	 * At top of loop, may have m bytes left over from
+	 * last pass, possibly representing a partial rune.
+	 */
+	while(n > 0){
+		n = read(fd, p+m, Maxblock);
+		if(n < 0){
+			error(Ebufload);
+			break;
+		}
+		m += n;
+		p[m] = 0;
+		l = m;
+		if(n > 0)
+			l -= UTFmax;
+		cvttorunes(p, l, r, &nb, &nr, nulls);
+		memmove(p, p+nb, m-nb);
+		m -= nb;
+		bufinsert(b, q1, r, nr);
+		q1 += nr;
+	}
+	free(p);
+	free(r);
+	return q1-q0;
+}
+
+void
+bufread(Buffer *b, uint q0, Rune *s, uint n)
+{
+	uint m;
+
+	if(!(q0<=b->nc && q0+n<=b->nc))
+		panic("bufread: internal error");
+
+	while(n > 0){
+		setcache(b, q0);
+		m = min(n, b->cnc-(q0-b->cq));
+		runemove(s, b->c+(q0-b->cq), m);
+		q0 += m;
+		s += m;
+		n -= m;
+	}
+}
+
+void
+bufreset(Buffer *b)
+{
+	int i;
+
+	b->nc = 0;
+	b->cnc = 0;
+	b->cq = 0;
+	b->cdirty = 0;
+	b->cbi = 0;
+	/* delete backwards to avoid n² behavior */
+	for(i=b->nbl-1; --i>=0; )
+		delblock(b, i);
+}
+
+void
+bufclose(Buffer *b)
+{
+	bufreset(b);
+	free(b->c);
+	b->c = nil;
+	b->cnc = 0;
+	free(b->bl);
+	b->bl = nil;
+	b->nbl = 0;
+}
--- /dev/null
+++ b/sys/src/cmd/jam/cmd.c
@@ -1,0 +1,616 @@
+#include "sam.h"
+#include "parse.h"
+
+static char	linex[]="\n";
+static char	wordx[]=" \t\n";
+Cmdtab cmdtab[]={
+/*	cmdc	text	regexp	addr	defcmd	defaddr	count	token	 fn	*/
+	'\n',	0,	0,	0,	0,	aDot,	0,	0,	nl_cmd,
+	'a',	1,	0,	0,	0,	aDot,	0,	0,	a_cmd,
+	'b',	0,	0,	0,	0,	aNo,	0,	linex,	b_cmd,
+	'B',	0,	0,	0,	0,	aNo,	0,	linex,	b_cmd,
+	'c',	1,	0,	0,	0,	aDot,	0,	0,	c_cmd,
+	'd',	0,	0,	0,	0,	aDot,	0,	0,	d_cmd,
+	'D',	0,	0,	0,	0,	aNo,	0,	linex,	D_cmd,
+	'e',	0,	0,	0,	0,	aNo,	0,	wordx,	e_cmd,
+	'f',	0,	0,	0,	0,	aNo,	0,	wordx,	f_cmd,
+	'g',	0,	1,	0,	'p',	aDot,	0,	0,	g_cmd,
+	'i',	1,	0,	0,	0,	aDot,	0,	0,	i_cmd,
+	'k',	0,	0,	0,	0,	aDot,	0,	0,	k_cmd,
+	'm',	0,	0,	1,	0,	aDot,	0,	0,	m_cmd,
+	'M',	0,	0,	0,	0,	aNo,	0,	linex,	M_cmd,
+	'n',	0,	0,	0,	0,	aNo,	0,	0,	n_cmd,
+	'p',	0,	0,	0,	0,	aDot,	0,	0,	p_cmd,
+	'q',	0,	0,	0,	0,	aNo,	0,	0,	q_cmd,
+	'r',	0,	0,	0,	0,	aDot,	0,	wordx,	e_cmd,
+	's',	0,	1,	0,	0,	aDot,	1,	0,	s_cmd,
+	't',	0,	0,	1,	0,	aDot,	0,	0,	m_cmd,
+	'u',	0,	0,	0,	0,	aNo,	2,	0,	u_cmd,
+	'v',	0,	1,	0,	'p',	aDot,	0,	0,	g_cmd,
+	'w',	0,	0,	0,	0,	aAll,	0,	wordx,	w_cmd,
+	'x',	0,	1,	0,	'p',	aDot,	0,	0,	x_cmd,
+	'y',	0,	1,	0,	'p',	aDot,	0,	0,	x_cmd,
+	'X',	0,	1,	0,	'f',	aNo,	0,	0,	X_cmd,
+	'Y',	0,	1,	0,	'f',	aNo,	0,	0,	X_cmd,
+	'!',	0,	0,	0,	0,	aNo,	0,	linex,	plan9_cmd,
+	'>',	0,	0,	0,	0,	aDot,	0,	linex,	plan9_cmd,
+	'<',	0,	0,	0,	0,	aDot,	0,	linex,	plan9_cmd,
+	'|',	0,	0,	0,	0,	aDot,	0,	linex,	plan9_cmd,
+	'^',	0,	0,	0,	0,	aNo,	0,	linex,	plan9_cmd,
+	'_',	0,	0,	0,	0,	aDot,	0,	linex,	plan9_cmd,
+	'=',	0,	0,	0,	0,	aDot,	0,	linex,	eq_cmd,
+	'c'|0x100,0,	0,	0,	0,	aNo,	0,	wordx,	cd_cmd,
+	0,	0,	0,	0,	0,	0,	0,	0,
+};
+Cmd	*parsecmd(int);
+Addr	*compoundaddr(void);
+Addr	*simpleaddr(void);
+void	freecmd(void);
+void	okdelim(int);
+
+Rune	line[BLOCKSIZE];
+Rune	termline[BLOCKSIZE];
+Rune	*linep = line;
+Rune	*terminp = termline;
+Rune	*termoutp = termline;
+
+List	cmdlist = { 'p' };
+List	addrlist = { 'p' };
+List	relist = { 'p' };
+List	stringlist = { 'p' };
+
+int	eof;
+
+void
+resetcmd(void)
+{
+	linep = line;
+	*linep = 0;
+	terminp = termoutp = termline;
+	freecmd();
+}
+
+int
+inputc(void)
+{
+	int n, nbuf;
+	char buf[UTFmax];
+	Rune r;
+
+    Again:
+	nbuf = 0;
+	if(cmdbufpos > cmdbuf.nc && cmdbuf.nc > 0){
+		cmdbufpos = 0;
+		bufreset(&cmdbuf);
+	}
+	if(cmdbufpos < cmdbuf.nc && cmdbuf.nc > 0)
+		bufread(&cmdbuf, cmdbufpos++, &r, 1);
+	else if(downloaded){
+		while(termoutp == terminp){
+			cmdupdate();
+			if(patset)
+				tellpat();
+			while(termlocked > 0){
+				outT0(Hunlock);
+				termlocked--;
+			}
+			if(rcv() == 0)
+				return -1;
+		}
+		r = *termoutp++;
+		if(termoutp == terminp)
+			terminp = termoutp = termline;
+	}else{
+   		do{
+			n = read(0, buf+nbuf, 1);
+			if(n <= 0)
+				return -1;
+			nbuf += n;
+		}while(!fullrune(buf, nbuf));
+		chartorune(&r, buf);
+	}
+	if(r == 0){
+		warn(Wnulls);
+		goto Again;
+	}
+	return r;
+}
+
+int
+inputline(void)
+{
+	int i, c, start;
+
+	/*
+	 * Could set linep = line and i = 0 here and just
+	 * error(Etoolong) below, but this way we keep
+	 * old input buffer history around for a while.
+	 * This is useful only for debugging.
+	 */
+	i = linep - line;
+	do{
+		if((c = inputc())<=0)
+			return -1;
+		if(i == nelem(line)-1){
+			if(linep == line)
+				error(Etoolong);
+			start = linep - line;
+			runemove(line, linep, i-start);
+			i -= start;
+			linep = line;
+		}
+	}while((line[i++]=c) != '\n');
+	line[i] = 0;
+	return 1;
+}
+
+int
+getch(void)
+{
+	if(eof)
+		return -1;
+	if(*linep==0 && inputline()<0){
+		eof = TRUE;
+		return -1;
+	}
+	return *linep++;
+}
+
+int
+nextc(void)
+{
+	if(*linep == 0)
+		return -1;
+	return *linep;
+}
+
+void
+ungetch(void)
+{
+	if(--linep < line)
+		panic("ungetch");
+}
+
+Posn
+getnum(int signok)
+{
+	Posn n=0;
+	int c, sign;
+
+	sign = 1;
+	if(signok>1 && nextc()=='-'){
+		sign = -1;
+		getch();
+	}
+	if((c=nextc())<'0' || '9'<c)	/* no number defaults to 1 */
+		return sign;
+	while('0'<=(c=getch()) && c<='9')
+		n = n*10 + (c-'0');
+	ungetch();
+	return sign*n;
+}
+
+int
+skipbl(void)
+{
+	int c;
+	do
+		c = getch();
+	while(c==' ' || c=='\t');
+	if(c >= 0)
+		ungetch();
+	return c;
+}
+
+void
+termcommand(void)
+{
+	Posn p;
+
+	for(p=cmdpt; p<cmd->nc; p++){
+		if(terminp >= termline+nelem(termline)){
+			cmdpt = cmd->nc;
+			error(Etoolong);
+		}
+		*terminp++ = filereadc(cmd, p);
+	}
+	cmdpt = cmd->nc;
+}
+
+void
+cmdloop(void)
+{
+	Cmd *cmdp;
+	File *ocurfile;
+	int loaded;
+
+	for(;;){
+		if(!downloaded && curfile && curfile->unread)
+			load(curfile);
+		if((cmdp = parsecmd(0))==0){
+			if(downloaded){
+				rescue();
+				exits("eof");
+			}
+			break;
+		}
+		ocurfile = curfile;
+		loaded = curfile && !curfile->unread;
+		if(cmdexec(curfile, cmdp) == 0)
+			break;
+		freecmd();
+		cmdupdate();
+		update();
+		if(downloaded && curfile &&
+		    (ocurfile!=curfile || (!loaded && !curfile->unread)))
+			outTs(Hcurrent, curfile->tag);
+			/* don't allow type ahead on files that aren't bound */
+		if(downloaded && curfile && curfile->rasp == 0)
+			terminp = termoutp;
+	}
+}
+
+Cmd *
+newcmd(void){
+	Cmd *p;
+
+	p = emalloc(sizeof(Cmd));
+	inslist(&cmdlist, cmdlist.nused, p);
+	return p;
+}
+
+Addr*
+newaddr(void)
+{
+	Addr *p;
+
+	p = emalloc(sizeof(Addr));
+	inslist(&addrlist, addrlist.nused, p);
+	return p;
+}
+
+String*
+newre(void)
+{
+	String *p;
+
+	p = emalloc(sizeof(String));
+	inslist(&relist, relist.nused, p);
+	Strinit(p);
+	return p;
+}
+
+String*
+newstring(void)
+{
+	String *p;
+
+	p = emalloc(sizeof(String));
+	inslist(&stringlist, stringlist.nused, p);
+	Strinit(p);
+	return p;
+}
+
+void
+freecmd(void)
+{
+	int i;
+
+	while(cmdlist.nused > 0)
+		free(cmdlist.voidpptr[--cmdlist.nused]);
+	while(addrlist.nused > 0)
+		free(addrlist.voidpptr[--addrlist.nused]);
+	while(relist.nused > 0){
+		i = --relist.nused;
+		Strclose(relist.stringpptr[i]);
+		free(relist.stringpptr[i]);
+	}
+	while(stringlist.nused>0){
+		i = --stringlist.nused;
+		Strclose(stringlist.stringpptr[i]);
+		free(stringlist.stringpptr[i]);
+	}
+}
+
+int
+lookup(int c)
+{
+	int i;
+
+	for(i=0; cmdtab[i].cmdc; i++)
+		if(cmdtab[i].cmdc == c)
+			return i;
+	return -1;
+}
+
+void
+okdelim(int c)
+{
+	if(c=='\\' || ('a'<=c && c<='z')
+	|| ('A'<=c && c<='Z') || ('0'<=c && c<='9'))
+		error_c(Edelim, c);
+}
+
+void
+atnl(void)
+{
+	skipbl();
+	if(getch() != '\n')
+		error(Enewline);
+}
+
+void
+getrhs(String *s, int delim, int cmd)
+{
+	int c;
+
+	while((c = getch())>0 && c!=delim && c!='\n'){
+		if(c == '\\'){
+			if((c=getch()) <= 0)
+				error(Ebadrhs);
+			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 delimeter, '\n' or whatever */
+}
+
+String *
+collecttoken(char *end)
+{
+	String *s = newstring();
+	int c;
+
+	while((c=nextc())==' ' || c=='\t')
+		Straddc(s, getch()); /* blanks significant for getname() */
+	while((c=getch())>0 && utfrune(end, c)==0)
+		Straddc(s, c);
+	Straddc(s, 0);
+	if(c != '\n')
+		atnl();
+	return s;
+}
+
+String *
+collecttext(void)
+{
+	String *s = newstring();
+	int begline, i, c, delim;
+
+	if(skipbl()=='\n'){
+		getch();
+		i = 0;
+		do{
+			begline = i;
+			while((c = getch())>0 && c!='\n')
+				i++, Straddc(s, c);
+			i++, Straddc(s, '\n');
+			if(c < 0)
+				goto Return;
+		}while(s->s[begline]!='.' || s->s[begline+1]!='\n');
+		Strdelete(s, s->n-2, s->n);
+	}else{
+		okdelim(delim = getch());
+		getrhs(s, delim, 'a');
+		if(nextc()==delim)
+			getch();
+		atnl();
+	}
+    Return:
+	Straddc(s, 0);		/* JUST FOR CMDPRINT() */
+	return s;
+}
+
+Cmd *
+parsecmd(int nest)
+{
+	int i, c;
+	Cmdtab *ct;
+	Cmd *cp, *ncp;
+	Cmd cmd;
+
+	cmd.next = cmd.ccmd = 0;
+	cmd.re = 0;
+	cmd.flag = cmd.num = 0;
+	cmd.addr = compoundaddr();
+	if(skipbl() == -1)
+		return 0;
+	if((c=getch())==-1)
+		return 0;
+	cmd.cmdc = c;
+	if(cmd.cmdc=='c' && nextc()=='d'){	/* sleazy two-character case */
+		getch();		/* the 'd' */
+		cmd.cmdc='c'|0x100;
+	}
+	i = lookup(cmd.cmdc);
+	if(i >= 0){
+		if(cmd.cmdc == '\n')
+			goto Return;	/* let nl_cmd work it all out */
+		ct = &cmdtab[i];
+		if(ct->defaddr==aNo && cmd.addr)
+			error(Enoaddr);
+		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')){
+				skipbl();
+				if((c = getch())=='\n' || c<0)
+					error(Enopattern);
+				okdelim(c);
+				cmd.re = getregexp(c);
+				if(ct->cmdc == 's'){
+					cmd.ctext = newstring();
+					getrhs(cmd.ctext, c, 's');
+					if(nextc() == c){
+						getch();
+						if(nextc() == 'g')
+							cmd.flag = getch();
+					}
+			
+				}
+			}
+		}
+		if(ct->addr && (cmd.caddr=simpleaddr())==0)
+			error(Eaddress);
+		if(ct->defcmd){
+			if(skipbl() == '\n'){
+				getch();
+				cmd.ccmd = newcmd();
+				cmd.ccmd->cmdc = ct->defcmd;
+			}else if((cmd.ccmd = parsecmd(nest))==0)
+				panic("defcmd");
+		}else if(ct->text)
+			cmd.ctext = collecttext();
+		else if(ct->token)
+			cmd.ctext = collecttoken(ct->token);
+		else
+			atnl();
+	}else
+		switch(cmd.cmdc){
+		case '{':
+			cp = 0;
+			do{
+				if(skipbl()=='\n')
+					getch();
+				ncp = parsecmd(nest+1);
+				if(cp)
+					cp->next = ncp;
+				else
+					cmd.ccmd = ncp;
+			}while(cp = ncp);
+			break;
+		case '}':
+			atnl();
+			if(nest==0)
+				error(Enolbrace);
+			return 0;
+		default:
+			error_c(Eunk, cmd.cmdc);
+		}
+    Return:
+	cp = newcmd();
+	*cp = cmd;
+	return cp;
+}
+
+String*				/* BUGGERED */
+getregexp(int delim)
+{
+	String *r = newre();
+	int c;
+
+	for(Strzero(&genstr); ; Straddc(&genstr, c))
+		if((c = getch())=='\\'){
+			if(nextc()==delim)
+				c = getch();
+			else if(nextc()=='\\'){
+				Straddc(&genstr, c);
+				c = getch();
+			}
+		}else if(c==delim || c=='\n')
+			break;
+	if(c!=delim && c)
+		ungetch();
+	if(genstr.n > 0){
+		patset = TRUE;
+		Strduplstr(&lastpat, &genstr);
+		Straddc(&lastpat, '\0');
+	}
+	if(lastpat.n <= 1)
+		error(Epattern);
+	Strduplstr(r, &lastpat);
+	return r;
+}
+
+Addr *
+simpleaddr(void)
+{
+	Addr addr;
+	Addr *ap, *nap;
+
+	addr.next = 0;
+	addr.left = 0;
+	switch(skipbl()){
+	case '#':
+		addr.type = getch();
+		addr.num = getnum(1);
+		break;
+	case '0': case '1': case '2': case '3': case '4':
+	case '5': case '6': case '7': case '8': case '9': 
+		addr.num = getnum(1);
+		addr.type='l';
+		break;
+	case '/': case '?': case '"':
+		addr.are = getregexp(addr.type = getch());
+		break;
+	case '.':
+	case '$':
+	case '+':
+	case '-':
+	case '\'':
+		addr.type = getch();
+		break;
+	default:
+		return 0;
+	}
+	if(addr.next = simpleaddr())
+		switch(addr.next->type){
+		case '.':
+		case '$':
+		case '\'':
+			if(addr.type!='"')
+		case '"':
+				error(Eaddress);
+			break;
+		case 'l':
+		case '#':
+			if(addr.type=='"')
+				break;
+			/* fall through */
+		case '/':
+		case '?':
+			if(addr.type!='+' && addr.type!='-'){
+				/* insert the missing '+' */
+				nap = newaddr();
+				nap->type='+';
+				nap->next = addr.next;
+				addr.next = nap;
+			}
+			break;
+		case '+':
+		case '-':
+			break;
+		default:
+			panic("simpleaddr");
+		}
+	ap = newaddr();
+	*ap = addr;
+	return ap;
+}
+
+Addr *
+compoundaddr(void)
+{
+	Addr addr;
+	Addr *ap, *next;
+
+	addr.left = simpleaddr();
+	if((addr.type = skipbl())!=',' && addr.type!=';')
+		return addr.left;
+	getch();
+	next = addr.next = compoundaddr();
+	if(next && (next->type==',' || next->type==';') && next->left==0)
+		error(Eaddress);
+	ap = newaddr();
+	*ap = addr;
+	return ap;
+}
--- /dev/null
+++ b/sys/src/cmd/jam/disk.c
@@ -1,0 +1,121 @@
+#include "sam.h"
+
+static	Block	*blist;
+
+static int
+tempdisk(void)
+{
+	char buf[128];
+	int i, fd;
+
+	snprint(buf, sizeof buf, "/tmp/X%d.%.4sjam", getpid(), getuser());
+	for(i='A'; i<='Z'; i++){
+		buf[5] = i;
+		if(access(buf, AEXIST) == 0)
+			continue;
+		fd = create(buf, ORDWR|ORCLOSE|OCEXEC, 0600);
+		if(fd >= 0)
+			return fd;
+	}
+	return -1;
+}
+
+Disk*
+diskinit()
+{
+	Disk *d;
+
+	d = emalloc(sizeof(Disk));
+	d->fd = tempdisk();
+	if(d->fd < 0){
+		fprint(2, "jam: can't create temp file: %r\n");
+		exits("diskinit");
+	}
+	return d;
+}
+
+static
+uint
+ntosize(uint n, uint *ip)
+{
+	uint size;
+
+	if(n > Maxblock)
+		panic("internal error: ntosize");
+	size = n;
+	if(size & (Blockincr-1))
+		size += Blockincr - (size & (Blockincr-1));
+	/* last bucket holds blocks of exactly Maxblock */
+	if(ip)
+		*ip = size/Blockincr;
+	return size * sizeof(Rune);
+}
+
+Block*
+disknewblock(Disk *d, uint n)
+{
+	uint i, j, size;
+	Block *b;
+
+	size = ntosize(n, &i);
+	b = d->free[i];
+	if(b)
+		d->free[i] = b->next;
+	else{
+		/* allocate in chunks to reduce malloc overhead */
+		if(blist == nil){
+			blist = emalloc(100*sizeof(Block));
+			for(j=0; j<100-1; j++)
+				blist[j].next = &blist[j+1];
+		}
+		b = blist;
+		blist = b->next;
+		b->addr = d->addr;
+		if(d->addr+size < d->addr){
+			panic("temp file overflow");
+		}
+		d->addr += size;
+	}
+	b->n = n;
+	return b;
+}
+
+void
+diskrelease(Disk *d, Block *b)
+{
+	uint i;
+
+	ntosize(b->n, &i);
+	b->next = d->free[i];
+	d->free[i] = b;
+}
+
+void
+diskwrite(Disk *d, Block **bp, Rune *r, uint n)
+{
+	int size, nsize;
+	Block *b;
+
+	b = *bp;
+	size = ntosize(b->n, nil);
+	nsize = ntosize(n, nil);
+	if(size != nsize){
+		diskrelease(d, b);
+		b = disknewblock(d, n);
+		*bp = b;
+	}
+	if(pwrite(d->fd, r, n*sizeof(Rune), b->addr) != n*sizeof(Rune))
+		panic("write error to temp file");
+	b->n = n;
+}
+
+void
+diskread(Disk *d, Block *b, Rune *r, uint n)
+{
+	if(n > b->n)
+		panic("internal error: diskread");
+
+	ntosize(b->n, nil);	/* called only for sanity check on Maxblock */
+	if(pread(d->fd, r, n*sizeof(Rune), b->addr) != n*sizeof(Rune))
+		panic("read error from temp file");
+}
--- /dev/null
+++ b/sys/src/cmd/jam/error.c
@@ -1,0 +1,144 @@
+#include "sam.h"
+
+static char *emsg[]={
+	/* error_s */
+	"can't open",
+	"can't create",
+	"not in menu:",
+	"changes to",
+	"I/O error:",
+	"can't write while changing:",
+	/* error_c */
+	"unknown command",
+	"no operand for",
+	"bad delimiter",
+	/* error */
+	"can't fork",
+	"interrupt",
+	"address",
+	"search",
+	"pattern",
+	"newline expected",
+	"blank expected",
+	"pattern expected",
+	"can't nest X or Y",
+	"unmatched `}'",
+	"command takes no address",
+	"addresses overlap",
+	"substitution",
+	"& match too long",
+	"bad \\ in rhs",
+	"address range",
+	"changes not in sequence",
+	"addresses out of order",
+	"no file name",
+	"unmatched `('",
+	"unmatched `)'",
+	"malformed `[]'",
+	"malformed regexp",
+	"reg. exp. list overflow",
+	"plan 9 command",
+	"can't pipe",
+	"no current file",
+	"string too long",
+	"changed files",
+	"empty string",
+	"file search",
+	"non-unique match for \"\"",
+	"tag match too long",
+	"too many subexpressions",
+	"temporary file too large",
+	"file is append-only",
+	"no destination for plumb message",
+	"internal read error in buffer load",
+};
+static char *wmsg[]={
+	/* warn_s */
+	"duplicate file name",
+	"no such file",
+	"write might change good version of",
+	/* warn_S */
+	"files might be aliased",
+	/* warn */
+	"null characters elided",
+	"can't run pwd",
+	"last char not newline",
+	"exit status",
+};
+
+void
+error(Err s)
+{
+	char buf[512];
+
+	sprint(buf, "?%s", emsg[s]);
+	hiccough(buf);
+}
+
+void
+error_s(Err s, char *a)
+{
+	char buf[512];
+
+	sprint(buf, "?%s \"%s\"", emsg[s], a);
+	hiccough(buf);
+}
+
+void
+error_r(Err s, char *a)
+{
+	char buf[512];
+
+	sprint(buf, "?%s \"%s\": %r", emsg[s], a);
+	hiccough(buf);
+}
+
+void
+error_c(Err s, int c)
+{
+	char buf[512];
+
+	sprint(buf, "?%s `%C'", emsg[s], c);
+	hiccough(buf);
+}
+
+void
+warn(Warn s)
+{
+	dprint("?warning: %s\n", wmsg[s]);
+}
+
+void
+warn_S(Warn s, String *a)
+{
+	print_s(wmsg[s], a);
+}
+
+void
+warn_SS(Warn s, String *a, String *b)
+{
+	print_ss(wmsg[s], a, b);
+}
+
+void
+warn_s(Warn s, char *a)
+{
+	dprint("?warning: %s `%s'\n", wmsg[s], a);
+}
+
+void
+termwrite(char *s)
+{
+	String *p;
+
+	if(downloaded){
+		p = tmpcstr(s);
+		if(cmd)
+			loginsert(cmd, cmdpt, p->s, p->n);
+		else
+			Strinsert(&cmdstr, p, cmdstr.n);
+		cmdptadv += p->n;
+		freetmpstr(p);
+	}else
+		Write(2, s, strlen(s));
+}
--- /dev/null
+++ b/sys/src/cmd/jam/errors.h
@@ -1,0 +1,65 @@
+typedef enum Err{
+	/* error_s */
+	Eopen,
+	Ecreate,
+	Emenu,
+	Emodified,
+	Eio,
+	Ewseq,
+	/* error_c */
+	Eunk,
+	Emissop,
+	Edelim,
+	/* error */
+	Efork,
+	Eintr,
+	Eaddress,
+	Esearch,
+	Epattern,
+	Enewline,
+	Eblank,
+	Enopattern,
+	EnestXY,
+	Enolbrace,
+	Enoaddr,
+	Eoverlap,
+	Enosub,
+	Elongrhs,
+	Ebadrhs,
+	Erange,
+	Esequence,
+	Eorder,
+	Enoname,
+	Eleftpar,
+	Erightpar,
+	Ebadclass,
+	Ebadregexp,
+	Eoverflow,
+	Enocmd,
+	Epipe,
+	Enofile,
+	Etoolong,
+	Echanges,
+	Eempty,
+	Efsearch,
+	Emanyfiles,
+	Elongtag,
+	Esubexp,
+	Etmpovfl,
+	Eappend,
+	Ecantplumb,
+	Ebufload,
+}Err;
+typedef enum Warn{
+	/* warn_s */
+	Wdupname,
+	Wfile,
+	Wdate,
+	/* warn_ss */
+	Wdupfile,
+	/* warn */
+	Wnulls,
+	Wpwd,
+	Wnotnewline,
+	Wbadstatus,
+}Warn;
--- /dev/null
+++ b/sys/src/cmd/jam/file.c
@@ -1,0 +1,610 @@
+#include "sam.h"
+
+/*
+ * Structure of Undo list:
+ * 	The Undo structure follows any associated data, so the list
+ *	can be read backwards: read the structure, then read whatever
+ *	data is associated (insert string, file name) and precedes it.
+ *	The structure includes the previous value of the modify bit
+ *	and a sequence number; successive Undo structures with the
+ *	same sequence number represent simultaneous changes.
+ */
+
+typedef struct Undo Undo;
+typedef struct Merge Merge;
+
+struct Undo
+{
+	short	type;		/* Delete, Insert, Filename, Dot, Mark */
+	short	mod;		/* modify bit */
+	uint	seq;		/* sequence number */
+	uint	p0;		/* location of change (unused in f) */
+	uint	n;		/* # runes in string or file name */
+};
+
+struct Merge
+{
+	File	*f;
+	uint	seq;		/* of logged change */
+	uint	p0;		/* location of change (unused in f) */
+	uint	n;		/* # runes to delete */
+	uint	nbuf;		/* # runes to insert */
+	Rune	buf[RBUFSIZE];
+};
+
+enum
+{
+	Maxmerge = 50,
+	Undosize = sizeof(Undo)/sizeof(Rune),
+};
+
+static Merge	merge;
+
+File*
+fileopen(void)
+{
+	File *f;
+
+	f = emalloc(sizeof(File));
+	f->dot.f = f;
+	f->ndot.f = f;
+	f->seq = 0;
+	f->mod = FALSE;
+	f->unread = TRUE;
+	Strinit0(&f->name);
+	return f;
+}
+
+int
+fileisdirty(File *f)
+{
+	return f->seq != f->cleanseq;
+}
+
+static void
+wrinsert(Buffer *delta, int seq, int mod, uint p0, Rune *s, uint ns)
+{
+	Undo u;
+
+	u.type = Insert;
+	u.mod = mod;
+	u.seq = seq;
+	u.p0 = p0;
+	u.n = ns;
+	bufinsert(delta, delta->nc, s, ns);
+	bufinsert(delta, delta->nc, (Rune*)&u, Undosize);
+}
+
+static void
+wrdelete(Buffer *delta, int seq, int mod, uint p0, uint p1)
+{
+	Undo u;
+
+	u.type = Delete;
+	u.mod = mod;
+	u.seq = seq;
+	u.p0 = p0;
+	u.n = p1 - p0;
+	bufinsert(delta, delta->nc, (Rune*)&u, Undosize);
+}
+
+void
+flushmerge(void)
+{
+	File *f;
+
+	f = merge.f;
+	if(f == nil)
+		return;
+	if(merge.seq != f->seq)
+		panic("flushmerge seq mismatch");
+	if(merge.n != 0)
+		wrdelete(&f->epsilon, f->seq, TRUE, merge.p0, merge.p0+merge.n);
+	if(merge.nbuf != 0)
+		wrinsert(&f->epsilon, f->seq, TRUE, merge.p0+merge.n, merge.buf, merge.nbuf);
+	merge.f = nil;
+	merge.n = 0;
+	merge.nbuf = 0;
+}
+
+void
+mergeextend(File *f, uint p0)
+{
+	uint mp0n;
+
+	mp0n = merge.p0+merge.n;
+	if(mp0n != p0){
+		bufread(f, mp0n, merge.buf+merge.nbuf, p0-mp0n);
+		merge.nbuf += p0-mp0n;
+		merge.n = p0-merge.p0;
+	}
+}
+
+/*
+ * like fileundelete, but get the data from arguments
+ */
+void
+loginsert(File *f, uint p0, Rune *s, uint ns)
+{
+	if(f->rescuing)
+		return;
+	if(ns == 0)
+		return;
+	if(ns>STRSIZE)
+		panic("loginsert");
+	if(f->seq < seq)
+		filemark(f);
+	if(p0 < f->hiposn)
+		error(Esequence);
+
+	if(merge.f != f
+	|| p0-(merge.p0+merge.n)>Maxmerge			/* too far */
+	|| merge.nbuf+((p0+ns)-(merge.p0+merge.n))>=RBUFSIZE)	/* too long */
+		flushmerge();
+
+	if(ns>=RBUFSIZE){
+		if(!(merge.n == 0 && merge.nbuf == 0 && merge.f == nil))
+			panic("loginsert bad merge state");
+		wrinsert(&f->epsilon, f->seq, TRUE, p0, s, ns);
+	}else{
+		if(merge.f != f){
+			merge.f = f;
+			merge.p0 = p0;
+			merge.seq = f->seq;
+		}
+		mergeextend(f, p0);
+
+		/* append string to merge */
+		runemove(merge.buf+merge.nbuf, s, ns);
+		merge.nbuf += ns;
+	}
+
+	f->hiposn = p0;
+	if(!f->unread && !f->mod)
+		state(f, Dirty);
+}
+
+void
+logdelete(File *f, uint p0, uint p1)
+{
+	if(f->rescuing)
+		return;
+	if(p0 == p1)
+		return;
+	if(f->seq < seq)
+		filemark(f);
+	if(p0 < f->hiposn)
+		error(Esequence);
+
+	if(merge.f != f
+	|| p0-(merge.p0+merge.n)>Maxmerge			/* too far */
+	|| merge.nbuf+(p0-(merge.p0+merge.n))>=RBUFSIZE){	/* too long */
+		flushmerge();
+		merge.f = f;
+		merge.p0 = p0;
+		merge.seq = f->seq;
+	}
+
+	mergeextend(f, p0);
+
+	/* add to deletion */
+	merge.n = p1-merge.p0;
+
+	f->hiposn = p1;
+	if(!f->unread && !f->mod)
+		state(f, Dirty);
+}
+
+/*
+ * like fileunsetname, but get the data from arguments
+ */
+void
+logsetname(File *f, String *s)
+{
+	Undo u;
+	Buffer *delta;
+
+	if(f->rescuing)
+		return;
+
+	if(f->unread){	/* This is setting initial file name */
+		filesetname(f, s);
+		return;
+	}
+
+	if(f->seq < seq)
+		filemark(f);
+
+	/* undo a file name change by restoring old name */
+	delta = &f->epsilon;
+	u.type = Filename;
+	u.mod = TRUE;
+	u.seq = f->seq;
+	u.p0 = 0;	/* unused */
+	u.n = s->n;
+	if(s->n)
+		bufinsert(delta, delta->nc, s->s, s->n);
+	bufinsert(delta, delta->nc, (Rune*)&u, Undosize);
+	if(!f->unread && !f->mod)
+		state(f, Dirty);
+}
+
+#ifdef NOTEXT
+File*
+fileaddtext(File *f, Text *t)
+{
+	if(f == nil){
+		f = emalloc(sizeof(File));
+		f->unread = TRUE;
+	}
+	f->text = realloc(f->text, (f->ntext+1)*sizeof(Text*));
+	f->text[f->ntext++] = t;
+	f->curtext = t;
+	return f;
+}
+
+void
+filedeltext(File *f, Text *t)
+{
+	int i;
+
+	for(i=0; i<f->ntext; i++)
+		if(f->text[i] == t)
+			goto Found;
+	panic("can't find text in filedeltext");
+
+    Found:
+	f->ntext--;
+	if(f->ntext == 0){
+		fileclose(f);
+		return;
+	}
+	memmove(f->text+i, f->text+i+1, (f->ntext-i)*sizeof(Text*));
+	if(f->curtext == t)
+		f->curtext = f->text[0];
+}
+#endif
+
+void
+fileuninsert(File *f, Buffer *delta, uint p0, uint ns)
+{
+	Undo u;
+
+	/* undo an insertion by deleting */
+	u.type = Delete;
+	u.mod = f->mod;
+	u.seq = f->seq;
+	u.p0 = p0;
+	u.n = ns;
+	bufinsert(delta, delta->nc, (Rune*)&u, Undosize);
+}
+
+void
+fileundelete(File *f, Buffer *delta, uint p0, uint p1)
+{
+	Undo u;
+	Rune *buf;
+	uint i, n;
+
+	/* undo a deletion by inserting */
+	u.type = Insert;
+	u.mod = f->mod;
+	u.seq = f->seq;
+	u.p0 = p0;
+	u.n = p1-p0;
+	buf = fbufalloc();
+	for(i=p0; i<p1; i+=n){
+		n = p1 - i;
+		if(n > RBUFSIZE)
+			n = RBUFSIZE;
+		bufread(f, i, buf, n);
+		bufinsert(delta, delta->nc, buf, n);
+	}
+	fbuffree(buf);
+	bufinsert(delta, delta->nc, (Rune*)&u, Undosize);
+
+}
+
+int
+filereadc(File *f, uint q)
+{
+	Rune r;
+
+	if(q >= f->nc)
+		return -1;
+	bufread(f, q, &r, 1);
+	return r;
+}
+
+void
+filesetname(File *f, String *s)
+{
+	if(!f->unread)	/* This is setting initial file name */
+		fileunsetname(f, &f->delta);
+	Strduplstr(&f->name, s);
+	sortname(f);
+	f->unread = TRUE;
+}
+
+void
+fileunsetname(File *f, Buffer *delta)
+{
+	String s;
+	Undo u;
+
+	/* undo a file name change by restoring old name */
+	u.type = Filename;
+	u.mod = f->mod;
+	u.seq = f->seq;
+	u.p0 = 0;	/* unused */
+	Strinit(&s);
+	Strduplstr(&s, &f->name);
+	fullname(&s);
+	u.n = s.n;
+	if(s.n)
+		bufinsert(delta, delta->nc, s.s, s.n);
+	bufinsert(delta, delta->nc, (Rune*)&u, Undosize);
+	Strclose(&s);
+}
+
+void
+fileunsetdot(File *f, Buffer *delta, Range dot)
+{
+	Undo u;
+
+	u.type = Dot;
+	u.mod = f->mod;
+	u.seq = f->seq;
+	u.p0 = dot.p1;
+	u.n = dot.p2 - dot.p1;
+	bufinsert(delta, delta->nc, (Rune*)&u, Undosize);
+}
+
+void
+fileunsetmark(File *f, Buffer *delta, Range mark)
+{
+	Undo u;
+
+	u.type = Mark;
+	u.mod = f->mod;
+	u.seq = f->seq;
+	u.p0 = mark.p1;
+	u.n = mark.p2 - mark.p1;
+	bufinsert(delta, delta->nc, (Rune*)&u, Undosize);
+}
+
+uint
+fileload(File *f, uint p0, int fd, int *nulls)
+{
+	if(f->seq > 0)
+		panic("undo in file.load unimplemented");
+	return bufload(f, p0, fd, nulls);
+}
+
+int
+fileupdate(File *f, int notrans, int toterm)
+{
+	uint p1, p2;
+	int mod;
+
+	if(f->rescuing)
+		return FALSE;
+
+	flushmerge();
+
+	/*
+	 * fix the modification bit
+	 * subtle point: don't save it away in the log.
+	 *
+	 * if another change is made, the correct f->mod
+	 * state is saved  in the undo log by filemark
+	 * when setting the dot and mark.
+	 *
+	 * if the change is undone, the correct state is
+	 * saved from f in the fileun... routines.
+	 */
+	mod = f->mod;
+	f->mod = f->prevmod;
+	if(f == cmd)
+		notrans = TRUE;
+	else{
+		fileunsetdot(f, &f->delta, f->prevdot);
+		fileunsetmark(f, &f->delta, f->prevmark);
+	}
+	f->dot = f->ndot;
+	fileundo(f, FALSE, !notrans, &p1, &p2, toterm);
+	f->mod = mod;
+
+	if(f->delta.nc == 0)
+		f->seq = 0;
+
+	if(f == cmd)
+		return FALSE;
+
+	if(f->mod){
+		f->closeok = 0;
+		quitok = 0;
+	}else
+		f->closeok = 1;
+	return TRUE;
+}
+
+long
+prevseq(Buffer *b)
+{
+	Undo u;
+	uint up;
+
+	up = b->nc;
+	if(up == 0)
+		return 0;
+	up -= Undosize;
+	bufread(b, up, (Rune*)&u, Undosize);
+	return u.seq;
+}
+
+long
+undoseq(File *f, int isundo)
+{
+	if(isundo)
+		return f->seq;
+
+	return prevseq(&f->epsilon);
+}
+
+void
+fileundo(File *f, int isundo, int canredo, uint *q0p, uint *q1p, int flag)
+{
+	Undo u;
+	Rune *buf;
+	uint i, n, up;
+	uint stop;
+	Buffer *delta, *epsilon;
+
+	if(isundo){
+		/* undo; reverse delta onto epsilon, seq decreases */
+		delta = &f->delta;
+		epsilon = &f->epsilon;
+		stop = f->seq;
+	}else{
+		/* redo; reverse epsilon onto delta, seq increases */
+		delta = &f->epsilon;
+		epsilon = &f->delta;
+		stop = 0;	/* don't know yet */
+	}
+
+	raspstart(f);
+	while(delta->nc > 0){
+		/* rasp and buffer are in sync; sync with wire if needed */
+		if(needoutflush())
+			raspflush(f);
+		up = delta->nc-Undosize;
+		bufread(delta, up, (Rune*)&u, Undosize);
+		if(isundo){
+			if(u.seq < stop){
+				f->seq = u.seq;
+				raspdone(f, flag);
+				return;
+			}
+		}else{
+			if(stop == 0)
+				stop = u.seq;
+			if(u.seq > stop){
+				raspdone(f, flag);
+				return;
+			}
+		}
+		switch(u.type){
+		default:
+			panic("undo unknown u.type");
+			break;
+
+		case Delete:
+			f->seq = u.seq;
+			if(canredo)
+				fileundelete(f, epsilon, u.p0, u.p0+u.n);
+			f->mod = u.mod;
+			bufdelete(f, u.p0, u.p0+u.n);
+			raspdelete(f, u.p0, u.p0+u.n, flag);
+			*q0p = u.p0;
+			*q1p = u.p0;
+			break;
+
+		case Insert:
+			f->seq = u.seq;
+			if(canredo)
+				fileuninsert(f, epsilon, u.p0, u.n);
+			f->mod = u.mod;
+			up -= u.n;
+			buf = fbufalloc();
+			for(i=0; i<u.n; i+=n){
+				n = u.n - i;
+				if(n > RBUFSIZE)
+					n = RBUFSIZE;
+				bufread(delta, up+i, buf, n);
+				bufinsert(f, u.p0+i, buf, n);
+				raspinsert(f, u.p0+i, buf, n, flag);
+			}
+			fbuffree(buf);
+			*q0p = u.p0;
+			*q1p = u.p0+u.n;
+			break;
+
+		case Filename:
+			f->seq = u.seq;
+			if(canredo)
+				fileunsetname(f, epsilon);
+			f->mod = u.mod;
+			up -= u.n;
+
+			Strinsure(&f->name, u.n+1);
+			bufread(delta, up, f->name.s, u.n);
+			f->name.s[u.n] = 0;
+			f->name.n = u.n;
+			fixname(&f->name);
+			sortname(f);
+			break;
+		case Dot:
+			f->seq = u.seq;
+			if(canredo)
+				fileunsetdot(f, epsilon, f->dot.r);
+			f->mod = u.mod;
+			f->dot.r.p1 = u.p0;
+			f->dot.r.p2 = u.p0 + u.n;
+			break;
+		case Mark:
+			f->seq = u.seq;
+			if(canredo)
+				fileunsetmark(f, epsilon, f->mark);
+			f->mod = u.mod;
+			f->mark.p1 = u.p0;
+			f->mark.p2 = u.p0 + u.n;
+			break;
+		}
+		bufdelete(delta, up, delta->nc);
+	}
+	if(isundo)
+		f->seq = 0;
+	raspdone(f, flag);
+}
+
+void
+filereset(File *f)
+{
+	bufreset(&f->delta);
+	bufreset(&f->epsilon);
+	f->seq = 0;
+}
+
+void
+fileclose(File *f)
+{
+	Strclose(&f->name);
+	bufclose(f);
+	bufclose(&f->delta);
+	bufclose(&f->epsilon);
+	if(f->rasp)
+		listfree(f->rasp);
+	free(f);
+}
+
+void
+filemark(File *f)
+{
+
+	if(f->unread)
+		return;
+	if(f->epsilon.nc)
+		bufdelete(&f->epsilon, 0, f->epsilon.nc);
+
+	if(f != cmd){
+		f->prevdot = f->dot.r;
+		f->prevmark = f->mark;
+		f->prevseq = f->seq;
+		f->prevmod = f->mod;
+	}
+
+	f->ndot = f->dot;
+	f->seq = seq;
+	f->hiposn = 0;
+}
--- /dev/null
+++ b/sys/src/cmd/jam/io.c
@@ -1,0 +1,283 @@
+#include "sam.h"
+
+#define	NSYSFILE	3
+#define	NOFILE		128
+
+void
+checkqid(File *f)
+{
+	int i, w;
+	File *g;
+
+	w = whichmenu(f);
+	for(i=1; i<file.nused; i++){
+		g = file.filepptr[i];
+		if(w == i)
+			continue;
+		if(f->dev==g->dev && f->qidpath==g->qidpath)
+			warn_SS(Wdupfile, &f->name, &g->name);
+	}
+}
+
+void
+writef(File *f)
+{
+	Posn n;
+	char *name;
+	int i, samename, newfile;
+	ulong dev;
+	uvlong qid;
+	long mtime, appendonly, length;
+
+	newfile = 0;
+	samename = Strcmp(&genstr, &f->name) == 0;
+	name = Strtoc(&f->name);
+	i = statfile(name, &dev, &qid, &mtime, 0, 0);
+	if(i == -1)
+		newfile++;
+	else if(samename &&
+	        (f->dev!=dev || f->qidpath!=qid || f->mtime<mtime)){
+		f->dev = dev;
+		f->qidpath = qid;
+		f->mtime = mtime;
+		warn_S(Wdate, &genstr);
+		free(name);
+		return;
+	}
+	if(genc)
+		free(genc);
+	genc = Strtoc(&genstr);
+	if((io=create(genc, 1, 0666L)) < 0)
+		error_r(Ecreate, genc);
+	dprint("%s: ", genc);
+	if(statfd(io, 0, 0, 0, &length, &appendonly) > 0 && appendonly && length>0){
+		free(name);
+		error(Eappend);
+	}
+	n = writeio(f);
+	if(f->name.s[0]==0 || samename){
+		if(addr.r.p1==0 && addr.r.p2==f->nc)
+			f->cleanseq = f->seq;
+		state(f, f->cleanseq==f->seq? Clean : Dirty);
+	}
+	if(newfile)
+		dprint("(new file) ");
+	if(addr.r.p2>0 && filereadc(f, addr.r.p2-1)!='\n')
+		warn(Wnotnewline);
+	closeio(n);
+	if(f->name.s[0]==0 || samename){
+		if(statfile(genc, &dev, &qid, &mtime, 0, 0) > 0){
+			f->dev = dev;
+			f->qidpath = qid;
+			f->mtime = mtime;
+			checkqid(f);
+		}
+	}
+	free(name);
+}
+
+Posn
+readio(File *f, int *nulls, int setdate, int toterm)
+{
+	int n, b, w;
+	Rune *r;
+	Posn nt;
+	Posn p = addr.r.p2;
+	ulong dev;
+	uvlong qid;
+	long mtime;
+	char buf[BLOCKSIZE+1], *s;
+
+	*nulls = FALSE;
+	b = 0;
+	if(f->unread){
+		nt = bufload(f, 0, io, nulls);
+		if(toterm)
+			raspload(f);
+	}else
+		for(nt = 0; (n = read(io, buf+b, BLOCKSIZE-b))>0; nt+=(r-genbuf)){
+			n += b;
+			b = 0;
+			r = genbuf;
+			s = buf;
+			while(n > 0){
+				if((*r = *(uchar*)s) < Runeself){
+					if(*r)
+						r++;
+					else
+						*nulls = TRUE;
+					--n;
+					s++;
+					continue;
+				}
+				if(fullrune(s, n)){
+					w = chartorune(r, s);
+					if(*r)
+						r++;
+					else
+						*nulls = TRUE;
+					n -= w;
+					s += w;
+					continue;
+				}
+				b = n;
+				memmove(buf, s, b);
+				break;
+			}
+			loginsert(f, p, genbuf, r-genbuf);
+		}
+	if(b)
+		*nulls = TRUE;
+	if(*nulls)
+		warn(Wnulls);
+	if(setdate){
+		if(statfd(io, &dev, &qid, &mtime, 0, 0) > 0){
+			f->dev = dev;
+			f->qidpath = qid;
+			f->mtime = mtime;
+			checkqid(f);
+		}
+	}
+	return nt;
+}
+
+Posn
+writeio(File *f)
+{
+	int m, n;
+	Posn p = addr.r.p1;
+	char *c;
+
+	while(p < addr.r.p2){
+		if(addr.r.p2-p>BLOCKSIZE)
+			n = BLOCKSIZE;
+		else
+			n = addr.r.p2-p;
+		bufread(f, p, genbuf, n);
+		c = Strtoc(tmprstr(genbuf, n));
+		m = strlen(c);
+		if(Write(io, c, m) != m){
+			free(c);
+			if(p > 0)
+				p += n;
+			break;
+		}
+		free(c);
+		p += n;
+	}
+	return p-addr.r.p1;
+}
+void
+closeio(Posn p)
+{
+	close(io);
+	io = 0;
+	if(p >= 0)
+		dprint("#%lud\n", p);
+}
+
+int	remotefd0 = 0;
+int	remotefd1 = 1;
+
+void
+bootterm(char *machine, char **argv)
+{
+	int ph2t[2], pt2h[2];
+
+	if(machine){
+		dup(remotefd0, 0);
+		dup(remotefd1, 1);
+		close(remotefd0);
+		close(remotefd1);
+		argv[0] = "jamterm";
+		exec(samterm, argv);
+		fprint(2, "can't exec: ");
+		perror(samterm);
+		_exits("damn");
+	}
+	if(pipe(ph2t)==-1 || pipe(pt2h)==-1)
+		panic("pipe");
+	switch(fork()){
+	case 0:
+		dup(ph2t[0], 0);
+		dup(pt2h[1], 1);
+		close(ph2t[0]);
+		close(ph2t[1]);
+		close(pt2h[0]);
+		close(pt2h[1]);
+		argv[0] = "jamterm";
+		exec(samterm, argv);
+		fprint(2, "can't exec: ");
+		perror(samterm);
+		_exits("damn");
+	case -1:
+		panic("can't fork jamterm");
+	}
+	dup(pt2h[0], 0);
+	dup(ph2t[1], 1);
+	close(ph2t[0]);
+	close(ph2t[1]);
+	close(pt2h[0]);
+	close(pt2h[1]);
+}
+
+void
+connectto(char *machine, char **argv)
+{
+	int p1[2], p2[2];
+	char **av;
+	int ac;
+	
+	// count args
+	for(av = argv; *av; av++)
+		;
+	av = malloc(sizeof(char*)*((av-argv) + 5));
+	if(av == nil){
+		dprint("out of memory\n");
+		exits("fork/exec");
+	}
+	ac = 0;
+	av[ac++] = RX;
+	av[ac++] = machine;
+	av[ac++] = rsamname;
+	av[ac++] = "-R";
+	while(*argv)
+		av[ac++] = *argv++;
+	av[ac] = 0;
+	if(pipe(p1)<0 || pipe(p2)<0){
+		dprint("can't pipe\n");
+		exits("pipe");
+	}
+	remotefd0 = p1[0];
+	remotefd1 = p2[1];
+	switch(fork()){
+	case 0:
+		dup(p2[0], 0);
+		dup(p1[1], 1);
+		close(p1[0]);
+		close(p1[1]);
+		close(p2[0]);
+		close(p2[1]);
+		exec(RXPATH, av);
+		dprint("can't exec %s\n", RXPATH);
+		exits("exec");
+
+	case -1:
+		dprint("can't fork\n");
+		exits("fork");
+	}
+	free(av);
+	close(p1[1]);
+	close(p2[0]);
+}
+
+void
+startup(char *machine, int Rflag, char **argv, char **files)
+{
+	if(machine)
+		connectto(machine, files);
+	if(!Rflag)
+		bootterm(machine, argv);
+	downloaded = 1;
+	outTs(Hversion, VERSION);
+}
--- /dev/null
+++ b/sys/src/cmd/jam/list.c
@@ -1,0 +1,96 @@
+#include "sam.h"
+
+/*
+ * Check that list has room for one more element.
+ */
+static void
+growlist(List *l, int esize)
+{
+	uchar *p;
+
+	if(l->listptr == nil || l->nalloc == 0){
+		l->nalloc = INCR;
+		l->listptr = emalloc(INCR*esize);
+		l->nused = 0;
+	}
+	else if(l->nused == l->nalloc){
+		p = erealloc(l->listptr, (l->nalloc+INCR)*esize);
+		l->listptr = p;
+		memset(p+l->nalloc*esize, 0, INCR*esize);
+		l->nalloc += INCR;
+	}
+}
+
+/*
+ * Remove the ith element from the list
+ */
+void
+dellist(List *l, int i)
+{
+	Posn *pp;
+	void **vpp;
+
+	l->nused--;
+
+	switch(l->type){
+	case 'P':
+		pp = l->posnptr+i;
+		memmove(pp, pp+1, (l->nused-i)*sizeof(*pp));
+		break;
+	case 'p':
+		vpp = l->voidpptr+i;
+		memmove(vpp, vpp+1, (l->nused-i)*sizeof(*vpp));
+		break;
+	}
+}
+
+/*
+ * Add a new element, whose position is i, to the list
+ */
+void
+inslist(List *l, int i, ...)
+{
+	Posn *pp;
+	void **vpp;
+	va_list list;
+
+
+	va_start(list, i);
+	switch(l->type){
+	case 'P':
+		growlist(l, sizeof(*pp));
+		pp = l->posnptr+i;
+		memmove(pp+1, pp, (l->nused-i)*sizeof(*pp));
+		*pp = va_arg(list, Posn);
+		break;
+	case 'p':
+		growlist(l, sizeof(*vpp));
+		vpp = l->voidpptr+i;
+		memmove(vpp+1, vpp, (l->nused-i)*sizeof(*vpp));
+		*vpp = va_arg(list, void*);
+		break;
+	}
+	va_end(list);
+
+	l->nused++;
+}
+
+void
+listfree(List *l)
+{
+	free(l->listptr);
+	free(l);
+}
+
+List*
+listalloc(int type)
+{
+	List *l;
+
+	l = emalloc(sizeof(List));
+	l->type = type;
+	l->nalloc = 0;
+	l->nused = 0;
+
+	return l;
+}
--- /dev/null
+++ b/sys/src/cmd/jam/mesg.c
@@ -1,0 +1,897 @@
+#include "sam.h"
+
+Header	h;
+uchar	indata[DATASIZE];
+uchar	outdata[2*DATASIZE+3];	/* room for overflow message */
+uchar	*inp;
+uchar	*outp;
+uchar	*outmsg = outdata;
+Posn	cmdpt;
+Posn	cmdptadv;
+Buffer	snarfbuf;
+int	waitack;
+int	outbuffered;
+int	tversion;
+
+int	inshort(void);
+long	inlong(void);
+vlong	invlong(void);
+int	inmesg(Tmesg);
+
+void	outshort(int);
+void	outlong(long);
+void	outvlong(vlong);
+void	outcopy(int, void*);
+void	outsend(void);
+void	outstart(Hmesg);
+
+void	setgenstr(File*, Posn, Posn);
+
+#ifdef DEBUG
+char *hname[] = {
+	[Hversion]	"Hversion",
+	[Hbindname]	"Hbindname",
+	[Hcurrent]	"Hcurrent",
+	[Hnewname]	"Hnewname",
+	[Hmovname]	"Hmovname",
+	[Hgrow]		"Hgrow",
+	[Hcheck0]	"Hcheck0",
+	[Hcheck]	"Hcheck",
+	[Hunlock]	"Hunlock",
+	[Hdata]		"Hdata",
+	[Horigin]	"Horigin",
+	[Hunlockfile]	"Hunlockfile",
+	[Hsetdot]	"Hsetdot",
+	[Hgrowdata]	"Hgrowdata",
+	[Hmoveto]	"Hmoveto",
+	[Hclean]	"Hclean",
+	[Hdirty]	"Hdirty",
+	[Hcut]		"Hcut",
+	[Hsetpat]	"Hsetpat",
+	[Hdelname]	"Hdelname",
+	[Hclose]	"Hclose",
+	[Hsetsnarf]	"Hsetsnarf",
+	[Hsnarflen]	"Hsnarflen",
+	[Hack]		"Hack",
+	[Hexit]		"Hexit",
+	[Hplumb]	"Hplumb",
+	[Hmenucmd]	"Hmenucmd",
+};
+
+char *tname[] = {
+	[Tversion]	"Tversion",
+	[Tstartcmdfile]	"Tstartcmdfile",
+	[Tcheck]	"Tcheck",
+	[Trequest]	"Trequest",
+	[Torigin]	"Torigin",
+	[Tstartfile]	"Tstartfile",
+	[Tworkfile]	"Tworkfile",
+	[Ttype]		"Ttype",
+	[Tcut]		"Tcut",
+	[Tpaste]	"Tpaste",
+	[Tsnarf]	"Tsnarf",
+	[Tstartnewfile]	"Tstartnewfile",
+	[Twrite]	"Twrite",
+	[Tclose]	"Tclose",
+	[Tlook]		"Tlook",
+	[Tsearch]	"Tsearch",
+	[Tsend]		"Tsend",
+	[Tdclick]	"Tdclick",
+	[Ttclick]	"Ttclick",
+	[Tstartsnarf]	"Tstartsnarf",
+	[Tsetsnarf]	"Tsetsnarf",
+	[Tack]		"Tack",
+	[Texit]		"Texit",
+	[Tplumb]	"Tplumb",
+	[Tmenucmd]	"Tmenucmd",
+	[Tmenucmdsend]	"Tmenucmdsend",
+};
+
+void
+journal(int out, char *s)
+{
+	static int fd = -1;
+
+	if(fd < 0)
+		fd = create("/tmp/jam.out", 1, 0666L);
+	if(fd >= 0)
+		fprint(fd, "%s%s\n", out? "out: " : "in:  ", s);
+}
+
+void
+journaln(int out, long n)
+{
+	char buf[32];
+
+	snprint(buf, sizeof(buf), "%ld", n);
+	journal(out, buf);
+}
+
+void
+journalv(int out, vlong v)
+{
+	char buf[32];
+
+	sprint(buf, sizeof(buf), "%lld", v);
+	journal(out, buf);
+}
+#else
+#define	journal(a, b)
+#define journaln(a, b)
+#define journalv(a, b)
+#endif
+
+int
+rcvchar(void){
+	static uchar buf[64];
+	static i, nleft = 0;
+
+	if(nleft <= 0){
+		nleft = read(0, (char *)buf, sizeof buf);
+		if(nleft <= 0)
+			return -1;
+		i = 0;
+	}
+	--nleft;
+	return buf[i++];
+}
+
+int
+rcv(void){
+	int c;
+	static state = 0;
+	static count = 0;
+	static i = 0;
+
+	while((c=rcvchar()) != -1)
+		switch(state){
+		case 0:
+			h.type = c;
+			state++;
+			break;
+
+		case 1:
+			h.count0 = c;
+			state++;
+			break;
+
+		case 2:
+			h.count1 = c;
+			count = h.count0|(h.count1<<8);
+			i = 0;
+			if(count > DATASIZE)
+				panic("count>DATASIZE");
+			if(count == 0)
+				goto zerocount;
+			state++;
+			break;
+
+		case 3:
+			indata[i++] = c;
+			if(i == count){
+		zerocount:
+				indata[i] = 0;
+				state = count = 0;
+				return inmesg(h.type);
+			}
+			break;
+		}
+	return 0;
+}
+
+File *
+whichfile(int tag)
+{
+	int i;
+
+	for(i = 0; i<file.nused; i++)
+		if(file.filepptr[i]->tag==tag)
+			return file.filepptr[i];
+	hiccough((char *)0);
+	return 0;
+}
+
+int
+inmesg(Tmesg type)
+{
+	Rune buf[1025];
+	char cbuf[64];
+	int i, m;
+	short s;
+	long l, l1;
+	vlong v;
+	File *f;
+	Posn p0, p1, p;
+	Range r;
+	String *str;
+	char *c, *wdir;
+	Rune *rp;
+	Plumbmsg *pm;
+
+	if(type > TMAX)
+		panic("inmesg");
+
+	journal(0, tname[type]);
+
+	inp = indata;
+	switch(type){
+	case -1:
+		panic("rcv error");
+
+	default:
+		fprint(2, "unknown type %d\n", type);
+		panic("rcv unknown");
+
+	case Tversion:
+		tversion = inshort();
+		journaln(0, tversion);
+		break;
+
+	case Tstartcmdfile:
+		v = invlong();		/* for 64-bit pointers */
+		journalv(0, v);
+		Strdupl(&genstr, samname);
+		cmd = newfile();
+		cmd->unread = 0;
+		outTsv(Hbindname, cmd->tag, v);
+		outTs(Hcurrent, cmd->tag);
+		logsetname(cmd, &genstr);
+		cmd->rasp = listalloc('P');
+		cmd->mod = 0;
+		if(cmdstr.n){
+			loginsert(cmd, 0L, cmdstr.s, cmdstr.n);
+			Strdelete(&cmdstr, 0L, (Posn)cmdstr.n);
+		}
+		fileupdate(cmd, FALSE, TRUE);
+		outT0(Hunlock);
+		break;
+
+	case Tcheck:
+		/* go through whichfile to check the tag */
+		outTs(Hcheck, whichfile(inshort())->tag);
+		break;
+
+	case Trequest:
+		f = whichfile(inshort());
+		p0 = inlong();
+		p1 = p0+inshort();
+		journaln(0, p0);
+		journaln(0, p1-p0);
+		if(f->unread)
+			panic("Trequest: unread");
+		if(p1>f->nc)
+			p1 = f->nc;
+		if(p0>f->nc) /* can happen e.g. scrolling during command */
+			p0 = f->nc;
+		if(p0 == p1){
+			i = 0;
+			r.p1 = r.p2 = p0;
+		}else{
+			r = rdata(f->rasp, p0, p1-p0);
+			i = r.p2-r.p1;
+			bufread(f, r.p1, buf, i);
+		}
+		buf[i]=0;
+		outTslS(Hdata, f->tag, r.p1, tmprstr(buf, i+1));
+		break;
+
+	case Torigin:
+		s = inshort();
+		l = inlong();
+		l1 = inlong();
+		journaln(0, l1);
+		lookorigin(whichfile(s), l, l1);
+		break;
+
+	case Tstartfile:
+		termlocked++;
+		f = whichfile(inshort());
+		if(!f->rasp)	/* this might be a duplicate message */
+			f->rasp = listalloc('P');
+		current(f);
+		outTsv(Hbindname, f->tag, invlong());	/* for 64-bit pointers */
+		outTs(Hcurrent, f->tag);
+		journaln(0, f->tag);
+		if(f->unread)
+			load(f);
+		else{
+			if(f->nc>0){
+				rgrow(f->rasp, 0L, f->nc);
+				outTsll(Hgrow, f->tag, 0L, f->nc);
+			}
+			outTs(Hcheck0, f->tag);
+			moveto(f, f->dot.r);
+		}
+		break;
+
+	case Tworkfile:
+		i = inshort();
+		f = whichfile(i);
+		current(f);
+		f->dot.r.p1 = inlong();
+		f->dot.r.p2 = inlong();
+		f->tdot = f->dot.r;
+		journaln(0, i);
+		journaln(0, f->dot.r.p1);
+		journaln(0, f->dot.r.p2);
+		break;
+
+	case Ttype:
+		f = whichfile(inshort());
+		p0 = inlong();
+		journaln(0, p0);
+		journal(0, (char*)inp);
+		str = tmpcstr((char*)inp);
+		i = str->n;
+		loginsert(f, p0, str->s, str->n);
+		if(fileupdate(f, FALSE, FALSE))
+			seq++;
+		if(f==cmd && p0==f->nc-i && i>0 && str->s[i-1]=='\n'){
+			freetmpstr(str);
+			termlocked++;
+			termcommand();
+		}else
+			freetmpstr(str);
+		f->dot.r.p1 = f->dot.r.p2 = p0+i; /* terminal knows this already */
+		f->tdot = f->dot.r;
+		break;
+
+	case Tcut:
+		f = whichfile(inshort());
+		p0 = inlong();
+		p1 = inlong();
+		journaln(0, p0);
+		journaln(0, p1);
+		logdelete(f, p0, p1);
+		if(fileupdate(f, FALSE, FALSE))
+			seq++;
+		f->dot.r.p1 = f->dot.r.p2 = p0;
+		f->tdot = f->dot.r;   /* terminal knows the value of dot already */
+		break;
+
+	case Tpaste:
+		f = whichfile(inshort());
+		p0 = inlong();
+		journaln(0, p0);
+		int fd = open("/dev/snarf", OREAD);
+		if(fd < 0)
+			panic("paste: open");
+		bufreset(&snarfbuf);
+		bufload(&snarfbuf, 0, fd, &i);
+		close(fd);
+		for(l=0; l<snarfbuf.nc; l+=m){
+			m = snarfbuf.nc-l;
+			if(m>BLOCKSIZE)
+				m = BLOCKSIZE;
+			bufread(&snarfbuf, l, genbuf, m);
+			loginsert(f, p0, tmprstr(genbuf, m)->s, m);
+		}
+		if(fileupdate(f, FALSE, TRUE))
+			seq++;
+		f->dot.r.p1 = p0;
+		f->dot.r.p2 = p0+snarfbuf.nc;
+		f->tdot.p1 = -1; /* force telldot to tell (arguably a BUG) */
+		telldot(f);
+		outTs(Hunlockfile, f->tag);
+		break;
+
+	case Tsnarf:
+		i = inshort();
+		p0 = inlong();
+		p1 = inlong();
+		snarf(whichfile(i), p0, p1, &snarfbuf, 0);
+		break;
+
+	case Tstartnewfile:
+		v = invlong();
+		Strdupl(&genstr, empty);
+		f = newfile();
+		f->rasp = listalloc('P');
+		outTsv(Hbindname, f->tag, v);
+		logsetname(f, &genstr);
+		outTs(Hcurrent, f->tag);
+		current(f);
+		load(f);
+		break;
+
+	case Twrite:
+		termlocked++;
+		i = inshort();
+		journaln(0, i);
+		f = whichfile(i);
+		addr.r.p1 = 0;
+		addr.r.p2 = f->nc;
+		if(f->name.s[0] == 0)
+			error(Enoname);
+		Strduplstr(&genstr, &f->name);
+		writef(f);
+		break;
+
+	case Tclose:
+		termlocked++;
+		i = inshort();
+		journaln(0, i);
+		f = whichfile(i);
+		current(f);
+		trytoclose(f);
+		/* if trytoclose fails, will error out */
+		delete(f);
+		break;
+
+	case Tlook:
+		f = whichfile(inshort());
+		termlocked++;
+		p0 = inlong();
+		p1 = inlong();
+		journaln(0, p0);
+		journaln(0, p1);
+		setgenstr(f, p0, p1);
+		for(l = 0; l<genstr.n; l++){
+			i = genstr.s[l];
+			if(utfrune(".*+?(|)\\[]^$", i)){
+				str = tmpcstr("\\");
+				Strinsert(&genstr, str, l++);
+				freetmpstr(str);
+			}
+		}
+		Straddc(&genstr, '\0');
+		nextmatch(f, &genstr, p1, 1);
+		moveto(f, sel.p[0]);
+		break;
+
+	case Tsearch:
+		termlocked++;
+		if(curfile == 0)
+			error(Enofile);
+		if(lastpat.s[0] == 0)
+			panic("Tsearch");
+		nextmatch(curfile, &lastpat, curfile->dot.r.p2, 1);
+		moveto(curfile, sel.p[0]);
+		break;
+
+	case Tsend:
+		termlocked++;
+		inshort();	/* ignored */
+		p0 = inlong();
+		p1 = inlong();
+		setgenstr(cmd, p0, p1);
+		bufreset(&snarfbuf);
+		bufinsert(&snarfbuf, (Posn)0, genstr.s, genstr.n);
+		outTl(Hsnarflen, genstr.n);
+		if(genstr.s[genstr.n-1] != '\n')
+			Straddc(&genstr, '\n');
+		loginsert(cmd, cmd->nc, genstr.s, genstr.n);
+		fileupdate(cmd, FALSE, TRUE);
+		cmd->dot.r.p1 = cmd->dot.r.p2 = cmd->nc;
+		telldot(cmd);
+		termcommand();
+		break;
+
+	case Tdclick:
+	case Ttclick:
+		f = whichfile(inshort());
+		p1 = inlong();
+		stretchsel(f, p1, type == Ttclick);
+		f->tdot.p1 = f->tdot.p2 = p1;
+		telldot(f);
+		outTs(Hunlockfile, f->tag);
+		break;
+
+	case Tstartsnarf:
+		if (snarfbuf.nc <= 0) {	/* nothing to export */
+			outTs(Hsetsnarf, 0);
+			break;
+		}
+		c = 0;
+		i = 0;
+		m = snarfbuf.nc;
+		if(m > SNARFSIZE) {
+			m = SNARFSIZE;
+			dprint("?warning: snarf buffer truncated\n");
+		}
+		rp = malloc(m*sizeof(Rune));
+		if(rp){
+			bufread(&snarfbuf, 0, rp, m);
+			c = Strtoc(tmprstr(rp, m));
+			free(rp);
+			i = strlen(c);
+		}
+		outTs(Hsetsnarf, i);
+		if(c){
+			Write(1, c, i);
+			free(c);
+		} else
+			dprint("snarf buffer too long\n");
+		break;
+
+	case Tsetsnarf:
+		m = inshort();
+		if(m > SNARFSIZE)
+			error(Etoolong);
+		c = malloc(m+1);
+		if(c){
+			for(i=0; i<m; i++)
+				c[i] = rcvchar();
+			c[m] = 0;
+			str = tmpcstr(c);
+			free(c);
+			bufreset(&snarfbuf);
+			bufinsert(&snarfbuf, (Posn)0, str->s, str->n);
+			freetmpstr(str);
+			outT0(Hunlock);
+		}
+		break;
+
+	case Tack:
+		waitack = 0;
+		break;
+
+	case Tplumb:
+		f = whichfile(inshort());
+		p0 = inlong();
+		p1 = inlong();
+		pm = emalloc(sizeof(Plumbmsg));
+		pm->src = strdup("jam");
+		pm->dst = 0;
+		/* construct current directory */
+		c = Strtoc(&f->name);
+		if(strcmp(c, "/tmp/jam.err") == 0){
+			pm->wdir = emalloc(1024);
+			getwd(pm->wdir, 1024);
+			cleanname(pm->wdir);
+			free(c);
+		}else if(c[0] == '/'){
+			pm->wdir = c;
+			if((c = strrchr(pm->wdir, '/')) != nil)
+				*c = '\0';
+		}else{
+			wdir = emalloc(1024);
+			getwd(wdir, 1024);
+			pm->wdir = emalloc(1024);
+			snprint(pm->wdir, 1024, "%s/%s", wdir, c);
+			cleanname(pm->wdir);
+			free(wdir);
+			free(c);
+			if((c = strrchr(pm->wdir, '/')) != nil)
+				*c = '\0';
+		}
+		pm->type = strdup("text");
+		if(p1 > p0)
+			pm->attr = nil;
+		else{
+			p = p0;
+			while(p0>0 && (i=filereadc(f, p0 - 1))!=' ' && i!='\t' && i!='\n')
+				p0--;
+			while(p1<f->nc && (i=filereadc(f, p1))!=' ' && i!='\t' && i!='\n')
+				p1++;
+			sprint(cbuf, "click=%ld", p-p0);
+			pm->attr = plumbunpackattr(cbuf);
+		}
+		if(p0==p1 || p1-p0>=BLOCKSIZE){
+			plumbfree(pm);
+			break;
+		}
+		setgenstr(f, p0, p1);
+		pm->data = Strtoc(&genstr);
+		pm->ndata = strlen(pm->data);
+		c = plumbpack(pm, &i);
+		if(c != 0){
+			outTs(Hplumb, i);
+			Write(1, c, i);
+			free(c);
+		}
+		plumbfree(pm);
+		break;
+
+	case Tmenucmd:
+		dprint((char*)inp);
+		break;
+
+	case Tmenucmdsend:
+		termlocked++;
+		str = tmpcstr((char*)inp);
+		Straddc(str, '\n');
+		loginsert(cmd, cmd->nc, str->s, str->n);
+		freetmpstr(str);
+		fileupdate(cmd, FALSE, TRUE);
+		cmd->dot.r.p1 = cmd->dot.r.p2 = cmd->nc;
+		telldot(cmd);
+		termcommand();
+		break;
+
+	case Texit:
+		exits(0);
+	}
+	return TRUE;
+}
+
+void
+snarf(File *f, Posn p1, Posn p2, Buffer *buf, int emptyok)
+{
+	Posn l;
+	int i, fd, n;
+	String *s;
+	char *cs;
+
+	if(!emptyok && p1==p2)
+		return;
+	bufreset(buf);
+	/* Stage through genbuf to avoid compaction problems (vestigial) */
+	if(p2 > f->nc){
+		fprint(2, "bad snarf addr p1=%ld p2=%ld f->nc=%d\n", p1, p2, f->nc); /*ZZZ should never happen, can remove */
+		p2 = f->nc;
+	}
+	fd = open("/dev/snarf", OWRITE);
+	if(fd < 0){
+		fprint(2, "snarf: %r\n");
+		return;
+	}
+	for(l=p1; l<p2; l+=i){
+		i = p2-l>BLOCKSIZE? BLOCKSIZE : p2-l;
+		bufread(f, l, genbuf, i);
+		s = tmprstr(genbuf, i);
+		cs = Strtoc(s);
+		n = strlen(cs);
+		if(write(fd, cs, n) != n)
+			fprint(2, "snarf: %r\n");
+		free(cs);
+		bufinsert(buf, buf->nc, s->s, i);
+	}
+	close(fd);
+}
+
+int
+inshort(void)
+{
+	ushort n;
+
+	n = inp[0] | (inp[1]<<8);
+	inp += 2;
+	return n;
+}
+
+long
+inlong(void)
+{
+	ulong n;
+
+	n = inp[0] | (inp[1]<<8) | (inp[2]<<16) | (inp[3]<<24);
+	inp += 4;
+	return n;
+}
+
+vlong
+invlong(void)
+{
+	vlong v;
+	
+	v = (inp[7]<<24) | (inp[6]<<16) | (inp[5]<<8) | inp[4];
+	v = (v<<16) | (inp[3]<<8) | inp[2];
+	v = (v<<16) | (inp[1]<<8) | inp[0];
+	inp += 8;
+	return v;
+}
+
+void
+setgenstr(File *f, Posn p0, Posn p1)
+{
+	if(p0 != p1){
+		if(p1-p0 >= TBLOCKSIZE)
+			error(Etoolong);
+		Strinsure(&genstr, p1-p0);
+		bufread(f, p0, genbuf, p1-p0);
+		memmove(genstr.s, genbuf, RUNESIZE*(p1-p0));
+		genstr.n = p1-p0;
+	}else{
+		if(snarfbuf.nc == 0)
+			error(Eempty);
+		if(snarfbuf.nc > TBLOCKSIZE)
+			error(Etoolong);
+		bufread(&snarfbuf, (Posn)0, genbuf, snarfbuf.nc);
+		Strinsure(&genstr, snarfbuf.nc);
+		memmove(genstr.s, genbuf, RUNESIZE*snarfbuf.nc);
+		genstr.n = snarfbuf.nc;
+	}
+}
+
+void
+outT0(Hmesg type)
+{
+	outstart(type);
+	outsend();
+}
+
+void
+outTl(Hmesg type, long l)
+{
+	outstart(type);
+	outlong(l);
+	outsend();
+}
+
+void
+outTs(Hmesg type, int s)
+{
+	outstart(type);
+	journaln(1, s);
+	outshort(s);
+	outsend();
+}
+
+void
+outS(String *s)
+{
+	char *c;
+	int i;
+
+	c = Strtoc(s);
+	i = strlen(c);
+	outcopy(i, c);
+	if(i > 99)
+		c[99] = 0;
+	journaln(1, i);
+	journal(1, c);
+	free(c);
+}
+
+void
+outTsS(Hmesg type, int s1, String *s)
+{
+	outstart(type);
+	outshort(s1);
+	outS(s);
+	outsend();
+}
+
+void
+outTslS(Hmesg type, int s1, Posn l1, String *s)
+{
+	outstart(type);
+	outshort(s1);
+	journaln(1, s1);
+	outlong(l1);
+	journaln(1, l1);
+	outS(s);
+	outsend();
+}
+
+void
+outTS(Hmesg type, String *s)
+{
+	outstart(type);
+	outS(s);
+	outsend();
+}
+
+void
+outTsllS(Hmesg type, int s1, Posn l1, Posn l2, String *s)
+{
+	outstart(type);
+	outshort(s1);
+	outlong(l1);
+	outlong(l2);
+	journaln(1, l1);
+	journaln(1, l2);
+	outS(s);
+	outsend();
+}
+
+void
+outTsll(Hmesg type, int s, Posn l1, Posn l2)
+{
+	outstart(type);
+	outshort(s);
+	outlong(l1);
+	outlong(l2);
+	journaln(1, l1);
+	journaln(1, l2);
+	outsend();
+}
+
+void
+outTsl(Hmesg type, int s, Posn l)
+{
+	outstart(type);
+	outshort(s);
+	outlong(l);
+	journaln(1, l);
+	outsend();
+}
+
+void
+outTsv(Hmesg type, int s, vlong v)
+{
+	outstart(type);
+	outshort(s);
+	outvlong(v);
+	journalv(1, v);
+	outsend();
+}
+
+void
+outstart(Hmesg type)
+{
+	journal(1, hname[type]);
+	outmsg[0] = type;
+	outp = outmsg+3;
+}
+
+void
+outcopy(int count, void *data)
+{
+	memmove(outp, data, count);
+	outp += count;
+}
+
+void
+outshort(int s)
+{
+	*outp++ = s;
+	*outp++ = s>>8; 
+}
+
+void
+outlong(long l)
+{
+	*outp++ = l;
+	*outp++ = l>>8;
+	*outp++ = l>>16;
+	*outp++ = l>>24;
+}
+
+void
+outvlong(vlong v)
+{
+	int i;
+
+	for(i = 0; i < 8; i++){
+		*outp++ = v;
+		v >>= 8;
+	}
+}
+
+void
+outsend(void)
+{
+	int outcount;
+
+	if(outp >= outdata+nelem(outdata))
+		panic("outsend");
+	outcount = outp-outmsg;
+	outcount -= 3;
+	outmsg[1] = outcount;
+	outmsg[2] = outcount>>8;
+	outmsg = outp;
+	if(!outbuffered){
+		outcount = outmsg-outdata;
+		if (write(1, (char*) outdata, outcount) != outcount)
+			rescue();
+		outmsg = outdata;
+		return;
+	}
+}
+
+int
+needoutflush(void)
+{
+	return outmsg >= outdata+DATASIZE;
+}
+
+void
+outflush(void)
+{
+	if(outmsg == outdata)
+		return;
+	outbuffered = 0;
+	/* flow control */
+	outT0(Hack);
+	waitack = 1;
+	do
+		if(rcv() == 0){
+			rescue();
+			exits("eof");
+		}
+	while(waitack);
+	outmsg = outdata;
+	outbuffered = 1;
+}
--- /dev/null
+++ b/sys/src/cmd/jam/mesg.h
@@ -1,0 +1,136 @@
+/* VERSION 1 introduces plumbing
+	2 increases SNARFSIZE from 4096 to 32000
+	3 adds a triple click
+ */
+#define	VERSION	3
+
+#define	TBLOCKSIZE 512		  /* largest piece of text sent to terminal */
+#define	DATASIZE  (UTFmax*TBLOCKSIZE+30) /* ... including protocol header stuff */
+#define	SNARFSIZE 32000		/* maximum length of exchanged snarf buffer, must fit in 15 bits */
+/*
+ * Messages originating at the terminal
+ */
+typedef enum Tmesg
+{
+	Tversion,	/* version */
+	Tstartcmdfile,	/* terminal just opened command frame */
+	Tcheck,		/* ask host to poke with Hcheck */
+	Trequest,	/* request data to fill a hole */
+	Torigin,	/* gimme an Horigin near here */
+	Tstartfile,	/* terminal just opened a file's frame */
+	Tworkfile,	/* set file to which commands apply */
+	Ttype,		/* add some characters, but terminal already knows */
+	Tcut,
+	Tpaste,
+	Tsnarf,
+	Tstartnewfile,	/* terminal just opened a new frame */
+	Twrite,		/* write file */
+	Tclose,		/* terminal requests file close; check mod. status */
+	Tlook,		/* search for literal current text */
+	Tsearch,	/* search for last regular expression */
+	Tsend,		/* pretend he typed stuff */
+	Tdclick,	/* double click */
+	Tstartsnarf,	/* initiate snarf buffer exchange */
+	Tsetsnarf,	/* remember string in snarf buffer */
+	Tack,		/* acknowledge Hack */
+	Texit,		/* exit */
+	Tplumb,		/* send plumb message */
+	Ttclick,	/* triple click */
+	Tmenucmd,	/* list custom cmds in b2 menu */
+	Tmenucmdsend,	/* execute custom cmd from b2 menu */
+	TMAX,
+}Tmesg;
+/*
+ * Messages originating at the host
+ */
+typedef enum Hmesg
+{
+	Hversion,	/* version */
+	Hbindname,	/* attach name[0] to text in terminal */
+	Hcurrent,	/* make named file the typing file */
+	Hnewname,	/* create "" name in menu */
+	Hmovname,	/* move file name in menu */
+	Hgrow,		/* insert space in rasp */
+	Hcheck0,	/* see below */
+	Hcheck,		/* ask terminal to check whether it needs more data */
+	Hunlock,	/* command is finished; user can do things */
+	Hdata,		/* store this data in previously allocated space */
+	Horigin,	/* set origin of file/frame in terminal */
+	Hunlockfile,	/* unlock file in terminal */
+	Hsetdot,	/* set dot in terminal */
+	Hgrowdata,	/* Hgrow + Hdata folded together */
+	Hmoveto,	/* scrolling, context search, etc. */
+	Hclean,		/* named file is now 'clean' */
+	Hdirty,		/* named file is now 'dirty' */
+	Hcut,		/* remove space from rasp */
+	Hsetpat,	/* set remembered regular expression */
+	Hdelname,	/* delete file name from menu */
+	Hclose,		/* close file and remove from menu */
+	Hsetsnarf,	/* remember string in snarf buffer */
+	Hsnarflen,	/* report length of implicit snarf */
+	Hack,		/* request acknowledgement */
+	Hexit,
+	Hplumb,		/* return plumb message to terminal - version 1 */
+	Hmenucmd,	/* modify custom cmds in b2 menu */
+	HMAX,
+}Hmesg;
+typedef struct Header{
+	uchar	type;		/* one of the above */
+	uchar	count0;		/* low bits of data size */
+	uchar	count1;		/* high bits of data size */
+	uchar	data[1];	/* variable size */
+}Header;
+
+/*
+ * File transfer protocol schematic, a la Holzmann
+ * #define N	6
+ * 
+ * chan h = [4] of { mtype };
+ * chan t = [4] of { mtype };
+ * 
+ * mtype = {	Hgrow, Hdata,
+ * 		Hcheck, Hcheck0,
+ * 		Trequest, Tcheck,
+ * 	};
+ * 
+ * active proctype host()
+ * {	byte n;
+ * 
+ * 	do
+ * 	:: n <  N -> n++; t!Hgrow
+ * 	:: n == N -> n++; t!Hcheck0
+ * 
+ * 	:: h?Trequest -> t!Hdata
+ * 	:: h?Tcheck   -> t!Hcheck
+ * 	od
+ * }
+ * 
+ * active proctype term()
+ * {
+ * 	do
+ * 	:: t?Hgrow   -> h!Trequest
+ * 	:: t?Hdata   -> skip
+ * 	:: t?Hcheck0 -> h!Tcheck
+ * 	:: t?Hcheck  ->
+ * 		if
+ * 		:: h!Trequest -> progress: h!Tcheck
+ * 		:: break
+ * 		fi
+ * 	od;
+ * 	printf("term exits\n")
+ * }
+ *
+ * From: gerard@research.bell-labs.com
+ * Date: Tue Jul 17 13:47:23 EDT 2001
+ * To: rob@research.bell-labs.com
+ * 
+ * spin -c 	(or -a) spec
+ * pcc -DNP -o pan pan.c
+ * pan -l
+ * 
+ * proves that there are no non-progress cycles
+ * (infinite executions *not* passing through
+ * the statement marked with a label starting
+ * with the prefix "progress")
+ * 
+ */
--- /dev/null
+++ b/sys/src/cmd/jam/mkfile
@@ -1,0 +1,40 @@
+</$objtype/mkfile
+
+TARG=jam
+OFILES=sam.$O\
+	address.$O\
+	buff.$O\
+	cmd.$O\
+	disk.$O\
+	error.$O\
+	file.$O\
+	io.$O\
+	list.$O\
+	mesg.$O\
+	moveto.$O\
+	multi.$O\
+	plan9.$O\
+	rasp.$O\
+	regexp.$O\
+	shell.$O\
+	string.$O\
+	sys.$O\
+	util.$O\
+	xec.$O\
+
+HFILES=sam.h\
+	errors.h\
+	mesg.h\
+
+BIN=/$objtype/bin
+
+UPDATE=\
+	mkfile\
+	parse.h\
+	$HFILES\
+	${OFILES:%.$O=%.c}\
+
+</sys/src/cmd/mkone
+
+address.$O cmd.$O parse.$O xec.$O unix.$O:	parse.h
+
--- /dev/null
+++ b/sys/src/cmd/jam/moveto.c
@@ -1,0 +1,193 @@
+#include "sam.h"
+
+void
+moveto(File *f, Range r)
+{
+	Posn p1 = r.p1, p2 = r.p2;
+
+	f->dot.r.p1 = p1;
+	f->dot.r.p2 = p2;
+	if(f->rasp){
+		telldot(f);
+		outTsl(Hmoveto, f->tag, f->dot.r.p1);
+	}
+}
+
+void
+telldot(File *f)
+{
+	if(f->rasp == 0)
+		panic("telldot");
+	if(f->dot.r.p1==f->tdot.p1 && f->dot.r.p2==f->tdot.p2)
+		return;
+	outTsll(Hsetdot, f->tag, f->dot.r.p1, f->dot.r.p2);
+	f->tdot = f->dot.r;
+}
+
+void
+tellpat(void)
+{
+	outTS(Hsetpat, &lastpat);
+	patset = FALSE;
+}
+
+#define	CHARSHIFT	128
+
+void
+lookorigin(File *f, Posn p0, Posn ls)
+{
+	int nl, nc, c;
+	Posn p, oldp0;
+
+	if(p0 > f->nc)
+		p0 = f->nc;
+	oldp0 = p0;
+	p = p0;
+	for(nl=nc=c=0; c!=-1 && nl<ls && nc<ls*CHARSHIFT; nc++)
+		if((c=filereadc(f, --p)) == '\n'){
+			nl++;
+			oldp0 = p0-nc;
+		}
+	if(c == -1)
+		p0 = 0;
+	else if(nl==0){
+		if(p0>=CHARSHIFT/2)
+			p0-=CHARSHIFT/2;
+		else
+			p0 = 0;
+	}else
+		p0 = oldp0;
+	outTsl(Horigin, f->tag, p0);
+}
+
+int
+isalnum(int c)
+{
+	/*
+	 * 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 0;
+	if(0x7F<=c && c<=0xA0)
+		return 0;
+	if(utfrune("!\"#$%&'()*+,-./:;<=>?@[\\]^`{|}~", c))
+		return 0;
+	return 1;
+}
+
+int
+isspace(Rune c)
+{
+	return c == 0 || c == ' ' || c == '\t' ||
+		c == '\n' || c == '\r' || c == '\v';
+}
+
+int
+inmode(Rune r, int mode)
+{
+	return (mode == 0) ? isalnum(r) : r && !isspace(r);
+}
+
+int
+clickmatch(File *f, int cl, int cr, int dir, Posn *p)
+{
+	int c;
+	int nest = 1;
+
+	for(;;){
+		if(dir > 0){
+			if(*p >= f->nc)
+				break;
+			c = filereadc(f, (*p)++);
+		}else{
+			if(*p == 0)
+				break;
+			c = filereadc(f, --(*p));
+		}
+		if(c == cr){
+			if(--nest==0)
+				return 1;
+		}else if(c == cl)
+			nest++;
+	}
+	return cl=='\n' && nest==1;
+}
+
+Rune*
+strrune(Rune *s, Rune c)
+{
+	Rune c1;
+
+	if(c == 0) {
+		while(*s++)
+			;
+		return s-1;
+	}
+
+	while(c1 = *s++)
+		if(c1 == c)
+			return s-1;
+	return 0;
+}
+
+/*
+ * Stretches a selection out over current text,
+ * selecting matching range if possible.
+ * If there's no matching range, mode 0 selects
+ * a single alphanumeric region. Mode 1 selects
+ * a non-whitespace region.
+ */
+void
+stretchsel(File *f, Posn p1, int mode)
+{
+	int c, i;
+	Rune *r, *l;
+	Posn p;
+
+	if(p1 > f->nc)
+		return;
+	f->dot.r.p1 = f->dot.r.p2 = p1;
+	for(i=0; left[i]; i++){
+		l = left[i];
+		r = right[i];
+		/* try left match */
+		p = p1;
+		if(p1 == 0)
+			c = '\n';
+		else
+			c = filereadc(f, p - 1);
+		if(strrune(l, c)){
+			if(clickmatch(f, c, r[strrune(l, c)-l], 1, &p)){
+				f->dot.r.p1 = p1;
+				f->dot.r.p2 = p-(c!='\n');
+			}
+			return;
+		}
+		/* try right match */
+		p = p1;
+		if(p1 == f->nc)
+			c = '\n';
+		else
+			c = filereadc(f, p);
+		if(strrune(r, c)){
+			if(clickmatch(f, c, l[strrune(r, c)-r], -1, &p)){
+				f->dot.r.p1 = p;
+				if(c!='\n' || p!=0 || filereadc(f, 0)=='\n')
+					f->dot.r.p1++;
+				f->dot.r.p2 = p1+(p1<f->nc && c=='\n');
+			}
+			return;
+		}
+	}
+	/* try filling out word to right */
+	p = p1;
+	while(p < f->nc && inmode(filereadc(f, p++), mode))
+		f->dot.r.p2++;
+	/* try filling out word to left */
+	p = p1;
+	while(--p >= 0 && inmode(filereadc(f, p), mode))
+		f->dot.r.p1--;
+}
+
--- /dev/null
+++ b/sys/src/cmd/jam/multi.c
@@ -1,0 +1,123 @@
+#include "sam.h"
+
+List	file = { 'p' };
+ushort	tag;
+
+File *
+newfile(void)
+{
+	File *f;
+
+	f = fileopen();
+	inslist(&file, 0, f);
+	f->tag = tag++;
+	if(downloaded)
+		outTs(Hnewname, f->tag);
+	/* already sorted; file name is "" */
+	return f;
+}
+
+int
+whichmenu(File *f)
+{
+	int i;
+
+	for(i=0; i<file.nused; i++)
+		if(file.filepptr[i]==f)
+			return i;
+	return -1;
+}
+
+void
+delfile(File *f)
+{
+	int w = whichmenu(f);
+
+	if(w < 0)	/* e.g. x/./D */
+		return;
+	if(downloaded)
+		outTs(Hdelname, f->tag);
+	dellist(&file, w);
+	fileclose(f);
+}
+
+void
+fullname(String *name)
+{
+	if(name->n > 0 && name->s[0]!='/' && name->s[0]!=0)
+		Strinsert(name, &curwd, (Posn)0);
+}
+
+void
+fixname(String *name)
+{
+	String *t;
+	char *s;
+
+	fullname(name);
+	s = Strtoc(name);
+	if(strlen(s) > 0)
+		s = cleanname(s);
+	t = tmpcstr(s);
+	Strduplstr(name, t);
+	free(s);
+	freetmpstr(t);
+
+	if(Strispre(&curwd, name))
+		Strdelete(name, 0, curwd.n);
+}
+
+void
+sortname(File *f)
+{
+	int i, cmp, w;
+	int dupwarned;
+
+	w = whichmenu(f);
+	dupwarned = FALSE;
+	dellist(&file, w);
+	if(f == cmd)
+		i = 0;
+	else{
+		for(i=0; i<file.nused; i++){
+			cmp = Strcmp(&f->name, &file.filepptr[i]->name);
+			if(cmp==0 && !dupwarned){
+				dupwarned = TRUE;
+				warn_S(Wdupname, &f->name);
+			}else if(cmp<0 && (i>0 || cmd==0))
+				break;
+		}
+	}
+	inslist(&file, i, f);
+	if(downloaded)
+		outTsS(Hmovname, f->tag, &f->name);
+}
+
+void
+state(File *f, int cleandirty)
+{
+	if(f == cmd)
+		return;
+	f->unread = FALSE;
+	if(downloaded && whichmenu(f)>=0){	/* else flist or menu */
+		if(f->mod && cleandirty!=Dirty)
+			outTs(Hclean, f->tag);
+		else if(!f->mod && cleandirty==Dirty)
+			outTs(Hdirty, f->tag);
+	}
+	if(cleandirty == Clean)
+		f->mod = FALSE;
+	else
+		f->mod = TRUE;
+}
+
+File *
+lookfile(String *s)
+{
+	int i;
+
+	for(i=0; i<file.nused; i++)
+		if(Strcmp(&file.filepptr[i]->name, s) == 0)
+			return file.filepptr[i];
+	return 0;
+}
--- /dev/null
+++ b/sys/src/cmd/jam/parse.h
@@ -1,0 +1,71 @@
+typedef struct Addr Addr;
+typedef struct Cmd Cmd;
+struct Addr
+{
+	char	type;	/* # (char addr), l (line addr), / ? . $ + - , ; */
+	union{
+		String	*re;
+		Addr	*aleft;		/* left side of , and ; */
+	} g;
+	Posn	num;
+	Addr	*next;			/* or right side of , and ; */
+};
+
+#define	are	g.re
+#define	left	g.aleft
+
+struct Cmd
+{
+	Addr	*addr;			/* address (range of text) */
+	String	*re;			/* regular expression for e.g. 'x' */
+	union{
+		Cmd	*cmd;		/* target of x, g, {, etc. */
+		String	*text;		/* text of a, c, i; rhs of s */
+		Addr	*addr;		/* address for m, t */
+	} g;
+	Cmd	*next;			/* pointer to next element in {} */
+	short	num;
+	ushort	flag;			/* whatever */
+	ushort	cmdc;			/* command character; 'x' etc. */
+};
+
+#define	ccmd	g.cmd
+#define	ctext	g.text
+#define	caddr	g.addr
+
+typedef struct Cmdtab Cmdtab;
+struct Cmdtab
+{
+	ushort	cmdc;		/* command character */
+	uchar	text;		/* takes a textual argument? */
+	uchar	regexp;		/* takes a regular expression? */
+	uchar	addr;		/* takes an address (m or t)? */
+	uchar	defcmd;		/* default command; 0==>none */
+	uchar	defaddr;	/* default address */
+	uchar	count;		/* takes a count e.g. s2/// */
+	char	*token;		/* takes text terminated by one of these */
+	int	(*fn)(File*, Cmd*);	/* function to call with parse tree */
+}cmdtab[];
+
+enum Defaddr{	/* default addresses */
+	aNo,
+	aDot,
+	aAll,
+};
+
+int	nl_cmd(File*, Cmd*), a_cmd(File*, Cmd*), b_cmd(File*, Cmd*);
+int	c_cmd(File*, Cmd*), cd_cmd(File*, Cmd*), d_cmd(File*, Cmd*);
+int	D_cmd(File*, Cmd*), e_cmd(File*, Cmd*);
+int	f_cmd(File*, Cmd*), g_cmd(File*, Cmd*), i_cmd(File*, Cmd*);
+int	k_cmd(File*, Cmd*), m_cmd(File*, Cmd*);
+int	M_cmd(File*, Cmd*), n_cmd(File*, Cmd*);
+int	p_cmd(File*, Cmd*), q_cmd(File*, Cmd*);
+int	s_cmd(File*, Cmd*), u_cmd(File*, Cmd*), w_cmd(File*, Cmd*);
+int	x_cmd(File*, Cmd*), X_cmd(File*, Cmd*), plan9_cmd(File*, Cmd*);
+int	eq_cmd(File*, Cmd*);
+
+
+String	*getregexp(int);
+Addr	*newaddr(void);
+Address	address(Addr*, Address, int);
+int	cmdexec(File*, Cmd*);
--- /dev/null
+++ b/sys/src/cmd/jam/plan9.acid
@@ -1,0 +1,817 @@
+sizeof_1_ = 8;
+aggr _1_
+{
+	'U' 0 lo;
+	'U' 4 hi;
+};
+
+defn
+_1_(addr) {
+	complex _1_ addr;
+	print("	lo	", addr.lo, "\n");
+	print("	hi	", addr.hi, "\n");
+};
+
+sizeofFPdbleword = 8;
+aggr FPdbleword
+{
+	'F' 0 x;
+	{
+	'U' 0 lo;
+	'U' 4 hi;
+	};
+};
+
+defn
+FPdbleword(addr) {
+	complex FPdbleword addr;
+	print("	x	", addr.x, "\n");
+	print("_1_ {\n");
+		_1_(addr+0);
+	print("}\n");
+};
+
+UTFmax = 4;
+Runesync = 128;
+Runeself = 128;
+Runeerror = 65533;
+Runemax = 1114111;
+Runemask = 2097151;
+sizeofFmt = 80;
+aggr Fmt
+{
+	'b' 0 runes;
+	'Y' 8 start;
+	'Y' 16 to;
+	'Y' 24 stop;
+	'Y' 32 flush;
+	'Y' 40 farg;
+	'D' 48 nfmt;
+	'Y' 56 args;
+	'D' 64 r;
+	'D' 68 width;
+	'D' 72 prec;
+	'U' 76 flags;
+};
+
+defn
+Fmt(addr) {
+	complex Fmt addr;
+	print("	runes	", addr.runes, "\n");
+	print("	start	", addr.start\Y, "\n");
+	print("	to	", addr.to\Y, "\n");
+	print("	stop	", addr.stop\Y, "\n");
+	print("	flush	", addr.flush\Y, "\n");
+	print("	farg	", addr.farg\Y, "\n");
+	print("	nfmt	", addr.nfmt, "\n");
+	print("	args	", addr.args\Y, "\n");
+	print("	r	", addr.r, "\n");
+	print("	width	", addr.width, "\n");
+	print("	prec	", addr.prec, "\n");
+	print("	flags	", addr.flags, "\n");
+};
+
+FmtWidth = 1;
+FmtLeft = 2;
+FmtPrec = 4;
+FmtSharp = 8;
+FmtSpace = 16;
+FmtSign = 32;
+FmtZero = 64;
+FmtUnsigned = 128;
+FmtShort = 256;
+FmtLong = 512;
+FmtVLong = 1024;
+FmtComma = 2048;
+FmtByte = 4096;
+FmtFlag = 8192;
+sizeofTm = 64;
+aggr Tm
+{
+	'D' 0 nsec;
+	'D' 4 sec;
+	'D' 8 min;
+	'D' 12 hour;
+	'D' 16 mday;
+	'D' 20 mon;
+	'D' 24 year;
+	'D' 28 wday;
+	'D' 32 yday;
+	'a' 36 zone;
+	'D' 52 tzoff;
+	'Y' 56 tz;
+};
+
+defn
+Tm(addr) {
+	complex Tm addr;
+	print("	nsec	", addr.nsec, "\n");
+	print("	sec	", addr.sec, "\n");
+	print("	min	", addr.min, "\n");
+	print("	hour	", addr.hour, "\n");
+	print("	mday	", addr.mday, "\n");
+	print("	mon	", addr.mon, "\n");
+	print("	year	", addr.year, "\n");
+	print("	wday	", addr.wday, "\n");
+	print("	yday	", addr.yday, "\n");
+	print("	zone	", addr.zone, "\n");
+	print("	tzoff	", addr.tzoff, "\n");
+	print("	tz	", addr.tz\Y, "\n");
+};
+
+sizeofTmfmt = 16;
+aggr Tmfmt
+{
+	'Y' 0 fmt;
+	'A' Tm 8 tm;
+};
+
+defn
+Tmfmt(addr) {
+	complex Tmfmt addr;
+	print("	fmt	", addr.fmt\Y, "\n");
+	print("	tm	", addr.tm\Y, "\n");
+};
+
+PNPROC = 1;
+PNGROUP = 2;
+Profoff = 0;
+Profuser = 1;
+Profkernel = 2;
+Proftime = 3;
+Profsample = 4;
+sizeofLock = 8;
+aggr Lock
+{
+	'D' 0 val;
+};
+
+defn
+Lock(addr) {
+	complex Lock addr;
+	print("	val	", addr.val, "\n");
+};
+
+sizeofQLp = 16;
+aggr QLp
+{
+	'D' 0 inuse;
+	'D' 4 state;
+	'A' QLp 8 next;
+};
+
+defn
+QLp(addr) {
+	complex QLp addr;
+	print("	inuse	", addr.inuse, "\n");
+	print("	state	", addr.state, "\n");
+	print("	next	", addr.next\Y, "\n");
+};
+
+sizeofQLock = 32;
+aggr QLock
+{
+	Lock 0 lock;
+	'D' 8 locked;
+	'A' QLp 16 $head;
+	'A' QLp 24 $tail;
+};
+
+defn
+QLock(addr) {
+	complex QLock addr;
+	print("Lock lock {\n");
+	Lock(addr.lock);
+	print("}\n");
+	print("	locked	", addr.locked, "\n");
+	print("	$head	", addr.$head\Y, "\n");
+	print("	$tail	", addr.$tail\Y, "\n");
+};
+
+sizeofRWLock = 32;
+aggr RWLock
+{
+	Lock 0 lock;
+	'D' 8 readers;
+	'D' 12 writer;
+	'A' QLp 16 $head;
+	'A' QLp 24 $tail;
+};
+
+defn
+RWLock(addr) {
+	complex RWLock addr;
+	print("Lock lock {\n");
+	Lock(addr.lock);
+	print("}\n");
+	print("	readers	", addr.readers, "\n");
+	print("	writer	", addr.writer, "\n");
+	print("	$head	", addr.$head\Y, "\n");
+	print("	$tail	", addr.$tail\Y, "\n");
+};
+
+sizeofRendez = 24;
+aggr Rendez
+{
+	'A' QLock 0 l;
+	'A' QLp 8 $head;
+	'A' QLp 16 $tail;
+};
+
+defn
+Rendez(addr) {
+	complex Rendez addr;
+	print("	l	", addr.l\Y, "\n");
+	print("	$head	", addr.$head\Y, "\n");
+	print("	$tail	", addr.$tail\Y, "\n");
+};
+
+sizeofNetConnInfo = 72;
+aggr NetConnInfo
+{
+	'Y' 0 dir;
+	'Y' 8 root;
+	'Y' 16 spec;
+	'Y' 24 lsys;
+	'Y' 32 lserv;
+	'Y' 40 rsys;
+	'Y' 48 rserv;
+	'Y' 56 laddr;
+	'Y' 64 raddr;
+};
+
+defn
+NetConnInfo(addr) {
+	complex NetConnInfo addr;
+	print("	dir	", addr.dir\Y, "\n");
+	print("	root	", addr.root\Y, "\n");
+	print("	spec	", addr.spec\Y, "\n");
+	print("	lsys	", addr.lsys\Y, "\n");
+	print("	lserv	", addr.lserv\Y, "\n");
+	print("	rsys	", addr.rsys\Y, "\n");
+	print("	rserv	", addr.rserv\Y, "\n");
+	print("	laddr	", addr.laddr\Y, "\n");
+	print("	raddr	", addr.raddr\Y, "\n");
+};
+
+RFNAMEG = 1;
+RFENVG = 2;
+RFFDG = 4;
+RFNOTEG = 8;
+RFPROC = 16;
+RFMEM = 32;
+RFNOWAIT = 64;
+RFCNAMEG = 1024;
+RFCENVG = 2048;
+RFCFDG = 4096;
+RFREND = 8192;
+RFNOMNT = 16384;
+sizeofQid = 16;
+aggr Qid
+{
+	'W' 0 path;
+	'U' 8 vers;
+	'b' 12 type;
+};
+
+defn
+Qid(addr) {
+	complex Qid addr;
+	print("	path	", addr.path, "\n");
+	print("	vers	", addr.vers, "\n");
+	print("	type	", addr.type, "\n");
+};
+
+sizeofDir = 80;
+aggr Dir
+{
+	'u' 0 type;
+	'U' 4 dev;
+	Qid 8 qid;
+	'U' 24 mode;
+	'U' 28 atime;
+	'U' 32 mtime;
+	'V' 40 length;
+	'Y' 48 name;
+	'Y' 56 uid;
+	'Y' 64 gid;
+	'Y' 72 muid;
+};
+
+defn
+Dir(addr) {
+	complex Dir addr;
+	print("	type	", addr.type, "\n");
+	print("	dev	", addr.dev, "\n");
+	print("Qid qid {\n");
+	Qid(addr.qid);
+	print("}\n");
+	print("	mode	", addr.mode, "\n");
+	print("	atime	", addr.atime, "\n");
+	print("	mtime	", addr.mtime, "\n");
+	print("	length	", addr.length, "\n");
+	print("	name	", addr.name\Y, "\n");
+	print("	uid	", addr.uid\Y, "\n");
+	print("	gid	", addr.gid\Y, "\n");
+	print("	muid	", addr.muid\Y, "\n");
+};
+
+sizeofWaitmsg = 24;
+aggr Waitmsg
+{
+	'D' 0 pid;
+	'a' 4 time;
+	'Y' 16 msg;
+};
+
+defn
+Waitmsg(addr) {
+	complex Waitmsg addr;
+	print("	pid	", addr.pid, "\n");
+	print("	time	", addr.time, "\n");
+	print("	msg	", addr.msg\Y, "\n");
+};
+
+sizeofIOchunk = 16;
+aggr IOchunk
+{
+	'Y' 0 addr;
+	'U' 8 len;
+};
+
+defn
+IOchunk(addr) {
+	complex IOchunk addr;
+	print("	addr	", addr.addr\Y, "\n");
+	print("	len	", addr.len, "\n");
+};
+
+sizeofPlumbmsg = 56;
+aggr Plumbmsg
+{
+	'Y' 0 src;
+	'Y' 8 dst;
+	'Y' 16 wdir;
+	'Y' 24 type;
+	'Y' 32 attr;
+	'D' 40 ndata;
+	'Y' 48 data;
+};
+
+defn
+Plumbmsg(addr) {
+	complex Plumbmsg addr;
+	print("	src	", addr.src\Y, "\n");
+	print("	dst	", addr.dst\Y, "\n");
+	print("	wdir	", addr.wdir\Y, "\n");
+	print("	type	", addr.type\Y, "\n");
+	print("	attr	", addr.attr\Y, "\n");
+	print("	ndata	", addr.ndata, "\n");
+	print("	data	", addr.data\Y, "\n");
+};
+
+sizeofPlumbattr = 24;
+aggr Plumbattr
+{
+	'Y' 0 name;
+	'Y' 8 value;
+	'A' Plumbattr 16 next;
+};
+
+defn
+Plumbattr(addr) {
+	complex Plumbattr addr;
+	print("	name	", addr.name\Y, "\n");
+	print("	value	", addr.value\Y, "\n");
+	print("	next	", addr.next\Y, "\n");
+};
+
+Eopen = 0;
+Ecreate = 1;
+Emenu = 2;
+Emodified = 3;
+Eio = 4;
+Ewseq = 5;
+Eunk = 6;
+Emissop = 7;
+Edelim = 8;
+Efork = 9;
+Eintr = 10;
+Eaddress = 11;
+Esearch = 12;
+Epattern = 13;
+Enewline = 14;
+Eblank = 15;
+Enopattern = 16;
+EnestXY = 17;
+Enolbrace = 18;
+Enoaddr = 19;
+Eoverlap = 20;
+Enosub = 21;
+Elongrhs = 22;
+Ebadrhs = 23;
+Erange = 24;
+Esequence = 25;
+Eorder = 26;
+Enoname = 27;
+Eleftpar = 28;
+Erightpar = 29;
+Ebadclass = 30;
+Ebadregexp = 31;
+Eoverflow = 32;
+Enocmd = 33;
+Epipe = 34;
+Enofile = 35;
+Etoolong = 36;
+Echanges = 37;
+Eempty = 38;
+Efsearch = 39;
+Emanyfiles = 40;
+Elongtag = 41;
+Esubexp = 42;
+Etmpovfl = 43;
+Eappend = 44;
+Ecantplumb = 45;
+Ebufload = 46;
+Wdupname = 0;
+Wfile = 1;
+Wdate = 2;
+Wdupfile = 3;
+Wnulls = 4;
+Wpwd = 5;
+Wnotnewline = 6;
+Wbadstatus = 7;
+Clean = 32;
+Dirty = 39;
+Unread = 45;
+sizeofRange = 8;
+aggr Range
+{
+	'D' 0 p1;
+	'D' 4 p2;
+};
+
+defn
+Range(addr) {
+	complex Range addr;
+	print("	p1	", addr.p1, "\n");
+	print("	p2	", addr.p2, "\n");
+};
+
+sizeofRangeset = 80;
+aggr Rangeset
+{
+	'a' 0 p;
+};
+
+defn
+Rangeset(addr) {
+	complex Rangeset addr;
+	print("	p	", addr.p, "\n");
+};
+
+sizeofAddress = 16;
+aggr Address
+{
+	Range 0 r;
+	'Y' 8 f;
+};
+
+defn
+Address(addr) {
+	complex Address addr;
+	print("Range r {\n");
+	Range(addr.r);
+	print("}\n");
+	print("	f	", addr.f\Y, "\n");
+};
+
+sizeofString = 16;
+aggr String
+{
+	'd' 0 n;
+	'd' 2 size;
+	'Y' 8 s;
+};
+
+defn
+String(addr) {
+	complex String addr;
+	print("	n	", addr.n, "\n");
+	print("	size	", addr.size, "\n");
+	print("	s	", addr.s\Y, "\n");
+};
+
+sizeof_2_ = 8;
+aggr _2_
+{
+	'Y' 0 listp;
+	'Y' 0 voidp;
+	'Y' 0 posnp;
+	'A' String 0 stringp;
+	'Y' 0 filep;
+};
+
+defn
+_2_(addr) {
+	complex _2_ addr;
+	print("	listp	", addr.listp\Y, "\n");
+	print("	voidp	", addr.voidp\Y, "\n");
+	print("	posnp	", addr.posnp\Y, "\n");
+	print("	stringp	", addr.stringp\Y, "\n");
+	print("	filep	", addr.filep\Y, "\n");
+};
+
+sizeofList = 24;
+aggr List
+{
+	'D' 0 type;
+	'D' 4 nalloc;
+	'D' 8 nused;
+	_2_ 16 g;
+};
+
+defn
+List(addr) {
+	complex List addr;
+	print("	type	", addr.type, "\n");
+	print("	nalloc	", addr.nalloc, "\n");
+	print("	nused	", addr.nused, "\n");
+	print("_2_ g {\n");
+	_2_(addr.g);
+	print("}\n");
+};
+
+Blockincr = 256;
+Maxblock = 8192;
+BUFSIZE = 8192;
+RBUFSIZE = 2048;
+Null = 45;
+Delete = 100;
+Insert = 105;
+Filename = 102;
+Dot = 68;
+Mark = 109;
+sizeof_3_ = 8;
+aggr _3_
+{
+	'U' 0 n;
+	'Y' 0 next;
+};
+
+defn
+_3_(addr) {
+	complex _3_ addr;
+	print("	n	", addr.n, "\n");
+	print("	next	", addr.next\Y, "\n");
+};
+
+sizeofBlock = 16;
+aggr Block
+{
+	'U' 0 addr;
+	{
+	'U' 8 n;
+	'A' Block 8 next;
+	};
+};
+
+defn
+Block(addr) {
+	complex Block addr;
+	print("	addr	", addr.addr, "\n");
+	print("_3_ {\n");
+		_3_(addr+8);
+	print("}\n");
+};
+
+sizeofDisk = 280;
+aggr Disk
+{
+	'D' 0 fd;
+	'V' 8 addr;
+	'a' 16 free;
+};
+
+defn
+Disk(addr) {
+	complex Disk addr;
+	print("	fd	", addr.fd, "\n");
+	print("	addr	", addr.addr, "\n");
+	print("	free	", addr.free, "\n");
+};
+
+sizeofBuffer = 56;
+aggr Buffer
+{
+	'U' 0 nc;
+	'Y' 8 c;
+	'U' 16 cnc;
+	'U' 20 cmax;
+	'U' 24 cq;
+	'D' 28 cdirty;
+	'U' 32 cbi;
+	'A' Block 40 bl;
+	'U' 48 nbl;
+};
+
+defn
+Buffer(addr) {
+	complex Buffer addr;
+	print("	nc	", addr.nc, "\n");
+	print("	c	", addr.c\Y, "\n");
+	print("	cnc	", addr.cnc, "\n");
+	print("	cmax	", addr.cmax, "\n");
+	print("	cq	", addr.cq, "\n");
+	print("	cdirty	", addr.cdirty, "\n");
+	print("	cbi	", addr.cbi, "\n");
+	print("	bl	", addr.bl\Y, "\n");
+	print("	nbl	", addr.nbl, "\n");
+};
+
+sizeofFile = 312;
+aggr File
+{
+	{
+	'U' 0 nc;
+	'Y' 8 c;
+	'U' 16 cnc;
+	'U' 20 cmax;
+	'U' 24 cq;
+	'D' 28 cdirty;
+	'U' 32 cbi;
+	'A' Block 40 bl;
+	'U' 48 nbl;
+	};
+	Buffer 56 delta;
+	Buffer 112 epsilon;
+	String 168 name;
+	'W' 184 qidpath;
+	'U' 192 mtime;
+	'D' 196 dev;
+	'D' 200 unread;
+	'D' 204 seq;
+	'D' 208 cleanseq;
+	'D' 212 mod;
+	'C' 216 rescuing;
+	'D' 220 hiposn;
+	Address 224 dot;
+	Address 240 ndot;
+	Range 256 tdot;
+	Range 264 mark;
+	'A' List 272 rasp;
+	'd' 280 tag;
+	'C' 282 closeok;
+	'C' 283 deleted;
+	Range 288 prevdot;
+	Range 296 prevmark;
+	'D' 304 prevseq;
+	'D' 308 prevmod;
+};
+
+defn
+File(addr) {
+	complex File addr;
+	print("Buffer {\n");
+		Buffer(addr+0);
+	print("}\n");
+	print("Buffer delta {\n");
+	Buffer(addr.delta);
+	print("}\n");
+	print("Buffer epsilon {\n");
+	Buffer(addr.epsilon);
+	print("}\n");
+	print("String name {\n");
+	String(addr.name);
+	print("}\n");
+	print("	qidpath	", addr.qidpath, "\n");
+	print("	mtime	", addr.mtime, "\n");
+	print("	dev	", addr.dev, "\n");
+	print("	unread	", addr.unread, "\n");
+	print("	seq	", addr.seq, "\n");
+	print("	cleanseq	", addr.cleanseq, "\n");
+	print("	mod	", addr.mod, "\n");
+	print("	rescuing	", addr.rescuing, "\n");
+	print("	hiposn	", addr.hiposn, "\n");
+	print("Address dot {\n");
+	Address(addr.dot);
+	print("}\n");
+	print("Address ndot {\n");
+	Address(addr.ndot);
+	print("}\n");
+	print("Range tdot {\n");
+	Range(addr.tdot);
+	print("}\n");
+	print("Range mark {\n");
+	Range(addr.mark);
+	print("}\n");
+	print("	rasp	", addr.rasp\Y, "\n");
+	print("	tag	", addr.tag, "\n");
+	print("	closeok	", addr.closeok, "\n");
+	print("	deleted	", addr.deleted, "\n");
+	print("Range prevdot {\n");
+	Range(addr.prevdot);
+	print("}\n");
+	print("Range prevmark {\n");
+	Range(addr.prevmark);
+	print("}\n");
+	print("	prevseq	", addr.prevseq, "\n");
+	print("	prevmod	", addr.prevmod, "\n");
+};
+
+complex Disk disk;
+complex Address addr;
+complex Buffer snarfbuf;
+complex Buffer plan9buf;
+complex Buffer cmdbuf;
+complex List file;
+complex List tempfile;
+complex File cmd;
+complex File curfile;
+complex File lastfile;
+complex Rangeset sel;
+complex String curwd;
+complex String cmdstr;
+complex String genstr;
+complex String lastpat;
+complex String lastregexp;
+complex String plan9cmd;
+Tversion = 0;
+Tstartcmdfile = 1;
+Tcheck = 2;
+Trequest = 3;
+Torigin = 4;
+Tstartfile = 5;
+Tworkfile = 6;
+Ttype = 7;
+Tcut = 8;
+Tpaste = 9;
+Tsnarf = 10;
+Tstartnewfile = 11;
+Twrite = 12;
+Tclose = 13;
+Tlook = 14;
+Tsearch = 15;
+Tsend = 16;
+Tdclick = 17;
+Tstartsnarf = 18;
+Tsetsnarf = 19;
+Tack = 20;
+Texit = 21;
+Tplumb = 22;
+Ttclick = 23;
+Tmenucmd = 24;
+Tmenucmdsend = 25;
+TMAX = 26;
+Hversion = 0;
+Hbindname = 1;
+Hcurrent = 2;
+Hnewname = 3;
+Hmovname = 4;
+Hgrow = 5;
+Hcheck0 = 6;
+Hcheck = 7;
+Hunlock = 8;
+Hdata = 9;
+Horigin = 10;
+Hunlockfile = 11;
+Hsetdot = 12;
+Hgrowdata = 13;
+Hmoveto = 14;
+Hclean = 15;
+Hdirty = 16;
+Hcut = 17;
+Hsetpat = 18;
+Hdelname = 19;
+Hclose = 20;
+Hsetsnarf = 21;
+Hsnarflen = 22;
+Hack = 23;
+Hexit = 24;
+Hplumb = 25;
+Hmenucmd = 26;
+HMAX = 27;
+sizeofHeader = 8;
+aggr Header
+{
+	'b' 0 type;
+	'b' 1 count0;
+	'b' 2 count1;
+	'a' 3 data;
+};
+
+defn
+Header(addr) {
+	complex Header addr;
+	print("	type	", addr.type, "\n");
+	print("	count0	", addr.count0, "\n");
+	print("	count1	", addr.count1, "\n");
+	print("	data	", addr.data, "\n");
+};
+
+complex String print_ss:a;
+complex String print_ss:b;
+complex String print_s:a;
+complex Dir statfile:dirb;
+complex Dir statfd:dirb;
+complex Waitmsg waitfor:w;
--- /dev/null
+++ b/sys/src/cmd/jam/plan9.c
@@ -1,0 +1,154 @@
+#include "sam.h"
+
+Rune	samname[] = L"~~jam~~";
+
+Rune *left[]= {
+	L"{[(<«",
+	L"\n",
+	L"'\"`",
+	0
+};
+Rune *right[]= {
+	L"}])>»",
+	L"\n",
+	L"'\"`",
+	0
+};
+
+char	RSAM[] = "jam";
+char	SAMTERM[] = "/bin/aux/jamterm";
+char	HOME[] = "home";
+char	TMPDIR[] = "/tmp";
+char	SH[] = "rc";
+char	SHPATH[] = "/bin/rc";
+char	RX[] = "rx";
+char	RXPATH[] = "/bin/rx";
+char	SAMSAVECMD[] = "/bin/rc\n/sys/lib/jamsave";
+
+void
+dprint(char *z, ...)
+{
+	char buf[BLOCKSIZE];
+	va_list arg;
+
+	va_start(arg, z);
+	vseprint(buf, &buf[BLOCKSIZE], z, arg);
+	va_end(arg);
+	termwrite(buf);
+}
+
+void
+print_ss(char *s, String *a, String *b)
+{
+	dprint("?warning: %s: `%.*S' and `%.*S'\n", s, a->n, a->s, b->n, b->s);
+}
+
+void
+print_s(char *s, String *a)
+{
+	dprint("?warning: %s `%.*S'\n", s, a->n, a->s);
+}
+
+int
+statfile(char *name, ulong *dev, uvlong *id, long *time, long *length, long *appendonly)
+{
+	Dir *dirb;
+
+	dirb = dirstat(name);
+	if(dirb == nil)
+		return -1;
+	if(dev)
+		*dev = dirb->type|(dirb->dev<<16);
+	if(id)
+		*id = dirb->qid.path;
+	if(time)
+		*time = dirb->mtime;
+	if(length)
+		*length = dirb->length;
+	if(appendonly)
+		*appendonly = dirb->mode & DMAPPEND;
+	free(dirb);
+	return 1;
+}
+
+int
+statfd(int fd, ulong *dev, uvlong *id, long *time, long *length, long *appendonly)
+{
+	Dir *dirb;
+
+	dirb = dirfstat(fd);
+	if(dirb == nil)
+		return -1;
+	if(dev)
+		*dev = dirb->type|(dirb->dev<<16);
+	if(id)
+		*id = dirb->qid.path;
+	if(time)
+		*time = dirb->mtime;
+	if(length)
+		*length = dirb->length;
+	if(appendonly)
+		*appendonly = dirb->mode & DMAPPEND;
+	free(dirb);
+	return 1;
+}
+
+void
+notifyf(void *a, char *s)
+{
+	USED(a);
+	if(bpipeok && strcmp(s, "sys: write on closed pipe") == 0)
+		noted(NCONT);
+	if(strcmp(s, "interrupt") == 0)
+		noted(NCONT);
+	panicking = 1;
+	rescue();
+	noted(NDFLT);
+}
+
+char*
+waitfor(int pid)
+{
+	Waitmsg *w;
+	static char msg[ERRMAX];
+
+	while((w = wait()) != nil){
+		if(w->pid != pid){
+			free(w);
+			continue;
+		}
+		strecpy(msg, msg+sizeof msg, w->msg);
+		free(w);
+		return msg;
+	}
+	rerrstr(msg, sizeof msg);
+	return msg;
+}
+
+void
+samerr(char *buf)
+{
+	sprint(buf, "%s/jam.err", TMPDIR);
+}
+
+void*
+emalloc(ulong n)
+{
+	void *p;
+
+	p = mallocz(n, 1);
+	if(p == 0)
+		panic("malloc fails");
+	setmalloctag(p, getcallerpc(&n));
+	return p;
+}
+
+void*
+erealloc(void *p, ulong n)
+{
+	p = realloc(p, n);
+	if(p == 0)
+		panic("realloc fails");
+	setmalloctag(p, getcallerpc(&p));
+	return p;
+}
--- /dev/null
+++ b/sys/src/cmd/jam/rasp.c
@@ -1,0 +1,339 @@
+#include "sam.h"
+/*
+ * GROWDATASIZE must be big enough that all errors go out as Hgrowdata's,
+ * so they will be scrolled into visibility in the ~~jam~~ window (yuck!).
+ */
+#define	GROWDATASIZE	50	/* if size is > this, send data with grow */
+
+void	rcut(List*, Posn, Posn);
+int	rterm(List*, Posn);
+void	rgrow(List*, Posn, Posn);
+
+static	Posn	growpos;
+static	Posn	grown;
+static	Posn	shrinkpos;
+static	Posn	shrunk;
+
+/*
+ * rasp routines inform the terminal of changes to the file.
+ *
+ * a rasp is a list of spans within the file, and an indication
+ * of whether the terminal knows about the span.
+ *
+ * optimize by coalescing multiple updates to the same span
+ * if it is not known by the terminal.
+ *
+ * other possible optimizations: flush terminal's rasp by cut everything,
+ * insert everything if rasp gets too large.
+ */
+
+/*
+ * only called for initial load of file
+ */
+void
+raspload(File *f)
+{
+	if(f->rasp == nil)
+		return;
+	grown = f->nc;
+	growpos = 0;
+	if(f->nc)
+		rgrow(f->rasp, 0, f->nc);
+	raspdone(f, 1);
+}
+
+void
+raspstart(File *f)
+{
+	if(f->rasp == nil)
+		return;
+	grown = 0;
+	shrunk = 0;
+	outbuffered = 1;
+}
+
+void
+raspdone(File *f, int toterm)
+{
+	if(f->dot.r.p1 > f->nc)
+		f->dot.r.p1 = f->nc;
+	if(f->dot.r.p2 > f->nc)
+		f->dot.r.p2 = f->nc;
+	if(f->mark.p1 > f->nc)
+		f->mark.p1 = f->nc;
+	if(f->mark.p2 > f->nc)
+		f->mark.p2 = f->nc;
+	if(f->rasp == nil)
+		return;
+	if(grown)
+		outTsll(Hgrow, f->tag, growpos, grown);
+	else if(shrunk)
+		outTsll(Hcut, f->tag, shrinkpos, shrunk);
+	if(toterm)
+		outTs(Hcheck0, f->tag);
+	outflush();
+	outbuffered = 0;
+	if(f == cmd){
+		cmdpt += cmdptadv;
+		cmdptadv = 0;
+	}
+}
+
+void
+raspflush(File *f)
+{
+	if(grown){
+		outTsll(Hgrow, f->tag, growpos, grown);
+		grown = 0;
+	}
+	else if(shrunk){
+		outTsll(Hcut, f->tag, shrinkpos, shrunk);
+		shrunk = 0;
+	}
+	outflush();
+}
+
+void
+raspdelete(File *f, uint p1, uint p2, int toterm)
+{
+	long n;
+
+	n = p2 - p1;
+	if(n == 0)
+		return;
+
+	if(p2 <= f->dot.r.p1){
+		f->dot.r.p1 -= n;
+		f->dot.r.p2 -= n;
+	}
+	if(p2 <= f->mark.p1){
+		f->mark.p1 -= n;
+		f->mark.p2 -= n;
+	}
+
+	if(f->rasp == nil)
+		return;
+
+	if(f==cmd && p1<cmdpt){
+		if(p2 <= cmdpt)
+			cmdpt -= n;
+		else
+			cmdpt = p1;
+	}
+	if(toterm){
+		if(grown){
+			outTsll(Hgrow, f->tag, growpos, grown);
+			grown = 0;
+		}else if(shrunk && shrinkpos!=p1 && shrinkpos!=p2){
+			outTsll(Hcut, f->tag, shrinkpos, shrunk);
+			shrunk = 0;
+		}
+		if(!shrunk || shrinkpos==p2)
+			shrinkpos = p1;
+		shrunk += n;
+	}
+	rcut(f->rasp, p1, p2);
+}
+
+void
+raspinsert(File *f, uint p1, Rune *buf, uint n, int toterm)
+{
+	Range r;
+
+	if(n == 0)
+		return;
+
+	if(p1 < f->dot.r.p1){
+		f->dot.r.p1 += n;
+		f->dot.r.p2 += n;
+	}
+	if(p1 < f->mark.p1){
+		f->mark.p1 += n;
+		f->mark.p2 += n;
+	}
+
+
+	if(f->rasp == nil)
+		return;
+	if(f==cmd && p1<cmdpt)
+		cmdpt += n;
+	if(toterm){
+		if(shrunk){
+			outTsll(Hcut, f->tag, shrinkpos, shrunk);
+			shrunk = 0;
+		}
+		if(n>GROWDATASIZE || !rterm(f->rasp, p1)){
+			rgrow(f->rasp, p1, n);
+			if(grown && growpos+grown!=p1 && growpos!=p1){
+				outTsll(Hgrow, f->tag, growpos, grown);
+				grown = 0;
+			}
+			if(!grown)
+				growpos = p1;
+			grown += n;
+		}else{
+			if(grown){
+				outTsll(Hgrow, f->tag, growpos, grown);
+				grown = 0;
+			}
+			rgrow(f->rasp, p1, n);
+			r = rdata(f->rasp, p1, n);
+			if(r.p1!=p1 || r.p2!=p1+n)
+				panic("rdata in toterminal");
+			outTsllS(Hgrowdata, f->tag, p1, n, tmprstr(buf, n));
+		}
+	}else{
+		rgrow(f->rasp, p1, n);
+		r = rdata(f->rasp, p1, n);
+		if(r.p1!=p1 || r.p2!=p1+n)
+			panic("rdata in toterminal");
+	}
+}
+
+#define	M	0x80000000L
+#define	P(i)	r->posnptr[i]
+#define	T(i)	(P(i)&M)	/* in terminal */
+#define	L(i)	(P(i)&~M)	/* length of this piece */
+
+void
+rcut(List *r, Posn p1, Posn p2)
+{
+	Posn p, x;
+	int i;
+
+	if(p1 == p2)
+		panic("rcut 0");
+	for(p=0,i=0; i<r->nused && p+L(i)<=p1; p+=L(i++))
+		;
+	if(i == r->nused)
+		panic("rcut 1");
+	if(p < p1){	/* chop this piece */
+		if(p+L(i) < p2){
+			x = p1-p;
+			p += L(i);
+		}else{
+			x = L(i)-(p2-p1);
+			p = p2;
+		}
+		if(T(i))
+			P(i) = x|M;
+		else
+			P(i) = x;
+		i++;
+	}
+	while(i<r->nused && p+L(i)<=p2){
+		p += L(i);
+		dellist(r, i);
+	}
+	if(p < p2){
+		if(i == r->nused)
+			panic("rcut 2");
+		x = L(i)-(p2-p);
+		if(T(i))
+			P(i) = x|M;
+		else
+			P(i) = x;
+	}
+	/* can we merge i and i-1 ? */
+	if(i>0 && i<r->nused && T(i-1)==T(i)){
+		x = L(i-1)+L(i);
+		dellist(r, i--);
+		if(T(i))
+			P(i)=x|M;
+		else
+			P(i)=x;
+	}
+}
+
+void
+rgrow(List *r, Posn p1, Posn n)
+{
+	Posn p;
+	int i;
+
+	if(n == 0)
+		panic("rgrow 0");
+	for(p=0,i=0; i<r->nused && p+L(i)<=p1; p+=L(i++))
+		;
+	if(i == r->nused){	/* stick on end of file */
+		if(p!=p1)
+			panic("rgrow 1");
+		if(i>0 && !T(i-1))
+			P(i-1)+=n;
+		else
+			inslist(r, i, n);
+	}else if(!T(i))		/* goes in this empty piece */
+		P(i)+=n;
+	else if(p==p1 && i>0 && !T(i-1))	/* special case; simplifies life */
+		P(i-1)+=n;
+	else if(p==p1)
+		inslist(r, i, n);
+	else{			/* must break piece in terminal */
+		inslist(r, i+1, (L(i)-(p1-p))|M);
+		inslist(r, i+1, n);
+		P(i) = (p1-p)|M;
+	}
+}
+
+int
+rterm(List *r, Posn p1)
+{
+	Posn p;
+	int i;
+
+	for(p = 0,i = 0; i<r->nused && p+L(i)<=p1; p+=L(i++))
+		;
+	if(i==r->nused && (i==0 || !T(i-1)))
+		return 0;
+	return T(i);
+}
+
+Range
+rdata(List *r, Posn p1, Posn n)
+{
+	Posn p;
+	int i;
+	Range rg;
+
+	if(n==0)
+		panic("rdata 0");
+	for(p = 0,i = 0; i<r->nused && p+L(i)<=p1; p+=L(i++))
+		;
+	if(i==r->nused)
+		panic("rdata 1");
+	if(T(i)){
+		n-=L(i)-(p1-p);
+		if(n<=0){
+			rg.p1 = rg.p2 = p1;
+			return rg;
+		}
+		p+=L(i++);
+		p1 = p;
+	}
+	if(T(i) || i==r->nused)
+		panic("rdata 2");
+	if(p+L(i)<p1+n)
+		n = L(i)-(p1-p);
+	rg.p1 = p1;
+	rg.p2 = p1+n;
+	if(p!=p1){
+		inslist(r, i+1, L(i)-(p1-p));
+		P(i)=p1-p;
+		i++;
+	}
+	if(L(i)!=n){
+		inslist(r, i+1, L(i)-n);
+		P(i)=n;
+	}
+	P(i)|=M;
+	/* now i is set; can we merge? */
+	if(i<r->nused-1 && T(i+1)){
+		P(i)=(n+=L(i+1))|M;
+		dellist(r, i+1);
+	}
+	if(i>0 && T(i-1)){
+		P(i)=(n+L(i-1))|M;
+		dellist(r, i-1);
+	}
+	return rg;
+}
--- /dev/null
+++ b/sys/src/cmd/jam/regexp.c
@@ -1,0 +1,805 @@
+#include "sam.h"
+
+Rangeset	sel;
+String		lastregexp;
+/*
+ * Machine Information
+ */
+typedef struct Inst Inst;
+
+struct Inst
+{
+	long	type;	/* <= Runemax ==> literal, otherwise action */
+	union {
+		int rsid;
+		int rsubid;
+		int class;
+		struct Inst *rother;
+		struct Inst *rright;
+	} r;
+	union{
+		struct Inst *lleft;
+		struct Inst *lnext;
+	} l;
+};
+#define	sid	r.rsid
+#define	subid	r.rsubid
+#define	rclass	r.class
+#define	other	r.rother
+#define	right	r.rright
+#define	left	l.lleft
+#define	next	l.lnext
+
+#define	NPROG	1024
+Inst	program[NPROG];
+Inst	*progp;
+Inst	*startinst;	/* First inst. of program; might not be program[0] */
+Inst	*bstartinst;	/* same for backwards machine */
+
+typedef struct Ilist Ilist;
+struct Ilist
+{
+	Inst	*inst;		/* Instruction of the thread */
+	Rangeset se;
+	Posn	startp;		/* first char of match */
+};
+
+#define	NLIST	127
+
+Ilist	*tl, *nl;		/* This list, next list */
+Ilist	list[2][NLIST+1];	/* +1 for trailing null */
+static	Rangeset sempty;
+
+/*
+ * Actions and Tokens
+ *
+ *	0x100xx are operators, value == precedence
+ *	0x200xx are tokens, i.e. operands for operators
+ */
+enum {
+	OPERATOR = Runemask+1,	/* Bitmask of all operators */
+	START	= OPERATOR,	/* Start, used for marker on stack */
+	RBRA,			/* Right bracket, ) */
+	LBRA,			/* Left bracket, ( */
+	OR,			/* Alternation, | */
+	CAT,			/* Concatentation, implicit operator */
+	STAR,			/* Closure, * */
+	PLUS,			/* a+ == aa* */
+	QUEST,			/* a? == a|nothing, i.e. 0 or 1 a's */
+
+	ANY	= OPERATOR<<1,	/* Any character but newline, . */
+	NOP,			/* No operation, internal use only */
+	BOL,			/* Beginning of line, ^ */
+	EOL,			/* End of line, $ */
+	CCLASS,			/* Character class, [] */
+	NCCLASS,		/* Negated character class, [^] */
+	END,			/* Terminate: match found */
+
+	ISATOR	= OPERATOR,
+	ISAND	= OPERATOR<<1,
+};
+
+/*
+ * Parser Information
+ */
+typedef struct Node Node;
+struct Node
+{
+	Inst	*first;
+	Inst	*last;
+};
+
+#define	NSTACK	20
+Node	andstack[NSTACK];
+Node	*andp;
+int	atorstack[NSTACK];
+int	*atorp;
+int	lastwasand;	/* Last token was operand */
+int	cursubid;
+int	subidstack[NSTACK];
+int	*subidp;
+int	backwards;
+int	nbra;
+Rune	*exprp;		/* pointer to next character in source expression */
+#define	DCLASS	10	/* allocation increment */
+int	nclass;		/* number active */
+int	Nclass;		/* high water mark */
+Rune	**class;
+int	negateclass;
+
+int	addinst(Ilist *l, Inst *inst, Rangeset *sep);
+void	newmatch(Rangeset*);
+void	bnewmatch(Rangeset*);
+void	pushand(Inst*, Inst*);
+void	pushator(int);
+Node	*popand(int);
+int	popator(void);
+void	startlex(Rune*);
+int	lex(void);
+void	operator(int);
+void	operand(int);
+void	evaluntil(int);
+void	optimize(Inst*);
+void	bldcclass(void);
+
+void
+regerror(Err e)
+{
+	Strzero(&lastregexp);
+	error(e);
+}
+
+void
+regerror_c(Err e, int c)
+{
+	Strzero(&lastregexp);
+	error_c(e, c);
+}
+
+Inst *
+newinst(int t)
+{
+	if(progp >= &program[NPROG])
+		regerror(Etoolong);
+	progp->type = t;
+	progp->left = 0;
+	progp->right = 0;
+	return progp++;
+}
+
+Inst *
+realcompile(Rune *s)
+{
+	int token;
+
+	startlex(s);
+	atorp = atorstack;
+	andp = andstack;
+	subidp = subidstack;
+	cursubid = 0;
+	lastwasand = FALSE;
+	/* Start with a low priority operator to prime parser */
+	pushator(START-1);
+	while((token=lex()) != END){
+		if((token&ISATOR) == OPERATOR)
+			operator(token);
+		else
+			operand(token);
+	}
+	/* Close with a low priority operator */
+	evaluntil(START);
+	/* Force END */
+	operand(END);
+	evaluntil(START);
+	if(nbra)
+		regerror(Eleftpar);
+	--andp;	/* points to first and only operand */
+	return andp->first;
+}
+
+void
+compile(String *s)
+{
+	int i;
+	Inst *oprogp;
+
+	if(Strcmp(s, &lastregexp)==0)
+		return;
+	for(i=0; i<nclass; i++)
+		free(class[i]);
+	nclass = 0;
+	progp = program;
+	backwards = FALSE;
+	startinst = realcompile(s->s);
+	optimize(program);
+	oprogp = progp;
+	backwards = TRUE;
+	bstartinst = realcompile(s->s);
+	optimize(oprogp);
+	Strduplstr(&lastregexp, s);
+}
+
+void
+operand(int t)
+{
+	Inst *i;
+	if(lastwasand)
+		operator(CAT);	/* catenate is implicit */
+	i = newinst(t);
+	if(t == CCLASS){
+		if(negateclass)
+			i->type = NCCLASS;	/* UGH */
+		i->rclass = nclass-1;		/* UGH */
+	}
+	pushand(i, i);
+	lastwasand = TRUE;
+}
+
+void
+operator(int t)
+{
+	if(t==RBRA && --nbra<0)
+		regerror(Erightpar);
+	if(t==LBRA){
+/*
+ *		if(++cursubid >= NSUBEXP)
+ *			regerror(Esubexp);
+ */
+		cursubid++;	/* silently ignored */
+		nbra++;
+		if(lastwasand)
+			operator(CAT);
+	}else
+		evaluntil(t);
+	if(t!=RBRA)
+		pushator(t);
+	lastwasand = FALSE;
+	if(t==STAR || t==QUEST || t==PLUS || t==RBRA)
+		lastwasand = TRUE;	/* these look like operands */
+}
+
+void
+cant(char *s)
+{
+	char buf[100];
+
+	sprint(buf, "regexp: can't happen: %s", s);
+	panic(buf);
+}
+
+void
+pushand(Inst *f, Inst *l)
+{
+	if(andp >= &andstack[NSTACK])
+		cant("operand stack overflow");
+	andp->first = f;
+	andp->last = l;
+	andp++;
+}
+
+void
+pushator(int t)
+{
+	if(atorp >= &atorstack[NSTACK])
+		cant("operator stack overflow");
+	*atorp++=t;
+	if(cursubid >= NSUBEXP)
+		*subidp++= -1;
+	else
+		*subidp++=cursubid;
+}
+
+Node *
+popand(int op)
+{
+	if(andp <= &andstack[0])
+		if(op)
+			regerror_c(Emissop, op);
+		else
+			regerror(Ebadregexp);
+	return --andp;
+}
+
+int
+popator(void)
+{
+	if(atorp <= &atorstack[0])
+		cant("operator stack underflow");
+	--subidp;
+	return *--atorp;
+}
+
+void
+evaluntil(int pri)
+{
+	Node *op1, *op2, *t;
+	Inst *inst1, *inst2;
+
+	while(pri==RBRA || atorp[-1]>=pri){
+		switch(popator()){
+		case LBRA:
+			op1 = popand('(');
+			inst2 = newinst(RBRA);
+			inst2->subid = *subidp;
+			op1->last->next = inst2;
+			inst1 = newinst(LBRA);
+			inst1->subid = *subidp;
+			inst1->next = op1->first;
+			pushand(inst1, inst2);
+			return;		/* must have been RBRA */
+		default:
+			panic("unknown regexp operator");
+			break;
+		case OR:
+			op2 = popand('|');
+			op1 = popand('|');
+			inst2 = newinst(NOP);
+			op2->last->next = inst2;
+			op1->last->next = inst2;
+			inst1 = newinst(OR);
+			inst1->right = op1->first;
+			inst1->left = op2->first;
+			pushand(inst1, inst2);
+			break;
+		case CAT:
+			op2 = popand(0);
+			op1 = popand(0);
+			if(backwards && op2->first->type!=END)
+				t = op1, op1 = op2, op2 = t;
+			op1->last->next = op2->first;
+			pushand(op1->first, op2->last);
+			break;
+		case STAR:
+			op2 = popand('*');
+			inst1 = newinst(OR);
+			op2->last->next = inst1;
+			inst1->right = op2->first;
+			pushand(inst1, inst1);
+			break;
+		case PLUS:
+			op2 = popand('+');
+			inst1 = newinst(OR);
+			op2->last->next = inst1;
+			inst1->right = op2->first;
+			pushand(op2->first, inst1);
+			break;
+		case QUEST:
+			op2 = popand('?');
+			inst1 = newinst(OR);
+			inst2 = newinst(NOP);
+			inst1->left = inst2;
+			inst1->right = op2->first;
+			op2->last->next = inst2;
+			pushand(inst1, inst2);
+			break;
+		}
+	}
+}
+
+
+void
+optimize(Inst *start)
+{
+	Inst *inst, *target;
+
+	for(inst=start; inst->type!=END; inst++){
+		target = inst->next;
+		while(target->type == NOP)
+			target = target->next;
+		inst->next = target;
+	}
+}
+
+#ifdef	DEBUG
+void
+dumpstack(void){
+	Node *stk;
+	int *ip;
+
+	dprint("operators\n");
+	for(ip = atorstack; ip<atorp; ip++)
+		dprint("0%o\n", *ip);
+	dprint("operands\n");
+	for(stk = andstack; stk<andp; stk++)
+		dprint("0%o\t0%o\n", stk->first->type, stk->last->type);
+}
+void
+dump(void){
+	Inst *l;
+
+	l = program;
+	do{
+		dprint("%d:\t0%o\t%d\t%d\n", l-program, l->type,
+			l->left-program, l->right-program);
+	}while(l++->type);
+}
+#endif
+
+void
+startlex(Rune *s)
+{
+	exprp = s;
+	nbra = 0;
+}
+
+
+int
+lex(void){
+	int c= *exprp++;
+
+	switch(c){
+	case '\\':
+		if(*exprp)
+			if((c= *exprp++)=='n')
+				c='\n';
+		break;
+	case 0:
+		c = END;
+		--exprp;	/* In case we come here again */
+		break;
+	case '*':
+		c = STAR;
+		break;
+	case '?':
+		c = QUEST;
+		break;
+	case '+':
+		c = PLUS;
+		break;
+	case '|':
+		c = OR;
+		break;
+	case '.':
+		c = ANY;
+		break;
+	case '(':
+		c = LBRA;
+		break;
+	case ')':
+		c = RBRA;
+		break;
+	case '^':
+		c = BOL;
+		break;
+	case '$':
+		c = EOL;
+		break;
+	case '[':
+		c = CCLASS;
+		bldcclass();
+		break;
+	}
+	return c;
+}
+
+long
+nextrec(void){
+	if(exprp[0]==0 || (exprp[0]=='\\' && exprp[1]==0))
+		regerror(Ebadclass);
+	if(exprp[0] == '\\'){
+		exprp++;
+		if(*exprp=='n'){
+			exprp++;
+			return '\n';
+		}
+		return *exprp++|(Runemask+1);
+	}
+	return *exprp++;
+}
+
+void
+bldcclass(void)
+{
+	long c1, c2, n, na;
+	Rune *classp;
+
+	classp = emalloc(DCLASS*RUNESIZE);
+	n = 0;
+	na = DCLASS;
+	/* we have already seen the '[' */
+	if(*exprp == '^'){
+		classp[n++] = '\n';	/* don't match newline in negate case */
+		negateclass = TRUE;
+		exprp++;
+	}else
+		negateclass = FALSE;
+	while((c1 = nextrec()) != ']'){
+		if(c1 == '-'){
+    Error:
+			free(classp);
+			regerror(Ebadclass);
+		}
+		if(n+4 >= na){		/* 3 runes plus NUL */
+			na += DCLASS;
+			classp = erealloc(classp, na*RUNESIZE);
+		}
+		if(*exprp == '-'){
+			exprp++;	/* eat '-' */
+			if((c2 = nextrec()) == ']')
+				goto Error;
+			classp[n+0] = Runemax;
+			classp[n+1] = c1 & Runemask;
+			classp[n+2] = c2 & Runemask;
+			n += 3;
+		}else
+			classp[n++] = c1 & Runemask;
+	}
+	classp[n] = 0;
+	if(nclass == Nclass){
+		Nclass += DCLASS;
+		class = erealloc(class, Nclass*sizeof(Rune*));
+	}
+	class[nclass++] = classp;
+}
+
+int
+classmatch(int classno, int c, int negate)
+{
+	Rune *p;
+
+	p = class[classno];
+	while(*p){
+		if(*p == Runemax){
+			if(p[1]<=c && c<=p[2])
+				return !negate;
+			p += 3;
+		}else if(*p++ == c)
+			return !negate;
+	}
+	return negate;
+}
+
+/*
+ * Note optimization in addinst:
+ * 	*l must be pending when addinst called; if *l has been looked
+ *		at already, the optimization is a bug.
+ */
+int
+addinst(Ilist *l, Inst *inst, Rangeset *sep)
+{
+	Ilist *p;
+
+	for(p = l; p->inst; p++){
+		if(p->inst==inst){
+			if((sep)->p[0].p1 < p->se.p[0].p1)
+				p->se= *sep;	/* this would be bug */
+			return 0;	/* It's already there */
+		}
+	}
+	p->inst = inst;
+	p->se= *sep;
+	(p+1)->inst = 0;
+	return 1;
+}
+
+int
+execute(File *f, Posn startp, Posn eof)
+{
+	int flag = 0;
+	Inst *inst;
+	Ilist *tlp;
+	Posn p = startp;
+	int nnl = 0, ntl;
+	int c;
+	int wrapped = 0;
+	int startchar = startinst->type<OPERATOR? startinst->type : 0;
+
+	list[0][0].inst = list[1][0].inst = 0;
+	sel.p[0].p1 = -1;
+	/* Execute machine once for each character */
+	for(;;p++){
+	doloop:
+		c = filereadc(f, p);
+		if(p>=eof || c<0){
+			switch(wrapped++){
+			case 0:		/* let loop run one more click */
+			case 2:
+				break;
+			case 1:		/* expired; wrap to beginning */
+				if(sel.p[0].p1>=0 || eof!=INFINITY)
+					goto Return;
+				list[0][0].inst = list[1][0].inst = 0;
+				p = 0;
+				goto doloop;
+			default:
+				goto Return;
+			}
+		}else if(((wrapped && p>=startp) || sel.p[0].p1>0) && nnl==0)
+			break;
+		/* fast check for first char */
+		if(startchar && nnl==0 && c!=startchar)
+			continue;
+		tl = list[flag];
+		nl = list[flag^=1];
+		nl->inst = 0;
+		ntl = nnl;
+		nnl = 0;
+		if(sel.p[0].p1<0 && (!wrapped || p<startp || startp==eof)){
+			/* Add first instruction to this list */
+			sempty.p[0].p1 = p;
+			if(addinst(tl, startinst, &sempty))
+			if(++ntl >= NLIST)
+	Overflow:
+				error(Eoverflow);
+		}
+		/* Execute machine until this list is empty */
+		for(tlp = tl; inst = tlp->inst; tlp++){	/* assignment = */
+	Switchstmt:
+			switch(inst->type){
+			default:	/* regular character */
+				if(inst->type==c){
+	Addinst:
+					if(addinst(nl, inst->next, &tlp->se))
+					if(++nnl >= NLIST)
+						goto Overflow;
+				}
+				break;
+			case LBRA:
+				if(inst->subid>=0)
+					tlp->se.p[inst->subid].p1 = p;
+				inst = inst->next;
+				goto Switchstmt;
+			case RBRA:
+				if(inst->subid>=0)
+					tlp->se.p[inst->subid].p2 = p;
+				inst = inst->next;
+				goto Switchstmt;
+			case ANY:
+				if(c!='\n')
+					goto Addinst;
+				break;
+			case BOL:
+				if(p==0 || filereadc(f, p - 1)=='\n'){
+	Step:
+					inst = inst->next;
+					goto Switchstmt;
+				}
+				break;
+			case EOL:
+				if(c == '\n')
+					goto Step;
+				break;
+			case CCLASS:
+				if(c>=0 && classmatch(inst->rclass, c, 0))
+					goto Addinst;
+				break;
+			case NCCLASS:
+				if(c>=0 && classmatch(inst->rclass, c, 1))
+					goto Addinst;
+				break;
+			case OR:
+				/* evaluate right choice later */
+				if(addinst(tlp, inst->right, &tlp->se))
+				if(++ntl >= NLIST)
+					goto Overflow;
+				/* efficiency: advance and re-evaluate */
+				inst = inst->left;
+				goto Switchstmt;
+			case END:	/* Match! */
+				tlp->se.p[0].p2 = p;
+				newmatch(&tlp->se);
+				break;
+			}
+		}
+	}
+    Return:
+	return sel.p[0].p1>=0;
+}
+
+void
+newmatch(Rangeset *sp)
+{
+	int i;
+
+	if(sel.p[0].p1<0 || sp->p[0].p1<sel.p[0].p1 ||
+	   (sp->p[0].p1==sel.p[0].p1 && sp->p[0].p2>sel.p[0].p2))
+		for(i = 0; i<NSUBEXP; i++)
+			sel.p[i] = sp->p[i];
+}
+
+int
+bexecute(File *f, Posn startp)
+{
+	int flag = 0;
+	Inst *inst;
+	Ilist *tlp;
+	Posn p = startp;
+	int nnl = 0, ntl;
+	int c;
+	int wrapped = 0;
+	int startchar = bstartinst->type<OPERATOR? bstartinst->type : 0;
+
+	list[0][0].inst = list[1][0].inst = 0;
+	sel.p[0].p1= -1;
+	/* Execute machine once for each character, including terminal NUL */
+	for(;;--p){
+	doloop:
+		if((c = filereadc(f, p - 1))==-1){
+			switch(wrapped++){
+			case 0:		/* let loop run one more click */
+			case 2:
+				break;
+			case 1:		/* expired; wrap to end */
+				if(sel.p[0].p1>=0)
+			case 3:
+					goto Return;
+				list[0][0].inst = list[1][0].inst = 0;
+				p = f->nc;
+				goto doloop;
+			default:
+				goto Return;
+			}
+		}else if(((wrapped && p<=startp) || sel.p[0].p1>0) && nnl==0)
+			break;
+		/* fast check for first char */
+		if(startchar && nnl==0 && c!=startchar)
+			continue;
+		tl = list[flag];
+		nl = list[flag^=1];
+		nl->inst = 0;
+		ntl = nnl;
+		nnl = 0;
+		if(sel.p[0].p1<0 && (!wrapped || p>startp)){
+			/* Add first instruction to this list */
+			/* the minus is so the optimizations in addinst work */
+			sempty.p[0].p1 = -p;
+			if(addinst(tl, bstartinst, &sempty))
+			if(++ntl >= NLIST)
+	Overflow:
+				error(Eoverflow);
+		}
+		/* Execute machine until this list is empty */
+		for(tlp = tl; inst = tlp->inst; tlp++){	/* assignment = */
+	Switchstmt:
+			switch(inst->type){
+			default:	/* regular character */
+				if(inst->type == c){
+	Addinst:
+					if(addinst(nl, inst->next, &tlp->se))
+					if(++nnl >= NLIST)
+						goto Overflow;
+				}
+				break;
+			case LBRA:
+				if(inst->subid>=0)
+					tlp->se.p[inst->subid].p1 = p;
+				inst = inst->next;
+				goto Switchstmt;
+			case RBRA:
+				if(inst->subid >= 0)
+					tlp->se.p[inst->subid].p2 = p;
+				inst = inst->next;
+				goto Switchstmt;
+			case ANY:
+				if(c != '\n')
+					goto Addinst;
+				break;
+			case BOL:
+				if(c=='\n' || p==0){
+	Step:
+					inst = inst->next;
+					goto Switchstmt;
+				}
+				break;
+			case EOL:
+				if(p==f->nc || filereadc(f, p)=='\n')
+					goto Step;
+				break;
+			case CCLASS:
+				if(c>=0 && classmatch(inst->rclass, c, 0))
+					goto Addinst;
+				break;
+			case NCCLASS:
+				if(c>=0 && classmatch(inst->rclass, c, 1))
+					goto Addinst;
+				break;
+			case OR:
+				/* evaluate right choice later */
+				if(addinst(tl, inst->right, &tlp->se))
+				if(++ntl >= NLIST)
+					goto Overflow;
+				/* efficiency: advance and re-evaluate */
+				inst = inst->left;
+				goto Switchstmt;
+			case END:	/* Match! */
+				tlp->se.p[0].p1 = -tlp->se.p[0].p1; /* minus sign */
+				tlp->se.p[0].p2 = p;
+				bnewmatch(&tlp->se);
+				break;
+			}
+		}
+	}
+    Return:
+	return sel.p[0].p1>=0;
+}
+
+void
+bnewmatch(Rangeset *sp)
+{
+        int  i;
+        if(sel.p[0].p1<0 || sp->p[0].p1>sel.p[0].p2 || (sp->p[0].p1==sel.p[0].p2 && sp->p[0].p2<sel.p[0].p1))
+                for(i = 0; i<NSUBEXP; i++){       /* note the reversal; p1<=p2 */
+                        sel.p[i].p1 = sp->p[i].p2;
+                        sel.p[i].p2 = sp->p[i].p1;
+                }
+}
--- /dev/null
+++ b/sys/src/cmd/jam/sam.c
@@ -1,0 +1,735 @@
+#include "sam.h"
+
+Rune	genbuf[BLOCKSIZE];
+int	io;
+int	panicking;
+int	rescuing;
+String	genstr;
+String	rhs;
+String	curwd;
+String	cmdstr;
+Rune	empty[] = { 0 };
+char	*genc;
+File	*curfile;
+File	*flist;
+File	*cmd;
+jmp_buf	mainloop;
+List	tempfile = { 'p' };
+int	quitok = TRUE;
+int	downloaded;
+int	dflag;
+int	Rflag;
+char	*machine;
+char	*home;
+int	bpipeok;
+int	termlocked;
+char	*samterm = SAMTERM;
+char	*rsamname = RSAM;
+File	*lastfile;
+Disk	*disk;
+long	seq;
+
+Rune	baddir[] = { '<', 'b', 'a', 'd', 'd', 'i', 'r', '>', '\n'};
+
+void	usage(void);
+
+void main(int argc, char *argv[])
+{
+	int i;
+	String *t;
+	char *termargs[10], **ap;
+	
+	ap = termargs;
+	*ap++ = "jamterm";
+	ARGBEGIN{
+	case 'd':
+		dflag++;
+		break;
+	case 'r':
+		machine = EARGF(usage());
+		break;
+	case 'R':
+		Rflag++;
+		break;
+	case 't':
+		samterm = EARGF(usage());
+		break;
+	case 's':
+		rsamname = EARGF(usage());
+		break;
+	default:
+		dprint("jam: unknown flag %c\n", ARGC());
+		usage();
+	/* options for jamterm */
+	case 'A':
+		*ap++ = "-A";
+		if(ap >= termargs+nelem(termargs))
+			usage();
+		break;
+	case 'i':
+		*ap++ = "-i";
+		if(ap >= termargs+nelem(termargs))
+			usage();
+		break;
+	}ARGEND
+	*ap = nil;
+	
+	Strinit(&cmdstr);
+	Strinit0(&lastpat);
+	Strinit0(&lastregexp);
+	Strinit0(&genstr);
+	Strinit0(&rhs);
+	Strinit0(&curwd);
+	Strinit0(&plan9cmd);
+	home = getenv(HOME);
+	disk = diskinit();
+	if(home == 0)
+		home = "/";
+	if(!dflag)
+		startup(machine, Rflag, termargs, argv);
+	notify(notifyf);
+	getcurwd();
+	if(argc>0){
+		for(i=0; i<argc; i++){
+			if(!setjmp(mainloop)){
+				t = tmpcstr(argv[i]);
+				Straddc(t, '\0');
+				Strduplstr(&genstr, t);
+				freetmpstr(t);
+				fixname(&genstr);
+				logsetname(newfile(), &genstr);
+			}
+		}
+	}else if(!downloaded)
+		newfile();
+	seq++;
+	if(file.nused)
+		current(file.filepptr[0]);
+	setjmp(mainloop);
+	cmdloop();
+	trytoquit();	/* if we already q'ed, quitok will be TRUE */
+	exits(0);
+}
+
+void
+usage(void)
+{
+	dprint("usage: jam [-d] [-t jamterm] [-s jam name] -r machine\n");
+	exits("usage");
+}
+
+void
+rescue(void)
+{
+	int i, nblank = 0;
+	File *f;
+	char *c;
+	char buf[256];
+
+	if(rescuing++)
+		return;
+	io = -1;
+	for(i=0; i<file.nused; i++){
+		f = file.filepptr[i];
+		if(f==cmd || f->nc==0 || !fileisdirty(f))
+			continue;
+		if(io == -1){
+			sprint(buf, "%s/jam.save", home);
+			io = create(buf, 1, 0777);
+			if(io<0)
+				return;
+		}
+		if(f->name.s[0]){
+			c = Strtoc(&f->name);
+			strncpy(buf, c, sizeof buf-1);
+			buf[sizeof buf-1] = 0;
+			free(c);
+		}else
+			sprint(buf, "nameless.%d", nblank++);
+		fprint(io, "#!%s '%s' $* <<'---%s'\n", SAMSAVECMD, buf, buf);
+		addr.r.p1 = 0, addr.r.p2 = f->nc;
+		writeio(f);
+		fprint(io, "\n---%s\n", (char *)buf);
+	}
+}
+
+void
+panic(char *s)
+{
+	int wasd;
+
+	if(!panicking++ && !setjmp(mainloop)){
+		wasd = downloaded;
+		downloaded = 0;
+		dprint("jam: panic: %s: %r\n", s);
+		if(wasd)
+			fprint(2, "jam: panic: %s: %r\n", s);
+		rescue();
+		abort();
+	}
+}
+
+void
+hiccough(char *s)
+{
+	File *f;
+	int i;
+
+	if(rescuing)
+		exits("rescue");
+	if(s)
+		dprint("%s\n", s);
+	resetcmd();
+	resetxec();
+	resetsys();
+	if(io > 0)
+		close(io);
+
+	/*
+	 * back out any logged changes & restore old sequences
+	 */
+	for(i=0; i<file.nused; i++){
+		f = file.filepptr[i];
+		if(f==cmd)
+			continue;
+		if(f->seq==seq){
+			bufdelete(&f->epsilon, 0, f->epsilon.nc);
+			f->seq = f->prevseq;
+			f->dot.r = f->prevdot;
+			f->mark = f->prevmark;
+			state(f, f->prevmod ? Dirty: Clean);
+		}
+	}
+
+	update();
+	if (curfile) {
+		if (curfile->unread)
+			curfile->unread = FALSE;
+		else if (downloaded)
+			outTs(Hcurrent, curfile->tag);
+	}
+	longjmp(mainloop, 1);
+}
+
+void
+intr(void)
+{
+	error(Eintr);
+}
+
+void
+trytoclose(File *f)
+{
+	char *t;
+	char buf[256];
+
+	if(f == cmd)	/* possible? */
+		return;
+	if(f->deleted)
+		return;
+	if(fileisdirty(f) && !f->closeok){
+		f->closeok = TRUE;
+		if(f->name.s[0]){
+			t = Strtoc(&f->name);
+			strncpy(buf, t, sizeof buf-1);
+			free(t);
+		}else
+			strcpy(buf, "nameless file");
+		error_s(Emodified, buf);
+	}
+	f->deleted = TRUE;
+}
+
+void
+trytoquit(void)
+{
+	int c;
+	File *f;
+
+	if(!quitok){
+		for(c = 0; c<file.nused; c++){
+			f = file.filepptr[c];
+			if(f!=cmd && fileisdirty(f)){
+				quitok = TRUE;
+				eof = FALSE;
+				error(Echanges);
+			}
+		}
+	}
+}
+
+void
+load(File *f)
+{
+	Address saveaddr;
+
+	Strduplstr(&genstr, &f->name);
+	filename(f);
+	if(f->name.s[0]){
+		saveaddr = addr;
+		edit(f, 'I');
+		addr = saveaddr;
+	}else{
+		f->unread = 0;
+		f->cleanseq = f->seq;
+	}
+
+	fileupdate(f, TRUE, TRUE);
+}
+
+void
+cmdupdate(void)
+{
+	if(cmd && cmd->seq!=0){
+		fileupdate(cmd, FALSE, downloaded);
+		cmd->dot.r.p1 = cmd->dot.r.p2 = cmd->nc;
+		telldot(cmd);
+	}
+}
+
+void
+delete(File *f)
+{
+	if(downloaded && f->rasp)
+		outTs(Hclose, f->tag);
+	delfile(f);
+	if(f == curfile)
+		current(0);
+}
+
+void
+update(void)
+{
+	int i, anymod;
+	File *f;
+
+	settempfile();
+	for(anymod = i=0; i<tempfile.nused; i++){
+		f = tempfile.filepptr[i];
+		if(f==cmd)	/* cmd gets done in main() */
+			continue;
+		if(f->deleted) {
+			delete(f);
+			continue;
+		}
+		if(f->seq==seq && fileupdate(f, FALSE, downloaded))
+			anymod++;
+		if(f->rasp)
+			telldot(f);
+	}
+	if(anymod)
+		seq++;
+}
+
+File *
+current(File *f)
+{
+	return curfile = f;
+}
+
+void
+edit(File *f, int cmd)
+{
+	int empty = TRUE;
+	Posn p;
+	int nulls;
+
+	if(cmd == 'r')
+		logdelete(f, addr.r.p1, addr.r.p2);
+	if(cmd=='e' || cmd=='I'){
+		logdelete(f, (Posn)0, f->nc);
+		addr.r.p2 = f->nc;
+	}else if(f->nc!=0 || (f->name.s[0] && Strcmp(&genstr, &f->name)!=0))
+		empty = FALSE;
+	if((io = open(genc, OREAD))<0) {
+		if (curfile && curfile->unread)
+			curfile->unread = FALSE;
+		error_r(Eopen, genc);
+	}
+	p = readio(f, &nulls, empty, TRUE);
+	closeio((cmd=='e' || cmd=='I')? -1 : p);
+	if(cmd == 'r')
+		f->ndot.r.p1 = addr.r.p2, f->ndot.r.p2 = addr.r.p2+p;
+	else
+		f->ndot.r.p1 = f->ndot.r.p2 = 0;
+	f->closeok = empty;
+	if (quitok)
+		quitok = empty;
+	else
+		quitok = FALSE;
+	state(f, empty && !nulls? Clean : Dirty);
+	if(empty && !nulls)
+		f->cleanseq = f->seq;
+	if(cmd == 'e')
+		filename(f);
+}
+
+int
+getname(File *f, String *s, int save)
+{
+	int c, i;
+
+	Strzero(&genstr);
+	if(genc){
+		free(genc);
+		genc = 0;
+	}
+	if(s==0 || (c = s->s[0])==0){		/* no name provided */
+		if(f)
+			Strduplstr(&genstr, &f->name);
+		goto Return;
+	}
+	if(c!=' ' && c!='\t')
+		error(Eblank);
+	for(i=0; (c=s->s[i])==' ' || c=='\t'; i++)
+		;
+	while(s->s[i] > ' ')
+		Straddc(&genstr, s->s[i++]);
+	if(s->s[i])
+		error(Enewline);
+	fixname(&genstr);
+	if(f && (save || f->name.s[0]==0)){
+		logsetname(f, &genstr);
+		if(Strcmp(&f->name, &genstr)){
+			quitok = f->closeok = FALSE;
+			f->qidpath = 0;
+			f->mtime = 0;
+			state(f, Dirty); /* if it's 'e', fix later */
+		}
+	}
+    Return:
+	genc = Strtoc(&genstr);
+	i = genstr.n;
+	if(i && genstr.s[i-1]==0)
+		i--;
+	return i;	/* strlen(name) */
+}
+
+void
+filename(File *f)
+{
+	if(genc)
+		free(genc);
+	genc = Strtoc(&genstr);
+	dprint("%c%c%c %s\n", " '"[f->mod],
+		"-+"[f->rasp!=0], " ."[f==curfile], genc);
+}
+
+void
+undostep(File *f, int isundo)
+{
+	uint p1, p2;
+	int mod;
+
+	mod = f->mod;
+	fileundo(f, isundo, 1, &p1, &p2, TRUE);
+	f->ndot = f->dot;
+	if(f->mod){
+		f->closeok = 0;
+		quitok = 0;
+	}else
+		f->closeok = 1;
+
+	if(f->mod != mod){
+		f->mod = mod;
+		if(mod)
+			mod = Clean;
+		else
+			mod = Dirty;
+		state(f, mod);
+	}
+}
+
+int
+undo(int isundo)
+{
+	File *f;
+	int i;
+	Mod max;
+
+	max = undoseq(curfile, isundo);
+	if(max == 0)
+		return 0;
+	settempfile();
+	for(i = 0; i<tempfile.nused; i++){
+		f = tempfile.filepptr[i];
+		if(f!=cmd && undoseq(f, isundo)==max)
+			undostep(f, isundo);
+	}
+	return 1;
+}
+
+int
+readcmd(String *s)
+{
+	int retcode;
+
+	if(flist != 0)
+		fileclose(flist);
+	flist = fileopen();
+
+	addr.r.p1 = 0, addr.r.p2 = flist->nc;
+	retcode = plan9(flist, '<', s, FALSE);
+	fileupdate(flist, FALSE, FALSE);
+	flist->seq = 0;
+	if (flist->nc > BLOCKSIZE)
+		error(Etoolong);
+	Strzero(&genstr);
+	Strinsure(&genstr, flist->nc);
+	bufread(flist, (Posn)0, genbuf, flist->nc);
+	memmove(genstr.s, genbuf, flist->nc*RUNESIZE);
+	genstr.n = flist->nc;
+	Straddc(&genstr, '\0');
+	return retcode;
+}
+
+void
+getcurwd(void)
+{
+	String *t;
+	char buf[256];
+
+	buf[0] = 0;
+	getwd(buf, sizeof(buf));
+	t = tmpcstr(buf);
+	Strduplstr(&curwd, t);
+	freetmpstr(t);
+	if(curwd.n == 0)
+		warn(Wpwd);
+	else if(curwd.s[curwd.n-1] != '/')
+		Straddc(&curwd, '/');
+}
+
+void
+cd(String *str)
+{
+	int i, fd;
+	char *s;
+	File *f;
+	String owd;
+
+	getcurwd();
+	if(getname((File *)0, str, FALSE))
+		s = genc;
+	else
+		s = home;
+	if(chdir(s))
+		syserror("chdir");
+	fd = open("/dev/wdir", OWRITE);
+	if(fd >= 0)
+		write(fd, s, strlen(s));
+	dprint("!\n");
+	Strinit(&owd);
+	Strduplstr(&owd, &curwd);
+	getcurwd();
+	settempfile();
+	for(i=0; i<tempfile.nused; i++){
+		f = tempfile.filepptr[i];
+		if(f!=cmd && f->name.s[0]!='/' && f->name.s[0]!=0){
+			Strinsert(&f->name, &owd, (Posn)0);
+			fixname(&f->name);
+			sortname(f);
+		}else if(f != cmd && Strispre(&curwd, &f->name)){
+			fixname(&f->name);
+			sortname(f);
+		}
+	}
+	Strclose(&owd);
+}
+
+int
+loadflist(String *s)
+{
+	int c, i;
+
+	c = s->s[0];
+	for(i = 0; s->s[i]==' ' || s->s[i]=='\t'; i++)
+		;
+	if((c==' ' || c=='\t') && s->s[i]!='\n'){
+		if(s->s[i]=='<'){
+			Strdelete(s, 0L, (long)i+1);
+			readcmd(s);
+		}else{
+			Strzero(&genstr);
+			while((c = s->s[i++]) && c!='\n')
+				Straddc(&genstr, c);
+			Straddc(&genstr, '\0');
+		}
+	}else{
+		if(c != '\n')
+			error(Eblank);
+		Strdupl(&genstr, empty);
+	}
+	if(genc)
+		free(genc);
+	genc = Strtoc(&genstr);
+	return genstr.s[0];
+}
+
+File *
+readflist(int readall, int delete)
+{
+	Posn i;
+	int c;
+	File *f;
+	String t;
+
+	Strinit(&t);
+	for(i=0,f=0; f==0 || readall || delete; i++){	/* ++ skips blank */
+		Strdelete(&genstr, (Posn)0, i);
+		for(i=0; (c = genstr.s[i])==' ' || c=='\t' || c=='\n'; i++)
+			;
+		if(i >= genstr.n)
+			break;
+		Strdelete(&genstr, (Posn)0, i);
+		for(i=0; (c=genstr.s[i]) && c!=' ' && c!='\t' && c!='\n'; i++)
+			;
+
+		if(i == 0)
+			break;
+		genstr.s[i] = 0;
+		Strduplstr(&t, tmprstr(genstr.s, i+1));
+		fixname(&t);
+		f = lookfile(&t);
+		if(delete){
+			if(f == 0)
+				warn_S(Wfile, &t);
+			else
+				trytoclose(f);
+		}else if(f==0 && readall)
+			logsetname(f = newfile(), &t);
+	}
+	Strclose(&t);
+	return f;
+}
+
+File *
+tofile(String *s)
+{
+	File *f;
+
+	if(s->s[0] != ' ')
+		error(Eblank);
+	if(loadflist(s) == 0){
+		f = lookfile(&genstr);	/* empty string ==> nameless file */
+		if(f == 0)
+			error_s(Emenu, genc);
+	}else if((f=readflist(FALSE, FALSE)) == 0)
+		error_s(Emenu, genc);
+	return current(f);
+}
+
+File *
+getfile(String *s)
+{
+	File *f;
+
+	if(loadflist(s) == 0)
+		logsetname(f = newfile(), &genstr);
+	else if((f=readflist(TRUE, FALSE)) == 0)
+		error(Eblank);
+	return current(f);
+}
+
+void
+closefiles(File *f, String *s)
+{
+	if(s->s[0] == 0){
+		if(f == 0)
+			error(Enofile);
+		trytoclose(f);
+		return;
+	}
+	if(s->s[0] != ' ')
+		error(Eblank);
+	if(loadflist(s) == 0)
+		error(Enewline);
+	readflist(FALSE, TRUE);
+}
+
+void
+copy(File *f, Address addr2)
+{
+	Posn p;
+	int ni;
+	for(p=addr.r.p1; p<addr.r.p2; p+=ni){
+		ni = addr.r.p2-p;
+		if(ni > BLOCKSIZE)
+			ni = BLOCKSIZE;
+		bufread(f, p, genbuf, ni);
+		loginsert(addr2.f, addr2.r.p2, tmprstr(genbuf, ni)->s, ni);
+	}
+	addr2.f->ndot.r.p2 = addr2.r.p2+(f->dot.r.p2-f->dot.r.p1);
+	addr2.f->ndot.r.p1 = addr2.r.p2;
+}
+
+void
+move(File *f, Address addr2)
+{
+	if(addr.r.p2 <= addr2.r.p2){
+		logdelete(f, addr.r.p1, addr.r.p2);
+		copy(f, addr2);
+	}else if(addr.r.p1 >= addr2.r.p2){
+		copy(f, addr2);
+		logdelete(f, addr.r.p1, addr.r.p2);
+	}else
+		error(Eoverlap);
+}
+
+Posn
+nlcount(File *f, Posn p0, Posn p1)
+{
+	Posn nl = 0;
+
+	while(p0 < p1)
+		if(filereadc(f, p0++)=='\n')
+			nl++;
+	return nl;
+}
+
+void
+printposn(File *f, int chars)
+{
+	Posn l1, l2;
+	char *s;
+
+	if(f->name.s[0]){
+		if(f->name.s[0]!='/'){
+			getcurwd();
+			s = Strtoc(&curwd);
+			dprint("%s", s);
+			free(s);
+		}
+		s = Strtoc(&f->name);
+		dprint("%s:", s);
+		free(s);
+	}
+	if(chars){
+		dprint("#%lud", addr.r.p1);
+		if(addr.r.p2 != addr.r.p1)
+			dprint(",#%lud", addr.r.p2);
+	}else{
+		l1 = 1+nlcount(f, (Posn)0, addr.r.p1);
+		l2 = l1+nlcount(f, addr.r.p1, addr.r.p2);
+		/* check if addr ends with '\n' */
+		if(addr.r.p2>0 && addr.r.p2>addr.r.p1 && filereadc(f, addr.r.p2-1)=='\n')
+			--l2;
+		dprint("%lud", l1);
+		if(l2 != l1)
+			dprint(",%lud", l2);
+	}
+	dprint("\n");
+}
+
+void
+settempfile(void)
+{
+	if(tempfile.nalloc < file.nused){
+		if(tempfile.filepptr)
+			free(tempfile.filepptr);
+		tempfile.filepptr = emalloc(sizeof(File*)*file.nused);
+		tempfile.nalloc = file.nused;
+	}
+	memmove(tempfile.filepptr, file.filepptr, sizeof(File*)*file.nused);
+	tempfile.nused = file.nused;
+}
--- /dev/null
+++ b/sys/src/cmd/jam/sam.h
@@ -1,0 +1,404 @@
+#include <u.h>
+#include <libc.h>
+#include <plumb.h>
+#include "errors.h"
+
+/*
+ * BLOCKSIZE is relatively small to keep memory consumption down.
+ */
+
+#define	BLOCKSIZE	2048
+#define	RUNESIZE	sizeof(Rune)
+#define	NDISC		5
+#define	NBUFFILES	3+2*NDISC	/* plan 9+undo+snarf+NDISC*(transcript+buf) */
+#define NSUBEXP	10
+
+#define	TRUE		1
+#define	FALSE		0
+
+#define	INFINITY	0x7FFFFFFFL
+#define	INCR		25
+#define	STRSIZE		(512<<20)
+
+typedef long		Posn;		/* file position or address */
+typedef	ushort		Mod;		/* modification number */
+
+typedef struct Address	Address;
+typedef struct Block	Block;
+typedef struct Buffer	Buffer;
+typedef struct Disk	Disk;
+typedef struct Discdesc	Discdesc;
+typedef struct File	File;
+typedef struct List	List;
+typedef struct Range	Range;
+typedef struct Rangeset	Rangeset;
+typedef struct String	String;
+
+enum State
+{
+	Clean =		' ',
+	Dirty =		'\'',
+	Unread =	'-',
+};
+
+struct Range
+{
+	Posn	p1, p2;
+};
+
+struct Rangeset
+{
+	Range	p[NSUBEXP];
+};
+
+struct Address
+{
+	Range	r;
+	File	*f;
+};
+
+struct String
+{
+	short	n;
+	short	size;
+	Rune	*s;
+};
+
+struct List
+{
+	int	type;	/* 'p' for pointer, 'P' for Posn */
+	int	nalloc;
+	int	nused;
+	union{
+		void*	listp;
+		void**	voidp;
+		Posn*	posnp;
+		String**stringp;
+		File**	filep;
+	}g;
+};
+
+#define	listptr		g.listp
+#define	voidpptr	g.voidp
+#define	posnptr		g.posnp
+#define	stringpptr	g.stringp
+#define	filepptr	g.filep
+
+enum
+{
+	Blockincr =	256,
+	Maxblock = 	8*1024,
+
+	BUFSIZE = Maxblock,	/* size from fbufalloc() */
+	RBUFSIZE = BUFSIZE/sizeof(Rune),
+};
+
+
+enum
+{
+	Null		= '-',
+	Delete		= 'd',
+	Insert		= 'i',
+	Filename	= 'f',
+	Dot		= 'D',
+	Mark		= 'm',
+};
+
+struct Block
+{
+	uint		addr;	/* disk address in bytes */
+	union
+	{
+		uint	n;	/* number of used runes in block */
+		Block	*next;	/* pointer to next in free list */
+	};
+};
+
+struct Disk
+{
+	int		fd;
+	vlong		addr;	/* length of temp file */
+	Block		*free[Maxblock/Blockincr+1];
+};
+
+Disk*		diskinit(void);
+Block*		disknewblock(Disk*, uint);
+void		diskrelease(Disk*, Block*);
+void		diskread(Disk*, Block*, Rune*, uint);
+void		diskwrite(Disk*, Block**, Rune*, uint);
+
+struct Buffer
+{
+	uint		nc;
+	Rune		*c;	/* cache */
+	uint		cnc;	/* bytes in cache */
+	uint		cmax;	/* size of allocated cache */
+	uint		cq;	/* position of cache */
+	int		cdirty;	/* cache needs to be written */
+	uint		cbi;	/* index of cache Block */
+	Block		**bl;	/* array of blocks */
+	uint		nbl;	/* number of blocks */
+};
+void		bufinsert(Buffer*, uint, Rune*, uint);
+void		bufdelete(Buffer*, uint, uint);
+uint		bufload(Buffer*, uint, int, int*);
+void		bufread(Buffer*, uint, Rune*, uint);
+void		bufclose(Buffer*);
+void		bufreset(Buffer*);
+
+struct File
+{
+	Buffer;				/* the data */
+	Buffer		delta;		/* transcript of changes */
+	Buffer		epsilon;	/* inversion of delta for redo */
+	String		name;		/* name of associated file */
+	uvlong		qidpath;	/* of file when read */
+	uint		mtime;		/* of file when read */
+	int		dev;		/* of file when read */
+	int		unread;		/* file has not been read from disk */
+
+	long		seq;		/* if seq==0, File acts like Buffer */
+	long		cleanseq;	/* f->seq at last read/write of file */
+	int		mod;		/* file appears modified in menu */
+	char		rescuing;	/* jam exiting; this file unusable */
+
+//	Text		*curtext;	/* most recently used associated text */
+//	Text		**text;		/* list of associated texts */
+//	int		ntext;
+//	int		dumpid;		/* used in dumping zeroxed windows */
+
+	Posn		hiposn;		/* highest address touched this Mod */
+	Address		dot;		/* current position */
+	Address		ndot;		/* new current position after update */
+	Range		tdot;		/* what terminal thinks is current range */
+	Range		mark;		/* tagged spot in text (don't confuse with Mark) */
+	List		*rasp;		/* map of what terminal's got */
+	short		tag;		/* for communicating with terminal */
+	char		closeok;	/* ok to close file? */
+	char		deleted;	/* delete at completion of command */
+	Range		prevdot;	/* state before start of change */
+	Range		prevmark;
+	long		prevseq;
+	int		prevmod;
+};
+//File*		fileaddtext(File*, Text*);
+void		fileclose(File*);
+void		filedelete(File*, uint, uint);
+//void		filedeltext(File*, Text*);
+void		fileinsert(File*, uint, Rune*, uint);
+uint		fileload(File*, uint, int, int*);
+void		filemark(File*);
+void		filereset(File*);
+void		filesetname(File*, String*);
+void		fileundelete(File*, Buffer*, uint, uint);
+void		fileuninsert(File*, Buffer*, uint, uint);
+void		fileunsetname(File*, Buffer*);
+void		fileundo(File*, int, int, uint*, uint*, int);
+int		fileupdate(File*, int, int);
+
+int		filereadc(File*, uint);
+File		*fileopen(void);
+void		loginsert(File*, uint, Rune*, uint);
+void		logdelete(File*, uint, uint);
+void		logsetname(File*, String*);
+int		fileisdirty(File*);
+long		undoseq(File*, int);
+long		prevseq(Buffer*);
+
+void		raspload(File*);
+void		raspstart(File*);
+void		raspdelete(File*, uint, uint, int);
+void		raspinsert(File*, uint, Rune*, uint, int);
+void		raspdone(File*, int);
+void		raspflush(File*);
+
+/*
+ * acme fns
+ */
+void*	fbufalloc(void);
+void	fbuffree(void*);
+uint	min(uint, uint);
+void	cvttorunes(char*, int, Rune*, int*, int*, int*);
+
+#define	runemalloc(a)		(Rune*)emalloc((a)*sizeof(Rune))
+#define	runerealloc(a, b)	(Rune*)realloc((a), (b)*sizeof(Rune))
+#define	runemove(a, b, c)	memmove((a), (b), (c)*sizeof(Rune))
+
+int	alnum(int);
+int	Read(int, void*, int);
+void	Seek(int, long, int);
+int	plan9(File*, int, String*, int);
+int	Write(int, void*, int);
+int	bexecute(File*, Posn);
+void	cd(String*);
+void	closefiles(File*, String*);
+void	closeio(Posn);
+void	cmdloop(void);
+void	cmdupdate(void);
+void	compile(String*);
+void	copy(File*, Address);
+File	*current(File*);
+void	delete(File*);
+void	delfile(File*);
+void	dellist(List*, int);
+void	stretchsel(File*, Posn, int);
+void	dprint(char*, ...);
+void	edit(File*, int);
+void	*emalloc(ulong);
+void	*erealloc(void*, ulong);
+void	error(Err);
+void	error_c(Err, int);
+void	error_r(Err, char*);
+void	error_s(Err, char*);
+int	execute(File*, Posn, Posn);
+int	filematch(File*, String*);
+void	filename(File*);
+void	fixname(String*);
+void	fullname(String*);
+void	getcurwd(void);
+File	*getfile(String*);
+int	getname(File*, String*, int);
+long	getnum(int);
+void	hiccough(char*);
+void	inslist(List*, int, ...);
+Address	lineaddr(Posn, Address, int);
+List	*listalloc(int);
+void	listfree(List*);
+void	load(File*);
+File	*lookfile(String*);
+void	lookorigin(File*, Posn, Posn);
+int	lookup(int);
+void	move(File*, Address);
+void	moveto(File*, Range);
+File	*newfile(void);
+void	nextmatch(File*, String*, Posn, int);
+void	notifyf(void*, char*);
+void	panic(char*);
+void	printposn(File*, int);
+void	print_ss(char*, String*, String*);
+void	print_s(char*, String*);
+int	rcv(void);
+Range	rdata(List*, Posn, Posn);
+Posn	readio(File*, int*, int, int);
+void	rescue(void);
+void	resetcmd(void);
+void	resetsys(void);
+void	resetxec(void);
+void	rgrow(List*, Posn, Posn);
+void	samerr(char*);
+void	settempfile(void);
+int	skipbl(void);
+void	snarf(File*, Posn, Posn, Buffer*, int);
+void	sortname(File*);
+void	startup(char*, int, char**, char**);
+void	state(File*, int);
+int	statfd(int, ulong*, uvlong*, long*, long*, long*);
+int	statfile(char*, ulong*, uvlong*, long*, long*, long*);
+void	Straddc(String*, int);
+void	Strclose(String*);
+int	Strcmp(String*, String*);
+void	Strdelete(String*, Posn, Posn);
+void	Strdupl(String*, Rune*);
+void	Strduplstr(String*, String*);
+void	Strinit(String*);
+void	Strinit0(String*);
+void	Strinsert(String*, String*, Posn);
+void	Strinsure(String*, ulong);
+int	Strispre(String*, String*);
+void	Strzero(String*);
+int	Strlen(Rune*);
+char	*Strtoc(String*);
+void	syserror(char*);
+void	telldot(File*);
+void	tellpat(void);
+String	*tmpcstr(char*);
+String	*tmprstr(Rune*, int);
+void	freetmpstr(String*);
+void	termcommand(void);
+void	termwrite(char*);
+File	*tofile(String*);
+void	trytoclose(File*);
+void	trytoquit(void);
+int	undo(int);
+void	update(void);
+char	*waitfor(int);
+void	warn(Warn);
+void	warn_s(Warn, char*);
+void	warn_SS(Warn, String*, String*);
+void	warn_S(Warn, String*);
+int	whichmenu(File*);
+void	writef(File*);
+Posn	writeio(File*);
+Discdesc *Dstart(void);
+
+extern Rune	samname[];	/* compiler dependent */
+extern Rune	*left[];
+extern Rune	*right[];
+
+extern char	RSAM[];		/* system dependent */
+extern char	SAMTERM[];
+extern char	HOME[];
+extern char	TMPDIR[];
+extern char	SH[];
+extern char	SHPATH[];
+extern char	RX[];
+extern char	RXPATH[];
+extern char	SAMSAVECMD[];
+
+/*
+ * acme globals
+ */
+extern long		seq;
+extern Disk		*disk;
+
+extern char	*rsamname;	/* globals */
+extern char	*samterm;
+extern Rune	genbuf[];
+extern char	*genc;
+extern int	io;
+extern int	patset;
+extern int	quitok;
+extern Address	addr;
+extern Buffer	snarfbuf;
+extern Buffer	plan9buf;
+extern Buffer	cmdbuf;
+extern int	cmdbufpos;
+extern List	file;
+extern List	tempfile;
+extern File	*cmd;
+extern File	*curfile;
+extern File	*lastfile;
+extern Mod	modnum;
+extern Posn	cmdpt;
+extern Posn	cmdptadv;
+extern Rangeset	sel;
+extern String	curwd;
+extern String	cmdstr;
+extern String	genstr;
+extern String	lastpat;
+extern String	lastregexp;
+extern String	plan9cmd;
+extern int	downloaded;
+extern int	eof;
+extern int	bpipeok;
+extern int	panicking;
+extern Rune	empty[];
+extern int	termlocked;
+extern int	outbuffered;
+
+#include "mesg.h"
+
+void	outTs(Hmesg, int);
+void	outT0(Hmesg);
+void	outTl(Hmesg, long);
+void	outTslS(Hmesg, int, long, String*);
+void	outTS(Hmesg, String*);
+void	outTsS(Hmesg, int, String*);
+void	outTsllS(Hmesg, int, long, long, String*);
+void	outTsll(Hmesg, int, long, long);
+void	outTsl(Hmesg, int, long);
+void	outTsv(Hmesg, int, vlong);
+void	outflush(void);
+int needoutflush(void);
+
+Posn	nlcount(File *f, Posn p0, Posn p1);
--- /dev/null
+++ b/sys/src/cmd/jam/shell.c
@@ -1,0 +1,190 @@
+#include "sam.h"
+#include "parse.h"
+
+extern	jmp_buf	mainloop;
+
+char	errfile[64];
+String	plan9cmd;	/* null terminated */
+Buffer	plan9buf;
+void	checkerrs(void);
+Buffer	cmdbuf;
+int	cmdbufpos;
+
+static void
+updateenv(File *f)
+{
+	static int fd = -1;
+	int n;
+	char buf[64], *p, *e;
+	
+	if(f == nil){
+		putenv("%", "");
+		putenv("%dot", "");
+		return;
+	}
+
+	p = Strtoc(&f->name);
+	putenv("%", p);
+	free(p);
+	
+	p = buf;
+	e = buf+sizeof(buf);
+	p = seprint(p, e, "%lud", 1+nlcount(f, 0, f->dot.r.p1));
+	p = seprint(p+1, e, "%lud", f->dot.r.p1);
+	p = seprint(p+1, e, "%lud", f->dot.r.p2);
+	n = p - buf;
+	if(fd == -1)
+	if((fd = create("/env/%dot", OWRITE, 0666)) < 0)
+		fprint(2, "updateenv create: %r\n");
+	if(write(fd, buf, n) != n)
+		fprint(2, "updateenv write: %r\n");
+}
+
+int
+plan9(File *f, int type, String *s, int nest)
+{
+	long l;
+	int m;
+	int pid, fd;
+	int retcode;
+	char *retmsg;
+	int pipe1[2], pipe2[2];
+
+	if(s->s[0]==0 && plan9cmd.s[0]==0)
+		error(Enocmd);
+	else if(s->s[0])
+		Strduplstr(&plan9cmd, s);
+	if(downloaded){
+		samerr(errfile);
+		remove(errfile);
+	}
+	if(type!='!' && pipe(pipe1)==-1)
+		error(Epipe);
+	if(type=='|' || type=='_')
+		snarf(f, addr.r.p1, addr.r.p2, &plan9buf, 1);
+	if((pid=fork()) == 0){
+		if(downloaded){	/* also put nasty fd's into errfile */
+			fd = create(errfile, 1, 0666L);
+			if(fd < 0)
+				fd = create("/dev/null", 1, 0666L);
+			dup(fd, 2);
+			close(fd);
+			/* 2 now points at err file */
+			if(type == '>')
+				dup(2, 1);
+			else if(type=='!'){
+				dup(2, 1);
+				fd = open("/dev/null", 0);
+				dup(fd, 0);
+				close(fd);
+			}
+		}
+		if(type != '!') {
+			if(type == '>')
+				dup(pipe1[0], 0);
+			else
+				dup(pipe1[1], 1);
+			close(pipe1[0]);
+			close(pipe1[1]);
+		}
+		if(type == '|' || type == '_'){
+			if(pipe(pipe2) == -1)
+				exits("pipe");
+			if((pid = fork())==0){
+				/*
+				 * It's ok if we get SIGPIPE here
+				 */
+				close(pipe2[0]);
+				io = pipe2[1];
+				if(retcode=!setjmp(mainloop)){	/* assignment = */
+					char *c;
+					for(l = 0; l<plan9buf.nc; l+=m){
+						m = plan9buf.nc-l;
+						if(m>BLOCKSIZE-1)
+							m = BLOCKSIZE-1;
+						bufread(&plan9buf, l, genbuf, m);
+						genbuf[m] = 0;
+						c = Strtoc(tmprstr(genbuf, m+1));
+						Write(pipe2[1], c, strlen(c));
+						free(c);
+					}
+				}
+				exits(retcode? "error" : 0);
+			}
+			if(pid==-1){
+				fprint(2, "Can't fork?!\n");
+				exits("fork");
+			}
+			dup(pipe2[0], 0);
+			close(pipe2[0]);
+			close(pipe2[1]);
+		}
+		if(type=='<' || type=='^'){
+			close(0);	/* so it won't read from terminal */
+			open("/dev/null", 0);
+		}
+		updateenv(f);
+		execl(SHPATH, SH, "-c", Strtoc(&plan9cmd), nil);
+		exits("exec");
+	}
+	if(pid == -1)
+		error(Efork);
+	if(type=='<' || type=='|'){
+		int nulls;
+		if(downloaded && addr.r.p1 != addr.r.p2)
+			outTl(Hsnarflen, addr.r.p2-addr.r.p1);
+		snarf(f, addr.r.p1, addr.r.p2, &snarfbuf, 0);
+		logdelete(f, addr.r.p1, addr.r.p2);
+		close(pipe1[1]);
+		io = pipe1[0];
+		f->tdot.p1 = -1;
+		f->ndot.r.p2 = addr.r.p2+readio(f, &nulls, 0, FALSE);
+		f->ndot.r.p1 = addr.r.p2;
+		closeio((Posn)-1);
+	}else if(type=='>'){
+		close(pipe1[0]);
+		io = pipe1[1];
+		bpipeok = 1;
+		writeio(f);
+		bpipeok = 0;
+		closeio((Posn)-1);
+	}else if(type == '^' || type == '_'){
+		int nulls;
+		close(pipe1[1]);
+		bufload(&cmdbuf, cmdbufpos, pipe1[0], &nulls);
+		close(pipe1[0]);
+	}
+	retmsg = waitfor(pid);
+	if(type=='|' || type=='<' || type=='_' || type=='^')
+		if(retmsg[0]!=0)
+			warn_s(Wbadstatus, retmsg);
+	if(downloaded)
+		checkerrs();
+	if(!nest)
+		dprint("!\n");
+	return retmsg[0] ? -1 : 0;
+}
+
+void
+checkerrs(void)
+{
+	char buf[256];
+	int f, n, nl;
+	char *p;
+	long l;
+
+	if(statfile(errfile, 0, 0, 0, &l, 0) > 0 && l != 0){
+		if((f=open((char *)errfile, 0)) != -1){
+			if((n=read(f, buf, sizeof buf-1)) > 0){
+				for(nl=0,p=buf; nl<3 && p<&buf[n]; p++)
+					if(*p=='\n')
+						nl++;
+				*p = 0;
+				dprint("%s", buf);
+				if(p-buf < l-1)
+					dprint("(jam: more in %s)\n", errfile);
+			}
+			close(f);
+		}
+	}
+}
--- /dev/null
+++ b/sys/src/cmd/jam/string.c
@@ -1,0 +1,193 @@
+#include "sam.h"
+
+#define	MINSIZE	16		/* minimum number of chars allocated */
+#define	MAXSIZE	256		/* maximum number of chars for an empty string */
+
+
+void
+Strinit(String *p)
+{
+	p->s = emalloc(MINSIZE*RUNESIZE);
+	p->n = 0;
+	p->size = MINSIZE;
+}
+
+void
+Strinit0(String *p)
+{
+	p->s = emalloc(MINSIZE*RUNESIZE);
+	p->s[0] = 0;
+	p->n = 1;
+	p->size = MINSIZE;
+}
+
+void
+Strclose(String *p)
+{
+	free(p->s);
+}
+
+void
+Strzero(String *p)
+{
+	if(p->size > MAXSIZE){
+		p->s = erealloc(p->s, RUNESIZE*MAXSIZE); /* throw away the garbage */
+		p->size = MAXSIZE;
+	}
+	p->n = 0;
+}
+
+int
+Strlen(Rune *r)
+{
+	Rune *s;
+
+	for(s=r; *s; s++)
+		;
+	return s-r;
+}
+
+void
+Strdupl(String *p, Rune *s)	/* copies the null */
+{
+	p->n = Strlen(s)+1;
+	Strinsure(p, p->n);
+	memmove(p->s, s, p->n*RUNESIZE);
+}
+
+void
+Strduplstr(String *p, String *q)	/* will copy the null if there's one there */
+{
+	Strinsure(p, q->n);
+	p->n = q->n;
+	memmove(p->s, q->s, q->n*RUNESIZE);
+}
+
+void
+Straddc(String *p, int c)
+{
+	Strinsure(p, p->n+1);
+	p->s[p->n++] = c;
+}
+
+void
+Strinsure(String *p, ulong n)
+{
+	if(n > STRSIZE)
+		error(Etoolong);
+	if(p->size < n){	/* p needs to grow */
+		n += 100;
+		p->s = erealloc(p->s, n*RUNESIZE);
+		p->size = n;
+	}
+}
+
+void
+Strinsert(String *p, String *q, Posn p0)
+{
+	Strinsure(p, p->n+q->n);
+	memmove(p->s+p0+q->n, p->s+p0, (p->n-p0)*RUNESIZE);
+	memmove(p->s+p0, q->s, q->n*RUNESIZE);
+	p->n += q->n;
+}
+
+void
+Strdelete(String *p, Posn p1, Posn p2)
+{
+	memmove(p->s+p1, p->s+p2, (p->n-p2)*RUNESIZE);
+	p->n -= p2-p1;
+}
+
+int
+Strcmp(String *a, String *b)
+{
+	int i, c;
+
+	for(i=0; i<a->n && i<b->n; i++)
+		if(c = (a->s[i] - b->s[i]))	/* assign = */
+			return c;
+	/* damn NULs confuse everything */
+	i = a->n - b->n;
+	if(i == 1){
+		if(a->s[a->n-1] == 0)
+			return 0;
+	}else if(i == -1){
+		if(b->s[b->n-1] == 0)
+			return 0;
+	}
+	return i;
+}
+
+int
+Strispre(String *a, String *b)
+{
+	int i;
+
+	for(i=0; i<a->n && i<b->n; i++){
+		if(a->s[i] - b->s[i]){	/* assign = */
+			if(a->s[i] == 0)
+				return 1;
+			return 0;
+		}
+	}
+	return i == a->n;
+}
+
+char*
+Strtoc(String *s)
+{
+	int i;
+	char *c, *d;
+	Rune *r;
+	c = emalloc(s->n*UTFmax + 1);  /* worst case UTFmax bytes per rune, plus NUL */
+	d = c;
+	r = s->s;
+	for(i=0; i<s->n; i++)
+		d += runetochar(d, r++);
+	if(d==c || d[-1]!=0)
+		*d = 0;
+	return c;
+
+}
+
+/*
+ * Build very temporary String from Rune*
+ */
+String*
+tmprstr(Rune *r, int n)
+{
+	static String p;
+
+	p.s = r;
+	p.n = n;
+	p.size = n;
+	return &p;
+}
+
+/*
+ * Convert null-terminated char* into String
+ */
+String*
+tmpcstr(char *s)
+{
+	String *p;
+	Rune *r;
+	int i, n;
+
+	n = utflen(s);	/* don't include NUL */
+	p = emalloc(sizeof(String));
+	r = emalloc(n*RUNESIZE);
+	p->s = r;
+	for(i=0; i<n; i++,r++)
+		s += chartorune(r, s);
+	p->n = n;
+	p->size = n;
+	return p;
+}
+
+void
+freetmpstr(String *s)
+{
+	free(s->s);
+	free(s);
+}
--- /dev/null
+++ b/sys/src/cmd/jam/sys.c
@@ -1,0 +1,60 @@
+#include "sam.h"
+
+static int inerror=FALSE;
+
+/*
+ * A reasonable interface to the system calls
+ */
+
+void
+resetsys(void)
+{
+	inerror = FALSE;
+}
+
+void
+syserror(char *a)
+{
+	char buf[ERRMAX];
+
+	if(!inerror){
+		inerror=TRUE;
+		errstr(buf, sizeof buf);
+		dprint("%s: ", a);
+		error_s(Eio, buf);
+	}
+}
+
+int
+Read(int f, void *a, int n)
+{
+	char buf[ERRMAX];
+
+	if(read(f, (char *)a, n)!=n) {
+		if (lastfile)
+			lastfile->rescuing = 1;
+		errstr(buf, sizeof buf);
+		if (downloaded)
+			fprint(2, "read error: %s\n", buf);
+		rescue();
+		exits("read");
+	}
+	return n;
+}
+
+int
+Write(int f, void *a, int n)
+{
+	int m;
+
+	if((m=write(f, (char *)a, n))!=n)
+		syserror("write");
+	return m;
+}
+
+void
+Seek(int f, long n, int w)
+{
+	if(seek(f, n, w)==-1)
+		syserror("seek");
+}
--- /dev/null
+++ b/sys/src/cmd/jam/util.c
@@ -1,0 +1,54 @@
+#include "sam.h"
+
+void
+cvttorunes(char *p, int n, Rune *r, int *nb, int *nr, int *nulls)
+{
+	uchar *q;
+	Rune *s;
+	int j, w;
+
+	/*
+	 * Always guaranteed that n bytes may be interpreted
+	 * without worrying about partial runes.  This may mean
+	 * reading up to UTFmax-1 more bytes than n; the caller
+	 * knows this.  If n is a firm limit, the caller should
+	 * set p[n] = 0.
+	 */
+	q = (uchar*)p;
+	s = r;
+	for(j=0; j<n; j+=w){
+		if(*q < Runeself){
+			w = 1;
+			*s = *q++;
+		}else{
+			w = chartorune(s, (char*)q);
+			q += w;
+		}
+		if(*s)
+			s++;
+		else if(nulls)
+			*nulls = TRUE;
+	}
+	*nb = (char*)q-p;
+	*nr = s-r;
+}
+
+void*
+fbufalloc(void)
+{
+	return emalloc(BUFSIZE);
+}
+
+void
+fbuffree(void *f)
+{
+	free(f);
+}
+
+uint
+min(uint a, uint b)
+{
+	if(a < b)
+		return a;
+	return b;
+}
--- /dev/null
+++ b/sys/src/cmd/jam/xec.c
@@ -1,0 +1,520 @@
+#include "sam.h"
+#include "parse.h"
+
+int	Glooping;
+int	nest;
+
+int	append(File*, Cmd*, Posn);
+int	display(File*);
+void	looper(File*, Cmd*, int);
+void	filelooper(Cmd*, int);
+void	linelooper(File*, Cmd*);
+
+void
+resetxec(void)
+{
+	Glooping = nest = 0;
+}
+
+int
+cmdexec(File *f, Cmd *cp)
+{
+	int i;
+	Addr *ap;
+	Address a;
+
+	if(f && f->unread)
+		load(f);
+	if(f==0 && (cp->addr==0 || cp->addr->type!='"') &&
+	    !utfrune("bBnqUXY!^", cp->cmdc) &&
+	    cp->cmdc!=('c'|0x100) && !(cp->cmdc=='D' && cp->ctext))
+		error(Enofile);
+	i = lookup(cp->cmdc);
+	if(i >= 0 && cmdtab[i].defaddr != aNo){
+		if((ap=cp->addr)==0 && cp->cmdc!='\n'){
+			cp->addr = ap = newaddr();
+			ap->type = '.';
+			if(cmdtab[i].defaddr == aAll)
+				ap->type = '*';
+		}else if(ap && ap->type=='"' && ap->next==0 && cp->cmdc!='\n'){
+			ap->next = newaddr();
+			ap->next->type = '.';
+			if(cmdtab[i].defaddr == aAll)
+				ap->next->type = '*';
+		}
+		if(cp->addr){	/* may be false for '\n' (only) */
+			static Address none = {0,0,0};
+			if(f)
+				addr = address(ap, f->dot, 0);
+			else	/* a " */
+				addr = address(ap, none, 0);
+			f = addr.f;
+		}
+	}
+	current(f);
+	switch(cp->cmdc){
+	case '{':
+		a = cp->addr? address(cp->addr, f->dot, 0): f->dot;
+		for(cp = cp->ccmd; cp; cp = cp->next){
+			a.f->dot = a;
+			cmdexec(a.f, cp);
+		}
+		break;
+	default:
+		i=(*cmdtab[i].fn)(f, cp);
+		return i;
+	}
+	return 1;
+}
+
+
+int
+a_cmd(File *f, Cmd *cp)
+{
+	return append(f, cp, addr.r.p2);
+}
+
+int
+b_cmd(File *f, Cmd *cp)
+{
+	USED(f);
+	f = cp->cmdc=='b'? tofile(cp->ctext) : getfile(cp->ctext);
+	if(f->unread)
+		load(f);
+	else if(nest == 0)
+		filename(f);
+	return TRUE;
+}
+
+int
+c_cmd(File *f, Cmd *cp)
+{
+	logdelete(f, addr.r.p1, addr.r.p2);
+	f->ndot.r.p1 = f->ndot.r.p2 = addr.r.p2;
+	return append(f, cp, addr.r.p2);
+}
+
+int
+d_cmd(File *f, Cmd *cp)
+{
+	USED(cp);
+	logdelete(f, addr.r.p1, addr.r.p2);
+	f->ndot.r.p1 = f->ndot.r.p2 = addr.r.p1;
+	return TRUE;
+}
+
+int
+D_cmd(File *f, Cmd *cp)
+{
+	closefiles(f, cp->ctext);
+	return TRUE;
+}
+
+int
+e_cmd(File *f, Cmd *cp)
+{
+	if(getname(f, cp->ctext, cp->cmdc=='e')==0)
+		error(Enoname);
+	edit(f, cp->cmdc);
+	return TRUE;
+}
+
+int
+f_cmd(File *f, Cmd *cp)
+{
+	getname(f, cp->ctext, TRUE);
+	filename(f);
+	return TRUE;
+}
+
+int
+g_cmd(File *f, Cmd *cp)
+{
+	if(f!=addr.f)panic("g_cmd f!=addr.f");
+	compile(cp->re);
+	if(execute(f, addr.r.p1, addr.r.p2) ^ cp->cmdc=='v'){
+		f->dot = addr;
+		return cmdexec(f, cp->ccmd);
+	}
+	return TRUE;
+}
+
+int
+i_cmd(File *f, Cmd *cp)
+{
+	return append(f, cp, addr.r.p1);
+}
+
+int
+k_cmd(File *f, Cmd *cp)
+{
+	USED(cp);
+	f->mark = addr.r;
+	return TRUE;
+}
+
+int
+m_cmd(File *f, Cmd *cp)
+{
+	Address addr2;
+
+	addr2 = address(cp->caddr, f->dot, 0);
+	if(cp->cmdc=='m')
+		move(f, addr2);
+	else
+		copy(f, addr2);
+	return TRUE;
+}
+
+int
+M_cmd(File *f, Cmd *cp)
+{
+	USED(f);
+	if(downloaded)
+		outTS(Hmenucmd, cp->ctext);
+	else
+		dprint("not downloaded\n");
+	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;
+}
+
+int
+p_cmd(File *f, Cmd *cp)
+{
+	USED(cp);
+	return display(f);
+}
+
+int
+q_cmd(File *f, Cmd *cp)
+{
+	USED(cp);
+	USED(f);
+	trytoquit();
+	if(downloaded){
+		outT0(Hexit);
+		return TRUE;
+	}
+	return FALSE;
+}
+
+int
+s_cmd(File *f, Cmd *cp)
+{
+	int i, j, c, n;
+	Posn p1, op, didsub = 0, delta = 0;
+
+	n = cp->num;
+	op= -1;
+	compile(cp->re);
+	for(p1 = addr.r.p1; p1<=addr.r.p2 && execute(f, p1, addr.r.p2); ){
+		if(sel.p[0].p1==sel.p[0].p2){	/* empty match? */
+			if(sel.p[0].p1==op){
+				p1++;
+				continue;
+			}
+			p1 = sel.p[0].p2+1;
+		}else
+			p1 = sel.p[0].p2;
+		op = sel.p[0].p2;
+		if(--n>0)
+			continue;
+		Strzero(&genstr);
+		for(i = 0; i<cp->ctext->n; i++)
+			if((c = cp->ctext->s[i])=='\\' && i<cp->ctext->n-1){
+				c = cp->ctext->s[++i];
+				if('1'<=c && c<='9') {
+					j = c-'0';
+					if(sel.p[j].p2-sel.p[j].p1>BLOCKSIZE)
+						error(Elongtag);
+					bufread(f, sel.p[j].p1, genbuf, sel.p[j].p2-sel.p[j].p1);
+					Strinsert(&genstr, tmprstr(genbuf, (sel.p[j].p2-sel.p[j].p1)), genstr.n);
+				}else
+				 	Straddc(&genstr, c);
+			}else if(c!='&')
+				Straddc(&genstr, c);
+			else{
+				if(sel.p[0].p2-sel.p[0].p1>BLOCKSIZE)
+					error(Elongrhs);
+				bufread(f, sel.p[0].p1, genbuf, sel.p[0].p2-sel.p[0].p1);
+				Strinsert(&genstr,
+					tmprstr(genbuf, (int)(sel.p[0].p2-sel.p[0].p1)),
+					genstr.n);
+			}
+		if(sel.p[0].p1!=sel.p[0].p2){
+			logdelete(f, sel.p[0].p1, sel.p[0].p2);
+			delta-=sel.p[0].p2-sel.p[0].p1;
+		}
+		if(genstr.n){
+			loginsert(f, sel.p[0].p2, genstr.s, genstr.n);
+			delta+=genstr.n;
+		}
+		didsub = 1;
+		if(!cp->flag)
+			break;
+	}
+	if(!didsub && nest==0)
+		error(Enosub);
+	f->ndot.r.p1 = addr.r.p1, f->ndot.r.p2 = addr.r.p2+delta;
+	return TRUE;
+}
+
+int
+u_cmd(File *f, Cmd *cp)
+{
+	int n;
+
+	USED(f);
+	USED(cp);
+	n = cp->num;
+	if(n >= 0)
+		while(n-- && undo(TRUE))
+			;
+	else
+		while(n++ && undo(FALSE))
+			;
+	moveto(f, f->dot.r);
+	return TRUE;
+}
+
+int
+w_cmd(File *f, Cmd *cp)
+{
+	int fseq;
+
+	fseq = f->seq;
+	if(getname(f, cp->ctext, FALSE)==0)
+		error(Enoname);
+	if(fseq == seq)
+		error_s(Ewseq, genc);
+	writef(f);
+	return TRUE;
+}
+
+int
+x_cmd(File *f, Cmd *cp)
+{
+	if(cp->re)
+		looper(f, cp, cp->cmdc=='x');
+	else
+		linelooper(f, cp);
+	return TRUE;
+}
+
+int
+X_cmd(File *f, Cmd *cp)
+{
+	USED(f);
+	filelooper(cp, cp->cmdc=='X');
+	return TRUE;
+}
+
+int
+plan9_cmd(File *f, Cmd *cp)
+{
+	plan9(f, cp->cmdc, cp->ctext, nest);
+	return TRUE;
+}
+
+int
+eq_cmd(File *f, Cmd *cp)
+{
+	int charsonly;
+
+	switch(cp->ctext->n){
+	case 1:
+		charsonly = FALSE;
+		break;
+	case 2:
+		if(cp->ctext->s[0]=='#'){
+			charsonly = TRUE;
+			break;
+		}
+	default:
+		SET(charsonly);
+		error(Enewline);
+	}
+	printposn(f, charsonly);
+	return TRUE;
+}
+
+int
+nl_cmd(File *f, Cmd *cp)
+{
+	Address a;
+
+	if(cp->addr == 0){
+		/* First put it on newline boundaries */
+		addr = lineaddr((Posn)0, f->dot, -1);
+		a = lineaddr((Posn)0, f->dot, 1);
+		addr.r.p2 = a.r.p2;
+		if(addr.r.p1==f->dot.r.p1 && addr.r.p2==f->dot.r.p2)
+			addr = lineaddr((Posn)1, f->dot, 1);
+		display(f);
+	}else if(downloaded)
+		moveto(f, addr.r);
+	else
+		display(f);
+	return TRUE;
+}
+
+int
+cd_cmd(File *f, Cmd *cp)
+{
+	USED(f);
+	cd(cp->ctext);
+	return TRUE;
+}
+
+int
+append(File *f, Cmd *cp, Posn p)
+{
+	if(cp->ctext->n>0 && cp->ctext->s[cp->ctext->n-1]==0)
+		--cp->ctext->n;
+	if(cp->ctext->n>0)
+		loginsert(f, p, cp->ctext->s, cp->ctext->n);
+	f->ndot.r.p1 = p;
+	f->ndot.r.p2 = p+cp->ctext->n;
+	return TRUE;
+}
+
+int
+display(File *f)
+{
+	Posn p1, p2;
+	int np;
+	char *c;
+
+	p1 = addr.r.p1;
+	p2 = addr.r.p2;
+	if(p2 > f->nc){
+		fprint(2, "bad display addr p1=%ld p2=%ld f->nc=%d\n", p1, p2, f->nc); /*ZZZ should never happen, can remove */
+		p2 = f->nc;
+	}
+	while(p1 < p2){
+		np = p2-p1;
+		if(np>BLOCKSIZE-1)
+			np = BLOCKSIZE-1;
+		bufread(f, p1, genbuf, np);
+		genbuf[np] = 0;
+		c = Strtoc(tmprstr(genbuf, np+1));
+		if(downloaded)
+			termwrite(c);
+		else
+			Write(1, c, strlen(c));
+		free(c);
+		p1 += np;
+	}
+	f->dot = addr;
+	return TRUE;
+}
+
+void
+looper(File *f, Cmd *cp, int xy)
+{
+	Posn p, op;
+	Range r;
+
+	r = addr.r;
+	op= xy? -1 : r.p1;
+	nest++;
+	compile(cp->re);
+	for(p = r.p1; p<=r.p2; ){
+		if(!execute(f, p, r.p2)){ /* no match, but y should still run */
+			if(xy || op>r.p2)
+				break;
+			f->dot.r.p1 = op, f->dot.r.p2 = r.p2;
+			p = r.p2+1;	/* exit next loop */
+		}else{
+			if(sel.p[0].p1==sel.p[0].p2){	/* empty match? */
+				if(sel.p[0].p1==op){
+					p++;
+					continue;
+				}
+				p = sel.p[0].p2+1;
+			}else
+				p = sel.p[0].p2;
+			if(xy)
+				f->dot.r = sel.p[0];
+			else
+				f->dot.r.p1 = op, f->dot.r.p2 = sel.p[0].p1;
+		}
+		op = sel.p[0].p2;
+		cmdexec(f, cp->ccmd);
+		compile(cp->re);
+	}
+	--nest;
+}
+
+void
+linelooper(File *f, Cmd *cp)
+{
+	Posn p;
+	Range r, linesel;
+	Address a, a3;
+
+	nest++;
+	r = addr.r;
+	a3.f = f;
+	a3.r.p1 = a3.r.p2 = r.p1;
+	for(p = r.p1; p<r.p2; p = a3.r.p2){
+		a3.r.p1 = a3.r.p2;
+/*pjw		if(p!=r.p1 || (linesel = lineaddr((Posn)0, a3, 1)).r.p2==p)*/
+		if(p!=r.p1 || (a = lineaddr((Posn)0, a3, 1), linesel = a.r, linesel.p2==p)){
+			a = lineaddr((Posn)1, a3, 1);
+			linesel = a.r;
+		}
+		if(linesel.p1 >= r.p2)
+			break;
+		if(linesel.p2 >= r.p2)
+			linesel.p2 = r.p2;
+		if(linesel.p2 > linesel.p1)
+			if(linesel.p1>=a3.r.p2 && linesel.p2>a3.r.p2){
+				f->dot.r = linesel;
+				cmdexec(f, cp->ccmd);
+				a3.r = linesel;
+				continue;
+			}
+		break;
+	}
+	--nest;
+}
+
+void
+filelooper(Cmd *cp, int XY)
+{
+	File *f, *cur;
+	int i;
+
+	if(Glooping++)
+		error(EnestXY);
+	nest++;
+	settempfile();
+	cur = curfile;
+	for(i = 0; i<tempfile.nused; i++){
+		f = tempfile.filepptr[i];
+		if(f==cmd)
+			continue;
+		if(cp->re==0 || filematch(f, cp->re)==XY)
+			cmdexec(f, cp->ccmd);
+	}
+	if(cur && whichmenu(cur)>=0)	/* check that cur is still a file */
+		current(cur);
+	--Glooping;
+	--nest;
+}
--- /dev/null
+++ b/sys/src/cmd/jamterm/flayer.c
@@ -1,0 +1,512 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <frame.h>
+#include "flayer.h"
+#include "samterm.h"
+
+#define	DELTA	10
+
+static Flayer	**llist;	/* front to back */
+static int	nllist;
+static int	nlalloc;
+static Rectangle lDrect;
+
+Vis		visibility(Flayer *);
+void		newvisibilities(int);
+void		llinsert(Flayer*);
+void		lldelete(Flayer*);
+
+Image	*maincols[NCOL];
+Image	*cmdcols[NCOL];
+
+void
+flstart(Rectangle r)
+{
+	int i;
+
+	enum{
+		CBACK = HTEXT + 1,
+		CHIGH,
+		CBORD,
+		CTEXT,
+		CHTEXT,
+		Ncols,
+	};
+	Theme th[Ncols] = {
+		[BACK] { "back",	0xFFFFEAFF },
+		[HIGH] { "high",	DDarkyellow },
+		[BORD]	{ "border",	DYellowgreen },
+		[TEXT]	{ "text",	DBlack },
+		[HTEXT]{ "back",	DBlack },
+		[CBACK] { "menuback",	0xEAFFFFFF },
+		[CHIGH] { "menuhigh",	DPalegreen },
+		[CBORD]	{ "lhold",	DPurpleblue },
+		[CTEXT]	{ "hold",	DBlack },
+		[CHTEXT]{ "menuhtext",	DBlack },
+	};
+	readtheme(th, nelem(th), nil);
+	lDrect = r;
+	for(i=0; i<NCOL; i++){
+		r = i == BORD ? Rect(0,0,2,2) : Rect(0,0,1,1);
+ 		maincols[i] = allocimage(display, r, screen->chan, 1, th[i].c);
+ 		cmdcols[i] = allocimage(display, r, screen->chan, 1, th[CBACK+i].c);
+	}
+}
+
+void
+flnew(Flayer *l, Rune *(*fn)(Flayer*, long, ulong*), int u0, void *u1)
+{
+	if(nllist == nlalloc){
+		nlalloc += DELTA;
+		llist = realloc(llist, nlalloc*sizeof(Flayer**));
+		if(llist == 0)
+			panic("flnew");
+	}
+	l->textfn = fn;
+	l->user0 = u0;
+	l->user1 = u1;
+	l->lastsr = ZR;
+	llinsert(l);
+}
+
+Rectangle
+flrect(Flayer *l, Rectangle r)
+{
+	rectclip(&r, lDrect);
+	l->entire = r;
+	l->scroll = insetrect(r, FLMARGIN);
+	r.min.x =
+	 l->scroll.max.x = r.min.x+FLMARGIN+FLSCROLLWID+(FLGAP-FLMARGIN);
+	return r;
+}
+
+void
+flinit(Flayer *l, Rectangle r, Font *ft, Image **cols)
+{
+	Rectangle wr;
+
+	lldelete(l);
+	llinsert(l);
+	l->visible = All;
+	l->origin = l->p0 = l->p1 = 0;
+	frinit(&l->f, insetrect(flrect(l, r), FLMARGIN), ft, screen, cols);
+	l->f.maxtab = maxtab*stringwidth(ft, "0");
+	newvisibilities(1);
+	draw(screen, l->entire, l->f.cols[BACK], nil, ZP);
+	scrdraw(l, 0L);
+	flborder(l, 0);
+	wr = rectsubpt(l->entire, screen->r.min);
+	l->warpto = addpt(wr.min, divpt(subpt(wr.max, wr.min), 2));
+}
+
+void
+flclose(Flayer *l)
+{
+	if(l->visible == All)
+		draw(screen, l->entire, maincols[BACK], nil, ZP);
+	else if(l->visible == Some){
+		if(l->f.b == 0)
+			l->f.b = allocimage(display, l->entire, screen->chan, 0, DNofill);
+		if(l->f.b){
+			draw(l->f.b, l->entire, maincols[BACK], nil, ZP);
+			flrefresh(l, l->entire, 0);
+		}
+	}
+	frclear(&l->f, 1);
+	lldelete(l);
+	if(l->f.b && l->visible!=All)
+		freeimage(l->f.b);
+	l->textfn = 0;
+	newvisibilities(1);
+}
+
+void
+flborder(Flayer *l, int wide)
+{
+	if(flprepare(l)){
+		border(l->f.b, l->entire, FLMARGIN, l->f.cols[BACK], ZP);
+		if(wide)
+			border(l->f.b, l->entire, FLMARGIN, cmdcols[BORD], ZP);
+		else
+			border(l->f.b, l->entire, 1, l->f.cols[BORD], ZP);
+		if(l->visible==Some)
+			flrefresh(l, l->entire, 0);
+	}
+}
+
+Flayer *
+flwhich(Point p)
+{
+	int i;
+
+	if(p.x==0 && p.y==0)
+		return nllist? llist[0] : 0;
+	for(i=0; i<nllist; i++)
+		if(ptinrect(p, llist[i]->entire))
+			return llist[i];
+	return 0;
+}
+
+void
+flupfront(Flayer *l)
+{
+	int v = l->visible;
+
+	lldelete(l);
+	llinsert(l);
+	if(v!=All)
+		newvisibilities(0);
+}
+
+void
+newvisibilities(int redraw)
+	/* if redraw false, we know it's a flupfront, and needn't
+	 * redraw anyone becoming partially covered */
+{
+	int i;
+	Vis ov;
+	Flayer *l;
+
+	for(i = 0; i<nllist; i++){
+		l = llist[i];
+		l->lastsr = ZR;	/* make sure scroll bar gets redrawn */
+		ov = l->visible;
+		l->visible = visibility(l);
+#define	V(a, b)	(((a)<<2)|((b)))
+		switch(V(ov, l->visible)){
+		case V(Some, None):
+			if(l->f.b)
+				freeimage(l->f.b);
+		case V(All, None):
+		case V(All, Some):
+			l->f.b = 0;
+			frclear(&l->f, 0);
+			break;
+
+		case V(Some, Some):
+			if(l->f.b==0 && redraw)
+		case V(None, Some):
+				flprepare(l);
+			if(l->f.b && redraw){
+				flrefresh(l, l->entire, 0);
+				freeimage(l->f.b);
+				l->f.b = 0;
+				frclear(&l->f, 0);
+			}
+		case V(None, None):
+		case V(All, All):
+			break;
+
+		case V(Some, All):
+			if(l->f.b){
+				draw(screen, l->entire, l->f.b, nil, l->entire.min);
+				freeimage(l->f.b);
+				l->f.b = screen;
+				break;
+			}
+		case V(None, All):
+			flprepare(l);
+			break;
+		}
+		if(ov==None && l->visible!=None)
+			flnewlyvisible(l);
+	}
+}
+
+void
+llinsert(Flayer *l)
+{
+	int i;
+	for(i=nllist; i>0; --i)
+		llist[i]=llist[i-1];
+	llist[0]=l;
+	nllist++;
+}
+
+void
+lldelete(Flayer *l)
+{
+	int i;
+
+	for(i=0; i<nllist; i++)
+		if(llist[i]==l){
+			--nllist;
+			for(; i<nllist; i++)
+				llist[i] = llist[i+1];
+			return;
+		}
+	panic("lldelete");
+}
+
+void
+flinsert(Flayer *l, Rune *sp, Rune *ep, long p0)
+{
+	if(flprepare(l)){
+		frinsert(&l->f, sp, ep, p0-l->origin);
+		scrdraw(l, scrtotal(l));
+		if(l->visible==Some)
+			flrefresh(l, l->entire, 0);
+	}
+}
+
+void
+fldelete(Flayer *l, long p0, long p1)
+{
+	if(flprepare(l)){
+		p0 -= l->origin;
+		if(p0 < 0)
+			p0 = 0;
+		p1 -= l->origin;
+		if(p1<0)
+			p1 = 0;
+		frdelete(&l->f, p0, p1);
+		scrdraw(l, scrtotal(l));
+		if(l->visible==Some)
+			flrefresh(l, l->entire, 0);
+	}
+}
+
+int
+flselect(Flayer *l, ulong *p)
+{
+	static int clickcount;
+	static Point clickpt = {-10, -10};
+	int dt, dx, dy;
+
+	if(l->visible!=All)
+		flupfront(l);
+	dt = mousep->msec - l->click;
+	dx = abs(mousep->xy.x - clickpt.x);
+	dy = abs(mousep->xy.y - clickpt.y);
+	*p = frcharofpt(&l->f, mousep->xy) + l->origin;
+
+	l->click = mousep->msec;
+	clickpt = mousep->xy;
+
+	if(dx < 3 && dy < 3 && dt < Clicktime && clickcount < 3)
+		return ++clickcount;
+	clickcount = 0;
+
+	frselect(&l->f, mousectl);
+	l->p0 = l->f.p0+l->origin;
+	l->p1 = l->f.p1+l->origin;
+	return 0;
+}
+
+void
+flsetselect(Flayer *l, long p0, long p1)
+{
+	ulong fp0, fp1;
+
+	if(l->visible==None || !flprepare(l)){
+		l->p0 = p0, l->p1 = p1;
+		return;
+	}
+	l->p0 = p0, l->p1 = p1;
+	flfp0p1(l, &fp0, &fp1);
+	if(fp0==l->f.p0 && fp1==l->f.p1)
+		return;
+
+	if(fp1<=l->f.p0 || fp0>=l->f.p1 || l->f.p0==l->f.p1 || fp0==fp1){
+		/* no overlap or trivial repainting */
+		frdrawsel(&l->f, frptofchar(&l->f, l->f.p0), l->f.p0, l->f.p1, 0);
+		frdrawsel(&l->f, frptofchar(&l->f, fp0), fp0, fp1, 1);
+		goto Refresh;
+	}
+	/* the current selection and the desired selection overlap and are both non-empty */
+	if(fp0 < l->f.p0){
+		/* extend selection backwards */
+		frdrawsel(&l->f, frptofchar(&l->f, fp0), fp0, l->f.p0, 1);
+	}else if(fp0 > l->f.p0){
+		/* trim first part of selection */
+		frdrawsel(&l->f, frptofchar(&l->f, l->f.p0), l->f.p0, fp0, 0);
+	}
+	if(fp1 > l->f.p1){
+		/* extend selection forwards */
+		frdrawsel(&l->f, frptofchar(&l->f, l->f.p1), l->f.p1, fp1, 1);
+	}else if(fp1 < l->f.p1){
+		/* trim last part of selection */
+		frdrawsel(&l->f, frptofchar(&l->f, fp1), fp1, l->f.p1, 0);
+	}
+
+    Refresh:
+	l->f.p0 = fp0;
+	l->f.p1 = fp1;
+	if(l->visible==Some)
+		flrefresh(l, l->entire, 0);
+}
+
+void
+flfp0p1(Flayer *l, ulong *pp0, ulong *pp1)
+{
+	long p0 = l->p0-l->origin, p1 = l->p1-l->origin;
+
+	if(p0 < 0)
+		p0 = 0;
+	if(p1 < 0)
+		p1 = 0;
+	if(p0 > l->f.nchars)
+		p0 = l->f.nchars;
+	if(p1 > l->f.nchars)
+		p1 = l->f.nchars;
+	*pp0 = p0;
+	*pp1 = p1;
+}
+
+Rectangle
+rscale(Rectangle r, Point old, Point new)
+{
+	r.min.x = r.min.x*new.x/old.x;
+	r.min.y = r.min.y*new.y/old.y;
+	r.max.x = r.max.x*new.x/old.x;
+	r.max.y = r.max.y*new.y/old.y;
+	return r;
+}
+
+void
+flresize(Rectangle dr)
+{
+	int i;
+	Flayer *l;
+	Frame *f;
+	Rectangle r, olDrect;
+	int move;
+
+	olDrect = lDrect;
+	lDrect = dr;
+	move = 0;
+	/* no moving on rio; must repaint */
+	if(0 && Dx(dr)==Dx(olDrect) && Dy(dr)==Dy(olDrect))
+		move = 1;
+	else
+		draw(screen, lDrect, maincols[BACK], nil, ZP);
+	for(i=0; i<nllist; i++){
+		l = llist[i];
+		l->lastsr = ZR;
+		f = &l->f;
+		if(move)
+			r = rectaddpt(rectsubpt(l->entire, olDrect.min), dr.min);
+		else{
+			r = rectaddpt(rscale(rectsubpt(l->entire, olDrect.min),
+				subpt(olDrect.max, olDrect.min),
+				subpt(dr.max, dr.min)), dr.min);
+			if(l->visible==Some && f->b){
+				freeimage(f->b);
+				frclear(f, 0);
+			}
+			f->b = 0;
+			if(l->visible!=None)
+				frclear(f, 0);
+		}
+		if(!rectclip(&r, dr))
+			panic("flresize");
+		if(r.max.x-r.min.x<100)
+			r.min.x = dr.min.x;
+		if(r.max.x-r.min.x<100)
+			r.max.x = dr.max.x;
+		if(r.max.y-r.min.y<2*FLMARGIN+f->font->height)
+			r.min.y = dr.min.y;
+		if(r.max.y-r.min.y<2*FLMARGIN+f->font->height)
+			r.max.y = dr.max.y;
+		if(!move)
+			l->visible = None;
+		frsetrects(f, insetrect(flrect(l, r), FLMARGIN), f->b);
+		if(!move && f->b)
+			scrdraw(l, scrtotal(l));
+	}
+	newvisibilities(1);
+}
+
+int
+flprepare(Flayer *l)
+{
+	Frame *f;
+	ulong n;
+	Rune *r;
+
+	if(l->visible == None)
+		return 0;
+	f = &l->f;
+	if(f->b == 0){
+		if(l->visible == All)
+			f->b = screen;
+		else if((f->b = allocimage(display, l->entire, screen->chan, 0, 0))==0)
+			return 0;
+		draw(f->b, l->entire, f->cols[BACK], nil, ZP);
+		border(f->b, l->entire, l==llist[0]? FLMARGIN : 1, f->cols[BORD], ZP);
+		n = f->nchars;
+		frinit(f, f->entire, f->font, f->b, 0);
+		f->maxtab = maxtab*stringwidth(f->font, "0");
+		r = (*l->textfn)(l, n, &n);
+		frinsert(f, r, r+n, (ulong)0);
+		frdrawsel(f, frptofchar(f, f->p0), f->p0, f->p1, 0);
+		flfp0p1(l, &f->p0, &f->p1);
+		frdrawsel(f, frptofchar(f, f->p0), f->p0, f->p1, 1);
+		l->lastsr = ZR;
+		scrdraw(l, scrtotal(l));
+	}
+	return 1;
+}
+
+static	int	somevis, someinvis, justvis;
+
+Vis
+visibility(Flayer *l)
+{
+	somevis = someinvis = 0;
+	justvis = 1;
+	flrefresh(l, l->entire, 0);
+	justvis = 0;
+	if(somevis==0)
+		return None;
+	if(someinvis==0)
+		return All;
+	return Some;
+}
+
+void
+flrefresh(Flayer *l, Rectangle r, int i)
+{
+	Flayer *t;
+	Rectangle s;
+
+    Top:
+	if((t=llist[i++]) == l){
+		if(!justvis)
+			draw(screen, r, l->f.b, nil, r.min);
+		somevis = 1;
+	}else{
+		if(!rectXrect(t->entire, r))
+			goto Top;	/* avoid stacking unnecessarily */
+		if(t->entire.min.x>r.min.x){
+			s = r;
+			s.max.x = t->entire.min.x;
+			flrefresh(l, s, i);
+			r.min.x = t->entire.min.x;
+		}
+		if(t->entire.min.y>r.min.y){
+			s = r;
+			s.max.y = t->entire.min.y;
+			flrefresh(l, s, i);
+			r.min.y = t->entire.min.y;
+		}
+		if(t->entire.max.x<r.max.x){
+			s = r;
+			s.min.x = t->entire.max.x;
+			flrefresh(l, s, i);
+			r.max.x = t->entire.max.x;
+		}
+		if(t->entire.max.y<r.max.y){
+			s = r;
+			s.min.y = t->entire.max.y;
+			flrefresh(l, s, i);
+			r.max.y = t->entire.max.y;
+		}
+		/* remaining piece of r is blocked by t; forget about it */
+		someinvis = 1;
+	}
+}
--- /dev/null
+++ b/sys/src/cmd/jamterm/flayer.h
@@ -1,0 +1,54 @@
+typedef enum Vis{
+	None=0,
+	Some,
+	All,
+}Vis;
+
+enum{
+	Clicktime=500,		/* milliseconds */
+};
+
+typedef struct Flayer Flayer;
+
+struct Flayer
+{
+	Frame		f;
+	long		origin;	/* offset of first char in flayer */
+	long		p0, p1;
+	long		click;	/* time at which selection click occurred, in HZ */
+	Point		warpto;
+	Rune		*(*textfn)(Flayer*, long, ulong*);
+	int		user0;
+	void		*user1;
+	Rectangle	entire;
+	Rectangle	scroll;
+	Rectangle	lastsr;	/* geometry of scrollbar when last drawn */
+	Vis		visible;
+
+	Flayer *lprev;
+	Flayer *lnext;
+};
+
+void	flborder(Flayer*, int);
+void	flclose(Flayer*);
+void	fldelete(Flayer*, long, long);
+void	flfp0p1(Flayer*, ulong*, ulong*);
+void	flinit(Flayer*, Rectangle, Font*, Image**);
+void	flinsert(Flayer*, Rune*, Rune*, long);
+void	flnew(Flayer*, Rune *(*fn)(Flayer*, long, ulong*), int, void*);
+int	flprepare(Flayer*);
+Rectangle flrect(Flayer*, Rectangle);
+void	flrefresh(Flayer*, Rectangle, int);
+void	flresize(Rectangle);
+int	flselect(Flayer*, ulong*);
+void	flsetselect(Flayer*, long, long);
+void	flstart(Rectangle);
+void	flupfront(Flayer*);
+Flayer	*flwhich(Point);
+
+#define	FLMARGIN	2
+#define	FLSCROLLWID	18
+#define	FLGAP		0
+
+extern	Image	*maincols[NCOL];
+extern	Image	*cmdcols[NCOL];
--- /dev/null
+++ b/sys/src/cmd/jamterm/icons.c
@@ -1,0 +1,38 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <cursor.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <frame.h>
+#include "flayer.h"
+#include "samterm.h"
+
+Cursor deadmouse={
+	{-7, -7},
+	{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
+	 0x00, 0x00, 0x00, 0x0C, 0x00, 0x8E, 0x1D, 0xC7,
+	 0xFF, 0xE3, 0xFF, 0xF3, 0xFF, 0xFF, 0x7F, 0xFE, 
+	 0x3F, 0xF8, 0x17, 0xF0, 0x03, 0xE0, 0x00, 0x00,},
+	{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
+	 0x00, 0x00, 0x00, 0x08, 0x00, 0x04, 0x00, 0x82,
+	 0x04, 0x41, 0xFF, 0xE1, 0x5F, 0xF1, 0x3F, 0xFE, 
+	 0x17, 0xF0, 0x03, 0xE0, 0x00, 0x00, 0x00, 0x00,}
+};
+Cursor lockarrow={
+	{-7, -7},
+	{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,},
+	{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	 0x00, 0x00, 0x00, 0x00, 0x0F, 0xC0, 0x0F, 0xC0,
+	 0x03, 0xC0, 0x07, 0xC0, 0x0E, 0xC0, 0x1C, 0xC0,
+	 0x38, 0x00, 0x70, 0x00, 0xE0, 0xDB, 0xC0, 0xDB,}
+};
+
+void
+iconinit(void)
+{
+}
--- /dev/null
+++ b/sys/src/cmd/jamterm/io.c
@@ -1,0 +1,286 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <frame.h>
+#include "flayer.h"
+#include "samterm.h"
+
+int	cursorfd;
+int	plumbfd = -1;
+int	input;
+int	got;
+int	block;
+int	kbdc;
+int	resized;
+uchar	*hostp;
+uchar	*hoststop;
+uchar	*plumbbase;
+uchar	*plumbp;
+uchar	*plumbstop;
+Channel	*plumbc;
+Channel	*hostc;
+Mousectl	*mousectl;
+Mouse	*mousep;
+Keyboardctl *keyboardctl;
+void	panic(char*);
+
+void
+initio(void)
+{
+	threadsetname("main");
+	mousectl = initmouse(nil, display->image);
+	if(mousectl == nil){
+		fprint(2, "jamterm: mouse init failed: %r\n");
+		threadexitsall("mouse");
+	}
+	mousep = mousectl;
+	keyboardctl = initkeyboard(nil);
+	if(keyboardctl == nil){
+		fprint(2, "jamterm: keyboard init failed: %r\n");
+		threadexitsall("kbd");
+	}
+	hoststart();
+	plumbstart();
+}
+
+void
+getmouse(void)
+{
+	if(readmouse(mousectl) < 0)
+		panic("mouse");
+}
+
+void
+mouseunblock(void)
+{
+	got &= ~(1<<RMouse);
+}
+
+void
+kbdblock(void)
+{		/* ca suffit */
+	block = (1<<RKeyboard)|(1<<RPlumb);
+}
+
+int
+button(int but)
+{
+	getmouse();
+	return mousep->buttons&(1<<(but-1));
+}
+
+void
+externload(int i)
+{
+	plumbbase = malloc(plumbbuf[i].n);
+	if(plumbbase == 0)
+		return;
+	memmove(plumbbase, plumbbuf[i].data, plumbbuf[i].n);
+	plumbp = plumbbase;
+	plumbstop = plumbbase + plumbbuf[i].n;
+	got |= 1<<RPlumb;
+}
+
+int
+waitforio(void)
+{
+	Alt alts[NRes+1];
+	Rune r;
+	int i;
+	ulong type;
+
+again:
+
+	alts[RPlumb].c = plumbc;
+	alts[RPlumb].v = &i;
+	alts[RPlumb].op = CHANRCV;
+	if((block & (1<<RPlumb)) || plumbc == nil)
+		alts[RPlumb].op = CHANNOP;
+
+	alts[RHost].c = hostc;
+	alts[RHost].v = &i;
+	alts[RHost].op = CHANRCV;
+	if(block & (1<<RHost))
+		alts[RHost].op = CHANNOP;
+
+	alts[RKeyboard].c = keyboardctl->c;
+	alts[RKeyboard].v = &r;
+	alts[RKeyboard].op = CHANRCV;
+	if(block & (1<<RKeyboard))
+		alts[RKeyboard].op = CHANNOP;
+
+	alts[RMouse].c = mousectl->c;
+	alts[RMouse].v = &mousectl->Mouse;
+	alts[RMouse].op = CHANRCV;
+	if(block & (1<<RMouse))
+		alts[RMouse].op = CHANNOP;
+
+	alts[RResize].c = mousectl->resizec;
+	alts[RResize].v = nil;
+	alts[RResize].op = CHANRCV;
+	if(block & (1<<RResize))
+		alts[RResize].op = CHANNOP;
+
+	alts[NRes].op = CHANEND;
+
+	if(got & ~block)
+		return got & ~block;
+	if(display->bufp > display->buf)
+		flushimage(display, 1);
+	type = alt(alts);
+	switch(type){
+	case RHost:
+		hostp = hostbuf[i].data;
+		hoststop = hostbuf[i].data + hostbuf[i].n;
+		block = 0;
+		break;
+	case RPlumb:
+		externload(i);
+		break;
+	case RKeyboard:
+		kbdc = r;
+		break;
+	case RMouse:
+		break;
+	case RResize:
+		resized = 1;
+		/* do the resize in line if we've finished initializing and we're not in a blocking state */
+		if(hasunlocked && block==0 && RESIZED())
+			resize();
+		goto again;
+	}
+	got |= 1<<type;
+	return got; 
+}
+
+int
+rcvchar(void)
+{
+	int c;
+
+	if(!(got & (1<<RHost)))
+		return -1;
+	c = *hostp++;
+	if(hostp == hoststop)
+		got &= ~(1<<RHost);
+	return c;
+}
+
+char*
+rcvstring(void)
+{
+	*hoststop = 0;
+	got &= ~(1<<RHost);
+	return (char*)hostp;
+}
+
+int
+getch(void)
+{
+	int c;
+
+	while((c = rcvchar()) == -1){
+		block = ~(1<<RHost);
+		waitforio();
+		block = 0;
+	}
+	return c;
+}
+
+int
+externchar(void)
+{
+	Rune r;
+
+    loop:
+	if(got & ((1<<RPlumb) & ~block)){
+		plumbp += chartorune(&r, (char*)plumbp);
+		if(plumbp >= plumbstop){
+			got &= ~(1<<RPlumb);
+			free(plumbbase);
+		}
+		if(r == 0)
+			goto loop;
+		return r;
+	}
+	return -1;
+}
+
+int kpeekc = -1;
+int
+ecankbd(void)
+{
+	Rune r;
+
+	if(kpeekc >= 0)
+		return 1;
+	if(nbrecv(keyboardctl->c, &r) > 0){
+		kpeekc = r;
+		return 1;
+	}
+	return 0;
+}
+
+int
+ekbd(void)
+{
+	int c;
+	Rune r;
+
+	if(kpeekc >= 0){
+		c = kpeekc;
+		kpeekc = -1;
+		return c;
+	}
+	if(recv(keyboardctl->c, &r) < 0){
+		fprint(2, "jamterm: keybard recv error: %r\n");
+		panic("kbd");
+	}
+	return r;
+}
+
+int
+kbdchar(void)
+{
+	int c, i;
+
+	c = externchar();
+	if(c > 0)
+		return c;
+	if(got & (1<<RKeyboard)){
+		c = kbdc;
+		kbdc = -1;
+		got &= ~(1<<RKeyboard);
+		return c;
+	}
+	while(plumbc!=nil && nbrecv(plumbc, &i)>0){
+		externload(i);
+		c = externchar();
+		if(c > 0)
+			return c;
+	}
+	if(!ecankbd())
+		return -1;
+	return ekbd();
+}
+
+int
+qpeekc(void)
+{
+	return kbdc;
+}
+
+int
+RESIZED(void)
+{
+	if(resized){
+		if(getwindow(display, Refnone) < 0)
+			panic("can't reattach to window");
+		resized = 0;
+		return 1;
+	}
+	return 0;
+}
--- /dev/null
+++ b/sys/src/cmd/jamterm/main.acid
@@ -1,0 +1,1334 @@
+sizeof_1_ = 8;
+aggr _1_
+{
+	'U' 0 lo;
+	'U' 4 hi;
+};
+
+defn
+_1_(addr) {
+	complex _1_ addr;
+	print("	lo	", addr.lo, "\n");
+	print("	hi	", addr.hi, "\n");
+};
+
+sizeofFPdbleword = 8;
+aggr FPdbleword
+{
+	'F' 0 x;
+	{
+	'U' 0 lo;
+	'U' 4 hi;
+	};
+};
+
+defn
+FPdbleword(addr) {
+	complex FPdbleword addr;
+	print("	x	", addr.x, "\n");
+	print("_1_ {\n");
+		_1_(addr+0);
+	print("}\n");
+};
+
+UTFmax = 4;
+Runesync = 128;
+Runeself = 128;
+Runeerror = 65533;
+Runemax = 1114111;
+Runemask = 2097151;
+sizeofFmt = 80;
+aggr Fmt
+{
+	'b' 0 runes;
+	'Y' 8 start;
+	'Y' 16 to;
+	'Y' 24 stop;
+	'Y' 32 flush;
+	'Y' 40 farg;
+	'D' 48 nfmt;
+	'Y' 56 args;
+	'D' 64 r;
+	'D' 68 width;
+	'D' 72 prec;
+	'U' 76 flags;
+};
+
+defn
+Fmt(addr) {
+	complex Fmt addr;
+	print("	runes	", addr.runes, "\n");
+	print("	start	", addr.start\Y, "\n");
+	print("	to	", addr.to\Y, "\n");
+	print("	stop	", addr.stop\Y, "\n");
+	print("	flush	", addr.flush\Y, "\n");
+	print("	farg	", addr.farg\Y, "\n");
+	print("	nfmt	", addr.nfmt, "\n");
+	print("	args	", addr.args\Y, "\n");
+	print("	r	", addr.r, "\n");
+	print("	width	", addr.width, "\n");
+	print("	prec	", addr.prec, "\n");
+	print("	flags	", addr.flags, "\n");
+};
+
+FmtWidth = 1;
+FmtLeft = 2;
+FmtPrec = 4;
+FmtSharp = 8;
+FmtSpace = 16;
+FmtSign = 32;
+FmtZero = 64;
+FmtUnsigned = 128;
+FmtShort = 256;
+FmtLong = 512;
+FmtVLong = 1024;
+FmtComma = 2048;
+FmtByte = 4096;
+FmtFlag = 8192;
+sizeofTm = 64;
+aggr Tm
+{
+	'D' 0 nsec;
+	'D' 4 sec;
+	'D' 8 min;
+	'D' 12 hour;
+	'D' 16 mday;
+	'D' 20 mon;
+	'D' 24 year;
+	'D' 28 wday;
+	'D' 32 yday;
+	'a' 36 zone;
+	'D' 52 tzoff;
+	'Y' 56 tz;
+};
+
+defn
+Tm(addr) {
+	complex Tm addr;
+	print("	nsec	", addr.nsec, "\n");
+	print("	sec	", addr.sec, "\n");
+	print("	min	", addr.min, "\n");
+	print("	hour	", addr.hour, "\n");
+	print("	mday	", addr.mday, "\n");
+	print("	mon	", addr.mon, "\n");
+	print("	year	", addr.year, "\n");
+	print("	wday	", addr.wday, "\n");
+	print("	yday	", addr.yday, "\n");
+	print("	zone	", addr.zone, "\n");
+	print("	tzoff	", addr.tzoff, "\n");
+	print("	tz	", addr.tz\Y, "\n");
+};
+
+sizeofTmfmt = 16;
+aggr Tmfmt
+{
+	'Y' 0 fmt;
+	'A' Tm 8 tm;
+};
+
+defn
+Tmfmt(addr) {
+	complex Tmfmt addr;
+	print("	fmt	", addr.fmt\Y, "\n");
+	print("	tm	", addr.tm\Y, "\n");
+};
+
+PNPROC = 1;
+PNGROUP = 2;
+Profoff = 0;
+Profuser = 1;
+Profkernel = 2;
+Proftime = 3;
+Profsample = 4;
+sizeofLock = 8;
+aggr Lock
+{
+	'D' 0 val;
+};
+
+defn
+Lock(addr) {
+	complex Lock addr;
+	print("	val	", addr.val, "\n");
+};
+
+sizeofQLp = 16;
+aggr QLp
+{
+	'D' 0 inuse;
+	'D' 4 state;
+	'A' QLp 8 next;
+};
+
+defn
+QLp(addr) {
+	complex QLp addr;
+	print("	inuse	", addr.inuse, "\n");
+	print("	state	", addr.state, "\n");
+	print("	next	", addr.next\Y, "\n");
+};
+
+sizeofQLock = 32;
+aggr QLock
+{
+	Lock 0 lock;
+	'D' 8 locked;
+	'A' QLp 16 $head;
+	'A' QLp 24 $tail;
+};
+
+defn
+QLock(addr) {
+	complex QLock addr;
+	print("Lock lock {\n");
+	Lock(addr.lock);
+	print("}\n");
+	print("	locked	", addr.locked, "\n");
+	print("	$head	", addr.$head\Y, "\n");
+	print("	$tail	", addr.$tail\Y, "\n");
+};
+
+sizeofRWLock = 32;
+aggr RWLock
+{
+	Lock 0 lock;
+	'D' 8 readers;
+	'D' 12 writer;
+	'A' QLp 16 $head;
+	'A' QLp 24 $tail;
+};
+
+defn
+RWLock(addr) {
+	complex RWLock addr;
+	print("Lock lock {\n");
+	Lock(addr.lock);
+	print("}\n");
+	print("	readers	", addr.readers, "\n");
+	print("	writer	", addr.writer, "\n");
+	print("	$head	", addr.$head\Y, "\n");
+	print("	$tail	", addr.$tail\Y, "\n");
+};
+
+sizeofRendez = 24;
+aggr Rendez
+{
+	'A' QLock 0 l;
+	'A' QLp 8 $head;
+	'A' QLp 16 $tail;
+};
+
+defn
+Rendez(addr) {
+	complex Rendez addr;
+	print("	l	", addr.l\Y, "\n");
+	print("	$head	", addr.$head\Y, "\n");
+	print("	$tail	", addr.$tail\Y, "\n");
+};
+
+sizeofNetConnInfo = 72;
+aggr NetConnInfo
+{
+	'Y' 0 dir;
+	'Y' 8 root;
+	'Y' 16 spec;
+	'Y' 24 lsys;
+	'Y' 32 lserv;
+	'Y' 40 rsys;
+	'Y' 48 rserv;
+	'Y' 56 laddr;
+	'Y' 64 raddr;
+};
+
+defn
+NetConnInfo(addr) {
+	complex NetConnInfo addr;
+	print("	dir	", addr.dir\Y, "\n");
+	print("	root	", addr.root\Y, "\n");
+	print("	spec	", addr.spec\Y, "\n");
+	print("	lsys	", addr.lsys\Y, "\n");
+	print("	lserv	", addr.lserv\Y, "\n");
+	print("	rsys	", addr.rsys\Y, "\n");
+	print("	rserv	", addr.rserv\Y, "\n");
+	print("	laddr	", addr.laddr\Y, "\n");
+	print("	raddr	", addr.raddr\Y, "\n");
+};
+
+RFNAMEG = 1;
+RFENVG = 2;
+RFFDG = 4;
+RFNOTEG = 8;
+RFPROC = 16;
+RFMEM = 32;
+RFNOWAIT = 64;
+RFCNAMEG = 1024;
+RFCENVG = 2048;
+RFCFDG = 4096;
+RFREND = 8192;
+RFNOMNT = 16384;
+sizeofQid = 16;
+aggr Qid
+{
+	'W' 0 path;
+	'U' 8 vers;
+	'b' 12 type;
+};
+
+defn
+Qid(addr) {
+	complex Qid addr;
+	print("	path	", addr.path, "\n");
+	print("	vers	", addr.vers, "\n");
+	print("	type	", addr.type, "\n");
+};
+
+sizeofDir = 80;
+aggr Dir
+{
+	'u' 0 type;
+	'U' 4 dev;
+	Qid 8 qid;
+	'U' 24 mode;
+	'U' 28 atime;
+	'U' 32 mtime;
+	'V' 40 length;
+	'Y' 48 name;
+	'Y' 56 uid;
+	'Y' 64 gid;
+	'Y' 72 muid;
+};
+
+defn
+Dir(addr) {
+	complex Dir addr;
+	print("	type	", addr.type, "\n");
+	print("	dev	", addr.dev, "\n");
+	print("Qid qid {\n");
+	Qid(addr.qid);
+	print("}\n");
+	print("	mode	", addr.mode, "\n");
+	print("	atime	", addr.atime, "\n");
+	print("	mtime	", addr.mtime, "\n");
+	print("	length	", addr.length, "\n");
+	print("	name	", addr.name\Y, "\n");
+	print("	uid	", addr.uid\Y, "\n");
+	print("	gid	", addr.gid\Y, "\n");
+	print("	muid	", addr.muid\Y, "\n");
+};
+
+sizeofWaitmsg = 24;
+aggr Waitmsg
+{
+	'D' 0 pid;
+	'a' 4 time;
+	'Y' 16 msg;
+};
+
+defn
+Waitmsg(addr) {
+	complex Waitmsg addr;
+	print("	pid	", addr.pid, "\n");
+	print("	time	", addr.time, "\n");
+	print("	msg	", addr.msg\Y, "\n");
+};
+
+sizeofIOchunk = 16;
+aggr IOchunk
+{
+	'Y' 0 addr;
+	'U' 8 len;
+};
+
+defn
+IOchunk(addr) {
+	complex IOchunk addr;
+	print("	addr	", addr.addr\Y, "\n");
+	print("	len	", addr.len, "\n");
+};
+
+DOpaque = 4294967295;
+DTransparent = 0;
+DBlack = 255;
+DWhite = 4294967295;
+DRed = 4278190335;
+DGreen = 16711935;
+DBlue = 65535;
+DCyan = 16777215;
+DMagenta = 4278255615;
+DYellow = 4294902015;
+DPaleyellow = 4294945535;
+DDarkyellow = 4008615679;
+DDarkgreen = 1149781247;
+DPalegreen = 2868882175;
+DMedgreen = 2295105791;
+DDarkblue = 22015;
+DPalebluegreen = 2868903935;
+DPaleblue = 48127;
+DBluegreen = 8947967;
+DGreygreen = 1437248255;
+DPalegreygreen = 2666458879;
+DYellowgreen = 2576960767;
+DMedblue = 39423;
+DGreyblue = 6142975;
+DPalegreyblue = 1234427391;
+DPurpleblue = 2290666751;
+DNotacolor = 4294967040;
+DNofill = 4294967040;
+Displaybufsize = 8000;
+ICOSSCALE = 1024;
+Borderwidth = 4;
+Refbackup = 0;
+Refnone = 1;
+Refmesg = 2;
+Endsquare = 0;
+Enddisc = 1;
+Endarrow = 2;
+Endmask = 31;
+Clear = 0;
+SinD = 8;
+DinS = 4;
+SoutD = 2;
+DoutS = 1;
+S = 10;
+SoverD = 11;
+SatopD = 9;
+SxorD = 3;
+D = 5;
+DoverS = 7;
+DatopS = 6;
+DxorS = 3;
+Ncomp = 12;
+CRed = 0;
+CGreen = 1;
+CBlue = 2;
+CGrey = 3;
+CAlpha = 4;
+CMap = 5;
+CIgnore = 6;
+NChan = 7;
+GREY1 = 49;
+GREY2 = 50;
+GREY4 = 52;
+GREY8 = 56;
+CMAP8 = 88;
+RGB15 = 1627723045;
+RGB16 = 333349;
+RGB24 = 530472;
+RGBA32 = 135800904;
+ARGB32 = 1208490024;
+XRGB32 = 1745360936;
+BGR24 = 2627592;
+ABGR32 = 1210587144;
+XBGR32 = 1747458056;
+sizeofPoint = 8;
+aggr Point
+{
+	'D' 0 x;
+	'D' 4 y;
+};
+
+defn
+Point(addr) {
+	complex Point addr;
+	print("	x	", addr.x, "\n");
+	print("	y	", addr.y, "\n");
+};
+
+sizeofRectangle = 16;
+aggr Rectangle
+{
+	Point 0 min;
+	Point 8 max;
+};
+
+defn
+Rectangle(addr) {
+	complex Rectangle addr;
+	print("Point min {\n");
+	Point(addr.min);
+	print("}\n");
+	print("Point max {\n");
+	Point(addr.max);
+	print("}\n");
+};
+
+sizeofScreen = 32;
+aggr Screen
+{
+	'Y' 0 display;
+	'D' 8 id;
+	'Y' 16 image;
+	'Y' 24 fill;
+};
+
+defn
+Screen(addr) {
+	complex Screen addr;
+	print("	display	", addr.display\Y, "\n");
+	print("	id	", addr.id, "\n");
+	print("	image	", addr.image\Y, "\n");
+	print("	fill	", addr.fill\Y, "\n");
+};
+
+sizeofDisplay = 264;
+aggr Display
+{
+	QLock 0 qlock;
+	'D' 32 locking;
+	'D' 36 dirno;
+	'D' 40 fd;
+	'D' 44 reffd;
+	'D' 48 ctlfd;
+	'D' 52 imageid;
+	'D' 56 $local;
+	'Y' 64 error;
+	'Y' 72 devdir;
+	'Y' 80 windir;
+	'a' 88 oldlabel;
+	'U' 152 dataqid;
+	'Y' 160 white;
+	'Y' 168 black;
+	'Y' 176 opaque;
+	'Y' 184 transparent;
+	'Y' 192 image;
+	'Y' 200 buf;
+	'D' 208 bufsize;
+	'Y' 216 bufp;
+	'Y' 224 defaultfont;
+	'Y' 232 defaultsubfont;
+	'Y' 240 windows;
+	'Y' 248 screenimage;
+	'D' 256 _isnewdisplay;
+};
+
+defn
+Display(addr) {
+	complex Display addr;
+	print("QLock qlock {\n");
+	QLock(addr.qlock);
+	print("}\n");
+	print("	locking	", addr.locking, "\n");
+	print("	dirno	", addr.dirno, "\n");
+	print("	fd	", addr.fd, "\n");
+	print("	reffd	", addr.reffd, "\n");
+	print("	ctlfd	", addr.ctlfd, "\n");
+	print("	imageid	", addr.imageid, "\n");
+	print("	$local	", addr.$local, "\n");
+	print("	error	", addr.error\Y, "\n");
+	print("	devdir	", addr.devdir\Y, "\n");
+	print("	windir	", addr.windir\Y, "\n");
+	print("	oldlabel	", addr.oldlabel, "\n");
+	print("	dataqid	", addr.dataqid, "\n");
+	print("	white	", addr.white\Y, "\n");
+	print("	black	", addr.black\Y, "\n");
+	print("	opaque	", addr.opaque\Y, "\n");
+	print("	transparent	", addr.transparent\Y, "\n");
+	print("	image	", addr.image\Y, "\n");
+	print("	buf	", addr.buf\Y, "\n");
+	print("	bufsize	", addr.bufsize, "\n");
+	print("	bufp	", addr.bufp\Y, "\n");
+	print("	defaultfont	", addr.defaultfont\Y, "\n");
+	print("	defaultsubfont	", addr.defaultsubfont\Y, "\n");
+	print("	windows	", addr.windows\Y, "\n");
+	print("	screenimage	", addr.screenimage\Y, "\n");
+	print("	_isnewdisplay	", addr._isnewdisplay, "\n");
+};
+
+sizeofImage = 80;
+aggr Image
+{
+	'A' Display 0 display;
+	'D' 8 id;
+	Rectangle 16 r;
+	Rectangle 32 clipr;
+	'D' 48 depth;
+	'U' 52 chan;
+	'D' 56 repl;
+	'A' Screen 64 screen;
+	'A' Image 72 next;
+};
+
+defn
+Image(addr) {
+	complex Image addr;
+	print("	display	", addr.display\Y, "\n");
+	print("	id	", addr.id, "\n");
+	print("Rectangle r {\n");
+	Rectangle(addr.r);
+	print("}\n");
+	print("Rectangle clipr {\n");
+	Rectangle(addr.clipr);
+	print("}\n");
+	print("	depth	", addr.depth, "\n");
+	print("	chan	", addr.chan, "\n");
+	print("	repl	", addr.repl, "\n");
+	print("	screen	", addr.screen\Y, "\n");
+	print("	next	", addr.next\Y, "\n");
+};
+
+sizeofRGB = 16;
+aggr RGB
+{
+	'U' 0 red;
+	'U' 4 green;
+	'U' 8 blue;
+};
+
+defn
+RGB(addr) {
+	complex RGB addr;
+	print("	red	", addr.red, "\n");
+	print("	green	", addr.green, "\n");
+	print("	blue	", addr.blue, "\n");
+};
+
+sizeofFontchar = 8;
+aggr Fontchar
+{
+	'D' 0 x;
+	'b' 4 top;
+	'b' 5 bottom;
+	'C' 6 left;
+	'b' 7 width;
+};
+
+defn
+Fontchar(addr) {
+	complex Fontchar addr;
+	print("	x	", addr.x, "\n");
+	print("	top	", addr.top, "\n");
+	print("	bottom	", addr.bottom, "\n");
+	print("	left	", addr.left, "\n");
+	print("	width	", addr.width, "\n");
+};
+
+sizeofSubfont = 40;
+aggr Subfont
+{
+	'Y' 0 name;
+	'd' 8 n;
+	'b' 10 height;
+	'C' 11 ascent;
+	'A' Fontchar 16 info;
+	'A' Image 24 bits;
+	'D' 32 ref;
+};
+
+defn
+Subfont(addr) {
+	complex Subfont addr;
+	print("	name	", addr.name\Y, "\n");
+	print("	n	", addr.n, "\n");
+	print("	height	", addr.height, "\n");
+	print("	ascent	", addr.ascent, "\n");
+	print("	info	", addr.info\Y, "\n");
+	print("	bits	", addr.bits\Y, "\n");
+	print("	ref	", addr.ref, "\n");
+};
+
+LOG2NFCACHE = 6;
+NFCACHE = 64;
+NFLOOK = 5;
+NFSUBF = 2;
+MAXFCACHE = 1029;
+MAXSUBF = 50;
+DSUBF = 4;
+SUBFAGE = 10000;
+CACHEAGE = 10000;
+sizeofCachefont = 32;
+aggr Cachefont
+{
+	'U' 0 min;
+	'U' 4 max;
+	'D' 8 offset;
+	'Y' 16 name;
+	'Y' 24 subfontname;
+};
+
+defn
+Cachefont(addr) {
+	complex Cachefont addr;
+	print("	min	", addr.min, "\n");
+	print("	max	", addr.max, "\n");
+	print("	offset	", addr.offset, "\n");
+	print("	name	", addr.name\Y, "\n");
+	print("	subfontname	", addr.subfontname\Y, "\n");
+};
+
+sizeofCacheinfo = 16;
+aggr Cacheinfo
+{
+	'u' 0 x;
+	'b' 2 width;
+	'C' 3 left;
+	'U' 4 value;
+	'u' 8 age;
+};
+
+defn
+Cacheinfo(addr) {
+	complex Cacheinfo addr;
+	print("	x	", addr.x, "\n");
+	print("	width	", addr.width, "\n");
+	print("	left	", addr.left, "\n");
+	print("	value	", addr.value, "\n");
+	print("	age	", addr.age, "\n");
+};
+
+sizeofCachesubf = 24;
+aggr Cachesubf
+{
+	'U' 0 age;
+	'A' Cachefont 8 cf;
+	'A' Subfont 16 f;
+};
+
+defn
+Cachesubf(addr) {
+	complex Cachesubf addr;
+	print("	age	", addr.age, "\n");
+	print("	cf	", addr.cf\Y, "\n");
+	print("	f	", addr.f\Y, "\n");
+};
+
+sizeofFont = 72;
+aggr Font
+{
+	'Y' 0 name;
+	'A' Display 8 display;
+	'd' 16 height;
+	'd' 18 ascent;
+	'd' 20 width;
+	'd' 22 nsub;
+	'U' 24 age;
+	'D' 28 maxdepth;
+	'D' 32 ncache;
+	'D' 36 nsubf;
+	'A' Cacheinfo 40 cache;
+	'A' Cachesubf 48 subf;
+	'A' Cachefont 56 sub;
+	'A' Image 64 cacheimage;
+};
+
+defn
+Font(addr) {
+	complex Font addr;
+	print("	name	", addr.name\Y, "\n");
+	print("	display	", addr.display\Y, "\n");
+	print("	height	", addr.height, "\n");
+	print("	ascent	", addr.ascent, "\n");
+	print("	width	", addr.width, "\n");
+	print("	nsub	", addr.nsub, "\n");
+	print("	age	", addr.age, "\n");
+	print("	maxdepth	", addr.maxdepth, "\n");
+	print("	ncache	", addr.ncache, "\n");
+	print("	nsubf	", addr.nsubf, "\n");
+	print("	cache	", addr.cache\Y, "\n");
+	print("	subf	", addr.subf\Y, "\n");
+	print("	sub	", addr.sub\Y, "\n");
+	print("	cacheimage	", addr.cacheimage\Y, "\n");
+};
+
+complex Point ZP;
+complex Rectangle ZR;
+complex Display display;
+complex Font font;
+complex Image screen;
+complex Screen _screen;
+Nqwds = 2;
+Nqshift = 5;
+Nqmask = -1;
+Nqbits = 64;
+sizeofChannel = 48;
+aggr Channel
+{
+	'D' 0 s;
+	'U' 4 f;
+	'U' 8 n;
+	'D' 12 e;
+	'D' 16 freed;
+	'Y' 24 qentry;
+	'D' 32 nentry;
+	'D' 36 closed;
+	'a' 40 v;
+};
+
+defn
+Channel(addr) {
+	complex Channel addr;
+	print("	s	", addr.s, "\n");
+	print("	f	", addr.f, "\n");
+	print("	n	", addr.n, "\n");
+	print("	e	", addr.e, "\n");
+	print("	freed	", addr.freed, "\n");
+	print("	qentry	", addr.qentry\Y, "\n");
+	print("	nentry	", addr.nentry, "\n");
+	print("	closed	", addr.closed, "\n");
+	print("	v	", addr.v, "\n");
+};
+
+CHANEND = 0;
+CHANSND = 1;
+CHANRCV = 2;
+CHANNOP = 3;
+CHANNOBLK = 4;
+sizeofAlt = 48;
+aggr Alt
+{
+	'A' Channel 0 c;
+	'Y' 8 v;
+	'D' 16 op;
+	'Y' 24 err;
+	'A' Channel 32 tag;
+	'D' 40 entryno;
+};
+
+defn
+Alt(addr) {
+	complex Alt addr;
+	print("	c	", addr.c\Y, "\n");
+	print("	v	", addr.v\Y, "\n");
+	print("	op	", addr.op, "\n");
+	print("	err	", addr.err\Y, "\n");
+	print("	tag	", addr.tag\Y, "\n");
+	print("	entryno	", addr.entryno, "\n");
+};
+
+sizeofRef = 8;
+aggr Ref
+{
+	'D' 0 ref;
+};
+
+defn
+Ref(addr) {
+	complex Ref addr;
+	print("	ref	", addr.ref, "\n");
+};
+
+sizeofCursor = 72;
+aggr Cursor
+{
+	Point 0 offset;
+	'a' 8 clr;
+	'a' 40 set;
+};
+
+defn
+Cursor(addr) {
+	complex Cursor addr;
+	print("Point offset {\n");
+	Point(addr.offset);
+	print("}\n");
+	print("	clr	", addr.clr, "\n");
+	print("	set	", addr.set, "\n");
+};
+
+sizeofMouse = 24;
+aggr Mouse
+{
+	'D' 0 buttons;
+	Point 8 xy;
+	'U' 16 msec;
+};
+
+defn
+Mouse(addr) {
+	complex Mouse addr;
+	print("	buttons	", addr.buttons, "\n");
+	print("Point xy {\n");
+	Point(addr.xy);
+	print("}\n");
+	print("	msec	", addr.msec, "\n");
+};
+
+sizeofMousectl = 72;
+aggr Mousectl
+{
+	{
+	'D' 0 buttons;
+	Point 8 xy;
+	'U' 16 msec;
+	};
+	'A' Channel 24 c;
+	'A' Channel 32 resizec;
+	'Y' 40 file;
+	'D' 48 mfd;
+	'D' 52 cfd;
+	'D' 56 pid;
+	'A' Image 64 image;
+};
+
+defn
+Mousectl(addr) {
+	complex Mousectl addr;
+	print("Mouse {\n");
+		Mouse(addr+0);
+	print("}\n");
+	print("	c	", addr.c\Y, "\n");
+	print("	resizec	", addr.resizec\Y, "\n");
+	print("	file	", addr.file\Y, "\n");
+	print("	mfd	", addr.mfd, "\n");
+	print("	cfd	", addr.cfd, "\n");
+	print("	pid	", addr.pid, "\n");
+	print("	image	", addr.image\Y, "\n");
+};
+
+sizeofMenu = 24;
+aggr Menu
+{
+	'Y' 0 item;
+	'Y' 8 gen;
+	'D' 16 lasthit;
+};
+
+defn
+Menu(addr) {
+	complex Menu addr;
+	print("	item	", addr.item\Y, "\n");
+	print("	gen	", addr.gen\Y, "\n");
+	print("	lasthit	", addr.lasthit, "\n");
+};
+
+sizeofKeyboardctl = 32;
+aggr Keyboardctl
+{
+	'A' Channel 0 c;
+	'Y' 8 file;
+	'D' 16 consfd;
+	'D' 20 ctlfd;
+	'D' 24 pid;
+};
+
+defn
+Keyboardctl(addr) {
+	complex Keyboardctl addr;
+	print("	c	", addr.c\Y, "\n");
+	print("	file	", addr.file\Y, "\n");
+	print("	consfd	", addr.consfd, "\n");
+	print("	ctlfd	", addr.ctlfd, "\n");
+	print("	pid	", addr.pid, "\n");
+};
+
+KF = 61440;
+Spec = 63488;
+PF = 63520;
+Kview = 63488;
+Khome = 61453;
+Kup = 61454;
+Kdown = 63488;
+Kpgup = 61455;
+Kprint = 61456;
+Kleft = 61457;
+Kright = 61458;
+Kpgdown = 61459;
+Kins = 61460;
+Kalt = 61461;
+Kshift = 61462;
+Kctl = 61463;
+Kend = 61464;
+Kscroll = 61465;
+Kscrolloneup = 61472;
+Kscrollonedown = 61473;
+Ksoh = 1;
+Kstx = 2;
+Ketx = 3;
+Keof = 4;
+Kenq = 5;
+Kack = 6;
+Kbs = 8;
+Knack = 21;
+Ketb = 23;
+Kdel = 127;
+Kesc = 27;
+Kbreak = 63585;
+Kcaps = 63588;
+Knum = 63589;
+Kmiddle = 63590;
+Kaltgr = 63591;
+Kmod4 = 63592;
+Kmouse = 63744;
+BACK = 0;
+HIGH = 1;
+BORD = 2;
+TEXT = 3;
+HTEXT = 4;
+NCOL = 5;
+sizeof_2_ = 8;
+aggr _2_
+{
+	'd' 0 bc;
+	'd' 2 minwid;
+};
+
+defn
+_2_(addr) {
+	complex _2_ addr;
+	print("	bc	", addr.bc, "\n");
+	print("	minwid	", addr.minwid, "\n");
+};
+
+sizeof_3_ = 8;
+aggr _3_
+{
+	'Y' 0 ptr;
+	{
+	'd' 0 bc;
+	'd' 2 minwid;
+	};
+};
+
+defn
+_3_(addr) {
+	complex _3_ addr;
+	print("	ptr	", addr.ptr\Y, "\n");
+	print("_2_ {\n");
+		_2_(addr+0);
+	print("}\n");
+};
+
+sizeofFrbox = 16;
+aggr Frbox
+{
+	'D' 0 wid;
+	'D' 4 nrune;
+	{
+	'Y' 8 ptr;
+	{
+	'd' 8 bc;
+	'd' 10 minwid;
+	};
+	};
+};
+
+defn
+Frbox(addr) {
+	complex Frbox addr;
+	print("	wid	", addr.wid, "\n");
+	print("	nrune	", addr.nrune, "\n");
+	print("_3_ {\n");
+		_3_(addr+8);
+	print("}\n");
+};
+
+complex Font font;
+complex Display display;
+sizeofFrame = 160;
+aggr Frame
+{
+	'A' Font 0 font;
+	'A' Display 8 display;
+	'A' Image 16 b;
+	'a' 24 cols;
+	Rectangle 64 r;
+	Rectangle 80 entire;
+	'Y' 96 scroll;
+	'A' Frbox 104 box;
+	'U' 112 p0;
+	'U' 116 p1;
+	'u' 120 nbox;
+	'u' 122 nalloc;
+	'u' 124 maxtab;
+	'u' 126 nchars;
+	'u' 128 nlines;
+	'u' 130 maxlines;
+	'u' 132 lastlinefull;
+	'u' 134 modified;
+	'A' Image 136 tick;
+	'A' Image 144 tickback;
+	'D' 152 ticked;
+};
+
+defn
+Frame(addr) {
+	complex Frame addr;
+	print("	font	", addr.font\Y, "\n");
+	print("	display	", addr.display\Y, "\n");
+	print("	b	", addr.b\Y, "\n");
+	print("	cols	", addr.cols, "\n");
+	print("Rectangle r {\n");
+	Rectangle(addr.r);
+	print("}\n");
+	print("Rectangle entire {\n");
+	Rectangle(addr.entire);
+	print("}\n");
+	print("	scroll	", addr.scroll\Y, "\n");
+	print("	box	", addr.box\Y, "\n");
+	print("	p0	", addr.p0, "\n");
+	print("	p1	", addr.p1, "\n");
+	print("	nbox	", addr.nbox, "\n");
+	print("	nalloc	", addr.nalloc, "\n");
+	print("	maxtab	", addr.maxtab, "\n");
+	print("	nchars	", addr.nchars, "\n");
+	print("	nlines	", addr.nlines, "\n");
+	print("	maxlines	", addr.maxlines, "\n");
+	print("	lastlinefull	", addr.lastlinefull, "\n");
+	print("	modified	", addr.modified, "\n");
+	print("	tick	", addr.tick\Y, "\n");
+	print("	tickback	", addr.tickback\Y, "\n");
+	print("	ticked	", addr.ticked, "\n");
+};
+
+None = 0;
+Some = 1;
+All = 2;
+Clicktime = 500;
+sizeofFlayer = 280;
+aggr Flayer
+{
+	Frame 0 f;
+	'D' 160 origin;
+	'D' 164 p0;
+	'D' 168 p1;
+	'D' 172 click;
+	Point 176 warpto;
+	'Y' 184 textfn;
+	'D' 192 user0;
+	'Y' 200 user1;
+	Rectangle 208 entire;
+	Rectangle 224 scroll;
+	Rectangle 240 lastsr;
+	'D' 256 visible;
+	'A' Flayer 264 lprev;
+	'A' Flayer 272 lnext;
+};
+
+defn
+Flayer(addr) {
+	complex Flayer addr;
+	print("Frame f {\n");
+	Frame(addr.f);
+	print("}\n");
+	print("	origin	", addr.origin, "\n");
+	print("	p0	", addr.p0, "\n");
+	print("	p1	", addr.p1, "\n");
+	print("	click	", addr.click, "\n");
+	print("Point warpto {\n");
+	Point(addr.warpto);
+	print("}\n");
+	print("	textfn	", addr.textfn\Y, "\n");
+	print("	user0	", addr.user0, "\n");
+	print("	user1	", addr.user1\Y, "\n");
+	print("Rectangle entire {\n");
+	Rectangle(addr.entire);
+	print("}\n");
+	print("Rectangle scroll {\n");
+	Rectangle(addr.scroll);
+	print("}\n");
+	print("Rectangle lastsr {\n");
+	Rectangle(addr.lastsr);
+	print("}\n");
+	print("	visible	", addr.visible, "\n");
+	print("	lprev	", addr.lprev\Y, "\n");
+	print("	lnext	", addr.lnext\Y, "\n");
+};
+
+Up = 0;
+Down = 1;
+Kbel = 7;
+Ksyn = 22;
+sizeofSection = 24;
+aggr Section
+{
+	'D' 0 nrunes;
+	'Y' 8 text;
+	'A' Section 16 next;
+};
+
+defn
+Section(addr) {
+	complex Section addr;
+	print("	nrunes	", addr.nrunes, "\n");
+	print("	text	", addr.text\Y, "\n");
+	print("	next	", addr.next\Y, "\n");
+};
+
+sizeofRasp = 16;
+aggr Rasp
+{
+	'D' 0 nrunes;
+	'A' Section 8 sect;
+};
+
+defn
+Rasp(addr) {
+	complex Rasp addr;
+	print("	nrunes	", addr.nrunes, "\n");
+	print("	sect	", addr.sect\Y, "\n");
+};
+
+sizeofText = 2824;
+aggr Text
+{
+	Rasp 0 rasp;
+	'd' 16 nwin;
+	'd' 18 front;
+	'u' 20 tag;
+	'C' 22 lock;
+	'a' 24 l;
+};
+
+defn
+Text(addr) {
+	complex Text addr;
+	print("Rasp rasp {\n");
+	Rasp(addr.rasp);
+	print("}\n");
+	print("	nwin	", addr.nwin, "\n");
+	print("	front	", addr.front, "\n");
+	print("	tag	", addr.tag, "\n");
+	print("	lock	", addr.lock, "\n");
+	print("	l	", addr.l, "\n");
+};
+
+sizeofReadbuf = 8200;
+aggr Readbuf
+{
+	'd' 0 n;
+	'a' 2 data;
+};
+
+defn
+Readbuf(addr) {
+	complex Readbuf addr;
+	print("	n	", addr.n, "\n");
+	print("	data	", addr.data, "\n");
+};
+
+RHost = 0;
+RKeyboard = 1;
+RMouse = 2;
+RPlumb = 3;
+RResize = 4;
+NRes = 5;
+complex Text text;
+complex Cursor deadmouse;
+complex Cursor lockarrow;
+complex Cursor cursor;
+complex Flayer which;
+complex Flayer work;
+complex Text cmd;
+complex Mousectl mousectl;
+complex Keyboardctl keyboardctl;
+complex Mouse mousep;
+complex Channel plumbc;
+complex Channel hostc;
+Tversion = 0;
+Tstartcmdfile = 1;
+Tcheck = 2;
+Trequest = 3;
+Torigin = 4;
+Tstartfile = 5;
+Tworkfile = 6;
+Ttype = 7;
+Tcut = 8;
+Tpaste = 9;
+Tsnarf = 10;
+Tstartnewfile = 11;
+Twrite = 12;
+Tclose = 13;
+Tlook = 14;
+Tsearch = 15;
+Tsend = 16;
+Tdclick = 17;
+Tstartsnarf = 18;
+Tsetsnarf = 19;
+Tack = 20;
+Texit = 21;
+Tplumb = 22;
+Ttclick = 23;
+Tmenucmd = 24;
+Tmenucmdsend = 25;
+TMAX = 26;
+Hversion = 0;
+Hbindname = 1;
+Hcurrent = 2;
+Hnewname = 3;
+Hmovname = 4;
+Hgrow = 5;
+Hcheck0 = 6;
+Hcheck = 7;
+Hunlock = 8;
+Hdata = 9;
+Horigin = 10;
+Hunlockfile = 11;
+Hsetdot = 12;
+Hgrowdata = 13;
+Hmoveto = 14;
+Hclean = 15;
+Hdirty = 16;
+Hcut = 17;
+Hsetpat = 18;
+Hdelname = 19;
+Hclose = 20;
+Hsetsnarf = 21;
+Hsnarflen = 22;
+Hack = 23;
+Hexit = 24;
+Hplumb = 25;
+Hmenucmd = 26;
+HMAX = 27;
+sizeofHeader = 8;
+aggr Header
+{
+	'b' 0 type;
+	'b' 1 count0;
+	'b' 2 count1;
+	'a' 3 data;
+};
+
+defn
+Header(addr) {
+	complex Header addr;
+	print("	type	", addr.type, "\n");
+	print("	count0	", addr.count0, "\n");
+	print("	count1	", addr.count1, "\n");
+	print("	data	", addr.data, "\n");
+};
+
+complex Text cmd;
+complex Cursor cursor;
+complex Flayer which;
+complex Flayer work;
+complex Flayer flru;
+complex Text threadmain:t;
+complex Rectangle threadmain:r;
+complex Flayer threadmain:nwhich;
+complex Flayer flunlink:fl;
+complex Flayer fllinkhead:fl;
+complex Flayer warpmouse:l;
+complex Point warpmouse:p;
+complex Flayer current:nw;
+complex Text current:t;
+complex Flayer cycle:l;
+complex Flayer closeup:l;
+complex Text closeup:t;
+complex Text findl:t;
+complex Flayer duplicate:l;
+complex Rectangle duplicate:r;
+complex Font duplicate:f;
+complex Text duplicate:t;
+complex Flayer duplicate:nl;
+complex Text snarf:t;
+complex Flayer snarf:l;
+complex Text cut:t;
+complex Flayer cut:l;
+complex Text paste:t;
+complex Flayer scrorigin:l;
+complex Text scrorigin:t;
+complex Rasp raspc:r;
+complex Rasp getcol:r;
+complex Rasp del:r;
+complex Rasp ctlw:r;
+complex Rasp ctlu:r;
+complex Flayer center:l;
+complex Text center:t;
+complex Flayer onethird:l;
+complex Text onethird:t;
+complex Rectangle onethird:s;
+complex Text flushtyping:t;
+complex Flayer type:l;
+complex Text type:t;
+complex Flayer gettext:l;
+complex Text gettext:t;
+complex Flayer scrtotal:l;
--- /dev/null
+++ b/sys/src/cmd/jamterm/main.c
@@ -1,0 +1,805 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <cursor.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <frame.h>
+#include "flayer.h"
+#include "samterm.h"
+
+int	mainstacksize = 16*1024;
+
+Text	cmd;
+Rune	*scratch;
+long	nscralloc;
+Cursor	*cursor;
+Flayer	*which = 0;
+Flayer	*work = 0;
+Flayer	flru;
+long	snarflen;
+long	typestart = -1;
+long	typeend = -1;
+long	typeesc = -1;
+long	modified = 0;		/* strange lookahead for menus */
+char	hostlock = 1;
+char	hasunlocked = 0;
+int	maxtab = 8;
+int	autoindent = 1;
+int	spacesindent;
+
+void
+threadmain(int argc, char *argv[])
+{
+	int i, got, nclick, chord;
+	Text *t;
+	Rectangle r;
+	Flayer *nwhich;
+	ulong p;
+
+	getscreen(argc, argv);
+	iconinit();
+	initio();
+	flru.lprev = &flru;
+	flru.lnext = &flru;
+	scratch = alloc(100*RUNESIZE);
+	nscralloc = 100;
+	r = defaultcmdrect();
+	rinit(&cmd.rasp);
+	flnew(&cmd.l[0], gettext, 1, &cmd);
+	flinit(&cmd.l[0], r, font, cmdcols);
+	cmd.nwin = 1;
+	which = &cmd.l[0];
+	cmd.tag = Untagged;
+	outTs(Tversion, VERSION);
+	startnewfile(Tstartcmdfile, &cmd);
+	fmtinstall('P', Pfmt);
+
+	got = 0;
+	chord = 0;
+	for(;;got = waitforio()){
+		if(hasunlocked && RESIZED())
+			resize();
+		if(got&(1<<RHost))
+			rcv();
+		if(got&(1<<RPlumb)){
+			for(i=0; cmd.l[i].textfn==0; i++)
+				;
+			nwhich = which;
+			current(&cmd.l[i], 0, 0);
+			flsetselect(which, cmd.rasp.nrunes, cmd.rasp.nrunes);
+			type(which, RPlumb);
+			current(nwhich, 0, 0);
+		}
+		if(got&(1<<RKeyboard))
+			if(which)
+				type(which, RKeyboard);
+			else
+				kbdblock();
+		if(got&(1<<RMouse)){
+			if(hostlock==2 || !ptinrect(mousep->xy, screen->r)){
+				mouseunblock();
+				continue;
+			}
+			nwhich = flwhich(mousep->xy);
+			if(mousep->buttons)
+				flushtyping(1);
+			if((mousep->buttons&1)==0)
+				chord = 0;
+			if(chord && which && which==nwhich){
+				chord |= mousep->buttons;
+				t = (Text *)which->user1;
+				if(!t->lock){
+					int w = which-t->l;
+					if(chord&2){
+						cut(t, w, 1, 1);
+						chord &= ~2;
+					}
+					if(chord&4){
+						paste(t, w);
+						chord &= ~4;
+					}
+				}
+			}else if(mousep->buttons&(1|8)){
+				if(nwhich && nwhich!=which)
+					current(nwhich, 1, 1);
+				if(ptinrect(mousep->xy, which->scroll) || mousep->buttons & 8)
+					scroll(which, (mousep->buttons&8) ? 4 : 1);
+				else{
+					t=(Text *)which->user1;
+					nclick = flselect(which, &p);
+					if(nclick > 0){
+						if(nclick > 1)
+							outTsl(Ttclick, t->tag, p);
+						else
+							outTsl(Tdclick, t->tag, p);
+						t->lock++;
+					}else if(t!=&cmd)
+						outcmd();
+					if(mousep->buttons&1)
+						chord = mousep->buttons;
+				}
+			}else if((mousep->buttons&2) && which){
+				if(nwhich && nwhich!=which)
+					current(nwhich, 1, 1);
+				if(ptinrect(mousep->xy, which->scroll))
+					scroll(which, 2);
+				else
+					menu2hit();
+			}else if(mousep->buttons&(4|16)){
+				if(nwhich && nwhich!=which)
+					current(nwhich, 1, 1);
+				if(ptinrect(mousep->xy, which->scroll) || mousep->buttons & 16)
+					scroll(which, (mousep->buttons&16) ? 5 : 3);
+				else
+					menu3hit();
+			}
+			mouseunblock();
+		}
+	}
+}
+
+
+void
+resize(void)
+{
+	int i;
+
+	flresize(screen->clipr);
+	for(i = 0; i<nname; i++)
+		if(text[i])
+			hcheck(text[i]->tag);
+}
+
+static void
+flunlink(Flayer *fl)
+{
+	if(fl->lnext == nil || fl->lnext == fl)
+		return;
+	fl->lnext->lprev = fl->lprev;
+	fl->lprev->lnext = fl->lnext;
+	fl->lnext = nil;
+	fl->lprev = nil;
+}
+
+static void
+fllinkhead(Flayer *fl)
+{
+	flunlink(fl);
+	fl->lnext = flru.lnext;
+	fl->lprev = &flru;
+	fl->lnext->lprev = fl;
+	fl->lprev->lnext = fl;
+}
+
+void
+warpmouse(Flayer *l)
+{
+	Point p;
+
+	if(l == nil || ptinrect(mousectl->xy, l->entire))
+		return;
+	p = addpt(screen->r.min, l->warpto);
+	if(eqpt(p, ZP) || !ptinrect(p, l->entire)){
+		p = addpt(l->entire.min, divpt(subpt(l->entire.max, l->entire.min), 2));
+		l->warpto = subpt(p, screen->r.min);
+	}
+	moveto(mousectl, p);
+}
+
+void
+current(Flayer *nw, int warp, int up)
+{
+	Text *t;
+
+	if(which){
+		flborder(which, 0);
+		if(warp && ptinrect(mousectl->xy, insetrect(which->entire, 4)))
+			which->warpto = subpt(mousectl->xy, screen->r.min);
+		flushtyping(1);
+	}
+	if(nw){
+		flupfront(nw);
+		flborder(nw, 1);
+		//buttons(Up);
+		t = (Text *)nw->user1;
+		t->front = nw-&t->l[0];
+		//i = whichmenu(t->tag);
+		//ign = i >= 0 && strstr((char *)name[i], "jam.err") != nil;
+		if(t != &cmd){
+		//	work = ign ? nil : nw;
+		//	if(up && !ign)
+			work = nw;
+			if(up)
+				fllinkhead(nw);
+		}
+		if(warp)
+			warpmouse(nw);
+	}
+	which = nw;
+}
+
+Flayer *
+cycle(int fw)
+{
+	int i;
+	Flayer *l;
+
+	if(flru.lnext == &flru){
+		for(i=0; cmd.l[i].textfn==0; i++)
+			;
+		return &cmd.l[i];
+	}
+	if(fw)
+		l = work != nil && work->lnext != nil ? work->lnext : flru.lnext;
+	else
+		l = work != nil && work->lprev != nil ? work->lprev : flru.lprev;
+	for(; l==&flru; l=fw ? l->lnext : l->lprev)
+		;
+	return l;
+}
+
+void
+closeup(Flayer *l)
+{
+	Text *t=(Text *)l->user1;
+	int m;
+
+	m = whichmenu(t->tag);
+	if(m < 0)
+		return;
+	flunlink(l);
+	flclose(l);
+	if(l == work)
+		work = 0;
+	if(--t->nwin == 0){
+		rclear(&t->rasp);
+		free((uchar *)t);
+		text[m] = 0;
+		if(l == which){
+			which = nil;
+			current(cycle(1), 1, 0);
+		}
+	}else if(l == &t->l[t->front]){
+		for(m=0; m<NL; m++)	/* find one; any one will do */
+			if(t->l[m].textfn){
+				t->front = m;
+				if(l == which){
+					which = nil;
+					current(&t->l[m], 1, 0);
+				}
+				return;
+			}
+		panic("close");
+	}
+}
+
+Flayer *
+findl(Text *t)
+{
+	int i;
+	for(i = 0; i<NL; i++)
+		if(t->l[i].textfn==0)
+			return &t->l[i];
+	return 0;
+}
+
+void
+duplicate(Flayer *l, Rectangle r, Font *f, int close)
+{
+	Text *t=(Text *)l->user1;
+	Flayer *nl = findl(t);
+	Rune *rp;
+	ulong n;
+
+	if(nl){
+		flnew(nl, gettext, l->user0, (char *)t);
+		flinit(nl, r, f, l->f.cols);
+		nl->origin = l->origin;
+		rp = (*l->textfn)(l, l->f.nchars, &n);
+		flinsert(nl, rp, rp+n, l->origin);
+		flsetselect(nl, l->p0, l->p1);
+		if(close){
+			flclose(l);
+			flunlink(l);
+			if(l==which)
+				which = 0;
+		}else
+			t->nwin++;
+		current(nl, 1, 1);
+		hcheck(t->tag);
+	}
+	setcursor(mousectl, cursor);
+}
+
+void
+buttons(int updown)
+{
+	while(((mousep->buttons&7)!=0) != updown)
+		getmouse();
+}
+
+void
+snarf(Text *t, int w)
+{
+	Flayer *l = &t->l[w];
+
+	if(l->p1>l->p0){
+		snarflen = l->p1-l->p0;
+		outTsll(Tsnarf, t->tag, l->p0, l->p1);
+	}
+}
+
+void
+cut(Text *t, int w, int save, int check)
+{
+	long p0, p1;
+	Flayer *l;
+
+	l = &t->l[w];
+	p0 = l->p0;
+	p1 = l->p1;
+	if(p0 == p1)
+		return;
+	if(p0 < 0)
+		panic("cut");
+	if(save)
+		snarf(t, w);
+	outTsll(Tcut, t->tag, p0, p1);
+	flsetselect(l, p0, p0);
+	t->lock++;
+	hcut(t->tag, p0, p1-p0);
+	if(check)
+		hcheck(t->tag);
+}
+
+void
+paste(Text *t, int w)
+{
+	cut(t, w, 0, 0);
+	t->lock++;
+	outTsl(Tpaste, t->tag, t->l[w].p0);
+}
+
+void
+scrorigin(Flayer *l, int but, long p0)
+{
+	Text *t=(Text *)l->user1;
+
+	if(t->tag == Untagged)
+		return;
+
+	switch(but){
+	case 1:
+		outTsll(Torigin, t->tag, l->origin, p0);
+		break;
+	case 2:
+		outTsll(Torigin, t->tag, p0, 1L);
+		break;
+	case 3:
+		horigin(t->tag,p0);
+	}
+}
+
+int
+alnum(int c)
+{
+	/*
+	 * 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 0;
+	if(0x7F<=c && c<=0xA0)
+		return 0;
+	if(utfrune("!\"#$%&'()*+,-./:;<=>?@[\\]^`{|}~", c))
+		return 0;
+	return 1;
+}
+
+int
+raspc(Rasp *r, long p)
+{
+	ulong n;
+	rload(r, p, p+1, &n);
+	if(n)
+		return scratch[0];
+	return 0;
+}
+
+int
+getcol(Rasp *r, long p)
+{
+	int col;
+
+	for(col = 0; p > 0 && raspc(r, p-1)!='\n'; p--, col++)
+		;
+	return col;
+}
+
+long
+del(Rasp *r, long o, long p)
+{
+	int i, col, n;
+
+	if(--p < o)
+		return o;
+	if(!spacesindent || raspc(r, p)!=' ')
+		return p;
+	col = getcol(r, p) + 1;
+	if((n = col % maxtab) == 0)
+		n = maxtab;
+	for(i = 0; p-1>=o && raspc(r, p-1)==' ' && i<n-1; --p, i++)
+		;
+	return p>=o? p : o;
+}
+
+long
+ctlw(Rasp *r, long o, long p)
+{
+	int c;
+
+	if(--p < o)
+		return o;
+	if(raspc(r, p)=='\n')
+		return p;
+	for(; p>=o && !alnum(c=raspc(r, p)); --p)
+		if(c=='\n')
+			return p+1;
+	for(; p>o && alnum(raspc(r, p-1)); --p)
+		;
+	return p>=o? p : o;
+}
+
+long
+ctlu(Rasp *r, long o, long p)
+{
+	if(--p < o)
+		return o;
+	if(raspc(r, p)=='\n')
+		return p;
+	for(; p-1>=o && raspc(r, p-1)!='\n'; --p)
+		;
+	return p>=o? p : o;
+}
+
+int
+center(Flayer *l, long a)
+{
+	Text *t;
+
+	t = l->user1;
+	if(!t->lock && (a<l->origin || l->origin+l->f.nchars<a)){
+		if(a > t->rasp.nrunes)
+			a = t->rasp.nrunes;
+		outTsll(Torigin, t->tag, a, 2L);
+		return 1;
+	}
+	return 0;
+}
+
+int
+onethird(Flayer *l, long a)
+{
+	Text *t;
+	Rectangle s;
+	long lines;
+
+	t = l->user1;
+	if(!t->lock && (a<l->origin || l->origin+l->f.nchars<a)){
+		if(a > t->rasp.nrunes)
+			a = t->rasp.nrunes;
+		s = insetrect(l->scroll, 1);
+		lines = ((s.max.y-s.min.y)/l->f.font->height+1)/3;
+		if (lines < 2)
+			lines = 2;
+		outTsll(Torigin, t->tag, a, lines);
+		return 1;
+	}
+	return 0;
+}
+
+void
+flushtyping(int clearesc)
+{
+	Text *t;
+	ulong n;
+
+	if(clearesc)
+		typeesc = -1;	
+	if(typestart == typeend) {
+		modified = 0;
+		return;
+	}
+	t = which->user1;
+	if(t != &cmd)
+		modified = 1;
+	rload(&t->rasp, typestart, typeend, &n);
+	scratch[n] = 0;
+	if(t==&cmd && typeend==t->rasp.nrunes && scratch[typeend-typestart-1]=='\n'){
+		setlock();
+		outcmd();
+	}
+	outTslS(Ttype, t->tag, typestart, scratch);
+	typestart = -1;
+	typeend = -1;
+}
+
+int
+nontypingkey(int c)
+{
+	switch(c){
+	case Kup:
+	case Kdown:
+	case Khome:
+	case Kend:
+	case Kpgdown:
+	case Kpgup:
+	case Kleft:
+	case Kright:
+	case Ksoh:
+	case Kenq:
+	case Kstx:
+	case Kbel:
+	case Ksyn:
+		return 1;
+	}
+	return 0;
+}
+
+
+void
+type(Flayer *l, int res)	/* what a bloody mess this is */
+{
+	Text *t = (Text *)l->user1;
+	Rune buf[100];
+	Rune *p = buf;
+	int c, backspacing;
+	long a, a0;
+	int scrollkey;
+
+	scrollkey = 0;
+	if(res == RKeyboard)
+		scrollkey = nontypingkey(qpeekc());	/* ICK */
+
+	if(hostlock || t->lock){
+		kbdblock();
+		return;
+	}
+	a = l->p0;
+	if(a!=l->p1 && !scrollkey){
+		flushtyping(1);
+		cut(t, t->front, 1, 1);
+		return;	/* it may now be locked */
+	}
+	backspacing = 0;
+	while((c = kbdchar())>0){
+		if(res == RKeyboard){
+			if(nontypingkey(c) || c==Kesc)
+				break;
+			/* backspace, ctrl-u, ctrl-w, del */
+			if(c==Kbs || c==Knack || c==Ketb || c==Kdel){
+				backspacing = 1;
+				break;
+			}
+		}
+		if(spacesindent && c == '\t'){
+			int i, col, n;
+			col = getcol(&t->rasp, a);
+			n = maxtab - col % maxtab;
+			for(i = 0; i < n && p < buf+nelem(buf); i++)
+				*p++ = ' ';
+		} else
+			*p++ = c;
+		if(c == '\n' && autoindent && t != &cmd){
+			/* autoindent */
+			int cursor, ch;
+			cursor = ctlu(&t->rasp, 0, a+(p-buf)-1);
+			while(p < buf+nelem(buf)){
+				ch = raspc(&t->rasp, cursor++);
+				if(ch == ' ' || ch == '\t')
+					*p++ = ch;
+				else
+					break;
+			}
+		}
+		if(c == '\n' || p >= buf+sizeof(buf)/sizeof(buf[0]))
+			break;
+	}
+	if(p > buf){
+		if(typestart < 0)
+			typestart = a;
+		if(typeesc < 0)
+			typeesc = a;
+		hgrow(t->tag, a, p-buf, 0);
+		t->lock++;	/* pretend we Trequest'ed for hdatarune*/
+		hdatarune(t->tag, a, buf, p-buf);
+		a += p-buf;
+		l->p0 = a;
+		l->p1 = a;
+		typeend = a;
+		if(c=='\n' || typeend-typestart>100)
+			flushtyping(0);
+		onethird(l, a);
+	}
+	if(c==Kdown){
+		flushtyping(0);
+		scrorigin(l, 3, l->origin+frcharofpt(&l->f, Pt(l->scroll.max.x, l->scroll.min.y + l->f.font->height)));
+	}else if(c==Kup){
+		flushtyping(0);
+		scrorigin(l, 1, 2);
+	}else if(c==Kpgdown){
+		flushtyping(0);
+		scrorigin(l, 3, l->origin+frcharofpt(&l->f, Pt(l->scroll.max.x, l->scroll.max.y - l->f.font->height)));
+		/* backspacing immediately after outcmd(): sorry */
+	}else if(c==Kpgup){
+		flushtyping(0);
+		scrorigin(l, 1, Dy(l->scroll)/l->f.font->height);
+	}else if(c == Kright){
+		flushtyping(0);
+		a0 = l->p0;
+		if(a0 < t->rasp.nrunes)
+			a0++;
+		flsetselect(l, a0, a0);
+		center(l, a0);
+	}else if(c == Kleft){
+		flushtyping(0);
+		a0 = l->p0;
+		if(a0 > 0)
+			a0--;
+		flsetselect(l, a0, a0);
+		center(l, a0);
+	}else if(c == Khome){
+		flushtyping(0);
+		center(l, 0);
+	}else if(c == Kend){
+		flushtyping(0);
+		center(l, t->rasp.nrunes);
+	}else if(c == Ksoh || c == Kenq){
+		flushtyping(1);
+		if(c == Ksoh)
+			while(a > 0 && raspc(&t->rasp, a-1)!='\n')
+				a--;
+		else
+			while(a < t->rasp.nrunes && raspc(&t->rasp, a)!='\n')
+				a++;
+		l->p0 = l->p1 = a;
+		for(l=t->l; l<&t->l[NL]; l++)
+			if(l->textfn)
+				flsetselect(l, l->p0, l->p1);
+	}else if(backspacing && !hostlock){
+		/* backspacing immediately after outcmd(): sorry */
+		if(l->f.p0>0 && a>0){
+			switch(c){
+			case Kbs:
+			case Kdel:	/* del */
+				l->p0 = del(&t->rasp, l->origin, a);
+				break;
+			case Knack:	/* ctrl-u */
+				l->p0 = ctlu(&t->rasp, l->origin, a);
+				break;
+			case Ketb:	/* ctrl-w */
+				l->p0 = ctlw(&t->rasp, l->origin, a);
+				break;
+			}
+			l->p1 = a;
+			if(l->p1 != l->p0){
+				/* cut locally if possible */
+				if(typestart<=l->p0 && l->p1<=typeend){
+					t->lock++;	/* to call hcut */
+					hcut(t->tag, l->p0, l->p1-l->p0);
+					/* hcheck is local because we know rasp is contiguous */
+					hcheck(t->tag);
+				}else{
+					flushtyping(0);
+					cut(t, t->front, 0, 1);
+				}
+			}
+			if(typeesc >= l->p0)
+				typeesc = l->p0;
+			if(typestart >= 0){
+				if(typestart >= l->p0)
+					typestart = l->p0;
+				typeend = l->p0;
+				if(typestart == typeend){
+					typestart = -1;
+					typeend = -1;
+					modified = 0;
+				}
+			}
+		}
+	}else if(c == Kstx){
+		t = &cmd;
+		for(l=t->l; l->textfn==0; l++)
+			;
+		current(l, 1, 1);
+		flushtyping(0);
+		a = t->rasp.nrunes;
+		flsetselect(l, a, a);
+		center(l, a);
+	}else if(c == Ksyn || c == Kbel){
+		int up = 1;
+		t = &cmd;
+		if(work != nil){
+			if(which != work){
+				current(work, 1, up);
+				return;
+			}
+	 		t = (Text*)work->user1;
+			l = &t->l[t->front];
+		}
+		if(t == &cmd || t->nwin == 1 && nname > 1){
+			if(flru.lnext == &flru)
+				return;
+			l = cycle(c == Kbel);
+			up = 0;
+		}else{
+	 		for(int i=t->front; (i = (i+1)%NL) != t->front; )
+	 			if(t->l[i].textfn != 0){
+	 				l = &t->l[i];
+	 				break;
+	 			}
+		}
+		current(l, 1, up);
+	}else{
+		if(c==Kesc && typeesc>=0){
+			l->p0 = typeesc;
+			l->p1 = a;
+			flushtyping(1);
+		}
+		for(l=t->l; l<&t->l[NL]; l++)
+			if(l->textfn)
+				flsetselect(l, l->p0, l->p1);
+	}
+}
+
+
+void
+outcmd(void){
+	if(work)
+		outTsll(Tworkfile, ((Text *)work->user1)->tag, work->p0, work->p1);
+}
+
+void
+panic(char *s)
+{
+	panic1(display, s);
+}
+
+void
+panic1(Display*, char *s)
+{
+	fprint(2, "jamterm: panic: ");
+	perror(s);
+	abort();
+}
+
+Rune*
+gettext(Flayer *l, long n, ulong *np)
+{
+	Text *t;
+
+	t = l->user1;
+	rload(&t->rasp, l->origin, l->origin+n, np);
+	return scratch;
+}
+
+long
+scrtotal(Flayer *l)
+{
+	return ((Text *)l->user1)->rasp.nrunes;
+}
+
+void*
+alloc(ulong n)
+{
+	void *p;
+
+	p = malloc(n);
+	if(p == 0)
+		panic("alloc");
+	memset(p, 0, n);
+	return p;
+}
--- /dev/null
+++ b/sys/src/cmd/jamterm/menu.c
@@ -1,0 +1,458 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <cursor.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <frame.h>
+#include "flayer.h"
+#include "samterm.h"
+
+uchar	**name;	/* first byte is ' ' or '\'': modified state */
+Text	**text;	/* pointer to Text associated with file */
+ushort	*tag;		/* text[i].tag, even if text[i] not defined */
+int	nname;
+int	mname;
+int	mw;
+
+char	*genmenu3(int);
+char	*genmenu2(int);
+char	*genmenu2c(int);
+
+enum Menu2
+{
+	Cut,
+	Paste,
+	Snarf,
+	Plumb,
+	Look,
+	Search,
+};
+
+enum Menu3
+{
+	New,
+	Zerox,
+	Resize,
+	Close,
+	Write,
+	NMENU3
+};
+
+char	*menu2str[] = {
+	"cut",
+	"paste",
+	"snarf",
+	"plumb",
+	"look",
+	nil,		/* storage for last pattern */
+};
+
+int	ncmd;
+int	ncbuf;
+char	**cmds;
+char	**clabels;
+
+char	*menu3str[] = {
+	"new",
+	"zerox",
+	"resize",
+	"close",
+	"write",
+};
+
+Menu	menu2 =	{0, genmenu2};
+Menu	menu2c ={0, genmenu2c};
+Menu	menu3 =	{0, genmenu3};
+
+extern int kekfd[2];
+
+typedef struct Menucmd Menucmd;
+struct Menucmd{
+	char *cmd;
+	Menucmd *next;
+}*menucmds;
+
+char*
+findmenucmd(int n){
+	Menucmd *m;
+
+	for(m = menucmds; n > 0 && m != nil; n--)
+		m = m->next;
+	if(n == 0 && m != nil)
+		return m->cmd;
+	return nil;
+}
+
+void
+menucmdhit(char *s)
+{
+	if(s == nil)
+		return;
+	outstart(Tmenucmdsend);
+	outcopy(strlen(s), (uchar*)s);
+	outsend();
+}
+
+void
+menu2hit(void)
+{
+	Text *t=(Text *)which->user1;
+	int w = which-t->l;
+	int m;
+
+	if(hversion==0 || plumbfd<0)
+		menu2str[Plumb] = "(plumb)";
+	m = menuhit(2, mousectl, t==&cmd? &menu2c : &menu2, nil);
+	if(hostlock || t->lock)
+		return;
+
+	switch(m){
+	case Cut:
+		cut(t, w, 1, 1);
+		break;
+
+	case Paste:
+		paste(t, w);
+		break;
+
+	case Snarf:
+		snarf(t, w);
+		break;
+
+	case Plumb:
+		if(hversion > 0)
+			outTsll(Tplumb, t->tag, which->p0, which->p1);
+		break;
+
+	case Look:
+		outTsll(Tlook, t->tag, which->p0, which->p1);
+		setlock();
+		break;
+
+	case Search:
+		if(t == &cmd || menu2str[Search] != nil){
+			outcmd();
+			if(t == &cmd)
+				outTsll(Tsend, 0 /*ignored*/, which->p0, which->p1);
+			else
+				outT0(Tsearch);
+			setlock();
+			break;
+		}
+	default:
+		m -= Search+(menu2str[Search] != nil);
+		menucmdhit(findmenucmd(m));
+		break;
+	}
+}
+
+void
+menu3hit(void)
+{
+	Rectangle r;
+	Flayer *l;
+	int m, i;
+	Text *t;
+
+	l = which;
+	mw = -1;
+	if(l != nil){
+		t = (Text *)l->user1;
+		menu3.lasthit = whichmenu(t->tag) + NMENU3;
+	}
+	switch(m = menuhit(3, mousectl, &menu3, nil)){
+	case -1:
+		break;
+
+	case New:
+		if(!hostlock)
+			sweeptext(1, 0);
+		break;
+
+	case Zerox:
+	case Resize:
+		if(hostlock || l == nil)
+			break;
+		if(promptrect(&r, l, m != Resize))
+			duplicate(l, r, l->f.font, m == Resize);
+		break;
+
+	case Close:
+		if(hostlock || l == nil)
+			break;
+		t=(Text *)l->user1;
+		if (t->nwin>1)
+			closeup(l);
+		else if(t!=&cmd) {
+			outTs(Tclose, t->tag);
+			setlock();
+		}
+		break;
+
+	case Write:
+		if(hostlock || l == nil)
+			break;
+		outTs(Twrite, ((Text *)l->user1)->tag);
+		setlock();
+		break;
+
+	default:
+		if(t = text[m-NMENU3]){
+			i = t->front;
+			if(t->nwin==0 || t->l[i].textfn==0)
+				return;	/* not ready yet; try again later */
+			if(t->nwin>1 && which==&t->l[i])
+				do
+					if(++i==NL)
+						i = 0;
+				while(i!=t->front && t->l[i].textfn==0);
+			if(&t->l[i] != which)
+				current(&t->l[i], 1, 1);
+		}else if(!hostlock)
+			sweeptext(0, tag[m-NMENU3]);
+		break;
+	}
+}
+
+Text *
+sweeptext(int new, int tag)
+{
+	Rectangle r;
+	Text *t;
+
+	if((t = mallocz(sizeof(*t), 1)) == nil)
+		return nil;
+	if(!promptrect(&r, nil, new))
+		return nil;
+	current((Flayer *)0, 0, 0);
+	flnew(&t->l[0], gettext, 0, (char *)t);
+	flinit(&t->l[0], r, font, maincols);	/*bnl*/
+	t->nwin = 1;
+	rinit(&t->rasp);
+	if(new)
+		startnewfile(Tstartnewfile, t);
+	else{
+		rinit(&t->rasp);
+		t->tag = tag;
+		startfile(t);
+	}
+	return t;
+}
+
+int
+whichmenu(int tg)
+{
+	int i;
+
+	for(i=0; i<nname; i++)
+		if(tag[i] == tg)
+			return i;
+	return -1;
+}
+
+void
+menuins(int n, uchar *s, Text *t, int m, int tg)
+{
+	int i;
+
+	if(nname == mname){
+		if(mname == 0)
+			mname = 32;
+		else
+			mname *= 2;
+		name = realloc(name, sizeof(name[0])*mname);
+		text = realloc(text, sizeof(text[0])*mname);
+		tag = realloc(tag, sizeof(tag[0])*mname);
+		if(name==nil || text==nil || tag==nil)
+			panic("realloc");
+	}
+	for(i=nname; i>n; --i)
+		name[i]=name[i-1], text[i]=text[i-1], tag[i]=tag[i-1];
+	text[n] = t;
+	tag[n] = tg;
+	name[n] = alloc(strlen((char*)s)+2);
+	name[n][0] = m;
+	strcpy((char*)name[n]+1, (char*)s);
+	nname++;
+	menu3.lasthit = n+NMENU3;
+}
+
+void
+menudel(int n)
+{
+	int i;
+
+	if(nname==0 || n>=nname || text[n])
+		panic("menudel");
+	free(name[n]);
+	--nname;
+	for(i = n; i<nname; i++)
+		name[i]=name[i+1], text[i]=text[i+1], tag[i]=tag[i+1];
+}
+
+void
+setpat(char *s)
+{
+	static char pat[17];
+
+	pat[0] = '/';
+	strncpy(pat+1, s, 15);
+	menu2str[Search] = pat;
+}
+
+void
+menucmd(char *s)
+{
+	Menucmd **mp, *m;
+
+	while(*s == ' ' || *s == '\t')
+		s++;
+	if(*s == 0){
+		outstart(Tmenucmd);
+		for(m = menucmds; m != nil; m = m->next){
+			outcopy(3, (uchar*)"\tM ");
+			outcopy(strlen(m->cmd), (uchar*)m->cmd);
+			outcopy(1, (uchar*)"\n");
+		}
+		outsend();
+		return;
+	}
+	for(mp = &menucmds; *mp != nil; mp = &(*mp)->next)
+		if(!strcmp(s, (*mp)->cmd)){
+			m = *mp;
+			*mp = m->next;
+			free(m->cmd);
+			free(m);
+			return;
+		}
+	*mp = m = malloc(sizeof(Menucmd));
+	if(m == nil) panic("malloc");
+	m->cmd = strdup(s);
+	m->next = nil;
+}
+
+#define	NBUF	64
+static uchar buf[NBUF*UTFmax]={' ', ' ', ' ', ' '};
+
+char *
+paren(char *s)
+{
+	uchar *t = buf;
+
+	*t++ = '(';
+	do; while(*t++ = *s++);
+	t[-1] = ')';
+	*t = 0;
+	return (char *)buf;
+}
+
+char*
+genmenu2(int n)
+{
+	Text *t=(Text *)which->user1;
+	char *p;
+	if(n < Search || n == Search && (menu2str[Search] != nil))
+		p = menu2str[n];
+	else{
+		n -= Search + (menu2str[Search] != nil);
+		p = findmenucmd(n);
+		if(p == nil)
+			return nil;
+	}
+	if(!hostlock && !t->lock
+	|| p == menu2str[Search]
+	|| p == menu2str[Look])
+		return p;
+	return paren(p);
+}
+char*
+genmenu2c(int n)
+{
+	Text *t=(Text *)which->user1;
+	char *p;
+	if(n < Search)
+		p = menu2str[n];
+	else if(n == Search)
+		p = "send";
+	else if((p = findmenucmd(n - Search-1)) == nil)
+		return nil;
+	if(!hostlock && !t->lock)
+		return p;
+	return paren(p);
+}
+char *
+genmenu3(int n)
+{
+	Text *t;
+	int c, i, k, l, w;
+	Rune r;
+	char *p;
+
+	if(n >= NMENU3+nname)
+		return 0;
+	if(n < NMENU3){
+		p = menu3str[n];
+		if(hostlock)
+			p = paren(p);
+		return p;
+	}
+	n -= NMENU3;
+	if(n == 0)	/* unless we've been fooled, this is cmd */
+		return (char *)&name[n][1];
+	if(mw == -1){
+		mw = 7;	/* strlen("~~jam~~"); */
+		for(i=1; i<nname; i++){
+			w = utflen((char*)name[i]+1)+4;	/* include "'+. " */
+			if(w > mw)
+				mw = w;
+		}
+	}
+	if(mw > NBUF)
+		mw = NBUF;
+	t = text[n];
+	buf[0] = name[n][0];
+	buf[1] = '-';
+	buf[2] = ' ';
+	buf[3] = ' ';
+	if(t){
+		if(t->nwin == 1)
+			buf[1] = '+';
+		else if(t->nwin > 1)
+			buf[1] = '*';
+		if(work && t==(Text *)work->user1) {
+			buf[2]= '.';
+			if(modified)
+				buf[0] = '\'';
+		}
+	}
+	l = utflen((char*)name[n]+1);
+	if(l > NBUF-4-2){
+		i = 4;
+		k = 1;
+		while(i < NBUF/2){
+			k += chartorune(&r, (char*)name[n]+k);
+			i++;
+		}
+		c = name[n][k];
+		name[n][k] = 0;
+		strcpy((char*)buf+4, (char*)name[n]+1);
+		name[n][k] = c;
+		strcat((char*)buf, "...");
+		while((l-i) >= NBUF/2-4){
+			k += chartorune(&r, (char*)name[n]+k);
+			i++;
+		}
+		strcat((char*)buf, (char*)name[n]+k);
+	}else
+		strcpy((char*)buf+4, (char*)name[n]+1);
+	i = utflen((char*)buf);
+	k = strlen((char*)buf);
+	while(i<mw && k<sizeof buf-1){
+		buf[k++] = ' ';
+		i++;
+	}
+	buf[k] = 0;
+	return (char *)buf;
+}
--- /dev/null
+++ b/sys/src/cmd/jamterm/mesg.c
@@ -1,0 +1,806 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <cursor.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <frame.h>
+#include <plumb.h>
+#include "flayer.h"
+#include "samterm.h"
+
+#define	HSIZE	3	/* Type + short count */
+Header	h;
+uchar	indata[DATASIZE+1];	/* room for NUL */
+uchar	outdata[DATASIZE];
+short	outcount;
+int	hversion;
+int	exiting;
+
+void	inmesg(Hmesg, int);
+int	inshort(int);
+long	inlong(int);
+vlong	invlong(int);
+void	hsetdot(int, long, long);
+void	hmoveto(int, long);
+void	hsetsnarf(int);
+void	hplumb(int);
+void	clrlock(void);
+int	snarfswap(char*, int, char**);
+
+void
+rcv(void)
+{
+	int c;
+	static state = 0;
+	static count = 0;
+	static i = 0;
+	static int errs = 0;
+
+	while((c=rcvchar()) != -1)
+		switch(state){
+		case 0:
+			h.type = c;
+			state++;
+			break;
+
+		case 1:
+			h.count0 = c;
+			state++;
+			break;
+
+		case 2:
+			h.count1 = c;
+			count = h.count0|(h.count1<<8);
+			i = 0;
+			if(count > DATASIZE){
+				if(++errs < 5){
+					dumperrmsg(count, h.type, h.count0, c);
+					state = 0;
+					continue;
+				}
+				fprint(2, "type %d count %d\n", h.type, count);
+				panic("count>DATASIZE");
+			}
+			if(count == 0)
+				goto zerocount;
+			state++;
+			break;
+
+		case 3:
+			indata[i++] = c;
+			if(i == count){
+		zerocount:
+				indata[i] = 0;
+				inmesg(h.type, count);
+				state = count = 0;
+				continue;
+			}
+			break;
+		}
+}
+
+Text *
+whichtext(int tg)
+{
+	int i;
+
+	for(i=0; i<nname; i++)
+		if(tag[i] == tg)
+			return text[i];
+	panic("whichtext");
+	return 0;
+}
+
+void
+inmesg(Hmesg type, int count)
+{
+	Text *t;
+	int i, m;
+	long l;
+	vlong vl;
+	Flayer *lp;
+
+	m = inshort(0);
+	l = inlong(2);
+	switch(type){
+	case -1:
+		panic("rcv error");
+	default:
+		fprint(2, "type %d\n", type);
+		panic("rcv unknown");
+
+	case Hversion:
+		hversion = m;
+		break;
+
+	case Hbindname:
+		vl = invlong(2);		/* for 64-bit pointers */
+		if((i=whichmenu(m)) < 0)
+			break;
+		/* in case of a race, a bindname may already have occurred */
+		if((t=whichtext(m)) == 0)
+			t=(Text *)vl;
+		else	/* let the old one win; clean up the new one */
+			while(((Text *)vl)->nwin>0)
+				closeup(&((Text *)vl)->l[((Text *)vl)->front]);
+		text[i] = t;
+		text[i]->tag = m;
+		break;
+
+	case Hcurrent:
+		if(whichmenu(m) < 0)
+			break;
+		if((t = whichtext(m)) == nil
+		&& (t = sweeptext(0, m)) == nil)
+			break;
+		if(t->l[t->front].textfn == nil)
+			panic("Hcurrent");
+		lp = &t->l[t->front];
+		if(which != nil && ((Text *)which->user1) == &cmd && m != cmd.tag){
+			flupfront(lp);
+			flborder(lp, 0);
+			work = lp;
+		}else
+			current(lp, 1, 1);
+		break;
+
+	case Hmovname:
+		if((m=whichmenu(m)) < 0)
+			break;
+		t = text[m];
+		l = tag[m];
+		i = name[m][0];
+		text[m] = 0;	/* suppress panic in menudel */
+		menudel(m);
+		if(t == &cmd)
+			m = 0;
+		else{
+			if (nname>0 && text[0]==&cmd)
+				m = 1;
+			else m = 0;
+			for(; m<nname; m++)
+				if(strcmp((char*)indata+2, (char*)name[m]+1)<0)
+					break;
+		}
+		menuins(m, indata+2, t, i, (int)l);
+		break;
+
+	case Hgrow:
+		if(whichmenu(m) >= 0)
+			hgrow(m, l, inlong(6), 1);
+		break;
+
+	case Hnewname:
+		menuins(0, (uchar *)"", (Text *)0, ' ', m);
+		break;
+
+	case Hcheck0:
+		i = whichmenu(m);
+		if(i>=0) {
+			t = text[i];
+			if(t)
+				t->lock++;
+			outTs(Tcheck, m);
+		}
+		break;
+
+	case Hcheck:
+		i = whichmenu(m);
+		if(i>=0) {
+			t = text[i];
+			if(t && t->lock)
+				t->lock--;
+			hcheck(m);
+		}
+		break;
+
+	case Hunlock:
+		clrlock();
+		break;
+
+	case Hdata:
+		if(whichmenu(m) >= 0)
+			l += hdata(m, l, indata+6, count-6);
+	Checkscroll:
+		if(m == cmd.tag){
+			for(i=0; i<NL; i++){
+				lp = &cmd.l[i];
+				if(lp->textfn)
+					center(lp, l>=0? l : lp->p1);
+			}
+		}
+		break;
+
+	case Horigin:
+		if(whichmenu(m) >= 0)
+			horigin(m, l);
+		break;
+
+	case Hunlockfile:
+		if(whichmenu(m)>=0 && (t = whichtext(m))->lock){
+			--t->lock;
+			l = -1;
+			goto Checkscroll;
+		}
+		break;
+
+	case Hsetdot:
+		if(whichmenu(m) >= 0)
+			hsetdot(m, l, inlong(6));
+		break;
+
+	case Hgrowdata:
+		if(whichmenu(m)<0)
+			break;
+		hgrow(m, l, inlong(6), 0);
+		whichtext(m)->lock++;	/* fake the request */
+		l += hdata(m, l, indata+10, count-10);
+		goto Checkscroll;
+
+	case Hmoveto:
+		if(whichmenu(m)>=0)
+			hmoveto(m, l);
+		break;
+
+	case Hclean:
+		if((m = whichmenu(m)) >= 0)
+			name[m][0] = ' ';
+		break;
+
+	case Hdirty:
+		if((m = whichmenu(m))>=0)
+			name[m][0] = '\'';
+		break;
+
+	case Hdelname:
+		if((m=whichmenu(m)) >= 0)
+			menudel(m);
+		break;
+
+	case Hcut:
+		if(whichmenu(m) >= 0)
+			hcut(m, l, inlong(6));
+		break;
+
+	case Hclose:
+		if(whichmenu(m)<0 || (t = whichtext(m))==0)
+			break;
+		l = t->nwin;
+		for(i = 0,lp = t->l; l>0 && i<NL; i++,lp++)
+			if(lp->textfn){
+				closeup(lp);
+				--l;
+			}
+		break;
+
+	case Hsetpat:
+		setpat((char *)indata);
+		break;
+
+	case Hsetsnarf:
+		hsetsnarf(m);
+		break;
+
+	case Hsnarflen:
+		snarflen = inlong(0);
+		break;
+
+	case Hack:
+		outT0(Tack);
+		break;
+
+	case Hexit:
+		exiting = 1;
+		outT0(Texit);
+		threadexitsall(nil);
+		break;
+
+	case Hplumb:
+		hplumb(m);
+		break;
+
+	case Hmenucmd:
+		menucmd((char *)indata);
+		break;
+	}
+}
+
+void
+setlock(void)
+{
+	hostlock++;
+	setcursor(mousectl, cursor = &lockarrow);
+}
+
+void
+clrlock(void)
+{
+	hasunlocked = 1;
+	if(hostlock > 0)
+		hostlock--;
+	if(hostlock == 0)
+		setcursor(mousectl, cursor=(Cursor *)0);
+}
+
+void
+startfile(Text *t)
+{
+	outTsv(Tstartfile, t->tag, (vlong)t);	/* for 64-bit pointers */
+	setlock();
+}
+
+void
+startnewfile(int type, Text *t)
+{
+	t->tag = Untagged;
+	outTv(type, (vlong)t);			/* for 64-bit pointers */
+}
+
+int
+inshort(int n)
+{
+	return indata[n]|(indata[n+1]<<8);
+}
+
+long
+inlong(int n)
+{
+	return indata[n]|(indata[n+1]<<8)|
+		((long)indata[n+2]<<16)|((long)indata[n+3]<<24);
+}
+
+vlong
+invlong(int n)
+{
+	vlong v;
+
+	v = (indata[n+7]<<24) | (indata[n+6]<<16) | (indata[n+5]<<8) | indata[n+4];
+	v = (v<<16) | (indata[n+3]<<8) | indata[n+2];
+	v = (v<<16) | (indata[n+1]<<8) | indata[n];
+	return v;
+}
+
+void
+outT0(Tmesg type)
+{
+	outstart(type);
+	outsend();
+}
+
+void
+outTl(Tmesg type, long l)
+{
+	outstart(type);
+	outlong(l);
+	outsend();
+}
+
+void
+outTs(Tmesg type, int s)
+{
+	outstart(type);
+	outshort(s);
+	outsend();
+}
+
+void
+outTss(Tmesg type, int s1, int s2)
+{
+	outstart(type);
+	outshort(s1);
+	outshort(s2);
+	outsend();
+}
+
+void
+outTsll(Tmesg type, int s1, long l1, long l2)
+{
+	outstart(type);
+	outshort(s1);
+	outlong(l1);
+	outlong(l2);
+	outsend();
+}
+
+void
+outTsl(Tmesg type, int s1, long l1)
+{
+	outstart(type);
+	outshort(s1);
+	outlong(l1);
+	outsend();
+}
+
+void
+outTsv(Tmesg type, int s1, vlong v1)
+{
+	outstart(type);
+	outshort(s1);
+	outvlong(v1);
+	outsend();
+}
+
+void
+outTv(Tmesg type, vlong v1)
+{
+	outstart(type);
+	outvlong(v1);
+	outsend();
+}
+
+void
+outTslS(Tmesg type, int s1, long l1, Rune *s)
+{
+	char buf[DATASIZE*UTFmax+1];
+	char *c;
+
+	outstart(type);
+	outshort(s1);
+	outlong(l1);
+	c = buf;
+	while(*s)
+		c += runetochar(c, s++);
+	*c++ = 0;
+	outcopy(c-buf, (uchar *)buf);
+	outsend();
+}
+
+void
+outTsls(Tmesg type, int s1, long l1, int s2)
+{
+	outstart(type);
+	outshort(s1);
+	outlong(l1);
+	outshort(s2);
+	outsend();
+}
+
+void
+outstart(Tmesg type)
+{
+	outdata[0] = type;
+	outcount = 0;
+}
+
+void
+outcopy(int count, uchar *data)
+{
+	while(count--)
+		outdata[HSIZE+outcount++] = *data++;	
+}
+
+void
+outshort(int s)
+{
+	uchar buf[2];
+
+	buf[0]=s;
+	buf[1]=s>>8;
+	outcopy(2, buf);
+}
+
+void
+outlong(long l)
+{
+	uchar buf[4];
+
+	buf[0]=l;
+	buf[1]=l>>8;
+	buf[2]=l>>16;
+	buf[3]=l>>24;
+	outcopy(4, buf);
+}
+
+void
+outvlong(vlong v)
+{
+	int i;
+	uchar buf[8];
+
+	for(i = 0; i < sizeof(buf); i++){
+		buf[i] = v;
+		v >>= 8;
+	}
+
+	outcopy(8, buf);
+}
+
+void
+outsend(void)
+{
+	if(outcount>DATASIZE-HSIZE)
+		panic("outcount>sizeof outdata");
+	outdata[1]=outcount;
+	outdata[2]=outcount>>8;
+	if(write(1, (char *)outdata, outcount+HSIZE)!=outcount+HSIZE)
+		panic("write error");
+}
+
+
+void
+hsetdot(int m, long p0, long p1)
+{
+	Text *t = whichtext(m);
+	Flayer *l = &t->l[t->front];
+
+	flushtyping(1);
+	flsetselect(l, p0, p1);
+}
+
+void
+horigin(int m, long p0)
+{
+	Text *t = whichtext(m);
+	Flayer *l = &t->l[t->front];
+	long a;
+	ulong n;
+	Rune *r;
+
+	if(!flprepare(l)){
+		l->origin = p0;
+		return;
+	}
+	a = p0-l->origin;
+	if(a>=0 && a<l->f.nchars)
+		frdelete(&l->f, 0, a);
+	else if(a<0 && -a<l->f.nchars){
+		r = rload(&t->rasp, p0, l->origin, &n);
+		frinsert(&l->f, r, r+n, 0);
+	}else
+		frdelete(&l->f, 0, l->f.nchars);
+	l->origin = p0;
+	scrdraw(l, t->rasp.nrunes);
+	if(l->visible==Some)
+		flrefresh(l, l->entire, 0);
+	hcheck(m);
+}
+
+void
+hmoveto(int m, long p0)
+{
+	Text *t = whichtext(m);
+	Flayer *l = &t->l[t->front];
+
+	if(p0<l->origin || p0-l->origin>l->f.nchars*9/10)
+		outTsll(Torigin, m, p0, 2L);
+}
+
+void
+hcheck(int m)
+{
+	Flayer *l;
+	Text *t;
+	int reqd = 0, i;
+	long n, nl, a;
+	Rune *r;
+
+	if(m == Untagged)
+		return;
+	t = whichtext(m);
+	if(t == 0)		/* possible in a half-built window */
+		return;
+	for(l = &t->l[0], i = 0; i<NL; i++, l++){
+		if(l->textfn==0 || !flprepare(l))	/* BUG: don't
+							   need this if BUG below
+							   is fixed */
+			continue;
+		a = t->l[i].origin;
+		n = rcontig(&t->rasp, a, a+l->f.nchars, 1);
+		if(n<l->f.nchars)	/* text missing in middle of screen */
+			a+=n;
+		else{			/* text missing at end of screen? */
+        Again:
+		 	if(l->f.lastlinefull)
+				goto Checksel;	/* all's well */
+			a = t->l[i].origin+l->f.nchars;
+			n = t->rasp.nrunes-a;
+			if(n==0)
+				goto Checksel;
+			if(n>TBLOCKSIZE)
+				n = TBLOCKSIZE;
+			n = rcontig(&t->rasp, a, a+n, 1);
+			if(n>0){
+				rload(&t->rasp, a, a+n, 0);
+				nl = l->f.nchars;
+				r = scratch;
+				flinsert(l, r, r+n, l->origin+nl);
+				if(nl == l->f.nchars)	/* made no progress */
+					goto Checksel;
+				goto Again;
+			}
+		}
+		if(!reqd){
+			n = rcontig(&t->rasp, a, a+TBLOCKSIZE, 0);
+			if(n <= 0)
+				panic("hcheck request==0");
+			outTsls(Trequest, m, a, (int)n);
+			outTs(Tcheck, m);
+			t->lock++;	/* for the Trequest */
+			t->lock++;	/* for the Tcheck */
+			reqd++;
+		}
+	    Checksel:
+		flsetselect(l, l->p0, l->p1);
+	}
+}
+
+void
+flnewlyvisible(Flayer *l)
+{
+	hcheck(((Text *)l->user1)->tag);
+}
+
+void
+hsetsnarf(int nc)
+{
+	char *s2;
+	char *s1;
+	int i;
+	int n;
+
+	setcursor(mousectl, &deadmouse);
+	s2 = alloc(nc+1);
+	for(i=0; i<nc; i++)
+		s2[i] = getch();
+	s2[nc] = 0;
+	n = snarfswap(s2, nc, &s1);
+	if(n >= 0){
+		if(!s1)
+			n = 0;
+		s1 = realloc(s1, n+1);
+		if (!s1)
+			panic("realloc");
+		s1[n] = 0;
+		snarflen = n;
+		outTs(Tsetsnarf, n);
+		if(n>0 && write(1, s1, n)!=n)
+			panic("snarf write error");
+		free(s1);
+	}else
+		outTs(Tsetsnarf, 0);
+	free(s2);
+	setcursor(mousectl, cursor);
+}
+
+void
+hplumb(int nc)
+{
+	int i;
+	char *s;
+	Plumbmsg *m;
+
+	s = alloc(nc);
+	for(i=0; i<nc; i++)
+		s[i] = getch();
+	if(plumbfd >= 0){
+		m = plumbunpack(s, nc);
+		if(m != 0)
+			plumbsend(plumbfd, m);
+		plumbfree(m);
+	}
+	free(s);
+}
+
+void
+hgrow(int m, long a, long new, int req)
+{
+	int i;
+	Flayer *l;
+	Text *t = whichtext(m);
+	long o, b;
+
+	if(new <= 0)
+		panic("hgrow");
+	rresize(&t->rasp, a, 0L, new);
+	for(l = &t->l[0], i = 0; i<NL; i++, l++){
+		if(l->textfn == 0)
+			continue;
+		o = l->origin;
+		b = a-o-rmissing(&t->rasp, o, a);
+		if(a < o)
+			l->origin+=new;
+		if(a < l->p0)
+			l->p0+=new;
+		if(a < l->p1)
+			l->p1+=new;
+		/* must prevent b temporarily becoming unsigned */
+		if(!req || a<o || (b>0 && b>l->f.nchars) ||
+		    (l->f.nchars==0 && a-o>0))
+			continue;
+		if(new>TBLOCKSIZE)
+			new = TBLOCKSIZE;
+		outTsls(Trequest, m, a, (int)new);
+		t->lock++;
+		req = 0;
+	}
+}
+
+int
+hdata1(Text *t, long a, Rune *r, int len)
+{
+	int i;
+	Flayer *l;
+	long o, b;
+
+	for(l = &t->l[0], i=0; i<NL; i++, l++){
+		if(l->textfn==0)
+			continue;
+		o = l->origin;
+		b = a-o-rmissing(&t->rasp, o, a);
+		/* must prevent b temporarily becoming unsigned */
+		if(a<o || (b>0 && b>l->f.nchars))
+			continue;
+		flinsert(l, r, r+len, o+b);
+	}
+	rdata(&t->rasp, a, a+len, r);
+	rclean(&t->rasp);
+	return len;
+}
+
+int
+hdata(int m, long a, uchar *s, int len)
+{
+	int i, w;
+	Text *t = whichtext(m);
+	Rune buf[DATASIZE], *r;
+
+	if(t->lock)
+		--t->lock;
+	if(len == 0)
+		return 0;
+	r = buf;
+	for(i=0; i<len; i+=w,s+=w)
+		w = chartorune(r++, (char*)s);
+	return hdata1(t, a, buf, r-buf);
+}
+
+int
+hdatarune(int m, long a, Rune *r, int len)
+{
+	Text *t = whichtext(m);
+
+	if(t->lock)
+		--t->lock;
+	if(len == 0)
+		return 0;
+	return hdata1(t, a, r, len);
+}
+
+void
+hcut(int m, long a, long old)
+{
+	Flayer *l;
+	Text *t = whichtext(m);
+	int i;
+	long o, b;
+
+	if(t->lock)
+		--t->lock;
+	for(l = &t->l[0], i = 0; i<NL; i++, l++){
+		if(l->textfn == 0)
+			continue;
+		o = l->origin;
+		b = a-o-rmissing(&t->rasp, o, a);
+		/* must prevent b temporarily becoming unsigned */
+		if((b<0 || b<l->f.nchars) && a+old>=o){
+			fldelete(l, b<0? o : o+b,
+			    a+old-rmissing(&t->rasp, o, a+old));
+		}
+		if(a+old<o)
+			l->origin-=old;
+		else if(a<=o)
+			l->origin = a;
+		if(a+old<l->p0)
+			l->p0-=old;
+		else if(a<=l->p0)
+			l->p0 = a;
+		if(a+old<l->p1)
+			l->p1-=old;
+		else if(a<=l->p1)
+			l->p1 = a;
+	}
+	rresize(&t->rasp, a, old, 0L);
+	rclean(&t->rasp);
+}
--- /dev/null
+++ b/sys/src/cmd/jamterm/mkfile
@@ -1,0 +1,37 @@
+</$objtype/mkfile
+
+TARG=jamterm
+OFILES=main.$O\
+	icons.$O\
+	menu.$O\
+	mesg.$O\
+	rasp.$O\
+	scroll.$O\
+	flayer.$O\
+	io.$O\
+	plan9.$O\
+	win.$O\
+
+HFILES=samterm.h\
+	flayer.h\
+	/sys/include/frame.h\
+
+LIB=	/$objtype/lib/libdraw.a\
+	/$objtype/lib/libframe.a\
+
+BIN=/$objtype/bin/aux
+
+UPDATE=\
+	mkfile\
+	$HFILES\
+	${OFILES:%.$O=%.c}\
+
+</sys/src/cmd/mkone
+
+CFLAGS=-I../jam $CFLAGS
+
+mesg.$O:	../jam/mesg.h
+
+syms:V:
+	$CC -a $CFLAGS main.c		 > syms
+	for(i in *.c) $CC -aa $CFLAGS $i >> syms
--- /dev/null
+++ b/sys/src/cmd/jamterm/plan9.c
@@ -1,0 +1,278 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <frame.h>
+#include <plumb.h>
+#include "flayer.h"
+#include "samterm.h"
+
+enum {
+	STACK = 4096,
+};
+
+static char exname[64];
+
+void
+usage(void)
+{
+	fprint(2, "usage: jamterm [-Ai]\n");
+	threadexitsall("usage");
+}
+
+void
+getscreen(int argc, char **argv)
+{
+	char *t;
+
+	ARGBEGIN{
+	case 'A':
+		autoindent = 0;
+		break;
+	case 'i':
+		spacesindent = 1;
+		break;
+	default:
+		usage();
+	}ARGEND
+
+	if(initdraw(panic1, nil, "jam") < 0){
+		fprint(2, "jamterm: initdraw: %r\n");
+		threadexitsall("init");
+	}
+	t = getenv("tabstop");
+	if(t != nil)
+		maxtab = strtoul(t, nil, 0);
+	free(t);
+	flstart(screen->clipr);
+	draw(screen, screen->clipr, maincols[BACK], nil, ZP);
+}
+
+int
+screensize(int *w, int *h)
+{
+	int fd, n;
+	char buf[5*12+1];
+
+	fd = open("/dev/screen", OREAD);
+	if(fd < 0)
+		return 0;
+	n = read(fd, buf, sizeof(buf)-1);
+	close(fd);
+	if (n != sizeof(buf)-1)
+		return 0;
+	buf[n] = 0;
+	if (h) {
+		*h = atoi(buf+4*12)-atoi(buf+2*12);
+		if (*h < 0)
+			return 0;
+	}
+	if (w) {
+		*w = atoi(buf+3*12)-atoi(buf+1*12);
+		if (*w < 0)
+			return 0;
+	}
+	return 1;
+}
+
+int
+snarfswap(char *fromsam, int nc, char **tosam)
+{
+	char *s1;
+	int f, n, ss;
+
+	f = open("/dev/snarf", 0);
+	if(f < 0)
+		return -1;
+	ss = SNARFSIZE;
+	if(hversion < 2)
+		ss = 4096;
+	*tosam = s1 = alloc(ss);
+	n = read(f, s1, ss-1);
+	close(f);
+	if(n < 0)
+		n = 0;
+	if (n == 0) {
+		*tosam = 0;
+		free(s1);
+	} else
+		s1[n] = 0;
+	f = create("/dev/snarf", 1, 0666);
+	if(f >= 0){
+		write(f, fromsam, nc);
+		close(f);
+	}
+	return n;
+}
+
+void
+dumperrmsg(int count, int type, int count0, int c)
+{
+	fprint(2, "jamterm: host mesg: count %d %ux %ux %ux %s...ignored\n",
+		count, type, count0, c, rcvstring());
+}
+
+void
+removeextern(void)
+{
+	remove(exname);
+}
+
+Readbuf	hostbuf[2];
+Readbuf	plumbbuf[4];
+
+int kekfd[2];
+
+void
+extproc(void *chan)
+{
+	Channel *c;
+	int i, n, which;
+
+	c = chan;
+	close(kekfd[1]);
+	i = 3;
+	for(;;){
+		i = i == 2 ? 3 : 2;	/* toggle */
+		n = read(kekfd[0], plumbbuf[i].data, sizeof plumbbuf[i].data);
+		if(n <= 0){
+			fprint(2, "jamterm: extern read error: %r\n");
+			threadexits("extern");	/* not a fatal error */
+		}
+		plumbbuf[i].n = n;
+		which = i;
+		send(c, &which);
+	}
+}
+
+int
+plumbformat(int i)
+{
+	Plumbmsg *m;
+	char *addr, *data, *act;
+	int n;
+
+	data = (char*)plumbbuf[i].data;
+	m = plumbunpack(data, plumbbuf[i].n);
+	if(m == nil)
+		return 0;
+	n = m->ndata;
+	if(n == 0){
+		plumbfree(m);
+		return 0;
+	}
+	act = plumblookup(m->attr, "action");
+	if(act!=nil && strcmp(act, "showfile")!=0){
+		/* can't handle other cases yet */
+		plumbfree(m);
+		return 0;
+	}
+	addr = plumblookup(m->attr, "addr");
+	if(addr){
+		if(addr[0] == '\0')
+			addr = nil;
+		else
+			addr = strdup(addr);	/* copy to safe storage; we'll overwrite data */
+	}
+	memmove(data, "B ", 2);	/* we know there's enough room for this */
+	memmove(data+2, m->data, n);
+	n += 2;
+	if(data[n-1] != '\n')
+		data[n++] = '\n';
+	if(addr != nil){
+		if(n+strlen(addr)+1+1 <= READBUFSIZE)
+			n += sprint(data+n, "%s\n", addr);
+		free(addr);
+	}
+	plumbbuf[i].n = n;
+	plumbfree(m);
+	return 1;
+}
+
+void
+plumbproc(void *argv)
+{
+	Channel *c;
+	int i, n, which, *fdp;
+	void **arg;
+
+	arg = argv;
+	c = arg[0];
+	fdp = arg[1];
+
+	i = 0;
+	for(;;){
+		i = 1-i;	/* toggle */
+		n = read(*fdp, plumbbuf[i].data, READBUFSIZE);
+		if(n <= 0){
+			fprint(2, "jamterm: plumb read error: %r\n");
+			threadexits("plumb");	/* not a fatal error */
+		}
+		plumbbuf[i].n = n;
+		if(plumbformat(i)){
+			which = i;
+			send(c, &which);
+		}
+	}
+}
+
+int
+plumbstart(void)
+{
+	static int fd;
+	static void *arg[2];
+
+	plumbfd = plumbopen("send", OWRITE|OCEXEC);	/* not open is ok */
+	fd = plumbopen("edit", OREAD|OCEXEC);
+	if(fd < 0)
+		return -1;
+	plumbc = chancreate(sizeof(int), 0);
+	if(plumbc == nil){
+		close(fd);
+		return -1;
+	}
+	arg[0] =plumbc;
+	arg[1] = &fd;
+	proccreate(plumbproc, arg, STACK);
+	if(pipe(kekfd) < 0)
+		return 1;
+	procrfork(extproc, plumbc, STACK, RFFDG);
+	close(kekfd[0]);
+	return 1;
+}
+
+void
+hostproc(void *arg)
+{
+	Channel *c;
+	int i, n, which;
+
+	c = arg;
+
+	i = 0;
+	for(;;){
+		i = 1-i;	/* toggle */
+		n = read(0, hostbuf[i].data, sizeof hostbuf[i].data);
+		if(n <= 0){
+			if(n==0){
+				if(exiting)
+					threadexits(nil);
+				werrstr("unexpected eof");
+			}
+			fprint(2, "jamterm: host read error: %r\n");
+			threadexitsall("host");
+		}
+		hostbuf[i].n = n;
+		which = i;
+		send(c, &which);
+	}
+}
+
+void
+hoststart(void)
+{
+	hostc = chancreate(sizeof(int), 0);
+	proccreate(hostproc, hostc, STACK);
+}
--- /dev/null
+++ b/sys/src/cmd/jamterm/rasp.c
@@ -1,0 +1,265 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <frame.h>
+#include "flayer.h"
+#include "samterm.h"
+
+void
+rinit(Rasp *r)
+{
+	r->nrunes=0;
+	r->sect=0;
+}
+
+void
+rclear(Rasp *r)
+{
+	Section *s, *ns;
+
+	for(s=r->sect; s; s=ns){
+		ns = s->next;
+		free(s->text);
+		free(s);
+	}
+	r->sect = 0;
+}
+
+Section*
+rsinsert(Rasp *r, Section *s)	/* insert before s */
+{
+	Section *t;
+	Section *u;
+
+	t = alloc(sizeof(Section));
+	if(r->sect == s){	/* includes empty list case: r->sect==s==0 */
+		r->sect = t;
+		t->next = s;
+	}else{
+		u = r->sect;
+		if(u == 0)
+			panic("rsinsert 1");
+		do{
+			if(u->next == s){
+				t->next = s;
+				u->next = t;
+				goto Return;
+			}
+			u=u->next;
+		}while(u);
+		panic("rsinsert 2");
+	}
+    Return:
+	return t;
+}
+
+void
+rsdelete(Rasp *r, Section *s)
+{
+	Section *t;
+
+	if(s == 0)
+		panic("rsdelete");
+	if(r->sect == s){
+		r->sect = s->next;
+		goto Free;
+	}
+	for(t=r->sect; t; t=t->next)
+		if(t->next == s){
+			t->next = s->next;
+	Free:
+			if(s->text)
+				free(s->text);
+			free(s);
+			return;
+		}
+	panic("rsdelete 2");
+}
+
+void
+splitsect(Rasp *r, Section *s, long n0)
+{
+	if(s == 0)
+		panic("splitsect");
+	rsinsert(r, s->next);
+	if(s->text == 0)
+		s->next->text = 0;
+	else{
+		s->next->text = alloc(RUNESIZE*(TBLOCKSIZE+1));
+		Strcpy(s->next->text, s->text+n0);
+		s->text[n0] = 0;
+	}
+	s->next->nrunes = s->nrunes-n0;
+	s->nrunes = n0;
+}
+
+Section *
+findsect(Rasp *r, Section *s, long p, long q)	/* find sect containing q and put q on a sect boundary */
+{
+	if(s==0 && p!=q)
+		panic("findsect");
+	for(; s && p+s->nrunes<=q; s=s->next)
+		p += s->nrunes;
+	if(p != q){
+		splitsect(r, s, q-p);
+		s = s->next;
+	}
+	return s;
+}
+
+void
+rresize(Rasp *r, long a, long old, long new)
+{
+	Section *s, *t, *ns;
+
+	s = findsect(r, r->sect, 0L, a);
+	t = findsect(r, s, a, a+old);
+	for(; s!=t; s=ns){
+		ns=s->next;
+		rsdelete(r, s);
+	}
+	/* now insert the new piece before t */
+	if(new > 0){
+		ns=rsinsert(r, t);
+		ns->nrunes=new;
+		ns->text=0;
+	}
+	r->nrunes += new-old;
+}
+
+void
+rdata(Rasp *r, long p0, long p1, Rune *cp)
+{
+	Section *s, *t, *ns;
+
+	s = findsect(r, r->sect, 0L, p0);
+	t = findsect(r, s, p0, p1);
+	for(; s!=t; s=ns){
+		ns=s->next;
+		if(s->text)
+			panic("rdata");
+		rsdelete(r, s);
+	}
+	p1 -= p0;
+	s = rsinsert(r, t);
+	s->text = alloc(RUNESIZE*(TBLOCKSIZE+1));
+	memmove(s->text, cp, RUNESIZE*p1);
+	s->text[p1] = 0;
+	s->nrunes = p1;
+}
+
+void
+rclean(Rasp *r)
+{
+	Section *s;
+
+	for(s=r->sect; s; s=s->next)
+		while(s->next && (s->text!=0)==(s->next->text!=0)){
+			if(s->text){
+				if(s->nrunes+s->next->nrunes>TBLOCKSIZE)
+					break;
+				Strcpy(s->text+s->nrunes, s->next->text);
+			}
+			s->nrunes += s->next->nrunes;
+			rsdelete(r, s->next);
+		}
+}
+
+void
+Strcpy(Rune *to, Rune *from)
+{
+	do; while(*to++ = *from++);
+}
+
+Rune*
+rload(Rasp *r, ulong p0, ulong p1, ulong *nrp)
+{
+	Section *s;
+	long p;
+	int n, nb;
+
+	nb = 0;
+	Strgrow(&scratch, &nscralloc, p1-p0+1);
+	scratch[0] = 0;
+	for(p=0,s=r->sect; s && p+s->nrunes<=p0; s=s->next)
+		p += s->nrunes;
+	while(p<p1 && s){
+		/*
+		 * Subtle and important.  If we are preparing to handle an 'rdata'
+		 * call, it's because we have an 'rresize' hole here, so the
+		 * screen doesn't have data for that space anyway (it got cut
+		 * first).  So pretend it isn't there.
+		 */
+		if(s->text){
+			n = s->nrunes-(p0-p);
+			if(n>p1-p0)	/* all in this section */
+				n = p1-p0;
+			memmove(scratch+nb, s->text+(p0-p), n*RUNESIZE);
+			nb += n;
+			scratch[nb] = 0;
+		}
+		p += s->nrunes;
+		p0 = p;
+		s = s->next;
+	}
+	if(nrp)
+		*nrp = nb;
+	return scratch;
+}
+
+int
+rmissing(Rasp *r, ulong p0, ulong p1)
+{
+	Section *s;
+	long p;
+	int n, nm=0;
+
+	for(p=0,s=r->sect; s && p+s->nrunes<=p0; s=s->next)
+		p += s->nrunes;
+	while(p<p1 && s){
+		if(s->text == 0){
+			n = s->nrunes-(p0-p);
+			if(n > p1-p0)	/* all in this section */
+				n = p1-p0;
+			nm += n;
+		}
+		p += s->nrunes;
+		p0 = p;
+		s = s->next;
+	}
+	return nm;
+}
+
+int
+rcontig(Rasp *r, ulong p0, ulong p1, int text)
+{
+	Section *s;
+	long p, n;
+	int np=0;
+
+	for(p=0,s=r->sect; s && p+s->nrunes<=p0; s=s->next)
+		p += s->nrunes;
+	while(p<p1 && s && (text? (s->text!=0) : (s->text==0))){
+		n = s->nrunes-(p0-p);
+		if(n > p1-p0)	/* all in this section */
+			n = p1-p0;
+		np += n;
+		p += s->nrunes;
+		p0 = p;
+		s = s->next;
+	}
+	return np;
+}
+
+void
+Strgrow(Rune **s, long *n, int want)	/* can always toss the old data when called */
+{
+	if(*n >= want)
+		return;
+	free(*s);
+	*s = alloc(RUNESIZE*want);
+	*n = want;
+}
--- /dev/null
+++ b/sys/src/cmd/jamterm/samterm.h
@@ -1,0 +1,186 @@
+#define	SAMTERM
+
+#define	RUNESIZE	sizeof(Rune)
+#define	MAXFILES	256
+#define	READBUFSIZE 8192
+#define	NL	10
+
+enum{
+	Up,
+	Down,
+
+	Kbel=0x7,
+	Ksyn = 0x16,
+};
+
+typedef struct Text	Text;
+typedef struct Section	Section;
+typedef struct Rasp	Rasp;
+typedef struct Readbuf Readbuf;
+
+struct Section
+{
+	long	nrunes;
+	Rune	*text;		/* if null, we haven't got it */
+	Section	*next;
+};
+
+struct Rasp
+{
+	long	nrunes;
+	Section	*sect;
+};
+
+#define	Untagged	((ushort)65535)
+
+struct Text
+{
+	Rasp	rasp;
+	short	nwin;
+	short	front;		/* input window */
+	ushort	tag;
+	char	lock;
+	Flayer	l[NL];		/* screen storage */
+};
+
+struct Readbuf
+{
+	short	n;					/* # bytes in buf */
+	uchar	data[READBUFSIZE];		/* data bytes */
+};
+
+enum Resource
+{
+	RHost,
+	RKeyboard,
+	RMouse,
+	RPlumb,
+	RResize,
+	NRes,
+};
+
+extern Text	**text;
+extern uchar	**name;
+extern ushort	*tag;
+extern int	nname;
+extern int	mname;
+extern Cursor	deadmouse;
+extern Cursor	lockarrow;
+extern Cursor	*cursor;
+extern Flayer	*which;
+extern Flayer	*work;
+extern Text	cmd;
+extern Rune	*scratch;
+extern long	nscralloc;
+extern char	hostlock;
+extern char	hasunlocked;
+extern long	snarflen;
+extern Mousectl* mousectl;
+extern Keyboardctl* keyboardctl;
+extern Mouse*	mousep;
+extern long	modified;
+extern int	maxtab;
+extern Readbuf	hostbuf[2];	/* double buffer; it's synchronous communication */
+extern Readbuf	plumbbuf[4];	/* double buffer; it's synchronous communication */
+extern Channel *plumbc;
+extern Channel *hostc;
+extern int	hversion;
+extern int	plumbfd;
+extern int	exiting;
+extern int	autoindent;
+extern int	spacesindent;
+
+Rune	*gettext(Flayer*, long, ulong*);
+void	*alloc(ulong n);
+
+void	iconinit(void);
+void	getscreen(int, char**);
+void	initio(void);
+void	setlock(void);
+void	outcmd(void);
+void	rinit(Rasp*);
+void	startnewfile(int, Text*);
+void	getmouse(void);
+void	mouseunblock(void);
+void	kbdblock(void);
+void	hoststart(void);
+int	plumbstart(void);
+int	button(int but);
+int	load(char*, int);
+int	waitforio(void);
+int	rcvchar(void);
+int	getch(void);
+int	kbdchar(void);
+int	qpeekc(void);
+void	cut(Text*, int, int, int);
+void	paste(Text*, int);
+void	snarf(Text*, int);
+int	center(Flayer*, long);
+int	xmenuhit(int, Menu*);
+void	buttons(int);
+void	warpmouse(Flayer*);
+void	current(Flayer*, int, int);
+void	duplicate(Flayer*, Rectangle, Font*, int);
+void	startfile(Text*);
+void	panic(char*);
+void	panic1(Display*, char*);
+void	closeup(Flayer*);
+void	Strgrow(Rune**, long*, int);
+int	RESIZED(void);
+void	resize(void);
+void	rcv(void);
+void	type(Flayer*, int);
+void	menu2hit(void);
+void	menu3hit(void);
+void	scroll(Flayer*, int);
+void	hcheck(int);
+void	rclear(Rasp*);
+int	whichmenu(int);
+void	hcut(int, long, long);
+void	horigin(int, long);
+void	hgrow(int, long, long, int);
+int	hdata(int, long, uchar*, int);
+int	hdatarune(int, long, Rune*, int);
+Rune	*rload(Rasp*, ulong, ulong, ulong*);
+void	menuins(int, uchar*, Text*, int, int);
+void	menudel(int);
+Rectangle	expandempty(Point, Flayer*, int);
+Text	*sweeptext(int, int);
+void	setpat(char*);
+void	menucmd(char*);
+void	scrdraw(Flayer*, long tot);
+int	rcontig(Rasp*, ulong, ulong, int);
+int	rmissing(Rasp*, ulong, ulong);
+void	rresize(Rasp *, long, long, long);
+void	rdata(Rasp*, long, long, Rune*);
+void	rclean(Rasp*);
+void	scrorigin(Flayer*, int, long);
+long	scrtotal(Flayer*);
+void	flnewlyvisible(Flayer*);
+char	*rcvstring(void);
+void	Strcpy(Rune*, Rune*);
+void	Strncpy(Rune*, Rune*, long);
+void	flushtyping(int);
+void	dumperrmsg(int, int, int, int);
+int	screensize(int*,int*);
+void	getmouse(void);
+Rectangle inflatepoint(Point);
+int	promptrect(Rectangle*, Flayer*, int);
+Rectangle	defaultcmdrect(void);
+
+#include "mesg.h"
+
+void	outTs(Tmesg, int);
+void	outT0(Tmesg);
+void	outTl(Tmesg, long);
+void	outTslS(Tmesg, int, long, Rune*);
+void	outTsll(Tmesg, int, long, long);
+void	outTsl(Tmesg, int, long);
+void	outTsv(Tmesg, int, vlong);
+void	outTv(Tmesg, vlong);
+void	outstart(Tmesg);
+void	outcopy(int, uchar*);
+void	outshort(int);
+void	outlong(long);
+void	outvlong(vlong);
+void	outsend(void);
--- /dev/null
+++ b/sys/src/cmd/jamterm/scroll.c
@@ -1,0 +1,177 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <frame.h>
+#include "flayer.h"
+#include "samterm.h"
+
+static Image *scrtmp;
+static Image *scrback;
+
+void
+scrtemps(void)
+{
+	int h;
+
+	if(scrtmp)
+		return;
+	if(screensize(0, &h) == 0)
+		h = 2048;
+	scrtmp = allocimage(display, Rect(0, 0, 32, h), screen->chan, 0, 0);
+	scrback = allocimage(display, Rect(0, 0, 32, h), screen->chan, 0, 0);
+	if(scrtmp==0 || scrback==0)
+		panic("scrtemps");
+}
+
+Rectangle
+scrpos(Rectangle r, long p0, long p1, long tot)
+{
+	Rectangle q;
+	int h;
+
+	q = r;
+	h = q.max.y-q.min.y;
+	if(tot == 0)
+		return q;
+	if(tot > 1024L*1024L)
+		tot>>=10, p0>>=10, p1>>=10;
+	if(p0 > 0)
+		q.min.y += h*p0/tot;
+	if(p1 < tot)
+		q.max.y -= h*(tot-p1)/tot;
+	if(q.max.y < q.min.y+2){
+		if(q.min.y+2 <= r.max.y)
+			q.max.y = q.min.y+2;
+		else
+			q.min.y = q.max.y-2;
+	}
+	return q;
+}
+
+void
+scrmark(Flayer *l, Rectangle r)
+{
+	r.max.x--;
+	if(rectclip(&r, l->scroll)) {
+		if (l->f.b == nil)
+			panic("scrmark: nil l->f.b");
+		draw(l->f.b, r, l->f.cols[HIGH], nil, ZP);
+	}
+}
+
+void
+scrunmark(Flayer *l, Rectangle r)
+{
+	if(rectclip(&r, l->scroll)) {
+		if (l->f.b == nil)
+			panic("scrunmark: nil l->f.b");
+		draw(l->f.b, r, scrback, nil, Pt(0, r.min.y-l->scroll.min.y));
+	}
+}
+
+void
+scrdraw(Flayer *l, long tot)
+{
+	Rectangle r, r1, r2;
+	Image *b;
+
+	scrtemps();
+	if(l->f.b == 0)
+		panic("scrdraw");
+	r = l->scroll;
+	r1 = r;
+	if(l->visible == All){
+		b = scrtmp;
+		r1.min.x = 0;
+		r1.max.x = Dx(r);
+	}else
+		b = l->f.b;
+	r2 = scrpos(r1, l->origin, l->origin+l->f.nchars, tot);
+	if(!eqrect(r2, l->lastsr)){
+		l->lastsr = r2;
+		draw(b, r1, l->f.cols[BORD], nil, ZP);
+		draw(b, r2, l->f.cols[BACK], nil, r2.min);
+		r2 = r1;
+		r2.min.x = r2.max.x-1;
+		draw(b, r2, l->f.cols[BORD], nil, ZP);
+		if(b!=l->f.b)
+			draw(l->f.b, r, b, nil, r1.min);
+	}
+}
+
+void
+scroll(Flayer *l, int but)
+{
+	int in = 0, oin;
+	long tot = scrtotal(l);
+	Rectangle scr, r, s, rt;
+	int x, y, my, oy, h;
+	long p0;
+ 
+	if(l->visible==None)
+		return;
+
+	s = l->scroll;
+	x = s.min.x+FLSCROLLWID/2;
+	scr = scrpos(l->scroll, l->origin, l->origin+l->f.nchars, tot);
+	r = scr;
+	y = scr.min.y;
+	my = mousep->xy.y;
+	draw(scrback, Rect(0,0,Dx(l->scroll), Dy(l->scroll)), l->f.b, nil, l->scroll.min);
+	do{
+		oin = in;
+		in = (but > 3) || (but == 2) || abs(x-mousep->xy.x)<=FLSCROLLWID/2;
+		if(oin && !in)
+			scrunmark(l, r);
+		if(in){
+			scrmark(l, r);
+			oy = y;
+			my = mousep->xy.y;
+			if(my < s.min.y)
+				my = s.min.y;
+			if(my >= s.max.y)
+				my = s.max.y;
+			if(but == 1 || but == 4){
+				p0 = l->origin-frcharofpt(&l->f, Pt(s.max.x, my));
+				rt = scrpos(l->scroll, p0, p0+l->f.nchars, tot);
+				y = rt.min.y;
+			}else if(but == 2){
+				y = my;
+				if(y > s.max.y-2)
+					y = s.max.y-2;
+			}else if(but == 3 || but == 5){
+				p0 = l->origin+frcharofpt(&l->f, Pt(s.max.x, my));
+				rt = scrpos(l->scroll, p0, p0+l->f.nchars, tot);
+				y = rt.min.y;
+			}
+			if(y != oy){
+				scrunmark(l, r);
+				r = rectaddpt(scr, Pt(0, y-scr.min.y));
+				scrmark(l, r);
+			}
+		}
+	}while(but <= 3 && button(but));
+	if(in){
+		h = s.max.y-s.min.y;
+		scrunmark(l, r);
+		p0 = 0;
+		if(but == 1 || but == 4){
+			but = 1;
+			p0 = (long)(my-s.min.y)/l->f.font->height+1;
+		}else if(but == 2){
+			if(tot > 1024L*1024L)
+				p0 = ((tot>>10)*(y-s.min.y)/h)<<10;
+			else
+				p0 = tot*(y-s.min.y)/h;
+		}else if(but == 3 || but == 5){
+			but = 3;
+			p0 = l->origin+frcharofpt(&l->f, Pt(s.max.x, my));
+			if(p0 > tot)
+				p0 = tot;
+		}
+		scrorigin(l, but, p0);
+	}
+}
--- /dev/null
+++ b/sys/src/cmd/jamterm/win.c
@@ -1,0 +1,197 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <frame.h>
+#include "flayer.h"
+#include "samterm.h"
+
+enum{
+	SnapΔ = 24 * FLMARGIN,
+};
+
+int
+max(int a, int b)
+{
+	return a > b ? a : b;
+}
+
+int
+min(int a, int b)
+{
+	return a < b ? a : b;
+}
+
+Rectangle
+defaultcmdrect(void)
+{
+	int rw, rh, fw, fh;
+	Rectangle r;
+
+	fw = stringwidth(font, "0");
+	fh = font->height;
+	rw = Dx(screen->r) / fw;
+	rh = Dy(screen->r) / fh;
+	r = screen->r;
+	if(rw < 6 || rh < 3){
+		;
+	}else if(rw >= 120){
+		r.min.x = r.max.x - 72 * fw;
+		r.min.y = max(r.max.y - 8 * fh, r.min.y + 2*fh);
+	}else
+		r.max.y = r.min.y + 2*fh + min(max(Dy(screen->r) / 6, 0), 12*fh);
+	return r;
+}
+
+/* always prefer innermost border in case of ties */
+static int
+cansnap(int a, int b, int Δ)
+{
+	int δ;
+
+	return (δ = abs(a - b)) <= Δ && (δ != Δ || a < b);
+}
+
+/* use borders of all rects next to ours as guides to snap to */
+static Rectangle
+snaprect(Rectangle r, Flayer *l)
+{
+	int i;
+	Rectangle Δr, s, rr, c;
+	Flayer *fl;
+	Text *t;
+
+	rr = insetrect(r, -SnapΔ);
+	Δr = Rect(SnapΔ, SnapΔ, SnapΔ, SnapΔ);
+	s = r;
+	for(i=0; i<nname; i++){
+		t = text[i];
+		if(t == nil || t->nwin == 0)
+			continue;
+		for(fl=t->l; fl<t->l+NL; fl++){
+			if(fl->textfn == nil || fl == l)
+				continue;
+			c = fl->entire;
+			if(!rectXrect(rr, c))
+				continue;
+			if(cansnap(r.min.x, c.max.x, Δr.min.x))		/* adjacent border */
+				s.min.x = c.max.x, Δr.min.x = abs(r.min.x - c.max.x);
+			else if(cansnap(r.min.x, c.min.x, Δr.min.x))	/* adjacent guide line */
+				s.min.x = c.min.x, Δr.min.x = abs(r.min.x - c.min.x);
+			if(cansnap(r.min.y, c.max.y, Δr.min.y))
+				s.min.y = c.max.y, Δr.min.y = abs(r.min.y - c.max.y);
+			else if(cansnap(r.min.y, c.min.y, Δr.min.y))
+				s.min.y = c.min.y, Δr.min.y = abs(r.min.y - c.min.y);
+			if(cansnap(c.min.x, r.max.x, Δr.max.x))
+				s.max.x = c.min.x, Δr.max.x = abs(c.min.x - r.max.x);
+			else if(cansnap(c.max.x, r.max.x, Δr.max.x))
+				s.max.x = c.max.x, Δr.max.x = abs(c.max.x - r.max.x);
+			if(cansnap(c.min.y, r.max.y, Δr.max.y))
+				s.max.y = c.min.y, Δr.max.y = abs(c.min.y - r.max.y);
+			else if(cansnap(c.max.y, r.max.y, Δr.max.y))
+				s.max.y = c.max.y, Δr.max.y = abs(c.max.y - r.max.y);
+		}
+	}
+	if(cansnap(r.min.x, screen->r.min.x, Δr.min.x))
+		s.min.x = screen->r.min.x;
+	if(cansnap(r.min.y, screen->r.min.y, Δr.min.y))
+		s.min.y = screen->r.min.y;
+	if(cansnap(screen->r.max.x, r.max.x, Δr.max.x))
+		s.max.x = screen->r.max.x;
+	if(cansnap(screen->r.max.y, r.max.y, Δr.max.y))
+		s.max.y = screen->r.max.y;
+	return s;
+}
+
+/* in case of overlaps, always glob the smallest rect rather than
+ * the biggest to maintain splits and selectable flayers */
+static Rectangle
+stealrect(Point p)
+{
+	int i;
+	Rectangle c, r;
+	Flayer *fl;
+	Text *t;
+
+	r = screen->r;
+	for(i=0; i<nname; i++){
+		t = text[i];
+		if(t == nil || t->nwin == 0)
+			continue;
+		for(fl=t->l; fl<t->l+NL; fl++){
+			if(fl->textfn == nil
+			|| !ptinrect(p, fl->entire))
+				continue;
+			c = fl->entire;
+			if(Dx(c) * Dy(c) < Dx(r) * Dy(r))
+				r = c;
+		}
+	}
+	return r;
+}
+
+static Rectangle
+fillvoid(Point p, Flayer *l, int new)
+{
+	int i;
+	Rectangle c, r;
+	Flayer *fl;
+	Text *t;
+
+	r = screen->r;
+	for(i=0; i<nname; i++){
+		t = text[i];
+		if(t == nil || t->nwin == 0)
+			continue;
+		for(fl=t->l; fl<t->l+NL; fl++){
+			c = fl->entire;
+			if(fl->textfn == nil
+			|| !rectXrect(r, c))
+				continue;
+			if(!new && fl == l)
+				continue;
+			if(ptinrect(p, fl->entire))
+				return ZR;
+			if(c.max.x <= p.x && c.max.x > r.min.x)
+				r.min.x = c.max.x;
+			if(p.x <= c.min.x && c.min.x < r.max.x)
+				r.max.x = c.min.x;
+			if(!rectXrect(c, r))
+				continue;
+			if(c.max.y <= p.y && c.max.y > r.min.y)
+				r.min.y = c.max.y;
+			if(p.y <= c.min.y && c.min.y < r.max.y)
+				r.max.y = c.min.y;
+		}
+		if(Dx(r) < 2*font->width || Dy(r) < font->height)
+			return ZR;
+	}
+	return r;
+}
+
+int
+promptrect(Rectangle *rp, Flayer *l, int new)
+{
+	Point p;
+	Rectangle r;
+
+	r = getrect(3, mousectl);
+	if(eqrect(r, ZR)){
+		*rp = r;
+		return 0;
+	/* around the minimum for libframe to not choke */
+	}else if(Dx(r) < 2 * stringwidth(font, "0") + 2 * FLMARGIN
+	|| Dy(r) < font->height + 2 * FLMARGIN){
+		p = r.min;	/* mousep->xy == r.max */
+		if(!ptinrect(p, screen->r))
+			return 0;
+		r = fillvoid(p, l, new);
+		if(eqrect(r, ZR))
+			r = stealrect(p);
+	}else
+		r = snaprect(r, l);
+	*rp = r;
+	return 1;
+}