Dhog's 9term.
Updated for current libraries by Caerwyn Jones.
Button-3 plumbing (like in acme) by rsc.
diff --git a/src/cmd/9term/9term.c b/src/cmd/9term/9term.c
new file mode 100644
index 0000000..ec59756
--- /dev/null
+++ b/src/cmd/9term/9term.c
@@ -0,0 +1,1380 @@
+#include "9term.h"
+
+Rectangle	scrollr;	/* scroll bar rectangle */
+Rectangle	lastsr;		/* used for scroll bar */
+int		holdon;		/* hold mode */
+int		rawon;		/* raw mode */
+int		scrolling;	/* window scrolls */
+int		clickmsec;	/* time of last click */
+uint		clickq0;	/* point of last click */
+int		rcfd[2];
+int		rcpid;
+int		maxtab;
+Mousectl*	mc;
+Keyboardctl*	kc;
+Channel*	hostc;
+Readbuf		rcbuf[2];
+int		mainpid;
+int		plumbfd;
+int		label(Rune*, int);
+char		wdir[1024];
+void		hangupnote(void*, char*);
+
+char *menu2str[] = {
+	"cut",
+	"paste",
+	"snarf",
+	"send",
+	"scroll",
+	"plumb",
+	0
+};
+
+Image* cols[NCOL];
+Image* hcols[NCOL];
+Image *plumbcolor;
+
+Menu menu2 =
+{
+	menu2str
+};
+
+Text	t;
+
+Cursor whitearrow = {
+	{0, 0},
+	{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFC, 
+	 0xFF, 0xF0, 0xFF, 0xF0, 0xFF, 0xF8, 0xFF, 0xFC, 
+	 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFC, 
+	 0xF3, 0xF8, 0xF1, 0xF0, 0xE0, 0xE0, 0xC0, 0x40, },
+	{0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x06, 0xC0, 0x1C, 
+	 0xC0, 0x30, 0xC0, 0x30, 0xC0, 0x38, 0xC0, 0x1C, 
+	 0xC0, 0x0E, 0xC0, 0x07, 0xCE, 0x0E, 0xDF, 0x1C, 
+	 0xD3, 0xB8, 0xF1, 0xF0, 0xE0, 0xE0, 0xC0, 0x40, }
+};
+
+void
+threadmain(int argc, char *argv[])
+{
+	char *p;
+
+	rfork(RFNOTEG);
+	mainpid = getpid();
+	ARGBEGIN{
+	case 'T':
+		p = ARGF();
+		if(p == 0)
+			break;
+		maxtab = strtoul(p, 0, 0);
+		break;
+	case 's':
+		scrolling++;
+		break;
+	}ARGEND
+
+	p = getenv("tabstop");
+	if(p == 0)
+		p = getenv("TABSTOP");
+	if(p != 0 && maxtab <= 0)
+		maxtab = strtoul(p, 0, 0);
+	if(maxtab <= 0)
+		maxtab = 8;
+
+	initdraw(nil, nil, "9term");
+	notify(hangupnote);
+
+	mc = initmouse(nil, screen);
+	kc = initkeyboard(nil);
+	rcstart(rcfd);
+	hoststart();
+	plumbstart();
+
+	t.f = mallocz(sizeof(Frame), 1);
+
+	cols[BACK] = allocimagemix(display, DPaleyellow, DWhite);
+	cols[HIGH] = allocimage(display, Rect(0, 0, 1, 1), screen->chan, 1, DDarkyellow);
+	cols[BORD] = allocimage(display, Rect(0, 0, 1, 1), screen->chan, 1, DYellowgreen);
+	cols[TEXT] = display->black;
+	cols[HTEXT] = display->black;
+
+	hcols[BACK] = cols[BACK];
+	hcols[HIGH] = cols[HIGH];
+	hcols[BORD] = allocimage(display, Rect(0, 0, 1, 1), screen->chan, 1, DMedblue);
+	hcols[TEXT] = hcols[BORD];
+	hcols[HTEXT] = hcols[TEXT];
+
+	plumbcolor = allocimage(display, Rect(0,0,1,1), screen->chan, 1, 0x006600FF);
+
+	draw(screen, screen->r, cols[BACK], nil, ZP);
+	geom();
+
+	loop();
+}
+
+void
+hangupnote(void *a, char *msg)
+{
+	if(getpid() != mainpid)
+		noted(NDFLT);
+	if(strcmp(msg, "hangup") == 0 && rcpid != 0){
+		postnote(PNPROC, rcpid, "hangup");
+		noted(NDFLT);
+	}
+	noted(NDFLT);
+}
+
+void
+hostproc(void *arg)
+{
+	Channel *c;
+	int i, n, which;
+
+	c = arg;
+
+	i = 0;
+	for(;;){
+		i = 1-i;	/* toggle */
+		n = read(rcfd[0], rcbuf[i].data, sizeof rcbuf[i].data);
+		if(n <= 0){
+			if(n < 0)
+				fprint(2, "9term: host read error: %r\n");
+			threadexitsall("host");
+		}
+		rcbuf[i].n = n;
+		which = i;
+		send(c, &which);
+	}
+}
+
+void
+hoststart(void)
+{
+	hostc = chancreate(sizeof(int), 0);
+	proccreate(hostproc, hostc, 1024);
+}
+
+void
+loop(void)
+{
+	Rune r;
+	int i;
+	Alt a[] = {
+		{mc->c, &mc->m, CHANRCV},
+		{kc->c, &r, CHANRCV},
+		{hostc, &i, CHANRCV},
+		{mc->resizec, nil, CHANRCV},
+		{nil, nil, CHANEND},
+	};
+
+	for(;;) {
+		tcheck();
+
+		scrdraw();
+		flushimage(display, 1);
+		a[2].op = CHANRCV;
+		if(!scrolling && t.qh > t.org+t.f->nchars)
+			a[2].op = CHANNOP;;
+
+		switch(alt(a)) {
+		default:
+			fatal("impossible");
+		case 0:
+			t.m = mc->m;
+			mouse();
+			break;
+		case 1:
+			key(r);
+			break;
+		case 2:
+			conswrite(rcbuf[i].data, rcbuf[i].n);
+			break;
+		case 3:
+			doreshape();
+			break;
+		}
+	}
+}
+
+void
+doreshape(void)
+{
+	if(getwindow(display, Refnone) < 0)
+		fatal("can't reattach to window");
+	draw(screen, screen->r, cols[BACK], nil, ZP);
+	geom();
+	scrdraw();
+}
+
+void
+geom(void)
+{
+	Rectangle r;
+
+	r = screen->r;
+	scrollr = screen->r;
+	scrollr.max.x = r.min.x+Scrollwid;
+	lastsr = Rect(0,0,0,0);
+
+	r.min.x += Scrollwid+Scrollgap;
+
+	frclear(t.f, 0);
+	frinit(t.f, r, font, screen, holdon ? hcols : cols);
+	t.f->maxtab = maxtab*stringwidth(font, "0");
+	fill();
+	updatesel();
+}
+
+void
+drawhold(int holdon)
+{
+	if(holdon)
+		setcursor(mc, &whitearrow);
+	else
+		setcursor(mc, nil);
+
+	draw(screen, screen->r, cols[BACK], nil, ZP);
+	geom();
+	scrdraw();
+}
+
+
+void
+mouse(void)
+{
+	int cancel, but;
+	uint oldq0, oldq1, newq0, newq1;
+
+	but = t.m.buttons;
+
+	if(but != 1 && but != 2 && but != 4)
+		return;
+
+	if (ptinrect(t.m.xy, scrollr)) {
+		scroll(but);
+		if(t.qh<=t.org+t.f->nchars)
+			consread();;
+		return;
+	}
+		
+	switch(but) {
+	case 1:
+		mselect();
+		break;
+	case 2:
+		domenu2(2);
+		break;
+	case 4:
+		/* save old selection */
+		oldq0 = t.q0;
+		oldq1 = t.q1;
+
+		/* sweep out plumb area and record it */
+		t.f->cols[HIGH] = plumbcolor;
+		t.f->cols[HTEXT] = display->white;
+		mselect();
+		newq0 = t.q0;
+		newq1 = t.q1;
+
+		cancel = 0;
+		if(t.m.buttons != 0){
+			while(t.m.buttons){
+				readmouse(mc);
+				t.m = mc->m;
+			}
+			cancel = 1;
+		}
+
+		/* restore old selection */
+		t.f->cols[HIGH] = cols[HIGH];
+		t.f->cols[HTEXT] = cols[HTEXT];
+		t.q0 = oldq0;
+		t.q1 = oldq1;
+		updatesel();
+
+		if(cancel)
+			break;
+
+		/* process plumb area */
+		if(newq0 < newq1)
+			plumb(newq0, newq1);
+		else if(oldq0 <= newq0 && newq0 < oldq1)
+			plumb(oldq0, oldq1);
+		else
+			plumb(newq0, newq0);
+		break;
+	}
+}
+
+void
+mselect(void)
+{
+	int b, x, y;
+	uint q0;
+
+	b = t.m.buttons;
+	q0 = frcharofpt(t.f, t.m.xy) + t.org;
+	if(t.m.msec-clickmsec<500 && clickq0 == q0 && t.q0==t.q1 && b==1){
+		doubleclick(&t.q0, &t.q1);
+		updatesel();
+/*		t.t.i->flush(); */
+		x = t.m.xy.x;
+		y = t.m.xy.y;
+		/* stay here until something interesting happens */
+		do {
+			readmouse(mc);
+			t.m = mc->m;
+		} while(t.m.buttons==b && abs(t.m.xy.x-x)<4 && abs(t.m.xy.y-y)<4);
+		t.m.xy.x = x;	/* in case we're calling frselect */
+		t.m.xy.y = y;
+		clickmsec = 0;
+	}
+
+	if(t.m.buttons == b) {
+		frselect(t.f, mc);
+		t.m = mc->m;
+		t.q0 = t.f->p0 + t.org;
+		t.q1 = t.f->p1 + t.org;
+		clickmsec = t.m.msec;
+		clickq0 = t.q0;
+	}
+	if((t.m.buttons != b) && (b&1)){
+		enum {Cancut = 1, Canpaste = 2} state = Cancut | Canpaste;
+		while(t.m.buttons){
+			if(t.m.buttons&2) {
+				if (state&Cancut) {
+					snarf();
+					cut();
+					state = Canpaste;
+				}
+			} else if (t.m.buttons&4) {
+				if (state&Canpaste) {
+					snarfupdate();
+					if (t.nsnarf) {
+						paste(t.snarf, t.nsnarf, 0);
+					}
+					state = Cancut|Canpaste;
+				}
+			}
+			readmouse(mc);
+			t.m = mc->m;
+		}
+	}
+}
+
+Rune newline[] = { '\n', 0 };
+
+void
+domenu2(int but)
+{
+	if(scrolling)
+		menu2str[Scroll] = "noscroll";
+	else
+		menu2str[Scroll] = "scroll";
+
+	switch(menuhit(but, mc, &menu2, nil)){
+	case -1:
+		break;
+	case Cut:
+		snarf();
+		cut();
+		if(scrolling)
+			show(t.q0);
+		break;
+	case Paste:
+		snarfupdate();
+		paste(t.snarf, t.nsnarf, 0);
+		if(scrolling)
+			show(t.q0);
+		break;
+	case Snarf:
+		snarf();
+		if(scrolling)
+			show(t.q0);
+		break;
+	case Send:
+		snarf();
+		t.q0 = t.q1 = t.nr;
+		updatesel();
+		snarfupdate();
+		paste(t.snarf, t.nsnarf, 1);
+		if(t.nsnarf == 0 || t.snarf[t.nsnarf-1] != '\n')
+			paste(newline, 1, 1);
+		show(t.nr);
+		consread();
+		break;
+	case Scroll:
+		scrolling = !scrolling;
+		if (scrolling) {
+			show(t.nr);
+			consread();
+		}
+		break;
+	case Plumb:
+		plumb(t.q0, t.q1);
+		break;
+	default:
+		fatal("bad menu item");
+	}
+}
+
+void
+key(Rune r)
+{
+	char buf[1];
+
+	if(r == 0)
+		return;
+	if(r==SCROLLKEY){	/* scroll key */
+		setorigin(line2q(t.f->maxlines*2/3), 1);
+		if(t.qh<=t.org+t.f->nchars)
+			consread();
+		return;
+	}else if(r == BACKSCROLLKEY){
+		setorigin(backnl(t.org, t.f->maxlines*2/3), 1);
+		return;
+	}else if(r == CUT){
+		snarf();
+		cut();
+		if(scrolling)
+			show(t.q0);
+		return;
+	}else if(r == COPY){
+		snarf();
+		if(scrolling)
+			show(t.q0);
+		return;
+	}else if(r == PASTE){
+		snarfupdate();
+		paste(t.snarf, t.nsnarf, 0);
+		if(scrolling)
+			show(t.q0);
+		return;
+	}
+
+	if(rawon && t.q0==t.nr){
+		addraw(&r, 1);
+		return;
+	}
+
+	if(r==ESC || (holdon && r==0x7F)){	/* toggle hold */
+		holdon = !holdon;
+		drawhold(holdon);
+		if(!holdon)
+			consread();
+		if(r == 0x1B)
+			return;
+	}
+
+	snarf();
+
+	switch(r) {
+	case 0x7F:	/* DEL: send interrupt */
+		t.qh = t.q0 = t.q1 = t.nr;
+		show(t.q0);
+		buf[0] = 0x7f;
+		if(write(rcfd[1], buf, 1) < 0)
+			exits(0);
+		/* get rc to print prompt */
+//		r = '\n';
+//		paste(&r, 1, 1);
+		break;
+	case 0x08:	/* ^H: erase character */
+	case 0x15:	/* ^U: erase line */
+	case 0x17:	/* ^W: erase word */
+		if (t.q0 != 0 && t.q0 != t.qh)
+			t.q0 -= bswidth(r);
+		cut();
+		break;
+	default:
+		paste(&r, 1, 1);
+		break;
+	}
+	if(scrolling)
+		show(t.q0);
+}
+
+int
+bswidth(Rune c)
+{
+	uint q, eq, stop;
+	Rune r;
+	int skipping;
+
+	/* there is known to be at least one character to erase */
+	if(c == 0x08)	/* ^H: erase character */
+		return 1;
+	q = t.q0;
+	stop = 0;
+	if(q > t.qh)
+		stop = t.qh;
+	skipping = 1;
+	while(q > stop){
+		r = t.r[q-1];
+		if(r == '\n'){		/* eat at most one more character */
+			if(q == t.q0)	/* eat the newline */
+				--q;
+			break; 
+		}
+		if(c == 0x17){
+			eq = isalnum(r);
+			if(eq && skipping)	/* found one; stop skipping */
+				skipping = 0;
+			else if(!eq && !skipping)
+				break;
+		}
+		--q;
+	}
+	return t.q0-q;
+}
+
+int
+consready(void)
+{
+	int i, c;
+
+	if(holdon)
+		return 0;
+
+	if(rawon) 
+		return t.nraw != 0;
+
+	/* look to see if there is a complete line */
+	for(i=t.qh; i<t.nr; i++){
+		c = t.r[i];
+		if(c=='\n' || c=='\004')
+			return 1;
+	}
+	return 0;
+}
+
+
+void
+consread(void)
+{
+	char buf[8000], *p;
+	int c, width, n;
+
+	for(;;) {
+		if(!consready())
+			return;
+
+		n = sizeof(buf);
+		p = buf;
+		while(n >= UTFmax && (t.qh<t.nr || t.nraw > 0)) {
+			if(t.qh == t.nr){
+				width = runetochar(p, &t.raw[0]);
+				t.nraw--;
+				runemove(t.raw, t.raw+1, t.nraw);
+			}else
+				width = runetochar(p, &t.r[t.qh++]);
+			c = *p;
+			p += width;
+			n -= width;
+			if(!rawon && (c == '\n' || c == '\004')) {
+				if(c == '\004')
+					p--;
+				break;
+			}
+		}
+		if(n < UTFmax && t.qh<t.nr && t.r[t.qh]=='\004')
+			t.qh++;
+		/* put in control-d when doing a zero length write */
+		if(p == buf)
+			*p++ = '\004';
+		if(write(rcfd[1], buf, p-buf) < 0)
+			exits(0);
+/*		mallocstats(); */
+	}
+}
+
+void
+conswrite(char *p, int n)
+{
+	int n2, i;
+	Rune buf2[1000], *q;
+
+	/* convert to runes */
+	i = t.npart;
+	if(i > 0){
+		/* handle partial runes */
+		while(i < UTFmax && n>0) {
+			t.part[i] = *p;
+			i++;
+			p++;
+			n--;
+			if(fullrune(t.part, i)) {
+				t.npart = 0;
+				chartorune(buf2, t.part);
+				runewrite(buf2, 1);
+				break;
+			}
+		}
+		/* there is a little extra room in a message buf */
+	}
+
+	while(n >= UTFmax || fullrune(p, n)) {
+		n2 = nelem(buf2);
+		q = buf2;
+
+		while(n2) {
+			if(n < UTFmax && !fullrune(p, n))
+				break;
+			i = chartorune(q, p);
+			p += i;
+			n -= i;
+			n2--;
+			q++;
+		}
+
+		runewrite(buf2, q-buf2);
+	}
+
+	if(n != 0) {
+		assert(n+t.npart < UTFmax);
+		memcpy(t.part+t.npart, p, n);
+		t.npart += n;
+	}
+
+	if(scrolling)
+		show(t.qh);
+}
+
+void
+runewrite(Rune *r, int n)
+{
+	uint m;
+	int i;
+	uint initial;
+	uint q0, q1;
+	uint p0, p1;
+	Rune *p, *q;
+
+	n = label(r, n);
+	if(n == 0)
+		return;
+
+	/* get ride of backspaces */
+	initial = 0;
+	p = q = r;
+	for(i=0; i<n; i++) {
+		if(*p == '\b') {
+			if(q == r)
+				initial++;
+			else
+				--q;
+		} else if(*p)
+			*q++ = *p;
+		p++;
+	}
+	n = q-r;
+
+	if(initial){
+		/* write turned into a delete */
+
+		if(initial > t.qh)
+			initial = t.qh;
+		q0 = t.qh-initial;
+		q1 = t.qh;
+
+		runemove(t.r+q0, t.r+q1, t.nr-q1);
+		t.nr -= initial;
+		t.qh -= initial;
+		if(t.q0 > q1)
+			t.q0 -= initial;
+		else if(t.q0 > q0)
+			t.q0 = q0;
+		if(t.q1 > q1)
+			t.q1 -= initial;
+		else if(t.q1 > q0)
+			t.q1 = q0;
+		if(t.org > q1)
+			t.org -= initial;
+		else if(q0 < t.org+t.f->nchars){
+			if(t.org < q0)
+				p0 = q0 - t.org;
+			else {
+				t.org = q0;
+				p0 = 0;
+			}
+			p1 = q1 - t.org;
+			if(p1 > t.f->nchars)
+				p1 = t.f->nchars;
+			frdelete(t.f, p0, p1);
+			fill();
+		}
+		updatesel();
+		return;
+	}
+
+	if(t.nr>HiWater && t.qh>=t.org){
+		m = HiWater-LoWater;
+		if(m > t.org);
+			m = t.org;
+		t.org -= m;
+		t.qh -= m;
+		if(t.q0 > m)
+			t.q0 -= m;
+		else
+			t.q0 = 0;
+		if(t.q1 > m)
+			t.q1 -= m;
+		else
+			t.q1 = 0;
+		t.nr -= m;
+		runemove(t.r, t.r+m, t.nr);
+	}
+	t.r = runerealloc(t.r, t.nr+n);
+	runemove(t.r+t.qh+n, t.r+t.qh, t.nr-t.qh);
+	runemove(t.r+t.qh, r, n);
+	t.nr += n;
+	if(t.qh < t.org)
+		t.org += n;
+	else if(t.qh <= t.f->nchars+t.org)
+		frinsert(t.f, r, r+n, t.qh-t.org);
+	if (t.qh <= t.q0)
+		t.q0 += n;
+	if (t.qh <= t.q1)
+		t.q1 += n;
+	t.qh += n;
+	updatesel();
+}
+
+
+void
+cut(void)
+{
+	uint n, p0, p1;
+	uint q0, q1;
+
+	q0 = t.q0;
+	q1 = t.q1;
+
+	if (q0 < t.org && q1 >= t.org)
+		show(q0);
+
+	n = q1-q0;
+	if(n == 0)
+		return;
+	runemove(t.r+q0, t.r+q1, t.nr-q1);
+	t.nr -= n;
+	t.q0 = t.q1 = q0;
+	if(q1 < t.qh)
+		t.qh -= n;
+	else if(q0 < t.qh)
+		t.qh = q0;
+	if(q1 < t.org)
+		t.org -= n;
+	else if(q0 < t.org+t.f->nchars){
+		assert(q0 >= t.org);
+		p0 = q0 - t.org;
+		p1 = q1 - t.org;
+		if(p1 > t.f->nchars)
+			p1 = t.f->nchars;
+		frdelete(t.f, p0, p1);
+		fill();
+	}
+	updatesel();
+}
+
+void
+snarfupdate(void)
+{
+
+	char *pp;
+	int n, i;
+	Rune *p;
+
+	pp = getsnarf();
+	n = strlen(pp);
+	if(n <= 0) {
+		 /*t.nsnarf = 0;*/
+		return;
+	}
+	t.snarf = runerealloc(t.snarf, n);
+	for(i=0,p=t.snarf; i<n; p++)
+		i += chartorune(p, pp+i);
+	t.nsnarf = p-t.snarf;
+
+}
+
+void
+snarf(void)
+{
+	char buf[SnarfSize], *p;
+	int i, n;
+	Rune *rp;
+
+	if(t.q1 == t.q0)
+		return;
+	n = t.q1-t.q0;
+	t.snarf =  runerealloc(t.snarf, n);
+	for(i=0,p=buf,rp=t.snarf; i<n && p < buf + SnarfSize-UTFmax; i++){
+		*rp++ = *(t.r+t.q0+i);
+		p += runetochar(p, t.r+t.q0+i);
+	}
+	t.nsnarf = rp-t.snarf;
+	*p = '\0';
+	putsnarf(buf);
+}
+
+void
+paste(Rune *r, int n, int advance)
+{
+	uint m;
+	uint q0;
+
+	if(rawon && t.q0==t.nr){
+		addraw(r, n);
+		return;
+	}
+
+	cut();
+	if(n == 0)
+		return;
+	if(t.nr>HiWater && t.q0>=t.org && t.q0>=t.qh){
+		m = HiWater-LoWater;
+		if(m > t.org)
+			m = t.org;
+		if(m > t.qh);
+			m = t.qh;
+		t.org -= m;
+		t.qh -= m;
+		t.q0 -= m;
+		t.q1 -= m;
+		t.nr -= m;
+		runemove(t.r, t.r+m, t.nr);
+	}
+	t.r = runerealloc(t.r, t.nr+n);
+	q0 = t.q0;
+	runemove(t.r+q0+n, t.r+q0, t.nr-q0);
+	runemove(t.r+q0, r, n);
+	t.nr += n;
+	if(q0 < t.qh)
+		t.qh += n;
+	else
+		consread();
+	if(q0 < t.org)
+		t.org += n;
+	else if(q0 <= t.f->nchars+t.org)
+		frinsert(t.f, r, r+n, q0-t.org);
+	if(advance)
+		t.q0 += n;
+	t.q1 += n;
+	updatesel();
+}
+
+void
+fill(void)
+{
+	if (t.f->nlines >= t.f->maxlines)
+		return;
+	frinsert(t.f, t.r + t.org + t.f->nchars, t.r + t.nr, t.f->nchars);
+}
+
+void
+updatesel(void)
+{
+	Frame *f;
+	uint n;
+
+	f = t.f;
+	if(t.org+f->p0 == t.q0 && t.org+f->p1 == t.q1)
+		return;
+
+	n = t.f->nchars;
+
+	frdrawsel(f, frptofchar(f, f->p0), f->p0, f->p1, 0);
+	if (t.q0 >= t.org)
+		f->p0 = t.q0-t.org;
+	else
+		f->p0 = 0;
+	if(f->p0 > n)
+		f->p0 = n;
+	if (t.q1 >= t.org)
+		f->p1 = t.q1-t.org;
+	else
+		f->p1 = 0;
+	if(f->p1 > n)
+		f->p1 = n;
+	frdrawsel(f, frptofchar(f, f->p0), f->p0, f->p1, 1);
+
+/*
+	if(t.qh<=t.org+t.f.nchars && t.cwqueue != 0)
+		t.cwqueue->wakeup <-= 0;
+*/
+
+	tcheck();
+}
+
+void
+show(uint q0)
+{
+	int nl;
+	uint q, oq;
+
+	if(cansee(q0))
+		return;
+	
+	if (q0<t.org)
+		nl = t.f->maxlines/5;
+	else
+		nl = 4*t.f->maxlines/5;
+	q = backnl(q0, nl);
+	/* avoid going in the wrong direction */
+	if (q0>t.org && q<t.org)
+		q = t.org;
+	setorigin(q, 0);
+	/* keep trying until q0 is on the screen */
+	while(!cansee(q0)) {
+		assert(q0 >= t.org);
+		oq = q;
+		q = line2q(t.f->maxlines-nl);
+		assert(q > oq);
+		setorigin(q, 1);
+	}
+}
+
+int
+cansee(uint q0)
+{
+	uint qe;
+
+	qe = t.org+t.f->nchars;
+
+	if(q0>=t.org && q0 < qe)
+		return 1;
+	if (q0 != qe)
+		return 0;
+	if (t.f->nlines < t.f->maxlines)
+		return 1;
+	if (q0 > 0 && t.r[t.nr-1] == '\n')
+		return 0;
+	return 1;
+}
+
+
+void
+setorigin(uint org, int exact)
+{
+	int i, a;
+	uint n;
+	
+	if(org>0 && !exact){
+		/* try and start after a newline */
+		/* don't try harder than 256 chars */
+		for(i=0; i<256 && org<t.nr; i++){
+			if(t.r[org-1] == '\n')
+				break;
+			org++;
+		}
+	}
+	a = org-t.org;
+
+	if(a>=0 && a<t.f->nchars)
+		frdelete(t.f, 0, a);
+	else if(a<0 && -a<100*t.f->maxlines){
+		n = t.org - org;
+		frinsert(t.f, t.r+org, t.r+org+n, 0);
+	}else
+		frdelete(t.f, 0, t.f->nchars);
+	t.org = org;
+	fill();
+	updatesel();
+}
+
+
+uint
+line2q(uint n)
+{
+	Frame *f;
+
+	f = t.f;
+	return frcharofpt(f, Pt(f->r.min.x, f->r.min.y + n*font->height))+t.org;
+}
+
+uint
+backnl(uint p, uint n)
+{
+	int i, j;
+
+	for (i = n;; i--) {
+		/* at 256 chars, call it a line anyway */
+		for(j=256; --j>0 && p>0; p--)
+			if(t.r[p-1]=='\n')
+				break;
+		if (p == 0 || i == 0)
+			return p;
+		p--;
+	}
+	return 0; /* alef bug */
+}
+
+
+void
+addraw(Rune *r, int nr)
+{
+	t.raw = runerealloc(t.raw, t.nraw+nr);
+	runemove(t.raw+t.nraw, r, nr);
+	t.nraw += nr;
+/*
+	if(t.crqueue != nil)
+		t.crqueue->wakeup <-= 0;
+*/	
+}
+
+
+Rune left1[] =  { '{', '[', '(', '<', 0xab, 0 };
+Rune right1[] = { '}', ']', ')', '>', 0xbb, 0 };
+Rune left2[] =  { '\n', 0 };
+Rune left3[] =  { '\'', '"', '`', 0 };
+
+Rune *left[] = {
+	left1,
+	left2,
+	left3,
+	0
+};
+
+Rune *right[] = {
+	right1,
+	left2,
+	left3,
+	0
+};
+
+void
+doubleclick(uint *q0, uint *q1)
+{
+	int c, i;
+	Rune *r, *l, *p;
+	uint q;
+
+	for(i=0; left[i]!=0; i++){
+		q = *q0;
+		l = left[i];
+		r = right[i];
+		/* try matching character to left, looking right */
+		if(q == 0)
+			c = '\n';
+		else
+			c = t.r[q-1];
+		p = strrune(l, c);
+		if(p != 0){
+			if(clickmatch(c, r[p-l], 1, &q))
+				*q1 = q-(c!='\n');
+			return;
+		}
+		/* try matching character to right, looking left */
+		if(q == t.nr)
+			c = '\n';
+		else
+			c = t.r[q];
+		p = strrune(r, c);
+		if(p != 0){
+			if(clickmatch(c, l[p-r], -1, &q)){
+				*q1 = *q0+(*q0<t.nr && c=='\n');
+				*q0 = q;
+				if(c!='\n' || q!=0 || t.r[0]=='\n')
+					(*q0)++;
+			}
+			return;
+		}
+	}
+	/* try filling out word to right */
+	while(*q1<t.nr && isalnum(t.r[*q1]))
+		(*q1)++;
+	/* try filling out word to left */
+	while(*q0>0 && isalnum(t.r[*q0-1]))
+		(*q0)--;
+}
+
+void
+plumbclick(uint *q0, uint *q1)
+{
+	while(*q1<t.nr && !isspace(t.r[*q1]))
+		(*q1)++;
+	while(*q0>0 && !isspace(t.r[*q0-1]))
+		(*q0)--;
+}
+
+int
+clickmatch(int cl, int cr, int dir, uint *q)
+{
+	Rune c;
+	int nest;
+
+	nest = 1;
+	for(;;){
+		if(dir > 0){
+			if(*q == t.nr)
+				break;
+			c = t.r[*q];
+			(*q)++;
+		}else{
+			if(*q == 0)
+				break;
+			(*q)--;
+			c = t.r[*q];
+		}
+		if(c == cr){
+			if(--nest==0)
+				return 1;
+		}else if(c == cl)
+			nest++;
+	}
+	return cl=='\n' && nest==1;
+}
+
+void
+rcstart(int fd[2])
+{
+	int pid;
+	char *argv[3];
+	char slave[256];
+	int sfd;
+
+	argv[0] = "rc";
+	argv[1] = "-i";
+	argv[2] = 0;
+
+	getpts(fd, slave);
+        switch(pid = fork()) {
+	case 0:
+		putenv("TERM=9term");
+		close(fd[1]);
+		setsid();
+		sfd = open(slave, ORDWR);
+	//	ioctl(sfd, I_PUSH, "ptem");
+	//	ioctl(sfd, I_PUSH, "ldterm");
+		dup(sfd, 0);
+		dup(sfd, 1);
+		dup(sfd, 2);
+		execvp(argv[0], argv);
+		break;
+	case -1:
+		fatal("proc failed: %r");
+		break;
+	}
+	close(fd[0]);
+	fd[0] = fd[1];
+
+	rcpid = pid;
+}
+
+void
+tcheck(void)
+{
+	Frame *f;
+		
+	f = t.f;
+
+	assert(t.q0 <= t.q1 && t.q1 <= t.nr);
+	assert(t.org <= t.nr && t.qh <= t.nr);
+	assert(f->p0 <= f->p1 && f->p1 <= f->nchars);
+	assert(t.org + f->nchars <= t.nr);
+	assert(t.org+f->nchars==t.nr || (f->nlines >= f->maxlines));
+}
+
+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;
+}
+
+void
+scrdraw(void)
+{
+	Rectangle r, r1, r2;
+	static Image *scrx;
+	
+	r = scrollr;
+	r.min.x += 1;	/* border between margin and bar */
+	r1 = r;
+	if(scrx==0 || scrx->r.max.y < r.max.y){
+		if(scrx)
+			freeimage(scrx);
+		scrx = allocimage(display, Rect(0, 0, 32, r.max.y), screen->chan, 1, DPaleyellow);
+		if(scrx == 0)
+			fatal("scroll balloc");
+	}
+	r1.min.x = 0;
+	r1.max.x = Dx(r);
+	r2 = scrpos(r1, t.org, t.org+t.f->nchars, t.nr);
+	if(!eqrect(r2, lastsr)){
+		lastsr = r2;
+		draw(scrx, r1, cols[BORD], nil, ZP);
+		draw(scrx, r2, cols[BACK], nil, r2.min);
+//		r2 = r1;
+//		r2.min.x = r2.max.x-1;
+//		draw(scrx, r2, cols[BORD], nil, ZP);
+		draw(screen, r, scrx, nil, r1.min);
+	}
+}
+
+Rectangle
+scrpos(Rectangle r, ulong p0, ulong p1, ulong tot)
+{
+	long h;
+	Rectangle q;
+
+	q = insetrect(r, 1);
+	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
+scroll(int but)
+{
+	uint p0, oldp0;
+	Rectangle s;
+	int x, y, my, h, first, exact;
+
+	s = insetrect(scrollr, 1);
+	h = s.max.y-s.min.y;
+	x = (s.min.x+s.max.x)/2;
+	oldp0 = ~0;
+	first = 1;
+	do{
+		if(t.m.xy.x<s.min.x || s.max.x<=t.m.xy.x){
+			readmouse(mc);
+			t.m = mc->m;
+		}else{
+			my = t.m.xy.y;
+			if(my < s.min.y)
+				my = s.min.y;
+			if(my >= s.max.y)
+				my = s.max.y;
+//			if(!eqpt(t.m.xy, Pt(x, my)))
+//				cursorset(Pt(x, my));
+			exact = 1;
+			if(but == 2){
+				y = my;
+				if(y > s.max.y-2)
+					y = s.max.y-2;
+				if(t.nr > 1024*1024)
+					p0 = ((t.nr>>10)*(y-s.min.y)/h)<<10;
+				else
+					p0 = t.nr*(y-s.min.y)/h;
+				exact = 0;
+			} else if(but == 1)
+				p0 = backnl(t.org, (my-s.min.y)/font->height);
+			else 
+				p0 = t.org+frcharofpt(t.f, Pt(s.max.x, my));
+
+			if(oldp0 != p0)
+				setorigin(p0, exact);
+			oldp0 = p0;
+			scrdraw();
+			readmouse(mc);
+			t.m = mc->m;
+		}
+	}while(t.m.buttons & (1<<(but-1)));
+}
+
+void
+plumbstart(void)
+{
+	char buf[256];
+	snprint(buf, sizeof buf,  "%s/mnt/plumb", getenv("HOME"));
+	if((plumbfd = plumbopen(buf, OWRITE)) < 0)
+		fatal("plumbopen");
+}
+
+void
+plumb(uint q0, uint q1)
+{
+	Plumbmsg *pm;
+	char *p;
+	int i, p0, n;
+	char cbuf[100];
+
+	pm = malloc(sizeof(Plumbmsg));
+	pm->src = strdup("9term");
+	pm->dst = 0;
+	pm->wdir = strdup(wdir);
+	pm->type = strdup("text");
+	if(q1 > q0)
+		pm->attr = nil;
+	else{
+		p0 = q0;
+		plumbclick(&q0, &q1);
+		sprint(cbuf, "click=%d", p0-q0);
+		pm->attr = plumbunpackattr(cbuf);
+	}
+	if(q0==q1){
+		plumbfree(pm);
+		return;
+	}
+	pm->data = malloc(SnarfSize);
+	n = q1 - q0;
+	for(i=0,p=pm->data; i<n && p < pm->data + SnarfSize-UTFmax; i++)
+		p += runetochar(p, t.r+q0+i);
+	*p = '\0';
+	pm->ndata = strlen(pm->data);
+	plumbsend(plumbfd, pm);
+	plumbfree(pm);
+}
+
+/*
+ * Process in-band messages about window title changes.
+ * The messages are of the form:
+ *
+ *	\033];xxx\007
+ *
+ * where xxx is the new directory.  This format was chosen
+ * because it changes the label on xterm windows.
+ */
+int
+label(Rune *sr, int n)
+{
+	Rune *sl, *el, *er, *r;
+
+	er = sr+n;
+	for(r=er-1; r>=sr; r--)
+		if(*r == '\007')
+			break;
+	if(r < sr)
+		return n;
+
+	el = r+1;
+	if(el-sr > sizeof wdir)
+		sr = el - sizeof wdir;
+	for(sl=el-3; sl>=sr; sl--)
+		if(sl[0]=='\033' && sl[1]==']' && sl[2]==';')
+			break;
+	if(sl < sr)
+		return n;
+
+	snprint(wdir, sizeof wdir, "%.*S", (el-1)-(sl+3), sl+3);
+	drawsetlabel(display, wdir);
+
+	runemove(sl, el, er-el);
+	n -= (el-sl);
+	return n;
+}
+
diff --git a/src/cmd/9term/9term.h b/src/cmd/9term/9term.h
new file mode 100644
index 0000000..be2fa4c
--- /dev/null
+++ b/src/cmd/9term/9term.h
@@ -0,0 +1,120 @@
+#include <u.h>
+#include <libc.h>
+#include <ctype.h>
+#include <draw.h>
+#include <thread.h>
+#include <mouse.h>
+#include <cursor.h>
+#include <keyboard.h>
+#include <frame.h>
+#include <plumb.h>
+#include <termios.h>
+
+#define fatal	sysfatal
+
+typedef struct Text	Text;
+typedef struct Readbuf	Readbuf;
+
+enum
+{
+	/* these are chosen to use malloc()'s properties well */
+	HiWater	= 640000,	/* max size of history */
+	LoWater	= 330000,	/* min size of history after max'ed */
+};
+
+/* various geometric paramters */
+enum
+{
+	Scrollwid 	= 12,		/* width of scroll bar */
+	Scrollgap 	= 4,		/* gap right of scroll bar */
+	Maxtab		= 4,
+};
+
+enum
+{
+	Cut,
+	Paste,
+	Snarf,
+	Send,
+	Scroll,
+	Plumb,
+};
+
+
+#define	SCROLLKEY	Kdown
+#define	ESC		0x1B
+#define CUT		0x18	/* ctrl-x */		
+#define COPY		0x03	/* crtl-c */
+#define PASTE		0x16	/* crtl-v */
+#define BACKSCROLLKEY	Kup
+
+#define	READBUFSIZE 8192
+
+struct Text
+{
+	Frame		*f;		/* frame ofr terminal */
+	Mouse		m;
+	uint		nr;		/* num of runes in term */
+	Rune		*r;		/* runes for term */
+	uint		nraw;		/* num of runes in raw buffer */
+	Rune		*raw;		/* raw buffer */
+	uint		org;		/* first rune on the screen */
+	uint		q0;		/* start of selection region */
+	uint		q1;		/* end of selection region */
+	uint		qh;		/* unix point */
+	int		npart;		/* partial runes read from console */
+	char		part[UTFmax];	
+	int		nsnarf;		/* snarf buffer */
+	Rune		*snarf;
+};
+
+struct Readbuf
+{
+	short	n;				/* # bytes in buf */
+	uchar	data[READBUFSIZE];		/* data bytes */
+};
+
+void	mouse(void);
+void	domenu2(int);
+void	loop(void);
+void	geom(void);
+void	fill(void);
+void	tcheck(void);
+void	updatesel(void);
+void	doreshape(void);
+void	rcstart(int fd[2]);
+void	runewrite(Rune*, int);
+void	consread(void);
+void	conswrite(char*, int);
+int	bswidth(Rune c);
+void	cut(void);
+void	paste(Rune*, int, int);
+void	snarfupdate(void);
+void	snarf(void);
+void	show(uint);
+void	key(Rune);
+void	setorigin(uint org, int exact);
+uint	line2q(uint);
+uint	backnl(uint, uint);
+int	cansee(uint);
+uint	backnl(uint, uint);
+void	addraw(Rune*, int);
+void	mselect(void);
+void	doubleclick(uint *q0, uint *q1);
+int	clickmatch(int cl, int cr, int dir, uint *q);
+Rune	*strrune(Rune *s, Rune c);
+int	consready(void);
+Rectangle scrpos(Rectangle r, ulong p0, ulong p1, ulong tot);
+void	scrdraw(void);
+void	scroll(int);
+void	hostproc(void *arg);
+void	hoststart(void);
+void	pdx(int, char*, int);
+void	plumbstart(void);
+void	plumb(uint, uint);
+void	plumbclick(uint*, uint*);
+int	getpts(int fd[], char *slave);
+
+#define	runemalloc(n)		malloc((n)*sizeof(Rune))
+#define	runerealloc(a, n)	realloc(a, (n)*sizeof(Rune))
+#define	runemove(a, b, n)	memmove(a, b, (n)*sizeof(Rune))
diff --git a/src/cmd/9term/FreeBSD.c b/src/cmd/9term/FreeBSD.c
new file mode 100644
index 0000000..37dabe9
--- /dev/null
+++ b/src/cmd/9term/FreeBSD.c
@@ -0,0 +1 @@
+#include "Linux.c"
diff --git a/src/cmd/9term/Linux.c b/src/cmd/9term/Linux.c
new file mode 100644
index 0000000..7cdb513
--- /dev/null
+++ b/src/cmd/9term/Linux.c
@@ -0,0 +1,22 @@
+#include "9term.h"
+
+void
+pdx(int pid, char *wdir, int bufn)
+{
+	char path[256];
+	int n;
+
+	snprint(path, sizeof path, "/proc/%d/cwd", pid);
+	n = readlink(path, wdir, bufn);
+	if(n < 0)
+		n = 0;
+	wdir[n] = '\0';
+}
+
+int
+getpts(int fd[], char *slave)
+{
+
+	openpty(&fd[1], &fd[0], slave, 0, 0);
+	return 0;
+}
diff --git a/src/cmd/9term/mkfile b/src/cmd/9term/mkfile
new file mode 100644
index 0000000..d524605
--- /dev/null
+++ b/src/cmd/9term/mkfile
@@ -0,0 +1,14 @@
+PLAN9=../../..
+<$PLAN9/src/mkhdr
+
+TARG=9term
+
+OFILES=\
+	9term.$O\
+	$SYSNAME.$O\
+
+<$PLAN9/src/mkone
+
+LDFLAGS=-lframe -ldraw -lplumb -lthread -l9 -lfmt -lutf -L$X11/lib -lX11 -lutil
+
+