shithub: gopher

Download patch

ref: c9c50ad0e0ef6e035fb54386909e25169ddcb9c9
author: telephil9 <telephil9@gmail.com>
date: Sat Apr 25 16:17:05 EDT 2020

initial import

diff: cannot open b/libpanel//null: file does not exist: 'b/libpanel//null'
--- /dev/null
+++ b/README.md
@@ -1,0 +1,15 @@
+Usage:
+------
+Install with ``mk install``
+Run with ``gopher [address]``
+
+gopher
+=======
+A mostly functional gopher browser for plan9.
+
+![gopher](gopher.png)
+
+Text and submenu items are displayed within the browser, whereas images and documents are opened through page and HTML items sent to the plumber.
+Other item types (bin, dos, uuencoded files, etc.) are not implemented yet.
+
+This has not been thoroughly tested so many bugs are just waiting to be found.
--- /dev/null
+++ b/dat.h
@@ -1,0 +1,46 @@
+typedef struct Gmenu Gmenu;
+typedef struct Link Link;
+typedef struct Hist Hist;
+
+struct Gmenu
+{
+	Link	*link;
+	Rtext	*text;
+};
+
+struct Link
+{
+	char *addr;
+	char *sel;
+	int  type;
+};
+
+struct Hist
+{
+	Hist *p;
+	Hist *n;
+	Gmenu *m;
+};
+
+enum
+{
+	Ttext,
+	Tmenu,
+	Tns,
+	Terror,
+	Tbinhex,
+	Tdos,
+	Tuuencoded,
+	Tsearch,
+	Ttelnet,
+	Tbinary,
+	Tmirror,
+	Tgif,
+	Timage,
+	Tt3270,
+	Tdoc,
+	Thtml,
+	Tinfo,
+	Tsound,
+	Teof
+};
--- /dev/null
+++ b/gopher.c
@@ -1,0 +1,505 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <keyboard.h>
+#include <panel.h>
+#include <bio.h>
+#include <plumb.h>
+#include "dat.h"
+#include "icons.h"
+
+void texthit(Panel *p, int b, Rtext *t);
+
+Image *backi;
+Image *fwdi;
+Panel *root;
+Panel *backp;
+Panel *fwdp;
+Panel *urlp;
+Panel *textp;
+Panel *statusp;
+char *url;
+Mouse *mouse;
+Hist *hist = nil;
+
+Gmenu*
+mkmenu(Link *l)
+{
+	Gmenu *m;
+
+	m = malloc(sizeof *m);
+	if(m==nil)
+		sysfatal("malloc: %r");
+	m->link = l;
+	m->text = nil;
+	return m;
+}
+
+Link*
+mklink(char *addr, char *sel, int type)
+{
+	Link *l;
+
+	l = malloc(sizeof *l);
+	if(l==nil)
+		sysfatal("malloc: %r");
+	l->addr = strdup(addr);
+	l->sel  = sel!=nil ? strdup(sel) : nil;
+	l->type = type;
+	return l;
+}
+
+int
+seltype(char c)
+{
+	int t;
+
+	t = Tinfo;
+	switch(c){
+	case '0': t = Ttext; break;
+	case '1': t = Tmenu; break;
+	case '2': t = Tns; break;
+	case '3': t = Terror; break;
+	case '4': t = Tbinhex; break;
+	case '5': t = Tdos; break;
+	case '6': t = Tuuencoded; break;
+	case '7': t = Tsearch; break;
+	case '8': t = Ttelnet; break;
+	case '9': t = Tbinary; break;
+	case '+': t = Tmirror; break;
+	case 'g': t = Tgif; break;
+	case 'I': t = Timage; break;
+	case 'T': t = Tt3270; break;
+	case 'd': t = Tdoc; break;
+	case 'h': t = Thtml; break;
+	case 'i': t = Tinfo; break;
+	case 's': t = Tsound; break;
+	case '.': t = Teof; break;
+	default:
+		fprint(2, "unknown seltype '%c'\n", c);
+		break;
+	}
+	return t;
+}
+
+char*
+seltypestr(int type)
+{
+	static char *Typestr[] = {
+		"FILE", "DIR", "NS", "ERR", "HEX",
+		"DOS", "UU", "?", "TELNET", "BIN",
+		"MIRROR", "GIF", "IMG", "T3270", "DOC",
+		"HTML", "", "SND", "EOF",
+	};
+	return smprint("%6s", Typestr[type]);
+};
+
+
+Gmenu*
+rendermenu(Link *l, Biobuf *bp)
+{
+	char *s, *f[5], *t;
+	Gmenu *m;
+	Link *n;
+	int type;
+
+	m = malloc(sizeof *m);
+	if(m==nil)
+		sysfatal("malloc: %r");
+	m->link = l;
+	m->text = nil;
+	plrtstr(&m->text, 1000000, 0, 0, font, " ", 0, 0);
+	for(;;){
+		n = nil;
+		s = Brdstr(bp, '\n', 0);
+		if(s==nil || s[0]=='.')
+			break;
+		type = seltype(s[0]);
+		getfields(s+1, f, 5, 0, "\t\r\n");
+		switch(type){
+		case Tinfo:
+			break;
+		case Thtml:
+			n = mklink(f[1]+4, nil, Thtml); /* +4 skip URL: */
+			break;
+		default:
+			n = mklink(netmkaddr(f[2], "tcp", f[3]), f[1], type);
+			break;
+		}
+		t = strdup(f[0]);
+		plrtstr(&m->text, 1000000, 8, 0, font, seltypestr(type), PL_HEAD, 0);
+		if(type == Tinfo)
+			plrtstr(&m->text, 8, 0, 0, font, t, 0, 0);
+		else
+			plrtstr(&m->text, 8, 0, 0, font, t, PL_HOT, n);
+
+	}
+	return m;
+}
+
+Gmenu*
+rendertext(Link *l, Biobuf *bp)
+{
+	Gmenu *m;
+	char *s;
+	int n;
+
+	m = malloc(sizeof *m);
+	if(m==nil)
+		sysfatal("malloc: %r");
+	m->link = l;
+	m->text = nil;
+	plrtstr(&m->text, 1000000, 0, 0, font, " ", 0, 0);
+	for(;;){
+		s = Brdstr(bp, '\n', 0);
+		if(s==nil || s[0]=='.')
+			break;
+		n = Blinelen(bp);
+		s[n-1] = 0;
+		if(s[n-2]=='\r')
+			s[n-2] = 0;
+		if(s[0]=='\t'){
+			plrtstr(&m->text, 1000000, 8, 0, font, "    ", 0, 0);
+			plrtstr(&m->text, 4, 0, 0, font, s+1, 0, 0);
+		}else
+			plrtstr(&m->text, 1000000, 8, 0, font, s, 0, 0);
+	}
+	return m;
+}
+
+Gmenu*
+render(Link *l)
+{
+	int fd;
+	Biobuf *bp;
+	Gmenu *m;
+
+	fd = dial(l->addr, 0, 0, 0);
+	if(fd < 0)
+		sysfatal("dial: %r");
+	fprint(fd, "%s\r\n", l->sel);
+	bp = Bfdopen(fd, OREAD);
+	if(bp==nil){
+		close(fd);
+		sysfatal("bfdopen: %r");
+	}
+	switch(l->type){
+	case Tmenu:
+		m = rendermenu(l, bp);
+		break;
+	case Ttext:
+		m = rendertext(l, bp);
+		break;
+	default:
+		/* TODO error */
+		m = nil;
+		break;
+	}
+	Bterm(bp);
+	close(fd);
+	return m;
+}
+
+void
+show(Gmenu *m)
+{
+	plinittextview(textp, PACKE|EXPAND, ZP, m->text, texthit);
+	pldraw(textp, screen);
+}
+
+void
+visit(Link *l)
+{
+	Gmenu *m;
+	Hist *h;
+
+	m = render(l);
+	show(m);
+	h = malloc(sizeof *h);
+	if(h == nil)
+		sysfatal("malloc: %r");
+	h->p = hist;
+	h->n = nil;
+	h->m = m;
+	hist = h;
+}
+
+void
+visitaddr(char *addr)
+{
+	visit(mklink(netmkaddr(addr, "tcp", "70"), "", Tmenu));
+}
+
+void
+plumburl(char *u)
+{
+	int fd;
+
+	fd = plumbopen("send", OWRITE|OCEXEC);
+	if(fd<0)
+		return;
+	plumbsendtext(fd, "gopher", nil, nil, u);
+	close(fd);
+}
+
+void
+dupfds(int fd, ...)
+{
+	int mfd, n, i;
+	va_list arg;
+	Dir *dir;
+
+	va_start(arg, fd);
+	for(mfd = 0; fd >= 0; fd = va_arg(arg, int), mfd++)
+		if(fd != mfd)
+			if(dup(fd, mfd) < 0)
+				sysfatal("dup: %r");
+	va_end(arg);
+	if((fd = open("/fd", OREAD)) < 0)
+		sysfatal("open: %r");
+	n = dirreadall(fd, &dir);
+	for(i=0; i<n; i++){
+		if(strstr(dir[i].name, "ctl"))
+			continue;
+		fd = atoi(dir[i].name);
+		if(fd >= mfd)
+			close(fd);
+	}
+	free(dir);
+}
+
+void
+page(Link *l)
+{
+	int fd;
+
+	fd = dial(l->addr, 0, 0, 0);
+	if(fd < 0)
+		sysfatal("dial: %r");
+	fprint(fd, "%s\r\n", l->sel);	
+	switch(rfork(RFFDG|RFPROC|RFMEM|RFREND|RFNOWAIT|RFNOTEG)){
+	case -1:
+		fprint(2, "Can't fork!");
+		break;
+	case 0:
+		dupfds(fd, 1, 2, -1);
+		execl("/bin/rc", "rc", "-c", "page -w", nil);
+		_exits(0);
+	}
+	close(fd);
+}
+
+void
+texthit(Panel *p, int b, Rtext *t)
+{
+	Link *l;
+	char *s, buf[1024] = {0};
+
+	USED(p);
+	if(b!=1)
+		return;
+	if(t->user==nil)
+		return;
+	l = t->user;
+	switch(l->type){
+	case Tmenu:
+	case Ttext:
+		visit(l);
+		break;
+	case Thtml:
+		plumburl(l->addr);
+		break;
+	case Tdoc:
+	case Tgif:
+	case Timage:
+		page(l);
+		break;
+	case Tsearch:
+		if(eenter("Search:", buf, sizeof buf, mouse)>0){
+			s = smprint("%s\t%s", l->sel, buf);
+			visit(mklink(l->addr, s, Tmenu));
+			free(s);
+		}
+		break;
+	}		
+}
+
+void
+backhit(Panel *p, int b)
+{
+	USED(p);
+	if(b!=1)
+		return;
+	if(hist==nil || hist->p==nil)
+		return;
+	hist->p->n = hist;
+	hist = hist->p;
+	show(hist->m);
+}
+
+void
+nexthit(Panel *p, int b)
+{
+	USED(p);
+	if(b!=1)
+		return;
+	if(hist==nil || hist->n==nil)
+		return;
+	hist = hist->n;
+	show(hist->m);
+}
+
+void
+entryhit(Panel *p, char *t)
+{
+	USED(p);
+	if(strlen(t)<=0)
+		return;
+	visitaddr(t);
+	plinitentry(p, PACKN|FILLX, 0, "", entryhit);
+	pldraw(p, screen);
+}
+
+void
+mkpanels(void)
+{
+	Panel *p, *ybar, *xbar;
+
+	root = plgroup(0, EXPAND);
+	p = plframe(root, PACKN|FILLX);
+	plbutton(p, PACKW|BITMAP, backi, backhit);
+	plbutton(p, PACKW|BITMAP, fwdi, nexthit);
+	pllabel(p, PACKW, "Go:");
+	plentry(p, PACKN|FILLX, 0, "", entryhit);
+	p = plgroup(root, PACKN|EXPAND);
+	ybar = plscrollbar(p, PACKW|USERFL);
+	xbar = plscrollbar(p, IGNORE);
+	textp = pltextview(p, PACKE|EXPAND, ZP, nil, nil);
+	plscroll(textp, xbar, ybar);
+	statusp = pllabel(root, PACKN|FILLX, url);
+	plplacelabel(statusp, PLACEW);
+}
+
+void
+eresized(int new)
+{
+	if(new && getwindow(display, Refnone)<0)
+		sysfatal("cannot reattach: %r");
+	plpack(root, screen->r);
+	pldraw(root, screen);
+}
+
+Image*
+loadicon(Rectangle r, uchar *data, int ndata)
+{
+	Image *i;
+	int n;
+
+	i = allocimage(display, r, RGBA32, 0, DNofill);
+	if(i==nil)
+		sysfatal("allocimage: %r");
+	n = loadimage(i, r, data, ndata);
+	if(n<0)
+		sysfatal("loadimage: %r");
+	return i;
+}
+
+void
+loadicons(void)
+{
+	Rectangle r = Rect(0,0,16,16);
+	
+	backi = loadicon(r, ibackdata, sizeof ibackdata);
+	fwdi  = loadicon(r, ifwddata, sizeof ifwddata);
+}
+
+void scrolltext(int dy, int whence)
+{
+	Scroll s;
+
+	s = plgetscroll(textp);
+	switch(whence){
+	case 0:
+		s.pos.y = dy;
+		break;
+	case 1:
+		s.pos.y += dy;
+		break;
+	case 2:
+		s.pos.y = s.size.y+dy;
+		break;
+	}
+	if(s.pos.y > s.size.y)
+		s.pos.y = s.size.y;
+	if(s.pos.y < 0)
+		s.pos.y = 0;
+	plsetscroll(textp, s);
+	pldraw(textp, screen);
+}
+	
+void
+main(int argc, char *argv[])
+{
+	Event e;
+	char *url;
+
+	if(argc == 2)
+		url = argv[1];
+	else
+		url = "gopher.floodgap.com";
+
+	if(initdraw(nil, nil, "gopher")<0)
+		sysfatal("initdraw: %r");
+	einit(Emouse|Ekeyboard);
+	plinit(screen->depth);
+	loadicons();
+	mkpanels();
+	visitaddr(url);
+	eresized(0);
+	for(;;){
+		switch(event(&e)){
+		case Ekeyboard:
+			switch(e.kbdc){
+			default:
+				plkeyboard(e.kbdc);
+				break;
+			case Khome:
+				scrolltext(0, 0);
+				break;
+			case Kup:
+				scrolltext(-textp->size.y/4, 1);
+				break;
+			case Kpgup:
+				scrolltext(-textp->size.y/2, 1);
+				break;
+			case Kdown:
+				scrolltext(textp->size.y/4, 1);
+				break;
+			case Kpgdown:
+				scrolltext(textp->size.y/2, 1);
+				break;
+			case Kend:
+				scrolltext(-textp->size.y, 2);
+				break;
+			case Kdel:
+				exits(nil);
+				break;
+			}
+			break;
+		case Emouse:
+			mouse = &e.mouse;
+			if(mouse->buttons & (8|16) && ptinrect(mouse->xy, textp->r)){
+				if(mouse->buttons & 8)
+					scrolltext(textp->r.min.y - mouse->xy.y, 1);
+				else
+					scrolltext(mouse->xy.y - textp->r.min.y, 1);
+				break;
+			}
+			plmouse(root, mouse);
+			pldraw(textp, screen);
+			break;
+		}
+	}
+}
+
binary files /dev/null b/gopher.png differ
--- /dev/null
+++ b/icons.h
@@ -1,0 +1,133 @@
+uchar ibackdata[] = {
+	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, 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, 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, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00,
+	0x30, 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, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00, 0xfb, 0x00, 0x00, 0x00,
+	0x4e, 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, 0x00, 0x00, 0x00,
+	0x28, 0x00, 0x00, 0x00, 0xcb, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00,
+	0x4e, 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, 0x02, 0x00, 0x00, 0x00, 0x6c, 0x00, 0x00, 0x00,
+	0xf5, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00,
+	0xfd, 0x00, 0x00, 0x00, 0xfd, 0x00, 0x00, 0x00, 0xfd, 0x00, 0x00, 0x00, 0xfb, 0x00, 0x00, 0x00,
+	0xfb, 0x00, 0x00, 0x00, 0xfb, 0x00, 0x00, 0x00, 0xfb, 0x00, 0x00, 0x00, 0xf1, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00, 0xb9, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00,
+	0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00,
+	0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00,
+	0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x00, 0x00,
+	0x54, 0x00, 0x00, 0x00, 0xed, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00,
+	0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00,
+	0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00,
+	0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x00, 0x00,
+	0x58, 0x00, 0x00, 0x00, 0xef, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00,
+	0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00,
+	0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00,
+	0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0xbd, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00,
+	0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00,
+	0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00,
+	0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x72, 0x00, 0x00, 0x00,
+	0xf7, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00,
+	0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00,
+	0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	0x2c, 0x00, 0x00, 0x00, 0xcf, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00,
+	0x52, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00,
+	0x04, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x85, 0x00, 0x00, 0x00, 0xfb, 0x00, 0x00, 0x00,
+	0x4e, 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, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00,
+	0x32, 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, 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, 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, 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,
+};
+
+uchar ifwddata[] = {
+	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, 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, 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, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00,
+	0x36, 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, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4e, 0x00, 0x00, 0x00,
+	0xfb, 0x00, 0x00, 0x00, 0x81, 0x00, 0x00, 0x00, 0x04, 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, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4e, 0x00, 0x00, 0x00,
+	0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xcb, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	0xf1, 0x00, 0x00, 0x00, 0xfb, 0x00, 0x00, 0x00, 0xfb, 0x00, 0x00, 0x00, 0xfb, 0x00, 0x00, 0x00,
+	0xfb, 0x00, 0x00, 0x00, 0xfd, 0x00, 0x00, 0x00, 0xfd, 0x00, 0x00, 0x00, 0xfd, 0x00, 0x00, 0x00,
+	0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xf5, 0x00, 0x00, 0x00,
+	0x6c, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	0xf7, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00,
+	0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00,
+	0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00,
+	0xff, 0x00, 0x00, 0x00, 0xb9, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	0xf7, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00,
+	0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00,
+	0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00,
+	0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xed, 0x00, 0x00, 0x00, 0x54, 0x00, 0x00, 0x00,
+	0xf7, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00,
+	0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00,
+	0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00,
+	0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xef, 0x00, 0x00, 0x00, 0x5a, 0x00, 0x00, 0x00,
+	0xf7, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00,
+	0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00,
+	0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00,
+	0xff, 0x00, 0x00, 0x00, 0xbf, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	0xf7, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00,
+	0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00,
+	0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x00, 0x00,
+	0x72, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00,
+	0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x52, 0x00, 0x00, 0x00,
+	0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xcf, 0x00, 0x00, 0x00, 0x2c, 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, 0x00, 0x00, 0x00, 0x4e, 0x00, 0x00, 0x00,
+	0xfb, 0x00, 0x00, 0x00, 0x87, 0x00, 0x00, 0x00, 0x06, 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, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00,
+	0x3a, 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, 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, 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, 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,
+};
--- /dev/null
+++ b/libpanel/button.c
@@ -1,0 +1,189 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <panel.h>
+#include "pldefs.h"
+typedef struct Button Button;
+struct Button{
+	int btype;			/* button type */
+	Icon *icon;			/* what to write on the button */
+	int check;			/* for check/radio buttons */
+	void (*hit)(Panel *, int, int);	/* call back user code on check/radio hit */
+	void (*menuhit)(int, int);	/* call back user code on menu item hit */
+	void (*pl_buttonhit)(Panel *, int);	/* call back user code on button hit */
+	int index;			/* arg to menuhit */
+	int buttons;
+};
+/*
+ * Button types
+ */
+#define	BUTTON	1
+#define	CHECK	2
+#define	RADIO	3
+void pl_drawbutton(Panel *p){
+	Rectangle r;
+	Button *bp;
+	bp=p->data;
+	r=pl_box(p->b, p->r, p->state);
+	switch(bp->btype){
+	case CHECK:
+		r=pl_check(p->b, r, bp->check);
+		break;
+	case RADIO:
+		r=pl_radio(p->b, r, bp->check);
+		break;
+	}
+	pl_drawicon(p->b, r, PLACECEN, p->flags, bp->icon);
+}
+int pl_hitbutton(Panel *p, Mouse *m){
+	int oldstate, hitme;
+	Panel *sib;
+	Button *bp;
+	bp=p->data;
+	oldstate=p->state;
+	if(m->buttons&OUT){
+		hitme=0;
+		p->state=UP;
+	}
+	else if(m->buttons&7){
+		hitme=0;
+		p->state=DOWN;
+		bp->buttons=m->buttons;
+	}
+	else{	/* mouse inside, but no buttons down */
+		hitme=p->state==DOWN;
+		p->state=UP;
+	}
+	if(hitme) switch(bp->btype){
+	case CHECK:
+		if(hitme) bp->check=!bp->check;
+		break;
+	case RADIO:
+		if(bp->check) bp->check=0;
+		else{
+			if(p->parent){
+				for(sib=p->parent->child;sib;sib=sib->next){
+					if(sib->hit==pl_hitbutton
+					&& ((Button *)sib->data)->btype==RADIO
+					&& ((Button *)sib->data)->check){
+						((Button *)sib->data)->check=0;
+						pldraw(sib, p->b);
+					}
+				}
+			}
+			bp->check=1;
+		}
+		break;
+	}
+	if(hitme || oldstate!=p->state) pldraw(p, p->b);
+	if(hitme && bp->hit){
+		bp->hit(p, bp->buttons, bp->check);
+		p->state=UP;
+	}
+	return 0;
+}
+void pl_typebutton(Panel *g, Rune c){
+	USED(g, c);
+}
+Point pl_getsizebutton(Panel *p, Point children){
+	Point s;
+	int ckw;
+	Button *bp;
+	USED(children);		/* shouldn't have any children */
+	bp=p->data;
+	s=pl_iconsize(p->flags, bp->icon);
+	if(bp->btype!=BUTTON){
+		ckw=pl_ckwid();
+		if(s.y<ckw){
+			s.x+=ckw;
+			s.y=ckw;
+		}
+		else s.x+=s.y;
+	}
+	return pl_boxsize(s, p->state);
+}
+void pl_childspacebutton(Panel *g, Point *ul, Point *size){
+	USED(g, ul, size);
+}
+void pl_initbtype(Panel *v, int flags, Icon *icon, void (*hit)(Panel *, int, int), int btype){
+	Button *bp;
+	bp=v->data;
+	v->flags=flags|LEAF;
+	v->state=UP;
+	v->draw=pl_drawbutton;
+	v->hit=pl_hitbutton;
+	v->type=pl_typebutton;
+	v->getsize=pl_getsizebutton;
+	v->childspace=pl_childspacebutton;
+	bp->btype=btype;
+	bp->check=0;
+	bp->hit=hit;
+	bp->icon=icon;
+	switch(btype){
+	case BUTTON: v->kind="button"; break;
+	case CHECK:  v->kind="checkbutton"; break;
+	case RADIO:  v->kind="radiobutton"; break;
+	}
+}
+void pl_buttonhit(Panel *p, int buttons, int check){
+	USED(check);
+	if(((Button *)p->data)->pl_buttonhit) ((Button *)p->data)->pl_buttonhit(p, buttons);
+}
+void plinitbutton(Panel *p, int flags, Icon *icon, void (*hit)(Panel *, int)){
+	((Button *)p->data)->pl_buttonhit=hit;
+	pl_initbtype(p, flags, icon, pl_buttonhit, BUTTON);
+}
+void plinitcheckbutton(Panel *p, int flags, Icon *icon, void (*hit)(Panel *, int, int)){
+	pl_initbtype(p, flags, icon, hit, CHECK);
+}
+void plinitradiobutton(Panel *p, int flags, Icon *icon, void (*hit)(Panel *, int, int)){
+	pl_initbtype(p, flags, icon, hit, RADIO);
+}
+Panel *plbutton(Panel *parent, int flags, Icon *icon, void (*hit)(Panel *, int)){
+	Panel *p;
+	p=pl_newpanel(parent, sizeof(Button));
+	plinitbutton(p, flags, icon, hit);
+	return p;
+}
+Panel *plcheckbutton(Panel *parent, int flags, Icon *icon, void (*hit)(Panel *, int, int)){
+	Panel *p;
+	p=pl_newpanel(parent, sizeof(Button));
+	plinitcheckbutton(p, flags, icon, hit);
+	return p;
+}
+Panel *plradiobutton(Panel *parent, int flags, Icon *icon, void (*hit)(Panel *, int, int)){
+	Panel *p;
+	p=pl_newpanel(parent, sizeof(Button));
+	plinitradiobutton(p, flags, icon, hit);
+	return p;
+}
+void pl_hitmenu(Panel *p, int buttons){
+	void (*hit)(int, int);
+	hit=((Button *)p->data)->menuhit;
+	if(hit) hit(buttons, ((Button *)p->data)->index);
+}
+void plinitmenu(Panel *v, int flags, Icon **item, int cflags, void (*hit)(int, int)){
+	Panel *b;
+	int i;
+	v->flags=flags;
+	v->kind="menu";
+	if(v->child){
+		plfree(v->child);
+		v->child=0;
+	}
+	for(i=0;item[i];i++){
+		b=plbutton(v, cflags, item[i], pl_hitmenu);
+		((Button *)b->data)->menuhit=hit;
+		((Button *)b->data)->index=i;
+	}
+}
+Panel *plmenu(Panel *parent, int flags, Icon **item, int cflags, void (*hit)(int, int)){
+	Panel *v;
+	v=plgroup(parent, flags);
+	plinitmenu(v, flags, item, cflags, hit);
+	return v;
+}
+void plsetbutton(Panel *p, int val){
+	((Button *)p->data)->check=val;
+}
--- /dev/null
+++ b/libpanel/canvas.c
@@ -1,0 +1,51 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <panel.h>
+#include "pldefs.h"
+typedef struct Canvas Canvas;
+struct Canvas{
+	void (*draw)(Panel *);
+	void (*hit)(Panel *, Mouse *);
+};
+void pl_drawcanvas(Panel *p){
+	Canvas *c;
+	c=p->data;
+	if(c->draw) c->draw(p);
+}
+int pl_hitcanvas(Panel *p, Mouse *m){
+	Canvas *c;
+	c=p->data;
+	if(c->hit) c->hit(p, m);
+	return 0;
+}
+void pl_typecanvas(Panel *p, Rune c){
+	USED(p, c);
+}
+Point pl_getsizecanvas(Panel *p, Point children){
+	USED(p, children);
+	return Pt(0,0);
+}
+void pl_childspacecanvas(Panel *p, Point *ul, Point *size){
+	USED(p, ul, size);
+}
+void plinitcanvas(Panel *v, int flags, void (*draw)(Panel *), void (*hit)(Panel *, Mouse *)){
+	Canvas *c;
+	v->flags=flags|LEAF;
+	v->draw=pl_drawcanvas;
+	v->hit=pl_hitcanvas;
+	v->type=pl_typecanvas;
+	v->getsize=pl_getsizecanvas;
+	v->childspace=pl_childspacecanvas;
+	v->kind="canvas";
+	c=v->data;
+	c->draw=draw;
+	c->hit=hit;
+}
+Panel *plcanvas(Panel *parent, int flags, void (*draw)(Panel *), void (*hit)(Panel *, Mouse *)){
+	Panel *p;
+	p=pl_newpanel(parent, sizeof(Canvas));
+	plinitcanvas(p, flags, draw, hit);
+	return p;
+}
--- /dev/null
+++ b/libpanel/draw.c
@@ -1,0 +1,288 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <panel.h>
+#include "pldefs.h"
+#define	PWID	1	/* width of label border */
+#define	BWID	1	/* width of button relief */
+#define	FWID	2	/* width of frame relief */
+#define	SPACE	1	/* space inside relief of button or frame */
+#define	CKSIZE	3	/* size of check mark */
+#define	CKSPACE	2	/* space around check mark */
+#define	CKWID	1	/* width of frame around check mark */
+#define	CKINSET	1	/* space around check mark frame */
+#define	CKBORDER 2	/* space around X inside frame */
+static int plldepth;
+static Image *pl_white, *pl_light, *pl_dark, *pl_black, *pl_hilit;
+int pl_drawinit(int ldepth){
+	plldepth=ldepth;
+	pl_white=allocimage(display, Rect(0,0,1,1), screen->chan, 1, 0xFFFFFFFF);
+	//pl_light=allocimagemix(display, DPalebluegreen, DWhite);
+	pl_light=display->white;
+	//pl_dark =allocimage(display, Rect(0,0,1,1), screen->chan, 1, DPurpleblue);
+	pl_dark = allocimage(display, Rect(0, 0, 1, 1), screen->chan, 1, 0X999999FF);
+	pl_black=allocimage(display, Rect(0,0,1,1), screen->chan, 1, 0x000000FF);
+	pl_hilit=allocimage(display, Rect(0,0,1,1), CHAN1(CAlpha,8), 1, 0x80);
+	if(pl_white==0 || pl_light==0 || pl_black==0 || pl_dark==0) return 0;
+	return 1;
+}
+void pl_relief(Image *b, Image *ul, Image *lr, Rectangle r, int wid){
+	int x, y;
+	draw(b, Rect(r.min.x, r.max.y-wid, r.max.x, r.max.y), lr, 0, ZP); /* bottom */
+	draw(b, Rect(r.max.x-wid, r.min.y, r.max.x, r.max.y), lr, 0, ZP); /* right */
+	draw(b, Rect(r.min.x, r.min.y, r.min.x+wid, r.max.y), ul, 0, ZP); /* left */
+	draw(b, Rect(r.min.x, r.min.y, r.max.x, r.min.y+wid), ul, 0, ZP); /* top */
+	for(x=0;x!=wid;x++) for(y=wid-1-x;y!=wid;y++){
+		draw(b, rectaddpt(Rect(0,0,1,1), Pt(x+r.max.x-wid, y+r.min.y)), lr, 0, ZP);
+		draw(b, rectaddpt(Rect(0,0,1,1), Pt(x+r.min.x, y+r.max.y-wid)), lr, 0, ZP);
+	}
+}
+Rectangle pl_boxoutline(Image *b, Rectangle r, int style, int fill){
+	if(plldepth==0) switch(style){
+	case UP:
+		pl_relief(b, pl_black, pl_black, r, BWID);
+		r=insetrect(r, BWID);
+		if(fill) draw(b, r, pl_white, 0, ZP);
+		else border(b, r, SPACE, pl_white, ZP);
+		break;
+	case DOWN:
+	case DOWN1:
+	case DOWN2:
+	case DOWN3:
+		pl_relief(b, pl_black, pl_black, r, BWID);
+		r=insetrect(r, BWID);
+		if(fill) draw(b, r, pl_black, 0, ZP);
+		border(b, r, SPACE, pl_black, ZP);
+		break;
+	case PASSIVE:
+		if(fill) draw(b, r, pl_white, 0, ZP);
+		r=insetrect(r, PWID);
+		if(!fill) border(b, r, SPACE, pl_white, ZP);
+		break;
+	case FRAME:
+		pl_relief(b, pl_white, pl_black, r, FWID);
+		r=insetrect(r, FWID);
+		pl_relief(b, pl_black, pl_white, r, FWID);
+		r=insetrect(r, FWID);
+		if(fill) draw(b, r, pl_white, 0, ZP);
+		else border(b, r, SPACE, pl_white, ZP);
+		break;
+	}
+	else switch(style){
+	case UP:
+		/*
+		pl_relief(b, pl_white, pl_black, r, BWID);
+		r=insetrect(r, BWID);
+		if(fill) draw(b, r, pl_light, 0, ZP);
+		else border(b, r, SPACE, pl_white, ZP);
+		*/
+		draw(b, r, pl_light, 0, ZP);
+		border(b, r, 1, pl_black, ZP);
+		break;
+	case DOWN:
+	case DOWN1:
+	case DOWN2:
+	case DOWN3:
+		pl_relief(b, pl_black, pl_white, r, BWID);
+		r=insetrect(r, BWID);
+		if(fill) draw(b, r, pl_dark, 0, ZP);
+		else border(b, r, SPACE, pl_black, ZP);
+		break;
+	case PASSIVE:
+		if(fill) draw(b, r, pl_light, 0, ZP);
+		r=insetrect(r, PWID);
+		if(!fill) border(b, r, SPACE, pl_white, ZP);
+		break;
+	case FRAME:
+		border(b, r, 1, pl_black, ZP);
+		/*
+		pl_relief(b, pl_white, pl_black, r, FWID);
+		r=insetrect(r, FWID);
+		pl_relief(b, pl_black, pl_white, r, FWID);
+		r=insetrect(r, FWID);
+		if(fill) draw(b, r, pl_light, 0, ZP);
+		else border(b, r, SPACE, pl_white, ZP);
+		*/
+		break;
+	}
+	return insetrect(r, SPACE);
+}
+Rectangle pl_outline(Image *b, Rectangle r, int style){
+	return pl_boxoutline(b, r, style, 0);
+}
+Rectangle pl_box(Image *b, Rectangle r, int style){
+	return pl_boxoutline(b, r, style, 1);
+}
+Point pl_boxsize(Point interior, int state){
+	switch(state){
+	case UP:
+	case DOWN:
+	case DOWN1:
+	case DOWN2:
+	case DOWN3:
+		return addpt(interior, Pt(2*(BWID+SPACE), 2*(BWID+SPACE)));
+	case PASSIVE:
+		return addpt(interior, Pt(2*(PWID+SPACE), 2*(PWID+SPACE)));
+	case FRAME:
+		return addpt(interior, Pt(4*FWID+2*SPACE, 4*FWID+2*SPACE));
+	}
+	return Pt(0, 0);
+}
+void pl_interior(int state, Point *ul, Point *size){
+	switch(state){
+	case UP:
+	case DOWN:
+	case DOWN1:
+	case DOWN2:
+	case DOWN3:
+		*ul=addpt(*ul, Pt(BWID+SPACE, BWID+SPACE));
+		*size=subpt(*size, Pt(2*(BWID+SPACE), 2*(BWID+SPACE)));
+		break;
+	case PASSIVE:
+		*ul=addpt(*ul, Pt(PWID+SPACE, PWID+SPACE));
+		*size=subpt(*size, Pt(2*(PWID+SPACE), 2*(PWID+SPACE)));
+		break;
+	case FRAME:
+		*ul=addpt(*ul, Pt(2*FWID+SPACE, 2*FWID+SPACE));
+		*size=subpt(*size, Pt(4*FWID+2*SPACE, 4*FWID+2*SPACE));
+	}
+}
+
+void pl_drawicon(Image *b, Rectangle r, int stick, int flags, Icon *s){
+	Rectangle save;
+	Point ul, offs;
+	ul=r.min;
+	offs=subpt(subpt(r.max, r.min), pl_iconsize(flags, s));
+	switch(stick){
+	case PLACENW:	                                break;
+	case PLACEN:	ul.x+=offs.x/2;                 break;
+	case PLACENE:	ul.x+=offs.x;                   break;
+	case PLACEW:	                ul.y+=offs.y/2; break;
+	case PLACECEN:	ul.x+=offs.x/2; ul.y+=offs.y/2; break;
+	case PLACEE:	ul.x+=offs.x;                   break;
+	case PLACESW:	                ul.y+=offs.y;   break;
+	case PLACES:	ul.x+=offs.x/2; ul.y+=offs.y;   break;
+	case PLACESE:	ul.x+=offs.x;   ul.y+=offs.y;   break;
+	}
+	save=b->clipr;
+	if(!rectclip(&r, save))
+		return;
+	replclipr(b, b->repl, r);
+	if(flags&BITMAP) draw(b, Rpt(ul, addpt(ul, pl_iconsize(flags, s))), s, 0, ZP);
+	else string(b, ul, pl_black, ZP, font, s);
+	replclipr(b, b->repl, save);
+}
+/*
+ * Place a check mark at the left end of r.  Return the unused space.
+ * Caller must guarantee that r.max.x-r.min.x>=r.max.y-r.min.y!
+ */
+Rectangle pl_radio(Image *b, Rectangle r, int val){
+	Rectangle remainder;
+	remainder=r;
+	r.max.x=r.min.x+r.max.y-r.min.y;
+	remainder.min.x=r.max.x;
+	r=insetrect(r, CKINSET);
+	if(plldepth==0)
+		pl_relief(b, pl_black, pl_black, r, CKWID);
+	else
+		pl_relief(b, pl_black, pl_white, r, CKWID);
+	r=insetrect(r, CKWID);
+	if(plldepth==0)
+		draw(b, r, pl_white, 0, ZP);
+	else
+		draw(b, r, pl_light, 0, ZP);
+	if(val) draw(b, insetrect(r, CKSPACE), pl_black, 0, ZP);
+	return remainder;
+}
+Rectangle pl_check(Image *b, Rectangle r, int val){
+	Rectangle remainder;
+	remainder=r;
+	r.max.x=r.min.x+r.max.y-r.min.y;
+	remainder.min.x=r.max.x;
+	r=insetrect(r, CKINSET);
+	if(plldepth==0)
+		pl_relief(b, pl_black, pl_black, r, CKWID);
+	else
+		pl_relief(b, pl_black, pl_white, r, CKWID);
+	r=insetrect(r, CKWID);
+	if(plldepth==0)
+		draw(b, r, pl_white, 0, ZP);
+	else
+		draw(b, r, pl_light, 0, ZP);
+	r=insetrect(r, CKBORDER);
+	if(val){
+		line(b, Pt(r.min.x,   r.min.y+1), Pt(r.max.x-1, r.max.y  ), Endsquare, Endsquare, 0, pl_black, ZP);
+		line(b, Pt(r.min.x,   r.min.y  ), Pt(r.max.x,   r.max.y  ), Endsquare, Endsquare, 0, pl_black, ZP);
+		line(b, Pt(r.min.x+1, r.min.y  ), Pt(r.max.x,   r.max.y-1), Endsquare, Endsquare, 0, pl_black, ZP);
+		line(b, Pt(r.min.x  , r.max.y-2), Pt(r.max.x-1, r.min.y-1), Endsquare, Endsquare, 0, pl_black, ZP);
+		line(b, Pt(r.min.x,   r.max.y-1), Pt(r.max.x,   r.min.y-1), Endsquare, Endsquare, 0, pl_black, ZP);
+		line(b, Pt(r.min.x+1, r.max.y-1), Pt(r.max.x,   r.min.y  ), Endsquare, Endsquare, 0, pl_black, ZP);
+	}
+	return remainder;
+}
+int pl_ckwid(void){
+	return 2*(CKINSET+CKSPACE+CKWID)+CKSIZE;
+}
+void pl_sliderupd(Image *b, Rectangle r1, int dir, int lo, int hi){
+	Rectangle r2, r3;
+	r2=r1;
+	r3=r1;
+	if(lo<0) lo=0;
+	if(hi<=lo) hi=lo+1;
+	switch(dir){
+	case HORIZ:
+		r1.max.x=r1.min.x+lo;
+		r2.min.x=r1.max.x;
+		r2.max.x=r1.min.x+hi;
+		if(r2.max.x>r3.max.x) r2.max.x=r3.max.x;
+		r3.min.x=r2.max.x;
+		break;
+	case VERT:
+		r1.max.y=r1.min.y+lo;
+		r2.min.y=r1.max.y;
+		r2.max.y=r1.min.y+hi;
+		if(r2.max.y>r3.max.y) r2.max.y=r3.max.y;
+		r3.min.y=r2.max.y;
+		break;
+	}
+	draw(b, r1, pl_light, 0, ZP);
+	draw(b, r2, pl_dark, 0, ZP);
+	draw(b, r3, pl_light, 0, ZP);
+}
+void pl_draw1(Panel *p, Image *b);
+void pl_drawall(Panel *p, Image *b){
+	if(p->flags&INVIS || p->flags&IGNORE) return;
+	p->b=b;
+	p->draw(p);
+	for(p=p->child;p;p=p->next) pl_draw1(p, b);
+}
+void pl_draw1(Panel *p, Image *b){
+	if(b!=0)
+		pl_drawall(p, b);
+}
+void pldraw(Panel *p, Image *b){
+	pl_draw1(p, b);
+}
+void pl_invis(Panel *p, int v){
+	for(;p;p=p->next){
+		if(v) p->flags|=INVIS; else p->flags&=~INVIS;
+		pl_invis(p->child, v);
+	}
+}
+Point pl_iconsize(int flags, Icon *p){
+	if(flags&BITMAP) return subpt(((Image *)p)->r.max, ((Image *)p)->r.min);
+	return stringsize(font, (char *)p);
+}
+void pl_highlight(Image *b, Rectangle r){
+	draw(b, r, pl_dark, pl_hilit, ZP);
+}
+void pl_clr(Image *b, Rectangle r){
+	draw(b, r, display->white, 0, ZP);
+}
+void pl_fill(Image *b, Rectangle r){
+	draw(b, r, plldepth==0? pl_white : pl_light, 0, ZP);
+}
+void pl_cpy(Image *b, Point dst, Rectangle src){
+	draw(b, Rpt(dst, addpt(dst, subpt(src.max, src.min))), b, 0, src.min);
+}
--- /dev/null
+++ b/libpanel/edit.c
@@ -1,0 +1,297 @@
+/*
+ * Interface includes:
+ *	void plescroll(Panel *p, int top);
+ *		move the given character position onto the top line
+ *	void plegetsel(Panel *p, int *sel0, int *sel1);
+ *		read the selection back
+ *	int plelen(Panel *p);
+ *		read the length of the text back
+ *	Rune *pleget(Panel *p);
+ *		get a pointer to the text
+ *	void plesel(Panel *p, int sel0, int sel1);
+ *		set the selection -- adjusts hiliting
+ *	void plepaste(Panel *p, Rune *text, int ntext);
+ *		replace the selection with the given text
+ */
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <panel.h>
+#include "pldefs.h"
+#include <keyboard.h>
+
+typedef struct Edit Edit;
+struct Edit{
+	Point minsize;
+	void (*hit)(Panel *);
+	int sel0, sel1;
+	Textwin *t;
+	Rune *text;
+	int ntext;
+};
+void pl_drawedit(Panel *p){
+	Edit *ep;
+	Panel *sb;
+	ep=p->data;
+	if(ep->t==0){
+		ep->t=twnew(p->b, font, ep->text, ep->ntext);
+		if(ep->t==0){
+			fprint(2, "pl_drawedit: can't allocate\n");
+			exits("no mem");
+		}
+	}
+	ep->t->b=p->b;
+	twreshape(ep->t, p->r);
+	twhilite(ep->t, ep->sel0, ep->sel1, 1);
+	sb=p->yscroller;
+	if(sb && sb->setscrollbar)
+		sb->setscrollbar(sb, ep->t->top, ep->t->bot, ep->t->etext-ep->t->text);
+}
+
+char *pl_snarfedit(Panel *p){
+	int s0, s1;
+	Rune *t;
+	t=pleget(p);
+	plegetsel(p, &s0, &s1);
+	if(t==0 || s0>=s1)
+		return nil;
+	return smprint("%.*S", s1-s0, t+s0);
+}
+void pl_pasteedit(Panel *p, char *s){
+	Rune *t;
+	if(t=runesmprint("%s", s)){
+		plepaste(p, t, runestrlen(t));
+		free(t);
+	}
+}
+
+/*
+ * Should do double-clicks:
+ *	If ep->sel0==ep->sel1 on entry and the
+ *	call to twselect returns the same selection, then
+ *	expand selections (| marks possible selection points, ... is expanded selection)
+ *	<|...|>			<> must nest
+ *	(|...|)			() must nest
+ *	[|...|]			[] must nest
+ *	{|...|}			{} must nest
+ *	'|...|'			no ' in ...
+ *	"|...|"			no " in ...
+ *	\n|...|\n		either newline may be the corresponding end of text
+ *				include the trailing newline in the selection
+ *	...|I...		I and ... are characters satisfying pl_idchar(I)
+ *	...I|
+ */
+int pl_hitedit(Panel *p, Mouse *m){
+	Edit *ep;
+	ep=p->data;
+	if(ep->t && m->buttons&1){
+		plgrabkb(p);
+		ep->t->b=p->b;
+		twhilite(ep->t, ep->sel0, ep->sel1, 0);
+		twselect(ep->t, m);
+		ep->sel0=ep->t->sel0;
+		ep->sel1=ep->t->sel1;
+		if((m->buttons&7)==3){
+			plsnarf(p);
+			plepaste(p, 0, 0);	/* cut */
+		}
+		else if((m->buttons&7)==5)
+			plpaste(p);
+		else if(ep->hit)
+			(*ep->hit)(p);
+	}
+	return 0;
+}
+void pl_scrolledit(Panel *p, int dir, int buttons, int num, int den){
+	Edit *ep;
+	Textwin *t;
+	Panel *sb;
+	int index, nline;
+	if(dir!=VERT) return;
+	ep=p->data;
+	t=ep->t;
+	if(t==0) return;
+	t->b=p->b;
+	switch(buttons){
+	default:
+		return;
+	case 1:		/* top line moves to mouse position */
+		nline=(t->r.max.y-t->r.min.y)/t->hgt*num/den;
+		index=t->top;
+		while(index!=0 && nline!=0)
+			if(t->text[--index]=='\n') --nline;
+		break;
+	case 2:		/* absolute */
+		index=(t->etext-t->text)*num/den;
+		break;
+	case 4:		/* mouse points at new top line */
+		index=twpt2rune(t,
+			Pt(t->r.min.x, t->r.min.y+(t->r.max.y-t->r.min.y)*num/den));
+		break;
+	}
+	while(index!=0 && t->text[index-1]!='\n') --index;
+	if(index!=t->top){
+		twhilite(ep->t, ep->sel0, ep->sel1, 0);
+		twscroll(t, index);
+		p->scr.pos.y=t->top;
+		twhilite(ep->t, ep->sel0, ep->sel1, 1);
+		sb=p->yscroller;
+		if(sb && sb->setscrollbar)
+			sb->setscrollbar(sb, t->top, t->bot, t->etext-t->text);
+	}
+}
+void pl_typeedit(Panel *p, Rune c){
+	Edit *ep;
+	Textwin *t;
+	int bot, scrolled;
+	Panel *sb;
+	ep=p->data;
+	t=ep->t;
+	if(t==0) return;
+	t->b=p->b;
+	twhilite(t, ep->sel0, ep->sel1, 0);
+	switch(c){
+	case Kesc:
+		plsnarf(p);
+		plepaste(p, 0, 0);	/* cut */
+		break;
+	case Kdel:	/* clear */
+		ep->sel0=0;
+		ep->sel1=plelen(p);
+		plepaste(p, 0, 0);	/* cut */
+		break;
+	case Kbs:	/* ^H: erase character */
+		if(ep->sel0!=0) --ep->sel0;
+		twreplace(t, ep->sel0, ep->sel1, 0, 0);
+		break;
+	case Knack:	/* ^U: erase line */
+		while(ep->sel0!=0 && t->text[ep->sel0-1]!='\n') --ep->sel0;
+		twreplace(t, ep->sel0, ep->sel1, 0, 0);
+		break;
+	case Ketb:	/* ^W: erase word */
+		while(ep->sel0!=0 && !pl_idchar(t->text[ep->sel0-1])) --ep->sel0;
+		while(ep->sel0!=0 && pl_idchar(t->text[ep->sel0-1])) --ep->sel0;
+		twreplace(t, ep->sel0, ep->sel1, 0, 0);
+		break;
+	default:
+		if((c & 0xFF00) == KF || (c & 0xFF00) == Spec)
+			break;
+		twreplace(t, ep->sel0, ep->sel1, &c, 1);
+		++ep->sel0;
+		break;
+	}
+	ep->sel1=ep->sel0;
+	/*
+	 * Scroll up until ep->sel0 is above t->bot.
+	 */
+	scrolled=0;
+	do{
+		bot=t->bot;
+		if(ep->sel0<=bot) break;
+		twscroll(t, twpt2rune(t, Pt(t->r.min.x, t->r.min.y+font->height)));
+		scrolled++;
+	}while(bot!=t->bot);
+	if(scrolled){
+		sb=p->yscroller;
+		if(sb && sb->setscrollbar)
+			sb->setscrollbar(sb, t->top, t->bot, t->etext-t->text);
+	}
+	twhilite(t, ep->sel0, ep->sel1, 1);
+}
+Point pl_getsizeedit(Panel *p, Point children){
+	USED(children);
+	return pl_boxsize(((Edit *)p->data)->minsize, p->state);
+}
+void pl_childspaceedit(Panel *g, Point *ul, Point *size){
+	USED(g, ul, size);
+}
+void pl_freeedit(Panel *p){
+	Edit *ep;
+	ep=p->data;
+	if(ep->t) twfree(ep->t);
+	ep->t=0;
+}
+void plinitedit(Panel *v, int flags, Point minsize, Rune *text, int ntext, void (*hit)(Panel *)){
+	Edit *ep;
+	ep=v->data;
+	v->flags=flags|LEAF;
+	v->state=UP;
+	v->draw=pl_drawedit;
+	v->hit=pl_hitedit;
+	v->type=pl_typeedit;
+	v->getsize=pl_getsizeedit;
+	v->childspace=pl_childspaceedit;
+	v->free=pl_freeedit;
+	v->snarf=pl_snarfedit;
+	v->paste=pl_pasteedit;
+	v->kind="edit";
+	ep->hit=hit;
+	ep->minsize=minsize;
+	ep->text=text;
+	ep->ntext=ntext;
+	if(ep->t!=0) twfree(ep->t);
+	ep->t=0;
+	ep->sel0=-1;
+	ep->sel1=-1;
+	v->scroll=pl_scrolledit;
+	v->scr.pos=Pt(0,0);
+	v->scr.size=Pt(ntext,0);
+}
+Panel *pledit(Panel *parent, int flags, Point minsize, Rune *text, int ntext, void (*hit)(Panel *)){
+	Panel *v;
+	v=pl_newpanel(parent, sizeof(Edit));
+	((Edit *)v->data)->t=0;
+	plinitedit(v, flags, minsize, text, ntext, hit);
+	return v;
+}
+void plescroll(Panel *p, int top){
+	Textwin *t;
+	t=((Edit*)p->data)->t;
+	if(t) twscroll(t, top);
+}
+void plegetsel(Panel *p, int *sel0, int *sel1){
+	Edit *ep;
+	ep=p->data;
+	*sel0=ep->sel0;
+	*sel1=ep->sel1;
+}
+int plelen(Panel *p){
+	Textwin *t;
+	t=((Edit*)p->data)->t;
+	if(t==0) return 0;
+	return t->etext-t->text;
+}
+Rune *pleget(Panel *p){
+	Textwin *t;
+	t=((Edit*)p->data)->t;
+	if(t==0) return 0;
+	return t->text;
+}
+void plesel(Panel *p, int sel0, int sel1){
+	Edit *ep;
+	ep=p->data;
+	if(ep->t==0) return;
+	ep->t->b=p->b;
+	twhilite(ep->t, ep->sel0, ep->sel1, 0);
+	ep->sel0=sel0;
+	ep->sel1=sel1;
+	twhilite(ep->t, ep->sel0, ep->sel1, 1);
+}
+void plepaste(Panel *p, Rune *text, int ntext){
+	Edit *ep;
+	ep=p->data;
+	if(ep->t==0) return;
+	ep->t->b=p->b;
+	twhilite(ep->t, ep->sel0, ep->sel1, 0);
+	twreplace(ep->t, ep->sel0, ep->sel1, text, ntext);
+	ep->sel1=ep->sel0+ntext;
+	twhilite(ep->t, ep->sel0, ep->sel1, 1);
+	p->scr.size.y=ep->t->etext-ep->t->text;
+	p->scr.pos.y=ep->t->top;
+}
+void plemove(Panel *p, Point d){
+	Edit *ep;
+	ep=p->data;
+	if(ep->t && !eqpt(d, Pt(0,0))) twmove(ep->t, d);
+}
--- /dev/null
+++ b/libpanel/entry.c
@@ -1,0 +1,192 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <panel.h>
+#include "pldefs.h"
+#include <keyboard.h>
+
+typedef struct Entry Entry;
+struct Entry{
+	char *entry;
+	char *entp;
+	char *eent;
+	void (*hit)(Panel *, char *);
+	Point minsize;
+};
+#define	SLACK	7	/* enough for one extra rune and ◀ and a nul */
+char *pl_snarfentry(Panel *p){
+	Entry *ep;
+	int n;
+
+	if(p->flags&USERFL)	/* no snarfing from password entry */
+		return nil;
+	ep=p->data;
+	n=utfnlen(ep->entry, ep->entp-ep->entry);
+	if(n<1) return nil;
+	return smprint("%.*s", n, ep->entry);
+}
+void pl_pasteentry(Panel *p, char *s){
+	Entry *ep;
+	char *e;
+	int n, m;
+
+	ep=p->data;
+	n=ep->entp-ep->entry;
+	m=strlen(s);
+	e=pl_erealloc(ep->entry,n+m+SLACK);
+	ep->entry=e;
+	e+=n;
+	strncpy(e, s, m);
+	e+=m;
+	*e='\0';
+	ep->entp=ep->eent=e;
+	pldraw(p, p->b);
+}
+void pl_drawentry(Panel *p){
+	Rectangle r;
+	Entry *ep;
+	char *s;
+
+	ep=p->data;
+	r=pl_box(p->b, p->r, p->state);
+	s=ep->entry;
+	if(p->flags & USERFL){
+		char *p;
+		s=strdup(s);
+		for(p=s; *p; p++)
+			*p='*';
+	}
+	if(stringwidth(font, s)<=r.max.x-r.min.x)
+		pl_drawicon(p->b, r, PLACEW, 0, s);
+	else
+		pl_drawicon(p->b, r, PLACEE, 0, s);
+	if(s != ep->entry)
+		free(s);
+}
+int pl_hitentry(Panel *p, Mouse *m){
+	if((m->buttons&7)==1){
+		plgrabkb(p);
+
+		p->state=DOWN;
+		pldraw(p, p->b);
+		while(m->buttons&1){
+			int old;
+			old=m->buttons;
+			if(display->bufp > display->buf)
+				flushimage(display, 1);
+			*m=emouse();
+			if((old&7)==1){
+				if((m->buttons&7)==3){
+					Entry *ep;
+
+					plsnarf(p);
+
+					/* cut */
+					ep=p->data;
+					ep->entp=ep->entry;
+					*ep->entp='\0';
+					pldraw(p, p->b);
+				}
+				if((m->buttons&7)==5)
+					plpaste(p);
+			}
+		}
+		p->state=UP;
+		pldraw(p, p->b);
+	}
+	return 0;
+}
+void pl_typeentry(Panel *p, Rune c){
+	int n;
+	Entry *ep;
+	ep=p->data;
+	switch(c){
+	case '\n':
+	case '\r':
+		*ep->entp='\0';
+		if(ep->hit) ep->hit(p, ep->entry);
+		return;
+	case Kesc:
+		plsnarf(p);
+		/* no break */
+	case Kdel:	/* clear */
+	case Knack:	/* ^U: erase line */
+		ep->entp=ep->entry;
+		*ep->entp='\0';
+		break;
+	case Kbs:	/* ^H: erase character */
+		while(ep->entp!=ep->entry && !pl_rune1st(ep->entp[-1])) *--ep->entp='\0';
+		if(ep->entp!=ep->entry) *--ep->entp='\0';
+		break;
+	case Ketb:	/* ^W: erase word */
+		while(ep->entp!=ep->entry && !pl_idchar(ep->entp[-1]))
+			--ep->entp;
+		while(ep->entp!=ep->entry && pl_idchar(ep->entp[-1]))
+			--ep->entp;
+		*ep->entp='\0';
+		break;
+	default:
+		if(c < 0x20 || (c & 0xFF00) == KF || (c & 0xFF00) == Spec)
+			break;
+		ep->entp+=runetochar(ep->entp, &c);
+		if(ep->entp>ep->eent){
+			n=ep->entp-ep->entry;
+			ep->entry=pl_erealloc(ep->entry, n+100+SLACK);
+			ep->entp=ep->entry+n;
+			ep->eent=ep->entp+100;
+		}
+		*ep->entp='\0';
+		break;
+	}
+	pldraw(p, p->b);
+}
+Point pl_getsizeentry(Panel *p, Point children){
+	USED(children);
+	return pl_boxsize(((Entry *)p->data)->minsize, p->state);
+}
+void pl_childspaceentry(Panel *p, Point *ul, Point *size){
+	USED(p, ul, size);
+}
+void pl_freeentry(Panel *p){
+	Entry *ep;
+	ep = p->data;
+	free(ep->entry);
+	ep->entry = ep->eent = ep->entp = 0;
+}
+void plinitentry(Panel *v, int flags, int wid, char *str, void (*hit)(Panel *, char *)){
+	int elen;
+	Entry *ep;
+	ep=v->data;
+	v->flags=flags|LEAF;
+	v->state=UP;
+	v->draw=pl_drawentry;
+	v->hit=pl_hitentry;
+	v->type=pl_typeentry;
+	v->getsize=pl_getsizeentry;
+	v->childspace=pl_childspaceentry;
+	ep->minsize=Pt(wid, font->height);
+	v->free=pl_freeentry;
+	v->snarf=pl_snarfentry;
+	v->paste=pl_pasteentry;
+	elen=100;
+	if(str) elen+=strlen(str);
+	ep->entry=pl_erealloc(ep->entry, elen+SLACK);
+	ep->eent=ep->entry+elen;
+	strecpy(ep->entry, ep->eent, str ? str : "");
+	ep->entp=ep->entry+strlen(ep->entry);
+	ep->hit=hit;
+	v->kind="entry";
+}
+Panel *plentry(Panel *parent, int flags, int wid, char *str, void (*hit)(Panel *, char *)){
+	Panel *v;
+	v=pl_newpanel(parent, sizeof(Entry));
+	plinitentry(v, flags, wid, str, hit);
+	return v;
+}
+char *plentryval(Panel *p){
+	Entry *ep;
+	ep=p->data;
+	*ep->entp='\0';
+	return ep->entry;
+}
--- /dev/null
+++ b/libpanel/event.c
@@ -1,0 +1,48 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <panel.h>
+#include "pldefs.h"
+
+void plgrabkb(Panel *g){
+	plkbfocus=g;
+}
+void plkeyboard(Rune c){
+	if(plkbfocus)
+		plkbfocus->type(plkbfocus, c);
+}
+
+/*
+ * Return the most leafward, highest priority panel containing p
+ */
+Panel *pl_ptinpanel(Point p, Panel *g){
+	Panel *v;
+	for(;g;g=g->next) if(ptinrect(p, g->r)){
+		v=pl_ptinpanel(p, g->child);
+		if(v && v->pri(v, p)>=g->pri(g, p)) return v;
+		return g;
+	}
+	return 0;
+}
+void plmouse(Panel *g, Mouse *m){
+	Panel *hit, *last;
+	if(g->flags&REMOUSE)
+		hit=g->lastmouse;
+	else{
+		hit=pl_ptinpanel(m->xy, g);
+		last=g->lastmouse;
+		if(last && last!=hit){
+			m->buttons|=OUT;
+			last->hit(last, m);
+			m->buttons&=~OUT;
+		}
+	}
+	if(hit){
+		if(hit->hit(hit, m))
+			g->flags|=REMOUSE;
+		else
+			g->flags&=~REMOUSE;
+		g->lastmouse=hit;
+	}
+}
--- /dev/null
+++ b/libpanel/frame.c
@@ -1,0 +1,39 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <panel.h>
+#include "pldefs.h"
+void pl_drawframe(Panel *p){
+	pl_box(p->b, p->r, FRAME);
+}
+int pl_hitframe(Panel *p, Mouse *m){
+	USED(p, m);
+	return 0;
+}
+void pl_typeframe(Panel *p, Rune c){
+	USED(p, c);
+}
+Point pl_getsizeframe(Panel *p, Point children){
+	USED(p);
+	return pl_boxsize(children, FRAME);
+}
+void pl_childspaceframe(Panel *p, Point *ul, Point *size){
+	USED(p);
+	pl_interior(FRAME, ul, size);
+}
+void plinitframe(Panel *v, int flags){
+	v->flags=flags;
+	v->draw=pl_drawframe;
+	v->hit=pl_hitframe;
+	v->type=pl_typeframe;
+	v->getsize=pl_getsizeframe;
+	v->childspace=pl_childspaceframe;
+	v->kind="frame";
+}
+Panel *plframe(Panel *parent, int flags){
+	Panel *p;
+	p=pl_newpanel(parent, 0);
+	plinitframe(p, flags);
+	return p;
+}
--- /dev/null
+++ b/libpanel/group.c
@@ -1,0 +1,38 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <panel.h>
+#include "pldefs.h"
+void pl_drawgroup(Panel *p){
+	USED(p);
+}
+int pl_hitgroup(Panel *p, Mouse *m){
+	USED(p, m);
+	return 0;
+}
+void pl_typegroup(Panel *p, Rune c){
+	USED(p, c);
+}
+Point pl_getsizegroup(Panel *p, Point children){
+	USED(p);
+	return children;
+}
+void pl_childspacegroup(Panel *p, Point *ul, Point *size){
+	USED(p, ul, size);
+}
+void plinitgroup(Panel *v, int flags){
+	v->flags=flags;
+	v->draw=pl_drawgroup;
+	v->hit=pl_hitgroup;
+	v->type=pl_typegroup;
+	v->getsize=pl_getsizegroup;
+	v->childspace=pl_childspacegroup;
+	v->kind="group";
+}
+Panel *plgroup(Panel *parent, int flags){
+	Panel *p;
+	p=pl_newpanel(parent, 0);
+	plinitgroup(p, flags);
+	return p;
+}
--- /dev/null
+++ b/libpanel/init.c
@@ -1,0 +1,13 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <panel.h>
+#include "pldefs.h"
+/*
+ * Just a wrapper for all the initialization routines
+ */
+int plinit(int ldepth){
+	if(!pl_drawinit(ldepth)) return 0;
+	return 1;
+}
--- /dev/null
+++ b/libpanel/label.c
@@ -1,0 +1,50 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <panel.h>
+#include "pldefs.h"
+typedef struct Label Label;
+struct Label{
+	int placement;
+	Icon *icon;
+};
+void pl_drawlabel(Panel *p){
+	Label *l;
+	l=p->data;
+	pl_drawicon(p->b, pl_box(p->b, p->r, PASSIVE), l->placement, p->flags, l->icon);
+}
+int pl_hitlabel(Panel *p, Mouse *m){
+	USED(p, m);
+	return 0;
+}
+void pl_typelabel(Panel *p, Rune c){
+	USED(p, c);
+}
+Point pl_getsizelabel(Panel *p, Point children){
+	USED(children);		/* shouldn't have any children */
+	return pl_boxsize(pl_iconsize(p->flags, ((Label *)p->data)->icon), PASSIVE);
+}
+void pl_childspacelabel(Panel *g, Point *ul, Point *size){
+	USED(g, ul, size);
+}
+void plinitlabel(Panel *v, int flags, Icon *icon){
+	v->flags=flags|LEAF;
+	((Label *)(v->data))->icon=icon;
+	v->draw=pl_drawlabel;
+	v->hit=pl_hitlabel;
+	v->type=pl_typelabel;
+	v->getsize=pl_getsizelabel;
+	v->childspace=pl_childspacelabel;
+	v->kind="label";
+}
+Panel *pllabel(Panel *parent, int flags, Icon *icon){
+	Panel *p;
+	p=pl_newpanel(parent, sizeof(Label));
+	plinitlabel(p, flags, icon);
+	plplacelabel(p, PLACECEN);
+	return p;
+}
+void plplacelabel(Panel *p, int placement){
+	((Label *)(p->data))->placement=placement;
+}
--- /dev/null
+++ b/libpanel/list.c
@@ -1,0 +1,190 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <panel.h>
+#include "pldefs.h"
+typedef struct List List;
+struct List{
+	void (*hit)(Panel *, int, int);	/* call user back on hit */
+	char *(*gen)(Panel *, int);	/* return text given index or 0 if out of range */
+	int lo;				/* indices of first, last items displayed */
+	int sel;			/* index of hilited item */
+	int len;			/* # of items in list */
+	Rectangle listr;
+	Point minsize;
+	int buttons;
+};
+#define	MAXHGT	12
+void pl_listsel(Panel *p, int sel, int on){
+	List *lp;
+	int hi;
+	Rectangle r;
+	lp=p->data;
+	hi=lp->lo+(lp->listr.max.y-lp->listr.min.y)/font->height;
+	if(lp->lo>=0 && lp->lo<=sel && sel<hi && sel<lp->len){
+		r=lp->listr;
+		r.min.y+=(sel-lp->lo)*font->height;
+		r.max.y=r.min.y+font->height;
+		if(on)
+			pl_highlight(p->b, r);
+		else{
+			pl_fill(p->b, r);
+			pl_drawicon(p->b, r, PLACEW, 0, lp->gen(p, sel));
+		}
+	}
+}
+void pl_liststrings(Panel *p, int lo, int hi, Rectangle r){
+	Panel *sb;
+	List *lp;
+	char *s;
+	int i;
+	lp=p->data;
+	for(i=lo;i!=hi && (s=lp->gen(p, i));i++){
+		r.max.y=r.min.y+font->height;
+		pl_drawicon(p->b, r, PLACEW, 0, s);
+		r.min.y+=font->height;
+	}
+	if(lo<=lp->sel && lp->sel<hi) pl_listsel(p, lp->sel, 1);
+	sb=p->yscroller;
+	if(sb && sb->setscrollbar)
+		sb->setscrollbar(sb, lp->lo,
+			lp->lo+(lp->listr.max.y-lp->listr.min.y)/font->height, lp->len);
+}
+void pl_drawlist(Panel *p){
+	List *lp;
+	lp=p->data;
+	lp->listr=pl_box(p->b, p->r, UP);
+	pl_liststrings(p, lp->lo, lp->lo+(lp->listr.max.y-lp->listr.min.y)/font->height,
+		lp->listr);
+}
+int pl_hitlist(Panel *p, Mouse *m){
+	int oldsel, hitme;
+	Point ul, size;
+	List *lp;
+	lp=p->data;
+	hitme=0;
+	ul=p->r.min;
+	size=subpt(p->r.max, p->r.min);
+	pl_interior(p->state, &ul, &size);
+	oldsel=lp->sel;
+	if(m->buttons&OUT){
+		p->state=UP;
+		if(m->buttons&~OUT) lp->sel=-1;
+	}
+	else if(p->state==DOWN || m->buttons&7){
+		lp->sel=(m->xy.y-ul.y)/font->height+lp->lo;
+		if(m->buttons&7){
+			lp->buttons=m->buttons;
+			p->state=DOWN;
+		}
+		else{
+			hitme=1;
+			p->state=UP;
+		}
+	}
+	if(oldsel!=lp->sel){
+		pl_listsel(p, oldsel, 0);
+		pl_listsel(p, lp->sel, 1);
+	}
+	if(hitme && 0<=lp->sel && lp->sel<lp->len && lp->hit)
+		lp->hit(p, lp->buttons, lp->sel);
+	return 0;
+}
+void pl_scrolllist(Panel *p, int dir, int buttons, int val, int len){
+	Point ul, size;
+	int nlist, oldlo, hi, nline, y;
+	List *lp;
+	Rectangle r;
+	lp=p->data;
+	ul=p->r.min;
+	size=subpt(p->r.max, p->r.min);
+	pl_interior(p->state, &ul, &size);
+	nlist=size.y/font->height;
+	oldlo=lp->lo;
+	if(dir==VERT) switch(buttons){
+	case 1: lp->lo-=nlist*val/len; break;
+	case 2: lp->lo=lp->len*val/len; break;
+	case 4:	lp->lo+=nlist*val/len; break;
+	}
+	if(lp->lo<0) lp->lo=0;
+	if(lp->lo>=lp->len) lp->lo=lp->len-1;
+	if(lp->lo==oldlo) return;
+	p->scr.pos.y=lp->lo;
+	r=lp->listr;
+	nline=(r.max.y-r.min.y)/font->height;
+	hi=lp->lo+nline;
+	if(hi<=oldlo || lp->lo>=oldlo+nline){
+		pl_box(p->b, r, PASSIVE);
+		pl_liststrings(p, lp->lo, hi, r);
+	}
+	else if(lp->lo<oldlo){
+		y=r.min.y+(oldlo-lp->lo)*font->height;
+		pl_cpy(p->b, Pt(r.min.x, y), 
+			Rect(r.min.x, r.min.y, r.max.x, r.min.y+(hi-oldlo)*font->height));
+		r.max.y=y;
+		pl_box(p->b, r, PASSIVE);
+		pl_liststrings(p, lp->lo, oldlo, r);
+	}
+	else{
+		pl_cpy(p->b, r.min, Rect(r.min.x, r.min.y+(lp->lo-oldlo)*font->height,
+			r.max.x, r.max.y));
+		r.min.y=r.min.y+(oldlo+nline-lp->lo)*font->height;
+		pl_box(p->b, r, PASSIVE);
+		pl_liststrings(p, oldlo+nline, hi, r);
+	}
+}
+void pl_typelist(Panel *g, Rune c){
+	USED(g, c);
+}
+Point pl_getsizelist(Panel *p, Point children){
+	USED(children);
+	return pl_boxsize(((List *)p->data)->minsize, p->state);
+}
+void pl_childspacelist(Panel *g, Point *ul, Point *size){
+	USED(g, ul, size);
+}
+void plinitlist(Panel *v, int flags, char *(*gen)(Panel *, int), int nlist, void (*hit)(Panel *, int, int)){
+	List *lp;
+	int wid, max;
+	char *str;
+	lp=v->data;
+	v->flags=flags|LEAF;
+	v->state=UP;
+	v->draw=pl_drawlist;
+	v->hit=pl_hitlist;
+	v->type=pl_typelist;
+	v->getsize=pl_getsizelist;
+	v->childspace=pl_childspacelist;
+	lp->gen=gen;
+	lp->hit=hit;
+	max=0;
+	for(lp->len=0;str=gen(v, lp->len);lp->len++){
+		wid=stringwidth(font, str);
+		if(wid>max) max=wid;
+	}
+	if(flags&(FILLX|EXPAND)){
+		for(lp->len=0;gen(v, lp->len);lp->len++);
+		lp->minsize=Pt(0, nlist*font->height);
+	}
+	else{
+		max=0;
+		for(lp->len=0;str=gen(v, lp->len);lp->len++){
+			wid=stringwidth(font, str);
+			if(wid>max) max=wid;
+		}
+		lp->minsize=Pt(max, nlist*font->height);
+	}
+	lp->sel=-1;
+	lp->lo=0;
+	v->scroll=pl_scrolllist;
+	v->scr.pos=Pt(0,0);
+	v->scr.size=Pt(0,lp->len);
+	v->kind="list";
+}
+Panel *pllist(Panel *parent, int flags, char *(*gen)(Panel *, int), int nlist, void (*hit)(Panel *, int, int)){
+	Panel *v;
+	v=pl_newpanel(parent, sizeof(List));
+	plinitlist(v, flags, gen, nlist, hit);
+	return v;
+}
--- /dev/null
+++ b/libpanel/mem.c
@@ -1,0 +1,123 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <panel.h>
+#include "pldefs.h"
+void *pl_emalloc(int n){
+	void *v;
+	v=mallocz(n, 1);
+	if(v==0){
+		fprint(2, "Can't malloc!\n");
+		exits("no mem");
+	}
+	setmalloctag(v, getcallerpc(&n));
+	return v;
+}
+void *pl_erealloc(void *v, int n)
+{
+	v=realloc(v, n);
+	if(v==0){
+		fprint(2, "Can't realloc!\n");
+		exits("no mem");
+	}
+	setrealloctag(v, getcallerpc(&v));
+	return v;
+}
+void pl_unexpected(Panel *g, char *rou){
+	fprint(2, "%s called unexpectedly (%s %#p)\n", rou, g->kind, g);
+	abort();
+}
+void pl_drawerror(Panel *g){
+	pl_unexpected(g, "draw");
+}
+int pl_hiterror(Panel *g, Mouse *m){
+	USED(m);
+	pl_unexpected(g, "hit");
+	return 0;
+}
+void pl_typeerror(Panel *g, Rune c){
+	USED(c);
+	pl_unexpected(g, "type");
+}
+Point pl_getsizeerror(Panel *g, Point childsize){
+	pl_unexpected(g, "getsize");
+	return childsize;
+}
+void pl_childspaceerror(Panel *g, Point *ul, Point *size){
+	USED(ul, size);
+	pl_unexpected(g, "childspace");
+}
+void pl_scrollerror(Panel *g, int dir, int button, int num, int den){
+	USED(dir, button, num, den);
+	pl_unexpected(g, "scroll");
+}
+void pl_setscrollbarerror(Panel *g, int top, int bot, int den){
+	USED(top, bot, den);
+	pl_unexpected(g, "setscrollbar");
+}
+int pl_prinormal(Panel *, Point){
+	return PRI_NORMAL;
+}
+Panel *pl_newpanel(Panel *parent, int ndata){
+	Panel *v;
+	if(parent && parent->flags&LEAF){
+		fprint(2, "newpanel: can't create child of %s %#p\n", parent->kind, parent);
+		exits("bad newpanel");
+	}
+	v=pl_emalloc(sizeof(Panel));
+	v->r=Rect(0,0,0,0);
+	v->flags=0;
+	v->ipad=Pt(0,0);
+	v->pad=Pt(0,0);
+	v->size=Pt(0,0);
+	v->sizereq=Pt(0,0);
+	v->lastmouse=0;
+	v->next=0;
+	v->child=0;
+	v->echild=0;
+	v->b=0;
+	v->pri=pl_prinormal;
+	v->scrollee=0;
+	v->xscroller=0;
+	v->yscroller=0;
+	v->parent=parent;
+	v->scr.pos=Pt(0,0);
+	v->scr.size=Pt(0,0);
+	if(parent){
+		if(parent->child==0)
+			parent->child=v;
+		else
+			parent->echild->next=v;
+		parent->echild=v;
+	}
+	v->draw=pl_drawerror;
+	v->hit=pl_hiterror;
+	v->type=pl_typeerror;
+	v->getsize=pl_getsizeerror;
+	v->childspace=pl_childspaceerror;
+	v->scroll=pl_scrollerror;
+	v->setscrollbar=pl_setscrollbarerror;
+	v->free=0;
+	v->snarf=0;
+	v->paste=0;
+	if(ndata)
+		v->data=pl_emalloc(ndata);
+	else
+		v->data=0;
+	return v;
+}
+void plfree(Panel *p){
+	Panel *cp, *ncp;
+	if(p==0)
+		return;
+	if(p==plkbfocus)
+		plkbfocus=0;
+	for(cp=p->child;cp;cp=ncp){
+		ncp=cp->next;
+		plfree(cp);
+	}
+	if(p->free) p->free(p);
+	if(p->data) free(p->data);
+	free(p);
+}
--- /dev/null
+++ b/libpanel/message.c
@@ -1,0 +1,104 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <panel.h>
+#include "pldefs.h"
+typedef struct Message Message;
+struct Message{
+	char *text;
+	Point minsize;
+};
+void pl_textmsg(Image *b, Rectangle r, Font *f, char *s){
+	char *start, *end;	/* of line */
+	Point where;
+	int lwid, c, wid;
+	where=r.min;
+	wid=r.max.x-r.min.x;
+	do{
+		start=s;
+		lwid=0;
+		end=s;
+		do{
+			for(;*s!=' ' && *s!='\0';s=pl_nextrune(s)) lwid+=pl_runewidth(f, s);
+			if(lwid>wid) break;
+			end=s;
+			for(;*s==' ';s=pl_nextrune(s)) lwid+=pl_runewidth(f, s);
+		}while(*s!='\0');
+		if(end==start)	/* can't even fit one word on line! */
+			end=s;
+		c=*end;
+		*end='\0';
+		string(b, where, display->black, ZP, f, start);
+		*end=c;
+		where.y+=font->height;
+		s=end;
+		while(*s==' ') s=pl_nextrune(s);
+	}while(*s!='\0');
+}
+Point pl_foldsize(Font *f, char *s, int wid){
+	char *start, *end;	/* of line */
+	Point size;
+	int lwid, ewid;
+	size=Pt(0,0);
+	do{
+		start=s;
+		lwid=0;
+		end=s;
+		ewid=lwid;
+		do{
+			for(;*s!=' ' && *s!='\0';s=pl_nextrune(s)) lwid+=pl_runewidth(f, s);
+			if(lwid>wid) break;
+			end=s;
+			ewid=lwid;
+			for(;*s==' ';s=pl_nextrune(s)) lwid+=pl_runewidth(f, s);
+		}while(*s!='\0');
+		if(end==start){	/* can't even fit one word on line! */
+			ewid=lwid;
+			end=s;
+		}
+		if(ewid>size.x) size.x=ewid;
+		size.y+=font->height;
+		s=end;
+		while(*s==' ') s=pl_nextrune(s);
+	}while(*s!='\0');
+	return size;
+}
+void pl_drawmessage(Panel *p){
+	pl_textmsg(p->b, pl_box(p->b, p->r, PASSIVE), font, ((Message *)p->data)->text);
+}
+int pl_hitmessage(Panel *g, Mouse *m){
+	USED(g, m);
+	return 0;
+}
+void pl_typemessage(Panel *g, Rune c){
+	USED(g, c);
+}
+Point pl_getsizemessage(Panel *p, Point children){
+	Message *mp;
+	USED(children);
+	mp=p->data;
+	return pl_boxsize(pl_foldsize(font, mp->text, mp->minsize.x), PASSIVE);
+}
+void pl_childspacemessage(Panel *p, Point *ul, Point *size){
+	USED(p, ul, size);
+}
+void plinitmessage(Panel *v, int flags, int wid, char *msg){
+	Message *mp;
+	mp=v->data;
+	v->flags=flags|LEAF;
+	v->draw=pl_drawmessage;
+	v->hit=pl_hitmessage;
+	v->type=pl_typemessage;
+	v->getsize=pl_getsizemessage;
+	v->childspace=pl_childspacemessage;
+	mp->text=msg;
+	mp->minsize=Pt(wid, font->height);
+	v->kind="message";
+}
+Panel *plmessage(Panel *parent, int flags, int wid, char *msg){
+	Panel *v;
+	v=pl_newpanel(parent, sizeof(Message));
+	plinitmessage(v, flags, wid, msg);
+	return v;
+}
--- /dev/null
+++ b/libpanel/mkfile
@@ -1,0 +1,34 @@
+</$objtype/mkfile
+
+LIB=libpanel.$O.a
+OFILES=\
+	button.$O\
+	canvas.$O\
+	draw.$O\
+	edit.$O\
+	entry.$O\
+	event.$O\
+	frame.$O\
+	group.$O\
+#	idollist.$O\
+	init.$O\
+	label.$O\
+	list.$O\
+	mem.$O\
+	message.$O\
+	pack.$O\
+	popup.$O\
+	print.$O\
+	pulldown.$O\
+	rtext.$O\
+	scroll.$O\
+	scrollbar.$O\
+	slider.$O\
+	textview.$O\
+	textwin.$O\
+	utf.$O\
+	snarf.$O
+
+HFILES=panel.h pldefs.h rtext.h
+
+</sys/src/cmd/mklib
--- /dev/null
+++ b/libpanel/pack.c
@@ -1,0 +1,161 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <panel.h>
+#include "pldefs.h"
+int pl_max(int a, int b){
+	return a>b?a:b;
+}
+Point pl_sizesibs(Panel *p){
+	Point s;
+	if(p==0) return Pt(0,0);
+	s=pl_sizesibs(p->next);
+	switch(p->flags&PACK){
+	case PACKN:
+	case PACKS:
+		s.x=pl_max(s.x, p->sizereq.x);
+		s.y+=p->sizereq.y;
+		break;
+	case PACKE:
+	case PACKW:
+		s.x+=p->sizereq.x;
+		s.y=pl_max(s.y, p->sizereq.y);
+		break;
+	}
+	return s;
+}
+/*
+ * Compute the requested size of p and its descendants.
+ */
+void pl_sizereq(Panel *p){
+	Panel *cp;
+	Point maxsize;
+	maxsize=Pt(0,0);
+	for(cp=p->child;cp;cp=cp->next){
+		pl_sizereq(cp);
+		if(cp->sizereq.x>maxsize.x) maxsize.x=cp->sizereq.x;
+		if(cp->sizereq.y>maxsize.y) maxsize.y=cp->sizereq.y;
+	}
+	for(cp=p->child;cp;cp=cp->next){
+		if(cp->flags&MAXX) cp->sizereq.x=maxsize.x;
+		if(cp->flags&MAXY) cp->sizereq.y=maxsize.y;
+	}
+	p->childreq=pl_sizesibs(p->child);
+	p->sizereq=addpt(addpt(p->getsize(p, p->childreq), p->ipad), p->pad);
+	if(p->flags&FIXEDX) p->sizereq.x=p->fixedsize.x;
+	if(p->flags&FIXEDY) p->sizereq.y=p->fixedsize.y;
+}
+Point pl_getshare(Panel *p){
+	Point share;
+	if(p==0) return Pt(0,0);
+	share=pl_getshare(p->next);
+	if(p->flags&EXPAND) switch(p->flags&PACK){
+	case PACKN:
+	case PACKS:
+		if(share.x==0) share.x=1;
+		share.y++;
+		break;
+	case PACKE:
+	case PACKW:
+		share.x++;
+		if(share.y==0) share.y=1;
+		break;
+	}
+	return share;
+}
+/*
+ * Set the sizes and rectangles of p and its descendants, given their requested sizes.
+ */
+void pl_setrect(Panel *p, Point ul, Point avail){
+	Point space, newul, newspace, slack, share;
+	int l;
+	Panel *c;
+	p->size=subpt(p->sizereq, p->pad);
+	ul=addpt(ul, divpt(p->pad, 2));
+	avail=subpt(avail, p->pad);
+	if(p->size.x>avail.x)
+		p->size.x = avail.x;
+	if(p->size.y>avail.y)
+		p->size.y = avail.y;
+	if(p->flags&(FILLX|EXPAND)) p->size.x=avail.x;
+	if(p->flags&(FILLY|EXPAND)) p->size.y=avail.y;
+	switch(p->flags&PLACE){
+	case PLACECEN:	ul.x+=(avail.x-p->size.x)/2; ul.y+=(avail.y-p->size.y)/2; break;
+	case PLACES:	ul.x+=(avail.x-p->size.x)/2; ul.y+= avail.y-p->size.y   ; break;
+	case PLACEE:	ul.x+= avail.x-p->size.x   ; ul.y+=(avail.y-p->size.y)/2; break;
+	case PLACEW:	                             ul.y+=(avail.y-p->size.y)/2; break;
+	case PLACEN:	ul.x+=(avail.x-p->size.x)/2;                              break;
+	case PLACENE:	ul.x+= avail.x-p->size.x   ;                              break;
+	case PLACENW:                                                             break;
+	case PLACESE:	ul.x+= avail.x-p->size.x   ; ul.y+= avail.y-p->size.y   ; break;
+	case PLACESW:                                ul.y+= avail.y-p->size.y   ; break;
+	}
+	p->r=Rpt(ul, addpt(ul, p->size));
+	space=p->size;
+	p->childspace(p, &ul, &space);
+	slack=subpt(space, p->childreq);
+	share=pl_getshare(p->child);
+	for(c=p->child;c;c=c->next){
+		if(c->flags&IGNORE) continue;
+		if(c->flags&EXPAND){
+			switch(c->flags&PACK){
+			case PACKN:
+			case PACKS:
+				c->sizereq.x+=slack.x;
+				l=slack.y/share.y;
+				c->sizereq.y+=l;
+				slack.y-=l;
+				--share.y;
+				break;
+			case PACKE:
+			case PACKW:
+				l=slack.x/share.x;
+				c->sizereq.x+=l;
+				slack.x-=l;
+				--share.x;
+				c->sizereq.y+=slack.y;
+				break;
+			}
+		}
+		switch(c->flags&PACK){
+		case PACKN:
+			newul=Pt(ul.x, ul.y+c->sizereq.y);
+			newspace=Pt(space.x, space.y-c->sizereq.y);
+			pl_setrect(c, ul, Pt(space.x, c->sizereq.y));
+			break;
+		case PACKW:
+			newul=Pt(ul.x+c->sizereq.x, ul.y);
+			newspace=Pt(space.x-c->sizereq.x, space.y);
+			pl_setrect(c, ul, Pt(c->sizereq.x, space.y));
+			break;
+		case PACKS:
+			newul=ul;
+			newspace=Pt(space.x, space.y-c->sizereq.y);
+			pl_setrect(c, Pt(ul.x, ul.y+space.y-c->sizereq.y),
+				Pt(space.x, c->sizereq.y));
+			break;
+		case PACKE:
+			newul=ul;
+			newspace=Pt(space.x-c->sizereq.x, space.y);
+			pl_setrect(c, Pt(ul.x+space.x-c->sizereq.x, ul.y),
+				Pt(c->sizereq.x, space.y));
+			break;
+		}
+		ul=newul;
+		space=newspace;
+	}
+}
+void plpack(Panel *p, Rectangle where){
+	pl_sizereq(p);
+	pl_setrect(p, where.min, subpt(where.max, where.min));
+}
+/*
+ * move an already-packed panel so that p->r=raddp(p->r, d)
+ */
+void plmove(Panel *p, Point d){
+	if(strcmp(p->kind, "edit") == 0)	/* sorry */
+		plemove(p, d);
+	p->r=rectaddpt(p->r, d);
+	for(p=p->child;p;p=p->next) plmove(p, d);
+}
--- /dev/null
+++ b/libpanel/panel.h
@@ -1,0 +1,202 @@
+//#pragma	src	"/sys/src/libpanel"
+//#pragma	lib	"libpanel.a"
+typedef struct Scroll Scroll;
+typedef struct Panel Panel;		/* a Graphical User Interface element */
+typedef struct Rtext Rtext;		/* formattable text */
+typedef void Icon;			/* Always used as Icon * -- Image or char */
+typedef struct Idol Idol;		/* A picture/text combo */
+struct Scroll{
+	Point pos, size;
+};
+struct Rtext{
+	int flags;		/* responds to hits? text selection? */
+	void *user;		/* user data */
+	int space;		/* how much space before, if no break */
+	int indent;		/* how much space before, after a break */
+	int voff;		/* vertical offset (for subscripts and superscripts) */
+	Image *b;		/* what to display, if nonzero */
+	Panel *p;		/* what to display, if nonzero and b==0 */
+	Font *font;		/* font in which to draw text */
+	char *text;		/* what to display, if b==0 and p==0 */
+	Rtext *next;		/* next piece */
+	/* private below */
+	Rtext *nextline;	/* links line to line */
+	Rtext *last;		/* last, for append */
+	Rectangle r;		/* where to draw, if origin were Pt(0,0) */
+	int topy;		/* y coord of top of line */
+	int wid;		/* not including space */
+};
+struct Panel{
+	Point ipad, pad;				/* extra space inside and outside */
+	Point fixedsize;				/* size of Panel, if FIXED */
+	int user;					/* available for user */
+	void *userp;					/* available for user */
+	Rectangle r;					/* where the Panel goes */
+	/* private below */
+	Panel *next;					/* It's a list! */
+	Panel *child, *echild, *parent;			/* No, it's a tree! */
+	Image *b;					/* where we're drawn */
+	int flags;					/* position flags, see below */
+	char *kind;					/* what kind of panel? */
+	int state;					/* for hitting & drawing purposes */
+	Point size;					/* space for this Panel */
+	Point sizereq;					/* size requested by this Panel */
+	Point childreq;					/* total size needed by children */
+	Panel *lastmouse;				/* who got the last mouse event? */
+	Panel *scrollee;				/* pointer to scrolled window */
+	Panel *xscroller, *yscroller;			/* pointers to scroll bars */
+	Scroll scr;					/* scroll data */
+	void *data;					/* kind-specific data */
+	void (*draw)(Panel *);				/* draw panel and children */
+	int (*pri)(Panel *, Point);			/* priority for hitting */
+	int (*hit)(Panel *, Mouse *);			/* process mouse event */
+	void (*type)(Panel *, Rune);			/* process keyboard event */
+	Point (*getsize)(Panel *, Point);		/* return size, given child size */
+	void (*childspace)(Panel *, Point *, Point *);	/* child ul & size given our size */
+	void (*scroll)(Panel *, int, int, int, int);	/* scroll bar to scrollee */
+	void (*setscrollbar)(Panel *, int, int, int);	/* scrollee to scroll bar */
+	void (*free)(Panel *);				/* free fields of data when done */
+	char* (*snarf)(Panel *);			/* snarf text from panel */
+	void (*paste)(Panel *, char *);			/* paste text into panel */
+};
+/*
+ * Panel flags
+ */
+#define	PACK	0x0007		/* which side of the parent is the Panel attached to? */
+#define		PACKN	0x0000
+#define		PACKE	0x0001
+#define		PACKS	0x0002
+#define		PACKW	0x0003
+#define		PACKCEN	0x0004	/* only used by pulldown */
+#define	FILLX	0x0008		/* grow horizontally to fill the available space */
+#define	FILLY	0x0010		/* grow vertically to fill the available space */
+#define	PLACE	0x01e0		/* which side of its space should the Panel adhere to? */
+#define		PLACECEN 0x0000
+#define		PLACES	0x0020
+#define		PLACEE	0x0040
+#define		PLACEW	0x0060
+#define		PLACEN	0x0080
+#define		PLACENE	0x00a0
+#define		PLACENW	0x00c0
+#define		PLACESE	0x00e0
+#define		PLACESW	0x0100
+#define	EXPAND	0x0200		/* use up all extra space in the parent */
+#define	FIXED	0x0c00		/* don't pass children's size requests through to parent */
+#define	FIXEDX	0x0400
+#define	FIXEDY	0x0800
+#define	MAXX	0x1000		/* make x size as big as biggest sibling's */
+#define	MAXY	0x2000		/* make y size as big as biggest sibling's */
+#define	BITMAP	0x4000		/* text argument is a bitmap, not a string */
+/* pldefs.h flags 0x08000-0x40000 */
+#define IGNORE	0x080000	/* ignore this panel totally */
+#define USERFL	0x100000	/* start of user flag */
+
+/*
+ * An extra bit in Mouse.buttons
+ */
+#define	OUT	8			/* Mouse.buttons bit, set when mouse leaves Panel */
+/*
+ * Priorities
+ */
+#define	PRI_NORMAL	0		/* ordinary panels */
+#define	PRI_POPUP	1		/* popup menus */
+#define	PRI_SCROLLBAR	2		/* scroll bars */
+
+/* Rtext.flags */
+#define PL_HOT		1
+#define PL_SEL		2
+#define PL_STR		4
+#define PL_HEAD		8
+
+Panel *plkbfocus;			/* the panel in keyboard focus */
+
+int plinit(int);			/* initialization */
+void plpack(Panel *, Rectangle);	/* figure out where to put the Panel & children */
+void plmove(Panel *, Point);		/* move an already-packed panel to a new location */
+void pldraw(Panel *, Image *);		/* display the panel on the bitmap */
+void plfree(Panel *);			/* give back space */
+void plgrabkb(Panel *);			/* this Panel should receive keyboard events */
+void plkeyboard(Rune);			/* send a keyboard event to the appropriate Panel */
+void plmouse(Panel *, Mouse *);		/* send a Mouse event to a Panel tree */
+void plscroll(Panel *, Panel *, Panel *); /* link up scroll bars */
+char *plentryval(Panel *);		/* entry delivers its value */
+void plsetbutton(Panel *, int);		/* set or clear the mark on a button */
+void plsetslider(Panel *, int, int);	/* set the value of a slider */
+Rune *pleget(Panel *);			/* get the text from an edit window */
+int plelen(Panel *);			/* get the length of the text from an edit window */
+void plegetsel(Panel *, int *, int *);	/* get the selection from an edit window */
+void plepaste(Panel *, Rune *, int);	/* paste in an edit window */
+void plesel(Panel *, int, int);		/* set the selection in an edit window */
+void plescroll(Panel *, int);		/* scroll an edit window */
+Scroll plgetscroll(Panel *);		/* get scrolling information from panel */
+void plsetscroll(Panel *, Scroll);	/* set scrolling information */
+void plplacelabel(Panel *, int);	/* label placement */
+
+/*
+ * Panel creation & reinitialization functions
+ */
+Panel *plbutton(Panel *pl, int, Icon *, void (*)(Panel *pl, int));
+Panel *plcanvas(Panel *pl, int, void (*)(Panel *), void (*)(Panel *pl, Mouse *));
+Panel *plcheckbutton(Panel *pl, int, Icon *, void (*)(Panel *pl, int, int));
+Panel *pledit(Panel *, int, Point, Rune *, int, void (*)(Panel *));
+Panel *plentry(Panel *pl, int, int, char *, void (*)(Panel *pl, char *));
+Panel *plframe(Panel *pl, int);
+Panel *plgroup(Panel *pl, int);
+Panel *plidollist(Panel*, int, Point, Font*, Idol*, void (*)(Panel*, int, void*));
+Panel *pllabel(Panel *pl, int, Icon *);
+Panel *pllist(Panel *pl, int, char *(*)(Panel *, int), int, void(*)(Panel *pl, int, int));
+Panel *plmenu(Panel *pl, int, Icon **, int, void (*)(int, int));
+Panel *plmenubar(Panel *pl, int, int, Icon *, Panel *pl, Icon *, ...);
+Panel *plmessage(Panel *pl, int, int, char *);
+Panel *plpopup(Panel *pl, int, Panel *pl, Panel *pl, Panel *pl);
+Panel *plpulldown(Panel *pl, int, Icon *, Panel *pl, int);
+Panel *plradiobutton(Panel *pl, int, Icon *, void (*)(Panel *pl, int, int));
+Panel *plscrollbar(Panel *plparent, int flags);
+Panel *plslider(Panel *pl, int, Point, void(*)(Panel *pl, int, int, int));
+Panel *pltextview(Panel *, int, Point, Rtext *, void (*)(Panel *, int, Rtext *));
+void plinitbutton(Panel *, int, Icon *, void (*)(Panel *, int));
+void plinitcanvas(Panel *, int, void (*)(Panel *), void (*)(Panel *, Mouse *));
+void plinitcheckbutton(Panel *, int, Icon *, void (*)(Panel *, int, int));
+void plinitedit(Panel *, int, Point, Rune *, int, void (*)(Panel *));
+void plinitentry(Panel *, int, int, char *, void (*)(Panel *, char *));
+void plinitframe(Panel *, int);
+void plinitgroup(Panel *, int);
+void plinitidollist(Panel*, int, Point, Font*, Idol*, void (*)(Panel*, int, void*));
+void plinitlabel(Panel *, int, Icon *);
+void plinitlist(Panel *, int, char *(*)(Panel *, int), int, void(*)(Panel *, int, int));
+void plinitmenu(Panel *, int, Icon **, int, void (*)(int, int));
+void plinitmessage(Panel *, int, int, char *);
+void plinitpopup(Panel *, int, Panel *, Panel *, Panel *);
+void plinitpulldown(Panel *, int, Icon *, Panel *, int);
+void plinitradiobutton(Panel *, int, Icon *, void (*)(Panel *, int, int));
+void plinitscrollbar(Panel *parent, int flags);
+void plinitslider(Panel *, int, Point, void(*)(Panel *, int, int, int));
+void plinittextview(Panel *, int, Point, Rtext *, void (*)(Panel *, int, Rtext *));
+/*
+ * Rtext constructors & destructor
+ */
+Rtext *plrtstr(Rtext **, int, int, int, Font *, char *, int, void *);
+Rtext *plrtbitmap(Rtext **, int, int, int, Image *, int, void *);
+Rtext *plrtpanel(Rtext **, int, int, int, Panel *, void *);
+void plrtfree(Rtext *);
+void plrtseltext(Rtext *, Rtext *, Rtext *);
+char *plrtsnarftext(Rtext *);
+
+int plgetpostextview(Panel *);
+void plsetpostextview(Panel *, int);
+
+/*
+ * Idols
+ */
+Idol *plmkidol(Idol**, Image*, Image*, char*, void*);
+void plfreeidol(Idol*);
+Point plidolsize(Idol*, Font*, int);
+void *plidollistgetsel(Panel*);
+
+/*
+ * Snarf
+ */
+void plputsnarf(char *);
+char *plgetsnarf(void);
+void plsnarf(Panel *);			/* snarf a panel */
+void plpaste(Panel *);			/* paste a panel */
binary files /dev/null b/libpanel/panel.pdf differ
--- /dev/null
+++ b/libpanel/pldefs.h
@@ -1,0 +1,104 @@
+/*
+ * Definitions for internal use only
+ */
+/*
+ * Variable-font text routines
+ * These could make a separate library.
+ */
+Point pl_rtfmt(Rtext *, int);
+void pl_rtdraw(Image *, Rectangle, Rtext *, Point);
+void pl_rtredraw(Image *, Rectangle, Rtext *, Point, Point, int);
+Rtext *pl_rthit(Rtext *, Point, Point, Point);
+#define	HITME	0x08000		/* tells ptinpanel not to look at children */
+#define	LEAF	0x10000		/* newpanel will refuse to attach children */
+#define	INVIS	0x20000		/* don't draw this */
+#define	REMOUSE	0x40000		/* send next mouse event here, even if not inside */
+/*
+ * States, also styles
+ */
+enum{
+	UP,
+	DOWN1,
+	DOWN2,
+	DOWN3,
+	DOWN,
+	PASSIVE,
+	FRAME
+};
+/*
+ * Scroll flags
+ */
+enum{
+	SCROLLUP,
+	SCROLLDOWN,
+	SCROLLABSY,
+	SCROLLLEFT,
+	SCROLLRIGHT,
+	SCROLLABSX,
+};
+/*
+ * Scrollbar, slider orientations
+ */
+enum{
+	HORIZ,
+	VERT
+};
+Panel *pl_newpanel(Panel *, int);	/* make a new Panel, given parent & data size */
+void *pl_emalloc(int);			/* allocate some space, exit on error */
+void *pl_erealloc(void*,int);		/* reallocate some space, exit on error */
+void pl_print(Panel *);			/* print a Panel tree */
+Panel *pl_ptinpanel(Point, Panel *);	/* highest-priority subpanel containing point */
+/*
+ * Drawing primitives
+ */
+int pl_drawinit(int);
+Rectangle pl_box(Image *, Rectangle, int);
+Rectangle pl_outline(Image *, Rectangle, int);
+Point pl_boxsize(Point, int);
+void pl_interior(int, Point *, Point *);
+void pl_drawicon(Image *, Rectangle, int, int, Icon *);
+Rectangle pl_check(Image *, Rectangle, int);
+Rectangle pl_radio(Image *, Rectangle, int);
+int pl_ckwid(void);
+void pl_sliderupd(Image *, Rectangle, int, int, int);
+void pl_invis(Panel *, int);
+Point pl_iconsize(int, Icon *);
+void pl_highlight(Image *, Rectangle);
+void pl_clr(Image *, Rectangle);
+void pl_fill(Image *, Rectangle);
+void pl_cpy(Image *, Point, Rectangle);
+
+/*
+ * Rune mangling functions
+ */
+int pl_idchar(int);
+int pl_rune1st(int);
+char *pl_nextrune(char *);
+int pl_runewidth(Font *, char *);
+/*
+ * Fixed-font Text-window routines
+ * These could be separated out into a separate library.
+ */
+typedef struct Textwin Textwin;
+struct Textwin{
+	Rune *text, *etext, *eslack;	/* text, with some slack off the end */
+	int top, bot;			/* range of runes visible on screen */
+	int sel0, sel1;			/* selection */
+	Point *loc, *eloc;		/* ul corners of visible runes (+1 more at end!) */
+	Image *b;			/* bitmap the text is drawn in */
+	Rectangle r;			/* rectangle the text is drawn in */
+	Font *font;			/* font text is drawn in */
+	int hgt;			/* same as font->height */
+	int tabstop;			/* tab settings are every tabstop pixels */
+	int mintab;			/* the minimum size of a tab */
+};
+Textwin *twnew(Image *, Font *, Rune *, int);
+void twfree(Textwin *);
+void twhilite(Textwin *, int, int, int);
+void twselect(Textwin *, Mouse *);
+void twreplace(Textwin *, int, int, Rune *, int);
+void twscroll(Textwin *, int);
+int twpt2rune(Textwin *, Point);
+void twreshape(Textwin *, Rectangle);
+void twmove(Textwin *, Point);
+void plemove(Panel *, Point);
--- /dev/null
+++ b/libpanel/popup.c
@@ -1,0 +1,116 @@
+/*
+ * popup
+ *	looks like a group, except diverts hits on certain buttons to
+ *	panels that it temporarily pops up.
+ */
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <panel.h>
+#include "pldefs.h"
+typedef struct Popup Popup;
+struct Popup{
+	Image *save;			/* where to save what the popup covers */
+	Panel *pop[3];			/* what to pop up */
+};
+void pl_drawpopup(Panel *p){
+	USED(p);
+}
+int pl_hitpopup(Panel *g, Mouse *m){
+	Panel *p;
+	Point d;
+	Popup *pp;
+
+	pp=g->data;
+	if(g->state==UP){
+		switch(m->buttons&7){
+		case 0: p=g->child; break;
+		case 1:	p=pp->pop[0]; g->state=DOWN1; break;
+		case 2: p=pp->pop[1]; g->state=DOWN2; break;
+		case 4: p=pp->pop[2]; g->state=DOWN3; break;
+		default: p=0; break;
+		}
+		if(p==0){
+			p=g->child;
+			g->state=DOWN;
+		}
+		else if(g->state!=UP){
+			plpack(p, screen->clipr);
+			if(p->lastmouse)
+				d=subpt(m->xy, divpt(addpt(p->lastmouse->r.min,
+						     p->lastmouse->r.max), 2));
+			else
+				d=subpt(m->xy, divpt(addpt(p->r.min, p->r.max), 2));
+			if(p->r.min.x+d.x<g->r.min.x) d.x=g->r.min.x-p->r.min.x;
+			if(p->r.max.x+d.x>g->r.max.x) d.x=g->r.max.x-p->r.max.x;
+			if(p->r.min.y+d.y<g->r.min.y) d.y=g->r.min.y-p->r.min.y;
+			if(p->r.max.y+d.y>g->r.max.y) d.y=g->r.max.y-p->r.max.y;
+			plmove(p, d);
+			pp->save=allocimage(display, p->r, g->b->chan, 0, DNofill);
+			if(pp->save!=0) draw(pp->save, p->r, g->b, 0, p->r.min);
+			pl_invis(p, 0);
+			pldraw(p, g->b);
+		}
+	}
+	else{
+		switch(g->state){
+		default: SET(p); break;			/* can't happen! */
+		case DOWN1: p=pp->pop[0]; break;
+		case DOWN2: p=pp->pop[1]; break;
+		case DOWN3: p=pp->pop[2]; break;
+		case DOWN:  p=g->child;  break;
+		}
+		if((m->buttons&7)==0){
+			if(g->state!=DOWN){
+				if(pp->save!=0){
+					draw(g->b, p->r, pp->save, 0, p->r.min);
+					freeimage(pp->save);
+					pp->save=0;
+				}
+				pl_invis(p, 1);
+			}
+			g->state=UP;
+		}
+	}
+	plmouse(p, m);
+	if((m->buttons&7)==0)
+		g->state=UP;
+	return (m->buttons&7)!=0;
+}
+void pl_typepopup(Panel *g, Rune c){
+	USED(g, c);
+}
+Point pl_getsizepopup(Panel *g, Point children){
+	USED(g);
+	return children;
+}
+void pl_childspacepopup(Panel *g, Point *ul, Point *size){
+	USED(g, ul, size);
+}
+int pl_pripopup(Panel *, Point){
+	return PRI_POPUP;
+}
+void plinitpopup(Panel *v, int flags, Panel *pop0, Panel *pop1, Panel *pop2){
+	Popup *pp;
+	pp=v->data;
+	v->flags=flags;
+	v->pri=pl_pripopup;
+	v->state=UP;
+	v->draw=pl_drawpopup;
+	v->hit=pl_hitpopup;
+	v->type=pl_typepopup;
+	v->getsize=pl_getsizepopup;
+	v->childspace=pl_childspacepopup;
+	pp->pop[0]=pop0;
+	pp->pop[1]=pop1;
+	pp->pop[2]=pop2;
+	pp->save=0;
+	v->kind="popup";
+}
+Panel *plpopup(Panel *parent, int flags, Panel *pop0, Panel *pop1, Panel *pop2){
+	Panel *v;
+	v=pl_newpanel(parent, sizeof(Popup));
+	plinitpopup(v, flags, pop0, pop1, pop2);
+	return v;
+}
--- /dev/null
+++ b/libpanel/print.c
@@ -1,0 +1,56 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <panel.h>
+#include "pldefs.h"
+void pl_iprint(int indent, char *fmt, ...){
+	char buf[8192];
+	va_list arg;
+	memset(buf, '\t', indent);
+	va_start(arg, fmt);
+	write(1, buf, vsnprint(buf+indent, sizeof(buf)-indent, fmt, arg));
+	va_end(arg);
+}
+void pl_ipprint(Panel *p, int n){
+	Panel *c;
+	char *place, *stick;
+	pl_iprint(n, "%s (0x%.8x)\n", p->kind, p);
+	pl_iprint(n, "  r=(%d %d, %d %d)\n",
+		p->r.min.x, p->r.min.y, p->r.max.x, p->r.max.y);
+	switch(p->flags&PACK){
+	default: SET(place); break;
+	case PACKN: place="n"; break;
+	case PACKE: place="e"; break;
+	case PACKS: place="s"; break;
+	case PACKW: place="w"; break;
+	}
+	switch(p->flags&PLACE){
+	default: SET(stick); break;
+	case PLACECEN:	stick=""; break;
+	case PLACES:	stick=" stick s"; break;
+	case PLACEE:	stick=" stick e"; break;
+	case PLACEW:	stick=" stick w"; break;
+	case PLACEN:	stick=" stick n"; break;
+	case PLACENE:	stick=" stick ne"; break;
+	case PLACENW:	stick=" stick nw"; break;
+	case PLACESE:	stick=" stick se"; break;
+	case PLACESW:	stick=" stick sw"; break;
+	}
+	pl_iprint(n, "  place %s%s%s%s%s%s\n",
+		place,
+		p->flags&FILLX?" fill x":"",
+		p->flags&FILLY?" fill y":"",
+		stick,
+		p->flags&EXPAND?" expand":"",
+		p->flags&FIXED?" fixed":"");
+	if(!eqpt(p->pad, Pt(0, 0))) pl_iprint(n, "  pad=%d,%d)\n", p->pad.x, p->pad.y);
+	if(!eqpt(p->ipad, Pt(0, 0))) pl_iprint(n, "  ipad=%d,%d)\n", p->ipad.x, p->ipad.y);
+	pl_iprint(n, "  size=(%d,%d), sizereq=(%d,%d)\n",
+		p->size.x, p->size.y, p->sizereq.x, p->sizereq.y);
+	for(c=p->child;c;c=c->next)
+		pl_ipprint(c, n+1);
+}
+void pl_print(Panel *p){
+	pl_ipprint(p, 0);
+}
--- /dev/null
+++ b/libpanel/pulldown.c
@@ -1,0 +1,160 @@
+/*
+ * pulldown
+ *	makes a button that pops up a panel when hit
+ */
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <panel.h>
+#include "pldefs.h"
+typedef struct Pulldown Pulldown;
+struct Pulldown{
+	Icon *icon;		/* button label */
+	Panel *pull;		/* Panel to pull down */
+	int side;		/* which side of the button to put the panel on */
+	Image *save;		/* where to save what we draw the panel on */
+};
+void pl_drawpulldown(Panel *p){
+	pl_drawicon(p->b, pl_box(p->b, p->r, p->state), PLACECEN,
+		p->flags, ((Pulldown *)p->data)->icon);
+}
+int pl_hitpulldown(Panel *g, Mouse *m){
+	int oldstate, passon;
+	Rectangle r;
+	Panel *p, *hitme;
+	Pulldown *pp;
+	pp=g->data;
+	oldstate=g->state;
+	p=pp->pull;
+	hitme=0;
+	switch(g->state){
+	case UP:
+		if(!ptinrect(m->xy, g->r))
+			g->state=UP;
+		else if(m->buttons&7){
+			r=g->b->r;
+			p->flags&=~PLACE;
+			switch(pp->side){
+			case PACKN:
+				r.min.x=g->r.min.x;
+				r.max.y=g->r.min.y;
+				p->flags|=PLACESW;
+				break;
+			case PACKS:
+				r.min.x=g->r.min.x;
+				r.min.y=g->r.max.y;
+				p->flags|=PLACENW;
+				break;
+			case PACKE:
+				r.min.x=g->r.max.x;
+				r.min.y=g->r.min.y;
+				p->flags|=PLACENW;
+				break;
+			case PACKW:
+				r.max.x=g->r.min.x;
+				r.min.y=g->r.min.y;
+				p->flags|=PLACENE;
+				break;
+			case PACKCEN:
+				r.min=g->r.min;
+				p->flags|=PLACENW;
+				break;
+			}
+			plpack(p, r);
+			pp->save=allocimage(display, p->r, g->b->chan, 0, DNofill);
+			if(pp->save!=0) draw(pp->save, p->r, g->b, 0, p->r.min);
+			pl_invis(p, 0);
+			pldraw(p, g->b);
+			g->state=DOWN;
+		}
+		break;
+	case DOWN:
+		if(!ptinrect(m->xy, g->r)){
+			switch(pp->side){
+			default: SET(passon); break;		/* doesn't happen */
+			case PACKN: passon=m->xy.y<g->r.min.y; break;
+			case PACKS: passon=m->xy.y>=g->r.max.y; break;
+			case PACKE: passon=m->xy.x>=g->r.max.x; break;
+			case PACKW: passon=m->xy.x<g->r.min.x; break;
+			case PACKCEN: passon=1; break;
+			}
+			if(passon){
+				hitme=p;
+				if((m->buttons&7)==0) g->state=UP;
+			}
+			else	g->state=UP;
+		}
+		else if((m->buttons&7)==0) g->state=UP;
+		else hitme=p;
+		if(g->state!=DOWN && pp->save){
+			draw(g->b, p->r, pp->save, 0, p->r.min);
+			freeimage(pp->save);
+			pp->save=0;
+			pl_invis(p, 1);
+			hitme=p;
+		}
+	}
+	if(g->state!=oldstate) pldraw(g, g->b);
+	if(hitme) plmouse(hitme, m);
+	return g->state==DOWN;
+}
+void pl_typepulldown(Panel *p, Rune c){
+	USED(p, c);
+}
+Point pl_getsizepulldown(Panel *p, Point children){
+	USED(p, children);
+	return pl_boxsize(pl_iconsize(p->flags, ((Pulldown *)p->data)->icon), p->state);
+}
+void pl_childspacepulldown(Panel *p, Point *ul, Point *size){
+	USED(p, ul, size);
+}
+void plinitpulldown(Panel *v, int flags, Icon *icon, Panel *pullthis, int side){
+	Pulldown *pp;
+	pp=v->data;
+	v->flags=flags|LEAF;
+	v->draw=pl_drawpulldown;
+	v->hit=pl_hitpulldown;
+	v->type=pl_typepulldown;
+	v->getsize=pl_getsizepulldown;
+	v->childspace=pl_childspacepulldown;
+	pp->pull=pullthis;
+	pp->side=side;
+	pp->icon=icon;
+	v->kind="pulldown";
+}
+Panel *plpulldown(Panel *parent, int flags, Icon *icon, Panel *pullthis, int side){
+	Panel *v;
+	v=pl_newpanel(parent, sizeof(Pulldown));
+	v->state=UP;
+	((Pulldown *)v->data)->save=0;
+	plinitpulldown(v, flags, icon, pullthis, side);
+	return v;
+}
+Panel *plmenubar(Panel *parent, int flags, int cflags, Icon *l1, Panel *m1, Icon *l2, ...){
+	Panel *v;
+	va_list arg;
+	Icon *s;
+	int pulldir;
+	switch(cflags&PACK){
+	default:
+		SET(pulldir);
+		break;
+	case PACKE:
+	case PACKW:
+		pulldir=PACKS;
+		break;
+	case PACKN:
+	case PACKS:
+		pulldir=PACKE;
+		break;
+	}
+	v=plgroup(parent, flags);
+	va_start(arg, cflags);
+	while((s=va_arg(arg, Icon *))!=0)
+		plpulldown(v, cflags, s, va_arg(arg, Panel *), pulldir);
+	va_end(arg);
+	USED(l1, m1, l2);
+	v->kind="menubar";
+	return v;
+}
--- /dev/null
+++ b/libpanel/rtext.c
@@ -1,0 +1,383 @@
+/*
+ * Rich text with images.
+ * Should there be an offset field, to do subscripts & kerning?
+ */
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <panel.h>
+#include "pldefs.h"
+#include "rtext.h"
+
+#define LEAD	4	/* extra space between lines */
+#define BORD	2	/* extra border for images */
+
+static Image *head, *blue;
+
+Rtext *pl_rtnew(Rtext **t, int space, int indent, int voff, Image *b, Panel *p, Font *f, char *s, int flags, void *user){
+	Rtext *new;
+	new=pl_emalloc(sizeof(Rtext));
+	new->flags=flags;
+	new->user=user;
+	new->space=space;
+	new->indent=indent;
+	new->voff=voff;
+	new->b=b;
+	new->p=p;
+	new->font=f;
+	new->text=s;
+	new->next=0;
+	new->nextline=0;
+	new->r=Rect(0,0,0,0);
+	if(*t)
+		(*t)->last->next=new;
+	else
+		*t=new;
+	(*t)->last=new;
+	return new;
+}
+Rtext *plrtpanel(Rtext **t, int space, int indent, int voff, Panel *p, void *user){
+	return pl_rtnew(t, space, indent, voff, 0, p, 0, 0, 1, user);
+}
+Rtext *plrtstr(Rtext **t, int space, int indent, int voff, Font *f, char *s, int flags, void *user){
+	return pl_rtnew(t, space, indent, voff, 0, 0, f, s, flags, user);
+}
+Rtext *plrtbitmap(Rtext **t, int space, int indent, int voff, Image *b, int flags, void *user){
+	return pl_rtnew(t, space, indent, voff, b, 0, 0, 0, flags, user);
+}
+void plrtfree(Rtext *t){
+	Rtext *next;
+	while(t){
+		next=t->next;
+		free(t);
+		t=next;
+	}
+}
+int pl_tabmin, pl_tabsize;
+void pltabsize(int min, int size){
+	pl_tabmin=min;
+	pl_tabsize=size;
+}
+int pl_space(int space, int pos, int indent){
+	if(space>=0) return space;
+	switch(PL_OP(space)){
+	default:
+		return 0;
+	case PL_TAB:
+		return ((pos-indent+pl_tabmin)/pl_tabsize+PL_ARG(space))*pl_tabsize+indent-pos;
+	}
+}
+/*
+ * initialize rectangles & nextlines of text starting at t,
+ * galley width is wid.  Returns the total width/height of the text
+ */
+Point pl_rtfmt(Rtext *t, int wid){
+	Rtext *tp, *eline;
+	int ascent, descent, x, space, a, d, w, topy, indent, maxwid;
+	Point p;
+
+	p=Pt(0,0);
+	eline=t;
+	maxwid=0;
+	while(t){
+		ascent=0;
+		descent=0;
+		indent=space=pl_space(t->indent, 0, 0);
+		x=0;
+		tp=t;
+		for(;;){
+			if(tp->b){
+				a=tp->b->r.max.y-tp->b->r.min.y+BORD;
+				d=BORD;
+				w=tp->b->repl?wid-x:tp->b->r.max.x-tp->b->r.min.x+BORD*2;
+			}
+			else if(tp->p){
+				/* what if plpack fails? */
+				plpack(tp->p, Rect(0,0,wid,wid));
+				plmove(tp->p, subpt(Pt(0,0), tp->p->r.min));
+				a=tp->p->r.max.y-tp->p->r.min.y;
+				d=0;
+				w=tp->p->r.max.x-tp->p->r.min.x;
+			}
+			else{
+				a=tp->font->ascent;
+				d=tp->font->height-a;
+				w=tp->wid=stringwidth(tp->font, tp->text);
+			}
+			a-=tp->voff,d+=tp->voff;
+			if(x+w+space>wid) break;
+			if(a>ascent) ascent=a;
+			if(d>descent) descent=d;
+			x+=w+space;
+			tp=tp->next;
+			if(tp==0){
+				eline=0;
+				break;
+			}
+			space=pl_space(tp->space, x, indent);
+			if(space) eline=tp;
+		}
+		if(eline==t){	/* No progress!  Force fit the first block! */
+			if(tp==t){
+				if(a>ascent) ascent=a;
+				if(d>descent) descent=d;
+				eline=tp->next;
+			}else
+				eline=tp;
+		}
+		topy=p.y;
+		p.y+=ascent;
+		p.x=indent=pl_space(t->indent, 0, 0);
+		for(;;){
+			t->topy=topy;
+			t->r.min.x=p.x;
+			p.y+=t->voff;
+			if(t->b){
+				t->r.max.y=p.y+BORD;
+				t->r.min.y=p.y-(t->b->r.max.y-t->b->r.min.y)-BORD;
+				p.x+=t->b->repl?wid-p.x:(t->b->r.max.x-t->b->r.min.x)+BORD*2;
+			}
+			else if(t->p){
+				t->r.max.y=p.y;
+				t->r.min.y=p.y-t->p->r.max.y;
+				p.x+=t->p->r.max.x;
+			}
+			else{
+				t->r.min.y=p.y-t->font->ascent;
+				t->r.max.y=t->r.min.y+t->font->height;
+				p.x+=t->wid;
+			}
+			p.y-=t->voff;
+			t->r.max.x=p.x;
+			t->nextline=eline;
+			t=t->next;
+			if(t==eline) break;
+			p.x+=pl_space(t->space, p.x, indent);
+		}
+		if(p.x>maxwid) maxwid=p.x;
+		p.y+=descent+LEAD;
+	}
+	return Pt(maxwid, p.y);
+}
+
+/*
+ * If we draw the text in a backup bitmap and copy it onto the screen,
+ * the bitmap pointers in all the subpanels point to the wrong bitmap.
+ * This code fixes them.
+ */
+void pl_stuffbitmap(Panel *p, Image *b){
+	p->b=b;
+	for(p=p->child;p;p=p->next)
+		pl_stuffbitmap(p, b);
+}
+
+void pl_rtdraw(Image *b, Rectangle r, Rtext *t, Point offs){
+	static Image *backup;
+	Point lp, sp;
+	Rectangle dr;
+	Image *bb;
+
+	bb = b;
+	if(backup==0 || backup->chan!=b->chan || rectinrect(r, backup->r)==0){
+		freeimage(backup);
+		backup=allocimage(display, bb->r, bb->chan, 0, DNofill);
+	}
+	if(backup)
+		b=backup;
+	pl_clr(b, r);
+	lp=ZP;
+	sp=ZP;
+	offs=subpt(r.min, offs);
+	for(;t;t=t->next) if(!eqrect(t->r, Rect(0,0,0,0))){
+		dr=rectaddpt(t->r, offs);
+		if(dr.max.y>r.min.y
+		&& dr.min.y<r.max.y
+		&& dr.max.x>r.min.x
+		&& dr.min.x<r.max.x){
+			if(t->b){
+				draw(b, insetrect(dr, BORD), t->b, 0, t->b->r.min);
+				if(t->flags&PL_HOT) border(b, dr, 1, display->black, ZP);
+				if(t->flags&PL_STR) {
+					line(b, Pt(dr.min.x, dr.min.y), Pt(dr.max.x, dr.max.y),
+						Endsquare, Endsquare, 0,
+						display->black, ZP);
+					line(b, Pt(dr.min.x, dr.max.y), Pt(dr.max.x, dr.min.y),
+						Endsquare, Endsquare, 0,
+						display->black, ZP);
+				}
+				if(t->flags&PL_SEL)
+					pl_highlight(b, dr);
+			}
+			else if(t->p){
+				plmove(t->p, subpt(dr.min, t->p->r.min));
+				pldraw(t->p, b);
+				if(b!=bb)
+					pl_stuffbitmap(t->p, bb);
+			}
+			else{
+				if(t->flags&PL_HEAD){
+					if(head==nil)
+						head=allocimage(display, Rect(0,0,1,1), screen->chan, 1, 0xAAAAAAFF);
+					string(b, dr.min, head, ZP, t->font, t->text);
+				}else if(t->flags&PL_HOT){
+					if(blue==nil)
+						blue=allocimage(display, Rect(0,0,1,1), screen->chan, 1, 0x0000FFFF);
+					string(b, dr.min, blue, ZP, t->font, t->text);
+				}else
+					string(b, dr.min, display->black, ZP, t->font, t->text);
+				if(t->flags&PL_SEL)
+					pl_highlight(b, dr);
+				if(t->flags&PL_STR){
+					int y = dr.max.y - t->font->height/2;
+					if(sp.y != y)
+						sp = Pt(dr.min.x, y);
+					line(b, sp, Pt(dr.max.x, y),
+						Endsquare, Endsquare, 0,
+						display->black, ZP);
+					sp = Pt(dr.max.x, y);
+				} else
+					sp = ZP;
+				/*if(t->flags&PL_HOT){
+					int y = dr.max.y - 1;
+					if(lp.y != y)
+						lp = Pt(dr.min.x, y);
+					line(b, lp, Pt(dr.max.x, y),
+						Endsquare, Endsquare, 0,
+						display->black, ZP);
+					lp = Pt(dr.max.x, y);
+				} else*/
+					lp = ZP;
+				continue;
+			}
+			lp = ZP;
+			sp = ZP;
+		}
+	}
+	if(b!=bb)
+		draw(bb, r, b, 0, r.min);
+}
+/*
+ * Reposition text already drawn in the window.
+ * We just move the pixels and update the positions of any
+ * enclosed panels
+ */
+void pl_reposition(Rtext *t, Image *b, Point p, Rectangle r){
+	Point offs;
+	pl_cpy(b, p, r);
+	offs=subpt(p, r.min);
+	for(;t;t=t->next)
+		if(!eqrect(t->r, Rect(0,0,0,0)) && !t->b && t->p)
+			plmove(t->p, offs);
+}
+/*
+ * Rectangle r of Image b contains an image of Rtext t, offset by oldoffs.
+ * Redraw the text to have offset yoffs.
+ */
+void pl_rtredraw(Image *b, Rectangle r, Rtext *t, Point offs, Point oldoffs, int dir){
+	int d, size;
+
+	if(dir==VERT){
+		d=oldoffs.y-offs.y;
+		size=r.max.y-r.min.y;
+		if(d>=size || -d>=size) /* move more than screenful */
+			pl_rtdraw(b, r, t, offs);
+		else if(d<0){ /* down */
+			pl_reposition(t, b, r.min,
+				Rect(r.min.x, r.min.y-d, r.max.x, r.max.y));
+			pl_rtdraw(b, Rect(r.min.x, r.max.y+d, r.max.x, r.max.y),
+				t, Pt(offs.x, offs.y+size+d));
+		}
+		else if(d>0){ /* up */
+			pl_reposition(t, b, Pt(r.min.x, r.min.y+d),
+				Rect(r.min.x, r.min.y, r.max.x, r.max.y-d));
+			pl_rtdraw(b, Rect(r.min.x, r.min.y, r.max.x, r.min.y+d),
+				t, offs);
+		}
+	}else{ /* dir==HORIZ */
+		d=oldoffs.x-offs.x;
+		size=r.max.x-r.min.x;
+		if(d>=size || -d>=size) /* move more than screenful */
+			pl_rtdraw(b, r, t, offs);
+		else if(d<0){ /* right */
+			pl_reposition(t, b, r.min,
+				Rect(r.min.x-d, r.min.y, r.max.x, r.max.y));
+			pl_rtdraw(b, Rect(r.max.x+d, r.min.y, r.max.x, r.max.y),
+				t, Pt(offs.x+size+d, offs.y));
+		}
+		else if(d>0){ /* left */
+			pl_reposition(t, b, Pt(r.min.x+d, r.min.y),
+				Rect(r.min.x, r.min.y, r.max.x-d, r.max.y));
+			pl_rtdraw(b, Rect(r.min.x, r.min.y, r.min.x+d, r.max.y),
+				t, offs);
+		}		
+	}
+}
+Rtext *pl_rthit(Rtext *t, Point offs, Point p, Point ul){
+	Rectangle r;
+	Point lp;
+	if(t==0) return 0;
+	p.x+=offs.x-ul.x;
+	p.y+=offs.y-ul.y;
+	while(t->nextline && t->nextline->topy<=p.y) t=t->nextline;
+	lp=ZP;
+	for(;t!=0;t=t->next){
+		if(t->topy>p.y) return 0;
+		r = t->r;
+		if((t->flags&PL_HOT) != 0 && t->b == nil && t->p == nil){
+			if(lp.y == r.max.y && lp.x < r.min.x)
+				r.min.x=lp.x;
+			lp=r.max;
+		} else
+			lp=ZP;
+		if(ptinrect(p, r)) return t;
+	}
+	return 0;
+}
+
+void plrtseltext(Rtext *t, Rtext *s, Rtext *e){
+	while(t){
+		t->flags &= ~PL_SEL;
+		t = t->next;
+	}
+	if(s==0 || e==0)
+		return;
+	for(t=s; t!=0 && t!=e; t=t->next)
+		;
+	if(t==e){
+		for(t=s; t!=e; t=t->next)
+			t->flags |= PL_SEL;
+	}else{
+		for(t=e; t!=s; t=t->next)
+			t->flags |= PL_SEL;
+	}
+	t->flags |= PL_SEL;
+}
+
+char *plrtsnarftext(Rtext *w){
+	char *b, *p, *e, *t;
+	int n;
+
+	b=p=e=0;
+	for(; w; w = w->next){
+		if((w->flags&PL_SEL)==0 || w->text==0)
+			continue;
+		n = strlen(w->text)+64;
+		if(p+n >= e){
+			n = (p+n+64)-b;
+			t = pl_erealloc(b, n);
+			p = t+(p-b);
+			e = t+n;
+			b = t;
+		}
+		if(w->space == 0)
+			p += sprint(p, "%s", w->text);
+		else if(w->space > 0)
+			p += sprint(p, " %s", w->text);
+		else if(PL_OP(w->space) == PL_TAB)
+			p += sprint(p, "\t%s", w->text);
+		if(w->nextline == w->next)
+			p += sprint(p, "\n");
+	}
+	return b;
+}
--- /dev/null
+++ b/libpanel/rtext.h
@@ -1,0 +1,11 @@
+/*
+ * Rtext definitions
+ */
+#define	PL_NOPBIT	4
+#define	PL_NARGBIT	12
+#define	PL_ARGMASK	((1<<PL_NARGBIT)-1)
+#define	PL_SPECIAL(op)	(((-1<<PL_NOPBIT)|op)<<PL_NARGBIT)
+#define	PL_OP(t)	((t)&~PL_ARGMASK)
+#define	PL_ARG(t)	((t)&PL_ARGMASK)
+#define	PL_TAB		PL_SPECIAL(0)		/* # of tab stops before text */
+void pltabsize(int, int);			/* set min tab and tab size */
--- /dev/null
+++ b/libpanel/scrltest.c
@@ -1,0 +1,65 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <panel.h>
+Panel *root, *list;
+char *genlist(Panel *, int which){
+	static char buf[7];
+	if(which<0 || 26<=which) return 0;
+	sprint(buf, "item %c", which+'a');
+	return buf;
+}
+void hitgen(Panel *p, int buttons, int sel){
+	USED(p, buttons, sel);
+}
+void ereshaped(Rectangle r){
+	screen.r=r;
+	r=inset(r, 4);
+	plpack(root, r);
+	bitblt(&screen, screen.r.min, &screen, screen.r, Zero);
+	pldraw(root, &screen);
+}
+void done(Panel *p, int buttons){
+	USED(p, buttons);
+	bitblt(&screen, screen.r.min, &screen, screen.r, Zero);
+	exits(0);
+}
+Panel *msg;
+void message(char *s, ...){
+	char buf[1024], *out;
+	va_list arg;
+	va_start(arg, s);
+	out = doprint(buf, buf+sizeof(buf), s, arg);
+	va_end(arg);
+	*out='\0';
+	plinitlabel(msg, PACKN|FILLX, buf);
+	pldraw(msg, &screen);
+}
+Scroll s;
+void save(Panel *p, int buttons){
+	USED(p, buttons);
+	s=plgetscroll(list);
+	message("save %d %d %d %d", s);
+}
+void revert(Panel *p, int buttons){
+	USED(p, buttons);
+	plsetscroll(list, s, &screen);
+	message("revert %d %d %d %d", s);
+}
+void main(void){
+	Panel *g;
+	binit(0,0,0);
+	einit(Emouse);
+	plinit(screen.ldepth);
+	root=plgroup(0, 0);
+	g=plgroup(root, PACKN|EXPAND);
+	list=pllist(g, PACKE|EXPAND, genlist, 8, hitgen);
+	plscroll(list, 0, plscrollbar(g, PACKW));
+	msg=pllabel(root, PACKN|FILLX, "");
+	plbutton(root, PACKW, "save", save);
+	plbutton(root, PACKW, "revert", revert);
+	plbutton(root, PACKE, "done", done);
+	ereshaped(screen.r);
+	for(;;) plmouse(root, emouse(), &screen);
+}
--- /dev/null
+++ b/libpanel/scroll.c
@@ -1,0 +1,21 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <panel.h>
+#include "pldefs.h"
+void plscroll(Panel *scrollee, Panel *xscroller, Panel *yscroller){
+	scrollee->xscroller=xscroller;
+	scrollee->yscroller=yscroller;
+	if(xscroller) xscroller->scrollee=scrollee;
+	if(yscroller) yscroller->scrollee=scrollee;
+}
+Scroll plgetscroll(Panel *p){
+	return p->scr;
+}
+void plsetscroll(Panel *p, Scroll s){
+	if(p->scroll){
+		if(s.size.x) p->scroll(p, HORIZ, 2, s.pos.x, s.size.x);
+		if(s.size.y) p->scroll(p, VERT, 2, s.pos.y, s.size.y);
+	}
+}
--- /dev/null
+++ b/libpanel/scrollbar.c
@@ -1,0 +1,146 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <panel.h>
+#include "pldefs.h"
+typedef struct Scrollbar Scrollbar;
+struct Scrollbar{
+	int dir;		/* HORIZ or VERT */
+	int lo, hi;		/* setting, in screen coordinates */
+	int buttons;		/* saved mouse buttons for transmittal to scrollee */
+	Rectangle interior;
+	Point minsize;
+};
+#define	SBWID	8	/* should come from draw.c? */
+void pl_drawscrollbar(Panel *p){
+	Scrollbar *sp;
+	sp=p->data;
+	sp->interior=pl_outline(p->b, p->r, p->state);
+	pl_sliderupd(p->b, sp->interior, sp->dir, sp->lo, sp->hi);
+}
+int pl_hitscrollbar(Panel *g, Mouse *m){
+	int oldstate, pos, len, dy;
+	Point ul, size;
+	Scrollbar *sp;
+	sp=g->data;
+	ul=g->r.min;
+	size=subpt(g->r.max, g->r.min);
+	pl_interior(g->state, &ul, &size);
+	oldstate=g->state;
+	if(!(g->flags & USERFL) && (m->buttons&OUT || !ptinrect(m->xy, g->r))){
+		m->buttons&=~OUT;
+		g->state=UP;
+		goto out;
+	}
+	if(sp->dir==HORIZ){
+		pos=m->xy.x-ul.x;
+		len=size.x;
+	}
+	else{
+		pos=m->xy.y-ul.y;
+		len=size.y;
+	}
+	if(pos<0) pos=0;
+	else if(pos>len) pos=len;
+	if(m->buttons&7){
+		g->state=DOWN;
+		sp->buttons=m->buttons;
+		switch(m->buttons){
+		case 1:
+			dy=pos*(sp->hi-sp->lo)/len;
+			pl_sliderupd(g->b, sp->interior, sp->dir, sp->lo-dy,
+				sp->hi-dy);
+			break;
+		case 2:
+			if(g->scrollee && g->scrollee->scroll)
+				g->scrollee->scroll(g->scrollee, sp->dir,
+					m->buttons, pos, len);
+			break;
+		case 4:
+			dy=pos*(sp->hi-sp->lo)/len;
+			pl_sliderupd(g->b, sp->interior, sp->dir, sp->lo+dy,
+				sp->hi+dy);
+			break;
+		}
+	}
+	else{
+		if(!(sp->buttons&2) && g->state==DOWN && g->scrollee && g->scrollee->scroll)
+			g->scrollee->scroll(g->scrollee, sp->dir, sp->buttons,
+				pos, len);
+		g->state=UP;
+	}
+out:
+	if(oldstate!=g->state) pldraw(g, g->b);
+	return g->state==DOWN;
+}
+void pl_typescrollbar(Panel *p, Rune c){
+	USED(p, c);
+}
+Point pl_getsizescrollbar(Panel *p, Point children){
+	USED(children);
+	return pl_boxsize(((Scrollbar *)p->data)->minsize, p->state);
+}
+void pl_childspacescrollbar(Panel *p, Point *ul, Point *size){
+	USED(p, ul, size);
+}
+/*
+ * Arguments lo, hi and len are in the scrollee's natural coordinates
+ */
+void pl_setscrollbarscrollbar(Panel *p, int lo, int hi, int len){
+	Point ul, size;
+	int mylen;
+	Scrollbar *sp;
+	sp=p->data;
+	ul=p->r.min;
+	size=subpt(p->r.max, p->r.min);
+	pl_interior(p->state, &ul, &size);
+	mylen=sp->dir==HORIZ?size.x:size.y;
+	if(len==0) len=1;
+	sp->lo=lo*mylen/len;
+	sp->hi=hi*mylen/len;
+	if(sp->lo<0) sp->lo=0;
+	if(sp->lo>=mylen) sp->hi=mylen-1;
+	if(sp->hi<=sp->lo) sp->hi=sp->lo+1;
+	if(sp->hi>mylen) sp->hi=mylen;
+	pldraw(p, p->b);
+}
+int pl_priscrollbar(Panel *, Point){
+	return PRI_SCROLLBAR;
+}
+void plinitscrollbar(Panel *v, int flags){
+	Scrollbar *sp;
+	sp=v->data;
+	v->flags=flags|LEAF;
+	v->pri=pl_priscrollbar;
+	v->state=UP;
+	v->draw=pl_drawscrollbar;
+	v->hit=pl_hitscrollbar;
+	v->type=pl_typescrollbar;
+	v->getsize=pl_getsizescrollbar;
+	v->childspace=pl_childspacescrollbar;
+	v->setscrollbar=pl_setscrollbarscrollbar;
+	switch(flags&PACK){
+	case PACKN:
+	case PACKS:
+		sp->dir=HORIZ;
+		sp->minsize=Pt(0, SBWID);
+		v->flags|=FILLX;
+		break;
+	case PACKE:
+	case PACKW:
+		sp->dir=VERT;
+		sp->minsize=Pt(SBWID, 0);
+		v->flags|=FILLY;
+		break;
+	}
+	sp->lo=0;
+	sp->hi=0;
+	v->kind="scrollbar";
+}
+Panel *plscrollbar(Panel *parent, int flags){
+	Panel *v;
+	v=pl_newpanel(parent, sizeof(Scrollbar));
+	plinitscrollbar(v, flags);
+	return v;
+}
--- /dev/null
+++ b/libpanel/slider.c
@@ -1,0 +1,97 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <panel.h>
+#include "pldefs.h"
+typedef struct Slider Slider;
+struct Slider{
+	int dir;			/* HORIZ or VERT */
+	int val;			/* setting, in screen coordinates */
+	Point minsize;
+	void (*hit)(Panel *, int, int, int);	/* call back to user when slider changes */
+	int buttons;
+};
+void pl_drawslider(Panel *p){
+	Rectangle r;
+	Slider *sp;
+	sp=p->data;
+	r=pl_box(p->b, p->r, UP);
+	switch(sp->dir){
+	case HORIZ: pl_sliderupd(p->b, r, sp->dir, 0, sp->val); break;
+	case VERT:  pl_sliderupd(p->b, r, sp->dir, r.max.y-sp->val, r.max.y); break;
+	}
+}
+int pl_hitslider(Panel *p, Mouse *m){
+	int oldstate, oldval, len;
+	Point ul, size;
+	Slider *sp;
+	sp=p->data;
+	ul=p->r.min;
+	size=subpt(p->r.max, p->r.min);
+	pl_interior(p->state, &ul, &size);
+	oldstate=p->state;
+	oldval=sp->val;
+	SET(len);
+	if(m->buttons&OUT)
+		p->state=UP;
+	else if(m->buttons&7){
+		p->state=DOWN;
+		sp->buttons=m->buttons;
+		if(sp->dir==HORIZ){
+			sp->val=m->xy.x-ul.x;
+			len=size.x;
+		}
+		else{
+			sp->val=ul.y+size.y-m->xy.y;
+			len=size.y;
+		}
+		if(sp->val<0) sp->val=0;
+		else if(sp->val>len) sp->val=len;
+	}
+	else	/* mouse inside, but no buttons down */
+		p->state=UP;
+	if(oldval!=sp->val || oldstate!=p->state) pldraw(p, p->b);
+	if(oldval!=sp->val && sp->hit) sp->hit(p, sp->buttons, sp->val, len);
+	return 0;
+}
+void pl_typeslider(Panel *p, Rune c){
+	USED(p, c);
+}
+Point pl_getsizeslider(Panel *p, Point children){
+	USED(children);
+	return pl_boxsize(((Slider *)p->data)->minsize, p->state);
+}
+void pl_childspaceslider(Panel *g, Point *ul, Point *size){
+	USED(g, ul, size);
+}
+void plinitslider(Panel *v, int flags, Point size, void (*hit)(Panel *, int, int, int)){
+	Slider *sp;
+	sp=v->data;
+	v->r=Rect(0,0,size.x,size.y);
+	v->flags=flags|LEAF;
+	v->state=UP;
+	v->draw=pl_drawslider;
+	v->hit=pl_hitslider;
+	v->type=pl_typeslider;
+	v->getsize=pl_getsizeslider;
+	v->childspace=pl_childspaceslider;
+	sp->minsize=size;
+	sp->dir=size.x>size.y?HORIZ:VERT;
+	sp->hit=hit;
+	v->kind="slider";
+}
+Panel *plslider(Panel *parent, int flags, Point size, void (*hit)(Panel *, int, int, int)){
+	Panel *p;
+	p=pl_newpanel(parent, sizeof(Slider));
+	plinitslider(p, flags, size, hit);
+	return p;
+}
+void plsetslider(Panel *p, int value, int range){
+	Slider *sp;
+	sp=p->data;
+	if(value<0) value=0;
+	else if(value>range) value=range;
+	if(sp->dir==HORIZ) sp->val=value*(p->r.max.x-p->r.min.x)/range;
+	else sp->val=value*(p->r.max.y-p->r.min.y)/range;
+}
--- /dev/null
+++ b/libpanel/snarf.c
@@ -1,0 +1,58 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <panel.h>
+#include "pldefs.h"
+
+void plputsnarf(char *s){
+	int fd;
+
+	if(s==0 || *s=='\0')
+		return;
+	if((fd=open("/dev/snarf", OWRITE|OTRUNC))>=0){
+		write(fd, s, strlen(s));
+		close(fd);
+	}
+}
+char *plgetsnarf(void){
+	int fd, n, r;
+	char *s;
+
+	if((fd=open("/dev/snarf", OREAD))<0)
+		return nil;
+	n=0;
+	s=nil;
+	for(;;){
+		s=pl_erealloc(s, n+1024);
+		if((r = read(fd, s+n, 1024)) <= 0)
+			break;
+		n += r;
+	}
+	close(fd);
+	if(n <= 0){
+		free(s);
+		return nil;
+	}
+	s[n] = '\0';
+	return s;
+}
+void plsnarf(Panel *p){
+	char *s;
+
+	if(p==0 || p->snarf==0)
+		return;
+	s=p->snarf(p);
+	plputsnarf(s);
+	free(s);
+}
+void plpaste(Panel *p){
+	char *s;
+
+	if(p==0 || p->paste==0)
+		return;
+	if(s=plgetsnarf()){
+		p->paste(p, s);
+		free(s);
+	}
+}
--- /dev/null
+++ b/libpanel/textview.c
@@ -1,0 +1,250 @@
+/*
+ * Fonted text viewer, calls out to code in rtext.c
+ *
+ * Should redo this to copy the already-visible parts on scrolling & only
+ * update the newly appearing stuff -- then the offscreen assembly bitmap can go away.
+ */
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <panel.h>
+#include "pldefs.h"
+
+typedef struct Textview Textview;
+struct Textview{
+	void (*hit)(Panel *, int, Rtext *); /* call back to user on hit */
+	Rtext *text;			/* text */
+	Point offs;			/* offset of left/top of screen */
+	Rtext *hitword;			/* text to hilite */
+	Rtext *hitfirst;		/* first word in range select */
+	int twid;			/* text width (visible) */
+	int thgt;			/* text height (total) */
+	int maxwid;			/* width of longest line */
+	Point minsize;			/* smallest acceptible window size */
+	int buttons;
+};
+
+void pl_setscrpos(Panel *p, Textview *tp, Rectangle r){
+	Panel *sb;
+	int lo, hi;
+
+	lo=tp->offs.y;
+	hi=lo+r.max.y-r.min.y;	/* wrong? */
+	sb=p->yscroller;	
+	if(sb && sb->setscrollbar)
+		sb->setscrollbar(sb, lo, hi, tp->thgt);
+	lo=tp->offs.x;
+	hi=lo+r.max.x-r.min.x;
+	sb=p->xscroller;
+	if(sb && sb->setscrollbar)
+		sb->setscrollbar(sb, lo, hi, tp->maxwid);
+}
+void pl_drawtextview(Panel *p){
+	int twid;
+	Rectangle r;
+	Textview *tp;
+	Point size;
+
+	tp=p->data;
+	r=pl_outline(p->b, p->r, UP);
+	twid=r.max.x-r.min.x;
+	if(twid!=tp->twid){
+		tp->twid=twid;
+		size=pl_rtfmt(tp->text, tp->twid);
+		p->scr.size.x=tp->maxwid=size.x;
+		p->scr.size.y=tp->thgt=size.y;
+	}
+	p->scr.pos = tp->offs;
+	pl_rtdraw(p->b, r, tp->text, tp->offs);
+	pl_setscrpos(p, tp, r);
+}
+/*
+ * If t is a panel word, pass the mouse event on to it
+ */
+void pl_passon(Rtext *t, Mouse *m){
+	if(t && t->b==0 && t->p!=0)
+		plmouse(t->p, m);
+}
+int pl_hittextview(Panel *p, Mouse *m){
+	Rtext *oldhitword, *oldhitfirst;
+	int hitme, oldstate;
+	Point ul, size;
+	Textview *tp;
+
+	tp=p->data;
+	hitme=0;
+	oldstate=p->state;
+	oldhitword=tp->hitword;
+	oldhitfirst=tp->hitfirst;
+	if(oldhitword==oldhitfirst)
+		pl_passon(oldhitword, m);
+	if(m->buttons&OUT)
+		p->state=UP;
+	else if(m->buttons&7){
+		p->state=DOWN;
+		tp->buttons=m->buttons;
+		if(oldhitword==0 || oldhitword->p==0 || (oldhitword->p->flags&REMOUSE)==0){
+			ul=p->r.min;
+			size=subpt(p->r.max, p->r.min);
+			pl_interior(p->state, &ul, &size);
+			tp->hitword=pl_rthit(tp->text, tp->offs, m->xy, ul);
+			if(tp->hitword==0)
+				if(oldhitword!=0 && oldstate==DOWN)
+					tp->hitword=oldhitword;
+				else
+					tp->hitfirst=0;
+			if(tp->hitword!=0 && oldstate!=DOWN)
+				tp->hitfirst=tp->hitword;
+		}
+	}
+	else{
+		if(p->state==DOWN) hitme=1;
+		p->state=UP;
+	}
+	if(tp->hitfirst!=oldhitfirst || tp->hitword!=oldhitword){
+		plrtseltext(tp->text, tp->hitword, tp->hitfirst);
+		pl_drawtextview(p);
+		if(tp->hitword==tp->hitfirst)
+			pl_passon(tp->hitword, m);
+	}
+	if(hitme && tp->hit && tp->hitword!=0 && tp->hitword==tp->hitfirst){
+		plrtseltext(tp->text, 0, 0);
+		pl_drawtextview(p);
+		tp->hit(p, tp->buttons, tp->hitword);
+		tp->hitword=0;
+		tp->hitfirst=0;
+	}
+	return 0;
+}
+void pl_scrolltextview(Panel *p, int dir, int buttons, int num, int den){
+	int xoffs, yoffs;
+	Point ul, size;
+	Textview *tp;
+	Rectangle r;
+
+	tp=p->data;
+	ul=p->r.min;
+	size=subpt(p->r.max, p->r.min);
+	pl_interior(p->state, &ul, &size);
+	if(dir==VERT){
+		switch(buttons){
+		default:
+			SET(yoffs);
+			break;
+		case 1:		/* left -- top moves to pointer */
+			yoffs=(vlong)tp->offs.y-num*size.y/den;
+			if(yoffs<0) yoffs=0;
+			break;
+		case 2:		/* middle -- absolute index of file */
+			yoffs=(vlong)tp->thgt*num/den;
+			break;
+		case 4:		/* right -- line pointed at moves to top */
+			yoffs=tp->offs.y+(vlong)num*size.y/den;
+			if(yoffs>tp->thgt) yoffs=tp->thgt;
+			break;
+		}
+		if(yoffs!=tp->offs.y){
+			r=pl_outline(p->b, p->r, p->state);
+			pl_rtredraw(p->b, r, tp->text,
+				Pt(tp->offs.x, yoffs), tp->offs, dir);
+			p->scr.pos.y=tp->offs.y=yoffs;
+			pl_setscrpos(p, tp, r);
+		}
+	}else{ /* dir==HORIZ */
+		switch(buttons){
+		default:
+			SET(xoffs);
+			break;
+		case 1:		/* left */
+			xoffs=(vlong)tp->offs.x-num*size.x/den;
+			if(xoffs<0) xoffs=0;
+			break;
+		case 2:		/* middle */
+			xoffs=(vlong)tp->maxwid*num/den;
+			break;
+		case 4:		/* right */
+			xoffs=tp->offs.x+(vlong)num*size.x/den;
+			if(xoffs>tp->maxwid) xoffs=tp->maxwid;
+			break;
+		}
+		if(xoffs!=tp->offs.x){
+			r=pl_outline(p->b, p->r, p->state);
+			pl_rtredraw(p->b, r, tp->text,
+				Pt(xoffs, tp->offs.y), tp->offs, dir);
+			p->scr.pos.x=tp->offs.x=xoffs;
+			pl_setscrpos(p, tp, r);
+		}
+	}
+}
+void pl_typetextview(Panel *g, Rune c){
+	USED(g, c);
+}
+Point pl_getsizetextview(Panel *p, Point children){
+	USED(children);
+	return pl_boxsize(((Textview *)p->data)->minsize, p->state);
+}
+void pl_childspacetextview(Panel *g, Point *ul, Point *size){
+	USED(g, ul, size);
+}
+/*
+ * Priority depends on what thing inside the panel we're pointing at.
+ */
+int pl_pritextview(Panel *p, Point xy){
+	Point ul, size;
+	Textview *tp;
+	Rtext *h;
+	tp=p->data;
+	ul=p->r.min;
+	size=subpt(p->r.max, p->r.min);
+	pl_interior(p->state, &ul, &size);
+	h=pl_rthit(tp->text, tp->offs, xy, ul);
+	if(h && h->b==0 && h->p!=0){
+		p=pl_ptinpanel(xy, h->p);
+		if(p) return p->pri(p, xy);
+	}
+	return PRI_NORMAL;
+}
+
+char* pl_snarftextview(Panel *p){
+	return plrtsnarftext(((Textview *)p->data)->text);
+}
+
+void plinittextview(Panel *v, int flags, Point minsize, Rtext *t, void (*hit)(Panel *, int, Rtext *)){
+	Textview *tp;
+	tp=v->data;
+	v->flags=flags|LEAF;
+	v->state=UP;
+	v->draw=pl_drawtextview;
+	v->hit=pl_hittextview;
+	v->type=pl_typetextview;
+	v->getsize=pl_getsizetextview;
+	v->childspace=pl_childspacetextview;
+	v->kind="textview";
+	v->pri=pl_pritextview;
+	tp->hit=hit;
+	tp->minsize=minsize;
+	tp->text=t;
+	tp->offs=ZP;
+	tp->hitfirst=0;
+	tp->hitword=0;
+	v->scroll=pl_scrolltextview;
+	v->snarf=pl_snarftextview;
+	tp->twid=-1;
+	tp->maxwid=0;
+	v->scr.pos=Pt(0,0);
+	v->scr.size=Pt(0,1);
+}
+Panel *pltextview(Panel *parent, int flags, Point minsize, Rtext *t, void (*hit)(Panel *, int, Rtext *)){
+	Panel *v;
+	v=pl_newpanel(parent, sizeof(Textview));
+	plinittextview(v, flags, minsize, t, hit);
+	return v;
+}
+int plgetpostextview(Panel *p){
+	return ((Textview *)p->data)->offs.y;
+}
+void plsetpostextview(Panel *p, int yoffs){
+	((Textview *)p->data)->offs.y=yoffs;
+	pldraw(p, p->b);
+}
--- /dev/null
+++ b/libpanel/textwin.c
@@ -1,0 +1,474 @@
+/*
+ * Text windows
+ *	void twhilite(Textwin *t, int sel0, int sel1, int on)
+ *		hilite (on=1) or unhilite (on=0) a range of characters
+ *	void twselect(Textwin *t, Mouse *m)
+ *		set t->sel0, t->sel1 from mouse input.
+ *		Also hilites selection.
+ *		Caller should first unhilite previous selection.
+ *	void twreplace(Textwin *t, int r0, int r1, Rune *ins, int nins)
+ *		Replace the given range of characters with the given insertion.
+ *		Caller should unhilite selection while this is called.
+ *	void twscroll(Textwin *t, int top)
+ *		Character with index top moves to the top line of the screen.
+ *	int twpt2rune(Textwin *t, Point p)
+ *		which character is displayed at point p?
+ *	void twreshape(Textwin *t, Rectangle r)
+ *		save r and redraw the text
+ *	Textwin *twnew(Bitmap *b, Font *f, Rune *text, int ntext)
+ *		create a new text window
+ *	void twfree(Textwin *t)
+ *		get rid of a surplus Textwin
+ */
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <panel.h>
+#include "pldefs.h"
+
+#define SLACK 100
+
+/*
+ * Is text at point a before or after that at point b?
+ */
+int tw_before(Textwin *t, Point a, Point b){
+	return a.y<b.y || a.y<b.y+t->hgt && a.x<b.x;
+}
+/*
+ * Return the character index indicated by point p, or -1
+ * if its off-screen.  The screen must be up-to-date.
+ *
+ * Linear search should be binary search.
+ */
+int twpt2rune(Textwin *t, Point p){
+	Point *el, *lp;
+	el=t->loc+(t->bot-t->top);
+	for(lp=t->loc;lp!=el;lp++)
+		if(tw_before(t, p, *lp)){
+			if(lp==t->loc) return t->top;
+			return lp-t->loc+t->top-1;
+		}
+	return t->bot;
+}
+/*
+ * Return ul corner of the character with the given index
+ */
+Point tw_rune2pt(Textwin *t, int i){
+	if(i<t->top) return t->r.min;
+	if(i>t->bot) return t->r.max;
+	return t->loc[i-t->top];
+}
+/*
+ * Store p at t->loc[l], extending t->loc if necessary
+ */
+void tw_storeloc(Textwin *t, int l, Point p){
+	int nloc;
+	if(l>=t->eloc-t->loc){
+		nloc=l+SLACK;
+		t->loc=pl_erealloc(t->loc, nloc*sizeof(Point));
+		t->eloc=t->loc+nloc;
+	}
+	t->loc[l]=p;
+}
+/*
+ * Set the locations at which the given runes should appear.
+ * Returns the index of the first rune not set, which might not
+ * be last because we reached the bottom of the window.
+ *
+ * N.B. this zaps the loc of r[last], so that value should be saved first,
+ * if it's important.
+ */
+int tw_setloc(Textwin *t, int first, int last, Point ul){
+	Rune *r, *er;
+	int x, dt, lp;
+	char buf[UTFmax+1];
+	er=t->text+last;
+	for(r=t->text+first,lp=first-t->top;r!=er && ul.y+t->hgt<=t->r.max.y;r++,lp++){
+		tw_storeloc(t, lp, ul);
+		switch(*r){
+		case '\n':
+			ul.x=t->r.min.x;
+			ul.y+=t->hgt;
+			break;
+		case '\t':
+			x=ul.x-t->r.min.x+t->mintab+t->tabstop;
+			x-=x%t->tabstop;
+			ul.x=x+t->r.min.x;
+			if(ul.x>t->r.max.x){
+				ul.x=t->r.min.x;
+				ul.y+=t->hgt;
+				tw_storeloc(t, lp, ul);
+				if(ul.y+t->hgt>t->r.max.y) return r-t->text;
+				ul.x+=+t->tabstop;
+			}
+			break;
+		default:
+			buf[runetochar(buf, r)]='\0';
+			dt=stringwidth(t->font, buf);
+			ul.x+=dt;
+			if(ul.x>t->r.max.x){
+				ul.x=t->r.min.x;
+				ul.y+=t->hgt;
+				tw_storeloc(t, lp, ul);
+				if(ul.y+t->hgt>t->r.max.y) return r-t->text;
+				ul.x+=dt;
+			}
+			break;
+		}
+	}
+	tw_storeloc(t, lp, ul);
+	return r-t->text;
+}
+/*
+ * Draw the given runes at their locations.
+ * Bug -- saving up multiple characters would
+ * reduce the number of calls to string,
+ * and probably make this a lot faster.
+ */
+void tw_draw(Textwin *t, int first, int last){
+	Rune *r, *er;
+	Point *lp, ul, ur;
+	char buf[UTFmax+1];
+	if(first<t->top) first=t->top;
+	if(last>t->bot) last=t->bot;
+	if(last<=first) return;
+	er=t->text+last;
+	for(r=t->text+first,lp=t->loc+(first-t->top);r!=er;r++,lp++){
+		if(lp->y+t->hgt>t->r.max.y){
+			fprint(2, "chr %C, index %ld of %d, loc %d %d, off bottom\n",
+				*r, lp-t->loc, t->bot-t->top, lp->x, lp->y);
+			return;
+		}
+		switch(*r){
+		case '\n':
+			ur=*lp;
+			break;
+		case '\t':
+			ur=*lp;
+			if(lp[1].y!=lp[0].y)
+				ul=Pt(t->r.min.x, lp[1].y);
+			else
+				ul=*lp;
+			pl_clr(t->b, Rpt(ul, Pt(lp[1].x, ul.y+t->hgt)));
+			break;
+		default:
+			buf[runetochar(buf, r)]='\0';
+	/***/		pl_clr(t->b, Rpt(*lp, addpt(*lp, stringsize(t->font, buf))));
+			ur=string(t->b, *lp, display->black, ZP, t->font, buf);
+			break;
+		}
+		if(lp[1].y!=lp[0].y)
+	/***/		pl_clr(t->b, Rpt(ur, Pt(t->r.max.x, ur.y+t->hgt)));
+	}
+}
+/*
+ * Hilight the characters with tops between ul and ur
+ */
+void tw_hilitep(Textwin *t, Point ul, Point ur){
+	Point swap;
+	int y;
+	if(tw_before(t, ur, ul)){ swap=ul; ul=ur; ur=swap;}
+	y=ul.y+t->hgt;
+	if(y>t->r.max.y) y=t->r.max.y;
+	if(ul.y==ur.y)
+		pl_highlight(t->b, Rpt(ul, Pt(ur.x, y)));
+	else{
+		pl_highlight(t->b, Rpt(ul, Pt(t->r.max.x, y)));
+		ul=Pt(t->r.min.x, y);
+		pl_highlight(t->b, Rpt(ul, Pt(t->r.max.x, ur.y)));
+		ul=Pt(t->r.min.x, ur.y);
+		y=ur.y+t->hgt;
+		if(y>t->r.max.y) y=t->r.max.y;
+		pl_highlight(t->b, Rpt(ul, Pt(ur.x, y)));
+	}
+}
+/*
+ * Hilite/unhilite the given range of characters
+ */
+void twhilite(Textwin *t, int sel0, int sel1, int on){
+	Point ul, ur;
+	int swap, y;
+	if(sel1<sel0){ swap=sel0; sel0=sel1; sel1=swap; }
+	if(sel1<t->top || t->bot<sel0) return;
+	if(sel0<t->top) sel0=t->top;
+	if(sel1>t->bot) sel1=t->bot;
+	if(!on){
+		if(sel1==sel0){
+			ul=t->loc[sel0-t->top];
+			y=ul.y+t->hgt;
+			if(y>t->r.max.y) y=t->r.max.y;
+			pl_clr(t->b, Rpt(ul, Pt(ul.x+1, y)));
+		}else
+			tw_draw(t, sel0, sel1);
+		return;
+	}
+	ul=t->loc[sel0-t->top];
+	if(sel1==sel0)
+		ur=addpt(ul, Pt(1, 0));
+	else
+		ur=t->loc[sel1-t->top];
+	tw_hilitep(t, ul, ur);
+}
+/*
+ * Set t->sel[01] from mouse input.
+ * Also hilites the selection.
+ * Caller should unhilite the previous
+ * selection before calling this.
+ */
+void twselect(Textwin *t, Mouse *m){
+	int sel0, sel1, newsel;
+	Point p0, p1, newp;
+	sel0=sel1=twpt2rune(t, m->xy);
+	p0=tw_rune2pt(t, sel0);
+	p1=addpt(p0, Pt(1, 0));
+	twhilite(t, sel0, sel1, 1);
+	for(;;){
+		if(display->bufp > display->buf)
+			flushimage(display, 1);
+		*m=emouse();
+		if((m->buttons&7)!=1) break;
+		newsel=twpt2rune(t, m->xy);
+		newp=tw_rune2pt(t, newsel);
+		if(eqpt(newp, p0)) newp=addpt(newp, Pt(1, 0));
+		if(!eqpt(newp, p1)){
+			if((sel0<=sel1 && sel1<newsel) || (newsel<sel1 && sel1<sel0))
+				tw_hilitep(t, p1, newp);
+			else if((sel0<=newsel && newsel<sel1) || (sel1<newsel && newsel<=sel0)){
+				twhilite(t, sel1, newsel, 0);
+				if(newsel==sel0)
+					tw_hilitep(t, p0, newp);
+			}else if((newsel<sel0 && sel0<=sel1) || (sel1<sel0 && sel0<=newsel)){
+				twhilite(t, sel0, sel1, 0);
+				tw_hilitep(t, p0, newp);
+			}
+			sel1=newsel;
+			p1=newp;
+		}
+	}
+	if(sel0<=sel1){
+		t->sel0=sel0;
+		t->sel1=sel1;
+	}
+	else{
+		t->sel0=sel1;
+		t->sel1=sel0;
+	}
+}
+/*
+ * Clear the area following the last displayed character
+ */
+void tw_clrend(Textwin *t){
+	Point ul;
+	int y;
+	ul=t->loc[t->bot-t->top];
+	y=ul.y+t->hgt;
+	if(y>t->r.max.y) y=t->r.max.y;
+	pl_clr(t->b, Rpt(ul, Pt(t->r.max.x, y)));
+	ul=Pt(t->r.min.x, y);
+	pl_clr(t->b, Rpt(ul, t->r.max));
+}
+/*
+ * Move part of a line of text, truncating the source or padding
+ * the destination on the right if necessary.
+ */
+void tw_moverect(Textwin *t, Point uld, Point urd, Point uls, Point urs){
+	int sw, dw, d;
+	if(urs.y!=uls.y) urs=Pt(t->r.max.x, uls.y);
+	if(urd.y!=uld.y) urd=Pt(t->r.max.x, uld.y);
+	sw=uls.x-urs.x;
+	dw=uld.x-urd.x;
+	if(dw>sw){
+		d=dw-sw;
+		pl_clr(t->b, Rect(urd.x-d, urd.y, urd.x, urd.y+t->hgt));
+		dw=sw;
+	}
+	pl_cpy(t->b, uld, Rpt(uls, Pt(uls.x+dw, uls.y+t->hgt)));
+}
+/*
+ * Move a block of characters up or to the left:
+ *	Identify contiguous runs of characters whose width doesn't change, and
+ *	move them in one bitblt per run.
+ *	If we get to a point where source and destination are x-aligned,
+ *	they will remain x-aligned for the rest of the block.
+ *	Then, if they are y-aligned, they're already in the right place.
+ *	Otherwise, we can move them in three bitblts; one if all the
+ *	remaining characters are on one line.
+ */
+void tw_moveup(Textwin *t, Point *dp, Point *sp, Point *esp){
+	Point uld, uls;			/* upper left of destination/source */
+	int y;
+	while(sp!=esp && sp->x!=dp->x){
+		uld=*dp;
+		uls=*sp;
+		while(sp!=esp && sp->y==uls.y && dp->y==uld.y && sp->x-uls.x==dp->x-uld.x){
+			sp++;
+			dp++;
+		}
+		tw_moverect(t, uld, *dp, uls, *sp);
+	}
+	if(sp==esp || esp->y==dp->y) return;
+	if(esp->y==sp->y){	/* one line only */
+		pl_cpy(t->b, *dp, Rpt(*sp, Pt(esp->x, sp->y+t->hgt)));
+		return;
+	}
+	y=sp->y+t->hgt;
+	pl_cpy(t->b, *dp, Rpt(*sp, Pt(t->r.max.x, y)));
+	pl_cpy(t->b, Pt(t->r.min.x, dp->y+t->hgt),
+		Rect(t->r.min.x, y, t->r.max.x, esp->y));
+	y=dp->y+esp->y-sp->y;
+	pl_cpy(t->b, Pt(t->r.min.x, y),
+		Rect(t->r.min.x, esp->y, esp->x, esp->y+t->hgt));
+}
+/*
+ * Same as above, but moving down and in reverse order, so as not to overwrite stuff
+ * not moved yet.
+ */
+void tw_movedn(Textwin *t, Point *dp, Point *bsp, Point *esp){
+	Point *sp, urs, urd;
+	int dy;
+	dp+=esp-bsp;
+	sp=esp;
+	dy=dp->y-sp->y;
+	while(sp!=bsp && dp[-1].x==sp[-1].x){
+		--dp;
+		--sp;
+	}
+	if(dy!=0){
+		if(sp->y==esp->y)
+			pl_cpy(t->b, *dp, Rect(sp->x, sp->y, esp->x, esp->y+t->hgt));
+		else{
+			pl_cpy(t->b, Pt(t->r.min.x, sp->x+dy),
+				Rect(t->r.min.x, sp->y, esp->x, esp->y+t->hgt));
+			pl_cpy(t->b, Pt(t->r.min.x, dp->y+t->hgt),
+				Rect(t->r.min.x, sp->y+t->hgt, t->r.max.x, esp->y));
+			pl_cpy(t->b, *dp,
+				Rect(sp->x, sp->y, t->r.max.x, sp->y+t->hgt));
+		}
+	}
+	while(sp!=bsp){
+		urd=*dp;
+		urs=*sp;
+		while(sp!=bsp && sp[-1].y==sp[0].y && dp[-1].y==dp[0].y
+		   && sp[-1].x-sp[0].x==dp[-1].x-dp[0].x){
+			--sp;
+			--dp;
+		}
+		tw_moverect(t, *dp, urd, *sp, urs);
+	}
+}
+/*
+ * Move the given range of characters, already drawn on
+ * the given textwin, to the given location.
+ * Start and end must both index characters that are initially on-screen.
+ */
+void tw_relocate(Textwin *t, int first, int last, Point dst){
+	Point *srcloc;
+	int nbyte;
+	if(first<t->top || last<first || t->bot<last) return;
+	nbyte=(last-first+1)*sizeof(Point);
+	srcloc=pl_emalloc(nbyte);
+	memmove(srcloc, &t->loc[first-t->top], nbyte);
+	tw_setloc(t, first, last, dst);
+	if(tw_before(t, dst, srcloc[0]))
+		tw_moveup(t, t->loc+first-t->top, srcloc, srcloc+(last-first));
+	else
+		tw_movedn(t, t->loc+first-t->top, srcloc, srcloc+(last-first));
+}
+/*
+ * Replace the runes with indices from r0 to r1-1 with the text
+ * pointed to by text, and with length ntext.
+ *	Open up a hole in t->text, t->loc.
+ *	Insert new text, calculate their locs (save the extra loc that's overwritten first)
+ *	(swap saved & overwritten locs)
+ *	move tail.
+ *	calc locs and draw new text after tail, if necessary.
+ *	draw new text, if necessary
+ */
+void twreplace(Textwin *t, int r0, int r1, Rune *ins, int nins){
+	int olen, nlen, tlen, dtop;
+	olen=t->etext-t->text;
+	nlen=olen+nins-(r1-r0);
+	tlen=t->eslack-t->text;
+	if(nlen>tlen){
+		tlen=nlen+SLACK;
+		t->text=pl_erealloc(t->text, tlen*sizeof(Rune));
+		t->eslack=t->text+tlen;
+	}
+	if(olen!=nlen)
+		memmove(t->text+r0+nins, t->text+r1, (olen-r1)*sizeof(Rune));
+	if(nins!=0)	/* ins can be 0 if nins==0 */
+		memmove(t->text+r0, ins, nins*sizeof(Rune));
+	t->etext=t->text+nlen;
+	if(r0>t->bot)		/* insertion is completely below visible text */
+		return;
+	if(r1<t->top){		/* insertion is completely above visible text */
+		dtop=nlen-olen;
+		t->top+=dtop;
+		t->bot+=dtop;
+		return;
+	}
+	if(1 || t->bot<=r0+nins){	/* no useful text on screen below r0 */
+		if(r0<=t->top)	/* no useful text above, either */
+			t->top=r0;
+		t->bot=tw_setloc(t, r0, nlen, t->loc[r0-t->top]);
+		tw_draw(t, r0, t->bot);
+		tw_clrend(t);
+		return;
+	}
+	/*
+	 * code for case where there is useful text below is missing (see `1 ||' above)
+	 */
+}
+/*
+ * This works but is stupid.
+ */
+void twscroll(Textwin *t, int top){
+	while(top!=0 && t->text[top-1]!='\n') --top;
+	t->top=top;
+	t->bot=tw_setloc(t, top, t->etext-t->text, t->r.min);
+	tw_draw(t, t->top, t->bot);
+	tw_clrend(t);
+}
+void twreshape(Textwin *t, Rectangle r){
+	t->r=r;
+	t->bot=tw_setloc(t, t->top, t->etext-t->text, t->r.min);
+	tw_draw(t, t->top, t->bot);
+	tw_clrend(t);
+}
+Textwin *twnew(Image *b, Font *f, Rune *text, int ntext){
+	Textwin *t;
+	t=pl_emalloc(sizeof(Textwin));
+	t->text=pl_emalloc((ntext+SLACK)*sizeof(Rune));
+	t->loc=pl_emalloc(SLACK*sizeof(Point));
+	t->eloc=t->loc+SLACK;
+	t->etext=t->text+ntext;
+	t->eslack=t->etext+SLACK;
+	if(ntext) memmove(t->text, text, ntext*sizeof(Rune));
+	t->top=0;
+	t->bot=0;
+	t->sel0=0;
+	t->sel1=0;
+	t->b=b;
+	t->font=f;
+	t->hgt=f->height;
+	t->mintab=stringwidth(f, "0");
+	t->tabstop=8*t->mintab;
+	return t;
+}
+void twfree(Textwin *t){
+	free(t->loc);
+	free(t->text);
+	free(t);
+}
+/*
+ * Correct the character locations in a textwin after the panel is moved.
+ * This horrid hack would not be necessary if loc values were relative
+ * to the panel, rather than absolute.
+ */
+void twmove(Textwin *t, Point d){
+	Point *lp;
+	t->r = rectaddpt(t->r, d);
+	for(lp=t->loc; lp<t->eloc; lp++)
+		*lp = addpt(*lp, d);
+}
--- /dev/null
+++ b/libpanel/utf.c
@@ -1,0 +1,30 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <panel.h>
+#include "pldefs.h"
+/*
+ * This is the same definition that 8½ uses
+ */
+int pl_idchar(int c){
+	if(c<=' '
+	|| 0x7F<=c && c<=0xA0
+	|| utfrune("!\"#$%&'()*+,-./:;<=>?@`[\\]^{|}~", c))
+		return 0;
+	return 1;
+}
+int pl_rune1st(int c){
+	return (c&0xc0)!=0x80;
+}
+char *pl_nextrune(char *s){
+	do s++; while(!pl_rune1st(*s));
+	return s;
+}
+int pl_runewidth(Font *f, char *s){
+	char r[4], *t;
+	t=r;
+	do *t++=*s++; while(!pl_rune1st(*s));
+	*t='\0';
+	return stringwidth(f, r);
+}
--- /dev/null
+++ b/mkfile
@@ -1,0 +1,19 @@
+</$objtype/mkfile
+
+TARG=gopher
+LIB=libpanel/libpanel.$O.a
+OFILES=gopher.$O
+HFILES=dat.h libpanel/panel.h libpanel/rtext.h
+BIN=/$objtype/bin/
+
+</sys/src/cmd/mkone
+
+CFLAGS=-FTVw -Ilibpanel
+
+$LIB:V:
+	cd libpanel
+	mk
+
+clean nuke:V:
+	@{ cd libpanel; mk $target }
+	rm -f *.[$OS] [$OS].out $TARG