git: 9front

Download patch

ref: f11c5c9ddcd47ce690210ad354c07f230c407e0e
parent: 87de2118e2cdac9af177f9d499b74676eb6e1669
author: cinap_lenrek <cinap_lenrek@centraldogma>
date: Sun Sep 4 15:16:30 EDT 2011

add mothra

--- /dev/null
+++ b/sys/src/cmd/mothra/auth.c
@@ -1,0 +1,68 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <panel.h>
+#include <bio.h>
+#include "mothra.h"
+
+static int
+basicauth(char *arg, char *str, int n)
+{
+	int i;
+	char *p;
+	char buf[1024];
+	Biobuf *b;
+
+	if(strncmp(arg, "realm=", 6) == 0)
+		arg += 6;
+	if(*arg == '"'){
+		arg++;
+		for(p = arg; *p && *p != '"'; p++);
+		*p = 0;
+	} else {
+		for(p = arg; *p && *p != ' ' && *p != '\t'; p++);
+		*p = 0;
+	}
+
+	p = getenv("home");
+	if(p == 0){
+		werrstr("$home not set");
+		return -1;
+	}
+	snprint(buf, sizeof(buf), "%s/lib/mothra/insecurity", p);
+	b = Bopen(buf, OREAD);
+	if(b == 0){
+		werrstr("www password file %s: %r", buf);
+		return -1;
+	}
+
+	i = strlen(arg);
+	while(p = Brdline(b, '\n'))
+		if(strncmp(arg, p, i) == 0 && p[i] == '\t')
+			break;
+	if(p == 0){
+		Bterm(b);
+		werrstr("no basic password for domain `%s'", arg);
+		return -1;
+	}
+
+	p[Blinelen(b)-1] = 0;
+	for(p += i; *p == '\t'; p++);
+	if (enc64(buf, sizeof buf, (uchar*)p, strlen(p)) < 0) {
+		Bterm(b);
+		werrstr("password too long: %s", p);
+		return -1;
+	}
+	snprint(str, n, "Authorization: Basic %s\r\n", buf);
+	return 0;
+}
+
+int
+auth(Url *url, char *str, int n)
+{
+	if(cistrcmp(url->authtype, "basic") == 0)
+		return basicauth(url->autharg, str, n);
+	werrstr("unknown auth method %s", url->authtype);
+	return -1;
+}
--- /dev/null
+++ b/sys/src/cmd/mothra/cistr.c
@@ -1,0 +1,29 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <panel.h>
+#include <ctype.h>
+#include "mothra.h"
+int cistrcmp(char *s1, char *s2){
+	int c1, c2;
+
+	for(; *s1; s1++, s2++){
+		c1 = isupper(*s1) ? tolower(*s1) : *s1;
+		c2 = isupper(*s2) ? tolower(*s2) : *s2;
+		if (c1 < c2) return -1;
+		if (c1 > c2) return 1;
+	}
+	return 0;
+}
+int cistrncmp(char *s1, char *s2, int n){
+	int c1, c2;
+
+	for(; *s1 && n!=0; s1++, s2++, --n){
+		c1 = isupper(*s1) ? tolower(*s1) : *s1;
+		c2 = isupper(*s2) ? tolower(*s2) : *s2;
+		if (c1 < c2) return -1;
+		if (c1 > c2) return 1;
+	}
+	return 0;
+}
--- /dev/null
+++ b/sys/src/cmd/mothra/crackurl.c
@@ -1,0 +1,188 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <panel.h>
+#include <ctype.h>
+#include "mothra.h"
+enum{
+	IP=1,		/* url can contain //ipaddress[:port] */
+	REL=2,	/* fill in ip address & root of name from current, if necessary */
+	SSL=4,	/* use SSL/TLS encryption */
+};
+Scheme scheme[]={
+	"http:",	HTTP,	IP|REL,		80,
+	"https:",	HTTP,	IP|REL|SSL,	443,
+	"ftp:",		FTP,	IP|REL,		21,
+	"file:",	FILE,	REL,	0,
+	"telnet:",	TELNET,	IP,	0,
+	"mailto:",	MAILTO,	0,	0,
+	"gopher:",	GOPHER,	IP,	70,
+	0,		HTTP,	IP|REL,	80,
+};
+int endaddr(int c){
+	return c=='/' || c==':' || c=='?' || c=='#' || c=='\0';
+}
+/*
+ * Remove ., mu/.. and empty components from path names.
+ * Empty last components of urls are significant, and
+ * therefore preserved.
+ */
+void urlcanon(char *name){
+	char *s, *t;
+	char **comp, **p, **q;
+	int rooted;
+	rooted=name[0]=='/';
+	/*
+	 * Break the name into a list of components
+	 */
+	comp=emalloc((strlen(name)+2)*sizeof(char *));
+	p=comp;
+	*p++=name;
+	for(s=name;;s++){
+		if(*s=='/'){
+			*p++=s+1;
+			*s='\0';
+		}
+		else if(*s=='\0' || *s=='?')
+			break;
+	}
+	*p=0;
+	/*
+	 * go through the component list, deleting components that are empty (except
+	 * the last component) or ., and any .. and its non-.. predecessor.
+	 */
+	p=q=comp;
+	while(*p){
+		if(strcmp(*p, "")==0 && p[1]!=0
+		|| strcmp(*p, ".")==0)
+			p++;
+		else if(strcmp(*p, "..")==0 && q!=comp && strcmp(q[-1], "..")!=0){
+			--q;
+			p++;
+		}
+		else
+			*q++=*p++;
+	}
+	*q=0;
+	/*
+	 * rebuild the path name
+	 */
+	s=name;
+	if(rooted) *s++='/';
+	for(p=comp;*p;p++){
+		t=*p;
+		while(*t) *s++=*t++;
+		if(p[1]!=0) *s++='/';
+	}
+	*s='\0';
+	free(comp);
+}
+/*
+ * True url parsing is a nightmare.
+ * This assumes that there are two basic syntaxes
+ * for url's -- with and without an ip address.
+ * If the type identifier or the ip address and port number
+ * or the relative address is missing from urlname or is empty, 
+ * it is copied from cur.
+ */
+void crackurl(Url *url, char *urlname, Url *cur){
+	char *relp, *tagp, *httpname;
+	int len;
+	Scheme *up;
+	char buf[30];
+	/*
+	 * The following lines `fix' the most egregious urlname syntax errors
+	 */
+	while(*urlname==' ' || *urlname=='\t' || *urlname=='\n') urlname++;
+	relp=strchr(urlname, '\n');
+	if(relp) *relp='\0';
+	/*
+	 * In emulation of Netscape, attach a free "http://"
+	 * to names beginning with "www.".
+	 */
+	if(strncmp(urlname, "www.", 4)==0){
+		httpname=emalloc(strlen(urlname)+8);
+		strcpy(httpname, "http://");
+		strcat(httpname, urlname);
+		crackurl(url, httpname, cur);
+		free(httpname);
+		return;
+	}
+	url->port=cur->port;
+	strncpy(url->ipaddr, cur->ipaddr, sizeof(url->ipaddr));
+	strncpy(url->reltext, cur->reltext, sizeof(url->reltext));
+	if(strchr(urlname, ':')==0){
+		up=cur->scheme;
+		if(up==0){
+			up=&scheme[0];
+			cur->scheme=up;
+		}
+	}
+	else{
+		for(up=scheme;up->name;up++){
+			len=strlen(up->name);
+			if(strncmp(urlname, up->name, len)==0){
+				urlname+=len;
+				break;
+			}
+		}
+		if(up->name==0) up=&scheme[0];	/* default to http: */
+	}
+	url->access=up->type;
+	url->scheme=up;
+	if(up!=cur->scheme)
+		url->reltext[0]='\0';
+	if(up->flags&IP && strncmp(urlname, "//", 2)==0){
+		urlname+=2;
+		for(relp=urlname;!endaddr(*relp);relp++);
+		len=relp-urlname;
+		strncpy(url->ipaddr, urlname, len);
+		url->ipaddr[len]='\0';
+		urlname=relp;
+		if(*urlname==':'){
+			urlname++;
+			url->port=atoi(urlname);
+			while(!endaddr(*urlname)) urlname++;
+		}
+		else
+			url->port=up->port;
+		if(*urlname=='\0') urlname="/";
+	}
+	url->ssl = up->flags&SSL;
+		
+	tagp=strchr(urlname, '#');
+	if(tagp){
+		*tagp='\0';
+		strncpy(url->tag, tagp+1, sizeof(url->tag));
+	}
+	else
+		url->tag[0]='\0';	
+	if(!(up->flags&REL) || *urlname=='/')
+		strncpy(url->reltext, urlname, sizeof(url->reltext));
+	else if(urlname[0]){
+		relp=strrchr(url->reltext, '/');
+		if(relp==0)
+			strncpy(url->reltext, urlname, sizeof(url->reltext));
+		else
+			strcpy(relp+1, urlname);
+	}
+	urlcanon(url->reltext);
+	if(tagp) *tagp='#';
+	/*
+	 * The following mess of strcpys and strcats
+	 * can't be changed to a few sprints because
+	 * urls are not necessarily composed of legal utf
+	 */
+	strcpy(url->fullname, up->name);
+	if(up->flags&IP){
+		strncat(url->fullname, "//", sizeof(url->fullname));
+		strncat(url->fullname, url->ipaddr, sizeof(url->fullname));
+		if(url->port!=up->port){
+			snprint(buf, sizeof(buf), ":%d", url->port);
+			strncat(url->fullname, buf, sizeof(url->fullname));
+		}
+	}
+	strcat(url->fullname, url->reltext);
+	url->map=0;
+}
--- /dev/null
+++ b/sys/src/cmd/mothra/file.c
@@ -1,0 +1,48 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <panel.h>
+#include "mothra.h"
+/*
+ * fd is the result of a successful open(name, OREAD),
+ * where name is the name of a directory.  We convert
+ * this into an html page containing links to the files
+ * in the directory.
+ */
+int dir2html(char *name, int fd){
+	int p[2], first;
+	Dir *dir;
+	int i, n;
+	if(pipe(p)==-1){
+		close(fd);
+		return -1;
+	}
+	switch(rfork(RFFDG|RFPROC|RFNOWAIT)){
+	case -1:
+		close(fd);
+		return -1;
+	case 0:
+		close(p[1]);
+		fprint(p[0], "<head>\n");
+		fprint(p[0], "<title>Directory %s</title>\n", name);
+		fprint(p[0], "</head>\n");
+		fprint(p[0], "<body>\n");
+		fprint(p[0], "<h1>%s</h1>\n", name);
+		fprint(p[0], "<ul>\n");
+		first=1;
+		while((n = dirread(fd, &dir)) > 0) {
+		  for (i = 0; i < n; i++)
+			fprint(p[0], "<li><a href=\"%s/%s\">%s%s</a>\n", name, dir[i].name, dir[i].name,
+				dir[i].mode&DMDIR?"/":"");
+		  free(dir);
+		}
+		fprint(p[0], "</ul>\n");
+		fprint(p[0], "</body>\n");
+		_exits(0);
+	default:
+		close(fd);
+		close(p[0]);
+		return p[1];
+	}
+}
--- /dev/null
+++ b/sys/src/cmd/mothra/filetype.c
@@ -1,0 +1,106 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <panel.h>
+#include <ctype.h>
+#include "mothra.h"
+typedef struct Kind Kind;
+struct Kind{
+	char *name;
+	int kind;
+};
+int klook(char *s, Kind *k){
+	while(k->name && cistrcmp(k->name, s)!=0)
+		k++;
+	return k->kind;
+}
+Kind suffix[]={
+	".html",	HTML,
+	".htm",		HTML,
+	"/",		HTML,
+	".gif",		GIF,
+	".jpe",		JPEG,
+	".jpg",		JPEG,
+	".jpeg",	JPEG,
+	".png",	PNG,
+	".pic",		PIC,
+	".au",		AUDIO,
+	".tif",		TIFF,
+	".tiff",	TIFF,
+	".xbm",		XBM,
+	".txt",		PLAIN,
+	".text",	PLAIN,
+	".ai",		POSTSCRIPT,
+	".eps",		POSTSCRIPT,
+	".ps",		POSTSCRIPT,
+	".pdf",		PDF,
+	".zip",		ZIP,
+	0,		HTML
+};
+int suflook(char *s, int len, Kind *k){
+	int l;
+	while(k->name){
+		l=strlen(k->name);
+		if(l<=len && cistrcmp(k->name, s+len-l)==0) return k->kind;
+		k++;
+	}
+	return k->kind;
+}
+int suffix2type(char *name){
+	int len, kind, restore;
+	char *s;
+	len=strlen(name);
+	if(len>=2 && cistrcmp(name+len-2, ".Z")==0){
+		kind=COMPRESS;
+		len-=2;
+	}
+	else if(len>=3 && cistrcmp(name+len-3, ".gz")==0){
+		kind=GUNZIP;
+		len-=3;
+	}
+	else
+		kind=0;
+	restore=name[len];
+	name[len]='\0';
+	for(s=name+len;s!=name && *s!='.';--s);
+	kind|=suflook(name, len, suffix);
+	name[len]=restore;
+	return kind;
+}
+Kind content[]={
+	"text/html",			HTML,
+	"text/x-html",			HTML,
+	"application/html",		HTML,
+	"application/x-html",		HTML,
+	"text/plain",			PLAIN,
+	"image/gif",			GIF,
+	"image/jpeg",			JPEG,
+	"image/pjpeg",			JPEG,
+	"image/png",			PNG,
+	"image/tiff",			TIFF,
+	"image/x-xbitmap",		XBM,
+	"image/x-bitmap",		XBM,
+	"image/xbitmap",		XBM,
+	"application/postscript",	POSTSCRIPT,
+	"application/pdf",		PDF,
+	"application/octet-stream",	SUFFIX,
+	"application/zip",		ZIP,
+	0,				HTML
+};
+int content2type(char *s, char *name){
+	int type;
+	type=klook(s, content);
+	if(type==SUFFIX) type=suffix2type(name);
+	return type;
+}
+Kind encoding[]={
+	"x-compress",	COMPRESS,
+	"compress",	COMPRESS,
+	"x-gzip",	GUNZIP,
+	"gzip",		GUNZIP,
+	0,		0
+};
+int encoding2type(char *s){
+	return klook(s, encoding);
+}
--- /dev/null
+++ b/sys/src/cmd/mothra/forms.c
@@ -1,0 +1,661 @@
+/*
+ * type=image is treated like submit
+ */
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <panel.h>
+#include "mothra.h"
+#include "html.h"
+typedef struct Field Field;
+typedef struct Option Option;
+struct Form{
+	int method;
+	Url *action;
+	Field *fields, *efields;
+	Form *next;
+};
+struct Field{
+	Field *next;
+	Form *form;
+	char *name;
+	char *value;
+	int checked;
+	int size;		/* should be a point, but that feature is deprecated */
+	int maxlength;
+	int type;
+	int rows, cols;
+	Option *options;
+	int multiple;
+	int state;		/* is the button marked? */
+	Panel *p;
+	Panel *pulldown;
+	Panel *textwin;
+};
+/*
+ * Field types
+ */
+enum{
+	TYPEIN=1,
+	CHECK,
+	PASSWD,
+	RADIO,
+	SUBMIT,
+	RESET,
+	SELECT,
+	TEXTWIN,
+	HIDDEN,
+	INDEX,
+};
+struct Option{
+	int selected;
+	int def;
+	char label[NLABEL+1];
+	char *value;
+	Option *next;
+};
+void h_checkinput(Panel *, int, int);
+void h_radioinput(Panel *, int, int);
+void h_submitinput(Panel *, int);
+void h_submittype(Panel *, char *);
+void h_submitindex(Panel *, char *);
+void h_resetinput(Panel *, int);
+void h_select(Panel *, int, int);
+void h_cut(Panel *, int);
+void h_paste(Panel *, int);
+void h_snarf(Panel *, int);
+void h_edit(Panel *);
+char *selgen(Panel *, int);
+char *nullgen(Panel *, int);
+Field *newfield(Form *form){
+	Field *f;
+	f=emallocz(sizeof(Field), 1);
+	if(form->efields==0)
+		form->fields=f;
+	else
+		form->efields->next=f;
+	form->efields=f;
+	f->next=0;
+	f->form=form;
+	return f;
+}
+/*
+ * Called by rdhtml on seeing a forms-related tag
+ */
+void rdform(Hglob *g){
+	char *s;
+	Field *f;
+	Option *o, **op;
+	Form *form;
+	switch(g->tag){
+	default:
+		fprint(2, "Bad tag <%s> in rdform (Can't happen!)\n", g->token);
+		return;
+	case Tag_form:
+		if(g->form){
+			htmlerror(g->name, g->lineno, "nested forms illegal\n");
+			break;
+		}
+		g->form=emallocz(sizeof(Form), 1);
+		g->form->action=emalloc(sizeof(Url));
+		s=pl_getattr(g->attr, "action");
+		if(s==0)
+			*g->form->action=*g->dst->url;
+		else
+			crackurl(g->form->action, s, g->dst->base);
+		s=pl_getattr(g->attr, "method");
+		if(s==0)
+			g->form->method=GET;
+		else if(cistrcmp(s, "post")==0)
+			g->form->method=POST;
+		else{
+			if(cistrcmp(s, "get")!=0)
+				htmlerror(g->name, g->lineno,
+					"unknown form method %s\n", s);
+			g->form->method=GET;
+		}
+		g->form->fields=0;
+
+		g->form->next = g->dst->form;
+		g->dst->form = g->form;
+
+		break;
+	case Tag_input:
+		if(g->form==0){
+		BadTag:
+			htmlerror(g->name, g->lineno, "<%s> not in form, ignored\n",
+				tag[g->tag].name);
+			break;
+		}
+		f=newfield(g->form);
+		s=pl_getattr(g->attr, "name");
+		if(s==0)
+			f->name=0;
+		else
+			f->name=strdup(s);
+		s=pl_getattr(g->attr, "value");
+		if(s==0)
+			f->value=strdup("");
+		else
+			f->value=strdup(s);
+		f->checked=pl_hasattr(g->attr, "checked");
+		s=pl_getattr(g->attr, "size");
+		if(s==0)
+			f->size=20;
+		else
+			f->size=atoi(s);
+		s=pl_getattr(g->attr, "maxlength");
+		if(s==0)
+			f->maxlength=0x3fffffff;
+		else
+			f->maxlength=atoi(s);
+		s=pl_getattr(g->attr, "type");
+		/* bug -- password treated as text */
+		if(s==0 || cistrcmp(s, "text")==0 || cistrcmp(s, "password")==0 || cistrcmp(s, "int")==0){
+			s=pl_getattr(g->attr, "name");
+			if(s!=0 && strcmp(s, "isindex")==0)
+				f->type=INDEX;
+			else
+				f->type=TYPEIN;
+			/*
+			 * If there's exactly one attribute, use its value as the name,
+			 * regardless of the attribute name.  This makes
+			 * http://linus.att.com/ias/puborder.html work.
+			 */
+			if(s==0){
+				if(g->attr[0].name && g->attr[1].name==0)
+					f->name=strdup(g->attr[0].value);
+				else
+					f->name=strdup("no-name");
+			}
+		}
+		else if(cistrcmp(s, "checkbox")==0)
+			f->type=CHECK;
+		else if(cistrcmp(s, "radio")==0)
+			f->type=RADIO;
+		else if(cistrcmp(s, "submit")==0)
+			f->type=SUBMIT;
+		else if(cistrcmp(s, "image")==0){
+			/* presotto's egregious hack to make image submits do something */
+			if(f->name){
+				free(f->name);
+				f->name=0;
+			}
+			f->type=SUBMIT;
+		} else if(cistrcmp(s, "reset")==0)
+			f->type=RESET;
+		else if(cistrcmp(s, "hidden")==0)
+			f->type=HIDDEN;
+		else{
+			htmlerror(g->name, g->lineno, "bad field type %s, ignored", s);
+			break;
+		}
+		if((f->type==CHECK || f->type==RADIO) && !pl_hasattr(g->attr, "value")){
+			free(f->value);
+			f->value=strdup("on");
+		}
+		if(f->type!=HIDDEN)
+			pl_htmloutput(g, g->nsp, f->value[0]?f->value:"blank field", f);
+		break;
+	case Tag_select:
+		if(g->form==0) goto BadTag;
+		f=newfield(g->form);
+		s=pl_getattr(g->attr, "name");
+		if(s==0){
+			f->name=strdup("select");
+			htmlerror(g->name, g->lineno, "select has no name=\n");
+		}
+		else
+			f->name=strdup(s);
+		s=pl_getattr(g->attr, "size");
+		if(s==0) f->size=4;
+		else{
+			f->size=atoi(s);
+			if(f->size<=0) f->size=1;
+		}
+		f->multiple=pl_hasattr(g->attr, "multiple");
+		f->type=SELECT;
+		f->options=0;
+		g->text=g->token;
+		g->tp=g->text;
+		g->etext=g->text;
+		break;
+	case Tag_option:
+		if(g->form==0) goto BadTag;
+		f=g->form->efields;
+		o=emallocz(sizeof(Option), 1);
+		for(op=&f->options;*op;op=&(*op)->next);
+		*op=o;
+		o->next=0;
+		g->text=o->label;
+		g->tp=o->label;
+		g->etext=o->label+NLABEL;
+		memset(o->label, 0, NLABEL+1);
+		*g->tp++=' ';
+		o->def=pl_hasattr(g->attr, "selected");
+		o->selected=o->def;
+		s=pl_getattr(g->attr, "value");
+		if(s==0)
+			o->value=o->label+1;
+		else
+			o->value=strdup(s);
+		break;
+	case Tag_textarea:
+		if(g->form==0) goto BadTag;
+		f=newfield(g->form);
+		s=pl_getattr(g->attr, "name");
+		if(s==0){
+			f->name=strdup("enter text");
+			htmlerror(g->name, g->lineno, "select has no name=\n");
+		}
+		else
+			f->name=strdup(s);
+		s=pl_getattr(g->attr, "rows");
+		f->rows=s?atoi(s):8;
+		s=pl_getattr(g->attr, "cols");
+		f->cols=s?atoi(s):30;
+		f->type=TEXTWIN;
+		/* suck up initial text */
+		pl_htmloutput(g, g->nsp, f->name, f);
+		break;
+	case Tag_isindex:
+		/*
+		 * Make up a form with one tag, of type INDEX
+		 * I have seen a page with <ISINDEX PROMPT="Enter a title here ">,
+		 * which is nonstandard and not handled here.
+		 */
+		form=emalloc(sizeof(Form));
+		form->fields=0;
+		form->efields=0;
+		form->action=emalloc(sizeof(Url));
+		s=pl_getattr(g->attr, "action");
+		if(s==0)
+			*form->action=*g->dst->url;
+		else
+			crackurl(form->action, s, g->dst->base);
+		form->method=GET;
+		form->fields=0;
+		f=newfield(form);
+		f->name=0;
+		f->value=strdup("");
+		f->size=20;
+		f->maxlength=0x3fffffff;
+		f->type=INDEX;
+		pl_htmloutput(g, g->nsp, f->value[0]?f->value:"blank field", f);
+		break;
+	}
+}
+/*
+ * Called by rdhtml on seeing a forms-related end tag
+ */
+void endform(Hglob *g){
+	switch(g->tag){
+	case Tag_form:
+		g->form=0;
+		break;
+	case Tag_select:
+		if(g->form==0)
+			htmlerror(g->name, g->lineno, "</select> not in form, ignored\n");
+		else
+			pl_htmloutput(g, g->nsp, g->form->efields->name,g->form->efields);
+		break;
+	case Tag_textarea:
+		break;
+	}
+}
+char *nullgen(Panel *, int ){
+	return 0;
+}
+char *selgen(Panel *p, int index){
+	Option *a;
+	Field *f;
+	f=p->userp;
+	if(f==0) return 0;
+	for(a=f->options;index!=0 && a!=0;--index,a=a->next);
+	if(a==0) return 0;
+	a->label[0]=a->selected?'*':' ';
+	return a->label;
+}
+char *seloption(Field *f){
+	Option *a;
+	for(a=f->options;a!=0;a=a->next)
+		if(a->selected)
+			return a->label+1;
+	return f->name;
+}
+void mkfieldpanel(Rtext *t){
+	Action *a;
+	Panel *win, *scrl, *menu, *pop, *button;
+	Field *f;
+
+	if((a = t->user) == nil)
+		return;
+	if((f = a->field) == nil)
+		return;
+
+	f->p=0;
+	switch(f->type){
+	case TYPEIN:
+		f->p=plentry(0, 0, f->size*chrwidth, f->value, h_submittype);
+		break;
+	case CHECK:
+		f->p=plcheckbutton(0, 0, "", h_checkinput);
+		f->state=f->checked;
+		plsetbutton(f->p, f->checked);
+		break;
+	case RADIO:
+		f->p=plradiobutton(0, 0, "", h_radioinput);
+		f->state=f->checked;
+		plsetbutton(f->p, f->checked);
+		break;
+	case SUBMIT:
+		f->p=plbutton(0, 0, f->value[0]?f->value:"submit", h_submitinput);
+		break;
+	case RESET:
+		f->p=plbutton(0, 0, f->value[0]?f->value:"reset", h_resetinput);
+		break;
+	case SELECT:
+		f->pulldown=plgroup(0,0);
+		scrl=plscrollbar(f->pulldown, PACKW|FILLY);
+		win=pllist(f->pulldown, PACKN, nullgen, f->size, h_select);
+		win->userp=f;
+		plinitlist(win, PACKN, selgen, f->size, h_select);
+		plscroll(win, 0, scrl);
+		plpack(f->pulldown, Rect(0,0,1024,1024));
+		f->p=plpulldown(0, FIXEDX, seloption(f), f->pulldown, PACKS);
+		f->p->fixedsize.x=f->pulldown->r.max.x-f->pulldown->r.min.x;
+		break;
+	case TEXTWIN:
+		menu=plgroup(0,0);
+		f->p=plframe(0,0);
+		pllabel(f->p, PACKN|FILLX, f->name);
+		scrl=plscrollbar(f->p, PACKW|FILLY);
+		pop=plpopup(f->p, PACKN|FILLX, 0, menu, 0);
+		f->textwin=pledit(pop, EXPAND, Pt(f->cols*chrwidth, f->rows*font->height),
+			0, 0, h_edit);
+		f->textwin->userp=f;
+		button=plbutton(menu, PACKN|FILLX, "cut", h_cut);
+		button->userp=f->textwin;
+		button=plbutton(menu, PACKN|FILLX, "paste", h_paste);
+		button->userp=f->textwin;
+		button=plbutton(menu, PACKN|FILLX, "snarf", h_snarf);
+		button->userp=f->textwin;
+		plscroll(f->textwin, 0, scrl);
+		break;
+	case INDEX:
+		f->p=plentry(0, 0, f->size*chrwidth, f->value, h_submitindex);
+		break;
+	}
+	if(f->p){
+		f->p->userp=f;
+		free(t->text);
+		t->text=0;
+		t->p=f->p;
+		t->hot=1;
+	}
+}
+void h_checkinput(Panel *p, int, int v){
+	((Field *)p->userp)->state=v;
+}
+void h_radioinput(Panel *p, int, int v){
+	Field *f, *me;
+	me=p->userp;
+	me->state=v;
+	if(v){
+		for(f=me->form->fields;f;f=f->next)
+			if(f->type==RADIO && f!=me && strcmp(f->name, me->name)==0){
+				plsetbutton(f->p, 0);
+				f->state=0;
+				pldraw(f->p, screen);
+			}
+	}
+}
+void h_select(Panel *p, int, int index){
+	Option *a;
+	Field *f;
+	f=p->userp;
+	if(f==0) return;
+	if(!f->multiple) for(a=f->options;a;a=a->next) a->selected=0;
+	for(a=f->options;index!=0 && a!=0;--index,a=a->next);
+	if(a==0) return;
+	a->selected=!a->selected;
+	plinitpulldown(f->p, FIXEDX, seloption(f), f->pulldown, PACKS);
+	pldraw(f->p, screen);
+}
+void h_resetinput(Panel *p, int){
+	Field *f;
+	Option *o;
+	for(f=((Field *)p->userp)->form->fields;f;f=f->next) switch(f->type){
+	case TYPEIN:
+	case PASSWD:
+		plinitentry(f->p, 0, f->size*chrwidth, f->value, 0);
+		break;
+	case CHECK:
+	case RADIO:
+		f->state=f->checked;
+		plsetbutton(f->p, f->checked);
+		break;
+	case SELECT:
+		for(o=f->options;o;o=o->next)
+			o->selected=o->def;
+		break;
+	}
+	pldraw(text, screen);
+}
+void h_edit(Panel *p){
+	plgrabkb(p);
+}
+Rune *snarfbuf=0;
+int nsnarfbuf=0;
+void h_snarf(Panel *p, int){
+	int s0, s1;
+	Rune *text;
+	p=p->userp;
+	plegetsel(p, &s0, &s1);
+	if(s0==s1) return;
+	text=pleget(p);
+	if(snarfbuf) free(snarfbuf);
+	nsnarfbuf=s1-s0;
+	snarfbuf=malloc(nsnarfbuf*sizeof(Rune));
+	if(snarfbuf==0){
+		fprint(2, "No mem\n");
+		exits("no mem");
+	}
+	memmove(snarfbuf, text+s0, nsnarfbuf*sizeof(Rune));
+}
+void h_cut(Panel *p, int b){
+	h_snarf(p, b);
+	plepaste(p->userp, 0, 0);
+}
+void h_paste(Panel *p, int){
+	plepaste(p->userp, snarfbuf, nsnarfbuf);
+}
+int ulen(char *s){
+	int len;
+	len=0;
+	for(;*s;s++){
+		if(strchr("/$-_@.!*'(), ", *s)
+		|| 'a'<=*s && *s<='z'
+		|| 'A'<=*s && *s<='Z'
+		|| '0'<=*s && *s<='9')
+			len++;
+		else
+			len+=3;
+	}
+	return len;
+}
+int hexdigit(int v){
+	return 0<=v && v<=9?'0'+v:'A'+v-10;
+}
+char *ucpy(char *buf, char *s){
+	for(;*s;s++){
+		if(strchr("/$-_@.!*'(),", *s)
+		|| 'a'<=*s && *s<='z'
+		|| 'A'<=*s && *s<='Z'
+		|| '0'<=*s && *s<='9')
+			*buf++=*s;
+		else if(*s==' ')
+			*buf++='+';
+		else{
+			*buf++='%';
+			*buf++=hexdigit((*s>>4)&15);
+			*buf++=hexdigit(*s&15);
+		}
+	}
+	*buf='\0';
+	return buf;
+}
+char *runetou(char *buf, Rune r){
+	char rbuf[2];
+	if(r<=255){
+		rbuf[0]=r;
+		rbuf[1]='\0';
+		buf=ucpy(buf, rbuf);
+	}
+	return buf;
+}
+/*
+ * If there's exactly one button with type=text, then
+ * a CR in the button is supposed to submit the form.
+ */
+void h_submittype(Panel *p, char *){
+	int ntype;
+	Field *f;
+	ntype=0;
+	for(f=((Field *)p->userp)->form->fields;f;f=f->next) if(f->type==TYPEIN) ntype++;
+	if(ntype==1) h_submitinput(p, 0);
+}
+void h_submitindex(Panel *p, char *){
+	h_submitinput(p, 0);
+}
+void h_submitinput(Panel *p, int){
+	Form *form;
+	int size, nrune;
+	char *buf, *bufp, sep;
+	Rune *rp;
+	Field *f;
+	Option *o;
+	form=((Field *)p->userp)->form;
+	if(form->method==GET) size=ulen(form->action->fullname)+1;
+	else size=1;
+	for(f=form->fields;f;f=f->next) switch(f->type){
+	case TYPEIN:
+	case PASSWD:
+		size+=ulen(f->name)+1+ulen(plentryval(f->p))+1;
+		break;
+	case INDEX:
+		size+=ulen(plentryval(f->p))+1;
+		break;
+	case CHECK:
+	case RADIO:
+		if(!f->state) break;
+	case HIDDEN:
+		size+=ulen(f->name)+1+ulen(f->value)+1;
+		break;
+	case SELECT:
+		for(o=f->options;o;o=o->next)
+			if(o->selected)
+				size+=ulen(f->name)+1+ulen(o->value)+1;
+		break;
+	case TEXTWIN:
+		size+=ulen(f->name)+1+plelen(f->textwin)*3+1;
+		break;
+	}
+	buf=emalloc(size);
+	if(form->method==GET){
+		strcpy(buf, form->action->fullname);
+		sep='?';
+	}
+	else{
+		buf[0]='\0';
+		sep=0;
+	}
+	bufp=buf+strlen(buf);
+	if(form->method==GET && bufp!=buf && bufp[-1]=='?') *--bufp='\0'; /* spurious ? */
+	for(f=form->fields;f;f=f->next) switch(f->type){
+	case TYPEIN:
+	case PASSWD:
+		if(sep) *bufp++=sep;
+		sep='&';
+		bufp=ucpy(bufp, f->name);
+		*bufp++='=';
+		bufp=ucpy(bufp, plentryval(f->p));
+		break;
+	case INDEX:
+		if(sep) *bufp++=sep;
+		sep='&';
+		bufp=ucpy(bufp, plentryval(f->p));
+		break;
+	case CHECK:
+	case RADIO:
+		if(!f->state) break;
+	case HIDDEN:
+		if(sep) *bufp++=sep;
+		sep='&';
+		bufp=ucpy(bufp, f->name);
+		*bufp++='=';
+		bufp=ucpy(bufp, f->value);
+		break;
+	case SELECT:
+		for(o=f->options;o;o=o->next)
+			if(o->selected){
+				if(sep) *bufp++=sep;
+				sep='&';
+				bufp=ucpy(bufp, f->name);
+				*bufp++='=';
+				bufp=ucpy(bufp, o->value);
+			}
+		break;
+	case TEXTWIN:
+		if(sep) *bufp++=sep;
+		sep='&';
+		bufp=ucpy(bufp, f->name);
+		*bufp++='=';
+		rp=pleget(f->textwin);
+		for(nrune=plelen(f->textwin);nrune!=0;--nrune)
+			bufp=runetou(bufp, *rp++);
+		*bufp='\0';
+		break;
+	}
+	if(form->method==GET){
+fprint(2, "GET %s\n", buf);
+		geturl(buf, GET, 0, 0, 0);
+	}
+	else{
+fprint(2, "POST %s: %s\n", form->action->fullname, buf);
+		geturl(form->action->fullname, POST, buf, 0, 0);
+	}
+	free(buf);
+}
+
+void freeform(void *p)
+{
+	Form *form;
+	Field *f;
+	Option *o;
+
+	while(form = p){
+		p = form->next;
+		free(form->action);
+		while(f = form->fields){
+			form->fields = f->next;
+
+			if(f->p!=0)
+				plfree(f->p);
+
+			free(f->name);
+			free(f->value);
+
+			while(o = f->options){
+				f->options = o->next;
+				if(o->value != o->label+1)
+					free(o->value);
+				free(o);
+			}
+
+			free(f);
+		}
+		free(form);
+	}
+}
--- /dev/null
+++ b/sys/src/cmd/mothra/ftp.c
@@ -1,0 +1,428 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <panel.h>
+#include <bio.h>
+#include <ndb.h>
+#include <ctype.h>
+#include <ip.h>
+#include "mothra.h"
+
+enum
+{
+	/* return codes */
+	Extra=		1,
+	Success=	2,
+	Incomplete=	3,
+	TempFail=	4,
+	PermFail=	5,
+
+	NAMELEN=	28,
+	Nnetdir=	3*NAMELEN,	/* max length of network directory paths */
+	Ndialstr=	64,		/* max length of dial strings */
+};
+
+typedef struct Ftp Ftp;
+struct Ftp
+{
+	char	net[Nnetdir];
+	Biobuf	*ftpctl;
+	Url	*u;
+};
+
+static int ftpdebug;
+
+
+/*
+ *  read from biobuf turning cr/nl into nl
+ */
+char*
+getcrnl(Biobuf *b)
+{
+	char *p, *ep;
+
+	p = Brdline(b, '\n');
+	if(p == nil)
+		return nil;
+	ep = p + Blinelen(b) - 1;
+	if(*(ep-1) == '\r')
+		ep--;
+	*ep = 0;
+	return p;
+}
+
+char*
+readfile(char *file, char *buf, int len)
+{
+	int n, fd;
+
+	fd = open(file, OREAD);
+	if(fd < 0)
+		return nil;
+	n = read(fd, buf, len-1);
+	close(fd);
+	if(n <= 0)
+		return nil;
+	buf[n] = 0;
+	return buf;
+}
+
+char*
+sysname(void)
+{
+	static char sys[Ndbvlen];
+	char *p;
+
+	p = readfile("/dev/sysname", sys, sizeof(sys));
+	if(p == nil)
+		return "unknown";
+	return p;
+}
+
+char*
+domainname(void)
+{
+	static char domain[Ndbvlen];
+	Ndbtuple *t;
+
+	if(*domain)
+		return domain;
+
+	t = csgetval(0, "sys", sysname(), "dom", domain);
+	if(t){
+		ndbfree(t);
+		return domain;
+	} else
+		return sysname();
+}
+
+static int
+sendrequest(Biobuf *b, char *fmt, ...)
+{
+	char buf[2*1024], *s;
+	va_list args;
+
+	va_start(args, fmt);
+	s = buf + vsnprint(buf, (sizeof(buf)-4) / sizeof(*buf), fmt, args);
+	va_end(args);
+	*s++ = '\r';
+	*s++ = '\n';
+	if(write(Bfildes(b), buf, s - buf) != s - buf)
+		return -1;
+	if(ftpdebug)
+		write(2, buf, s - buf);
+	return 0;
+}
+
+static int
+getreply(Biobuf *b, char *msg, int len)
+{
+	char *line;
+	int rv;
+	int i, n;
+
+	while(line = getcrnl(b)){
+		/* add line to message buffer, strip off \r */
+		n = Blinelen(b);
+		if(ftpdebug)
+			write(2, line, n);
+		if(n > len - 1)
+			i = len - 1;
+		else
+			i = n;
+		if(i > 0){
+			memmove(msg, line, i);
+			msg += i;
+			len -= i;
+			*msg = 0;
+		}
+
+		/* stop if not a continuation */
+		rv = atoi(line);
+		if(rv >= 100 && rv < 600 && (n == 4 || (n > 4 && line[3] == ' ')))
+			return rv/100;
+	}
+
+	return -1;
+}
+
+int
+terminateftp(Ftp *d)
+{
+	if(d->ftpctl){
+		close(Bfildes(d->ftpctl));
+		Bterm(d->ftpctl);
+		free(d->ftpctl);
+		d->ftpctl = nil;
+	}
+	free(d);
+	return -1;
+}
+
+Biobuf*
+hello(Ftp *d)
+{
+	int fd;
+	char *p;
+	Biobuf *b;
+	char msg[1024];
+	char ndir[Nnetdir];
+
+	snprint(msg, sizeof msg, "tcp!%s!%d", d->u->ipaddr, d->u->port);
+	fd = dial(msg, 0, ndir, 0);
+	if(fd < 0){
+		d->ftpctl = nil;
+		return nil;
+	}
+	b = emalloc(sizeof(Biobuf));
+	Binit(b, fd, OREAD);
+	d->ftpctl = b;
+
+	/* remember network for the data connections */
+	p = strrchr(ndir, '/');
+	if(p == 0){
+		fprint(2, "dial is out of date\n");
+		return nil;
+	}
+	*p = 0;
+	strcpy(d->net, ndir);
+
+	/* wait for hello from other side */
+	if(getreply(b, msg, sizeof(msg)) != Success){
+		fprint(2, "instead of hello: %s\n", msg);
+		return nil;
+	}
+	return b;
+}
+
+int
+logon(Ftp *d)
+{
+	char msg[1024];
+
+	/* login anonymous */
+	sendrequest(d->ftpctl, "USER anonymous");
+	switch(getreply(d->ftpctl, msg, sizeof(msg))){
+	case Success:
+		return 0;
+	case Incomplete:
+		break;	/* need password */
+	default:
+		fprint(2, "login failed: %s\n", msg);
+		werrstr(msg);
+		return -1;
+	}
+
+	/* send user id as password */
+	sprint(msg, "%s@", getuser());
+	sendrequest(d->ftpctl, "PASS %s", msg);
+	if(getreply(d->ftpctl, msg, sizeof(msg)) != Success){
+		fprint(2, "login failed: %s\n", msg);
+		werrstr(msg);
+		return -1;
+	}
+
+	return 0;
+}
+
+int
+xfertype(Ftp *d, char *t)
+{
+	char msg[1024];
+
+	sendrequest(d->ftpctl, "TYPE %s", t);
+	if(getreply(d->ftpctl, msg, sizeof(msg)) != Success){
+		fprint(2, "can't set type %s: %s\n", t, msg);
+		werrstr(msg);
+		return -1;
+	}
+	return 0;
+}
+
+int
+passive(Ftp *d)
+{
+	char msg[1024];
+	char dialstr[Ndialstr];
+	char *f[6];
+	char *p;
+	int fd;
+
+	sendrequest(d->ftpctl, "PASV");
+	if(getreply(d->ftpctl, msg, sizeof(msg)) != Success)
+		return -1;
+
+	/* get address and port number from reply, this is AI */
+	p = strchr(msg, '(');
+	if(p == nil){
+		for(p = msg+3; *p; p++)
+			if(isdigit(*p))
+				break;
+	} else
+		p++;
+	if(getfields(p, f, 6, 0, ",") < 6){
+		fprint(2, "passive mode protocol botch: %s\n", msg);
+		werrstr("ftp protocol botch");
+		return -1;
+	}
+	snprint(dialstr, sizeof(dialstr), "%s!%s.%s.%s.%s!%d", d->net,
+		f[0], f[1], f[2], f[3],
+		((atoi(f[4])&0xff)<<8) + (atoi(f[5])&0xff));
+
+
+	/* open data connection */
+	fd = dial(dialstr, 0, 0, 0);
+	if(fd < 0){
+		fprint(2, "passive mode connect to %s failed: %r\n", dialstr);
+		return -1;
+	}
+
+	/* tell remote to send a file */
+	sendrequest(d->ftpctl, "RETR %s", d->u->reltext);
+	if(getreply(d->ftpctl, msg, sizeof(msg)) != Extra){
+		fprint(2, "passive mode retrieve failed: %s\n", msg);
+		werrstr(msg);
+		return -1;
+	}
+	return fd;
+}
+
+int
+active(Ftp *d)
+{
+	char msg[1024];
+	char buf[Ndialstr];
+	char netdir[Nnetdir];
+	char newdir[Nnetdir];
+	uchar ipaddr[4];
+	int dfd, cfd, listenfd;
+	char *p;
+	int port;
+
+	/* get a channel to listen on, let kernel pick the port number */
+	sprint(buf, "%s!*!0", d->net);
+	listenfd = announce(buf, netdir);
+	if(listenfd < 0){
+		fprint(2, "can't listen for ftp callback: %r\n", buf);
+		return -1;
+	}
+
+	/* get the local address and port number */
+	sprint(newdir, "%s/local", netdir);
+	readfile(newdir, buf, sizeof buf);
+	p = strchr(buf, '!')+1;
+	parseip(ipaddr, buf);
+	port = atoi(p);
+
+	/* tell remote side address and port*/
+	sendrequest(d->ftpctl, "PORT %d,%d,%d,%d,%d,%d", ipaddr[0], ipaddr[1], ipaddr[2],
+		ipaddr[3], port>>8, port&0xff);
+	if(getreply(d->ftpctl, msg, sizeof(msg)) != Success){
+		close(listenfd);
+		werrstr("ftp protocol botch");
+		fprint(2, "active mode connect failed %s\n", msg);
+		return -1;
+	}
+
+	/* tell remote to send a file */
+	sendrequest(d->ftpctl, "RETR %s", d->u->reltext);
+	if(getreply(d->ftpctl, msg, sizeof(msg)) != Extra){
+		close(listenfd);
+		fprint(2, "active mode connect failed: %s\n", msg);
+		werrstr(msg);
+		return -1;
+	}
+
+	/* wait for a new call */
+	cfd = listen(netdir, newdir);
+	close(listenfd);
+	if(cfd < 0){
+		fprint(2, "active mode connect failed: %r\n");
+		return -1;
+	}
+
+	/* open the data connection and close the control connection */
+	dfd = accept(cfd, newdir);
+	close(cfd);
+	if(dfd < 0){
+		fprint(2, "active mode connect failed: %r\n");
+		werrstr("ftp protocol botch");
+		return -1;
+	}
+
+	return dfd;
+}
+
+/*
+ * Given a url, return a file descriptor on which caller can
+ * read an ftp document.
+ * The caller is responsible for processing redirection loops.
+ */
+int
+ftp(Url *url)
+{
+	int n;
+	int data;
+	Ftp *d;
+	int pfd[2];
+	char buf[2048];
+
+	if(url->type == 0)
+		url->type = PLAIN;
+
+	d = (Ftp*)emalloc(sizeof(Ftp));
+	d->u = url;
+	d->ftpctl = nil;
+
+	if(hello(d) == nil)
+		return terminateftp(d);
+	if(logon(d) < 0)
+		return terminateftp(d);
+
+	switch(url->type){
+	case PLAIN:
+	case HTML:
+		if(xfertype(d, "A") < 0)
+			return terminateftp(d);
+		break;
+	default:
+		if(xfertype(d, "I") < 0)
+			return terminateftp(d);
+		break;
+	}
+
+	/* first try passive mode, then active */
+	data = passive(d);
+	if(data < 0){
+		if(d->ftpctl == nil)
+			return -1;
+		data = active(d);
+		if(data < 0)
+			return -1;
+	}
+
+	if(pipe(pfd) < 0)
+		return -1;
+
+	switch(rfork(RFFDG|RFPROC|RFNOWAIT)){
+	case -1:
+		werrstr("Can't fork");
+		close(pfd[0]);
+		close(pfd[1]);
+		return terminateftp(d);
+	case 0:
+		close(pfd[0]);
+		while((n=read(data, buf, sizeof(buf)))>0)
+			write(pfd[1], buf, n);
+		if(n<0)
+			fprint(2, "ftp: %s: %r\n", url->fullname);
+		_exits(0);
+	default:
+		close(pfd[1]);
+		close(data);
+		terminateftp(d);
+		return pfd[0];
+	}
+	return -1;
+}
--- /dev/null
+++ b/sys/src/cmd/mothra/getpix.c
@@ -1,0 +1,126 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <panel.h>
+#include "mothra.h"
+
+typedef struct Pix Pix;
+struct Pix{
+	Pix *next;
+	Image *b;
+	int width;
+	int height;
+	char name[NNAME];
+};
+
+char *pixcmd[]={
+[GIF]	"gif -9t",
+[JPEG]	"jpg -9t",
+[PNG]	"png -9t",
+[PIC]	"fb/3to1 /lib/fb/cmap/rgbv",
+[TIFF]	"/sys/lib/mothra/tiffcvt",
+[XBM]	"fb/xbm2pic",
+};
+
+void storebitmap(Rtext *t, Image *b){
+	t->b=b;
+	free(t->text);
+	t->text=0;
+}
+
+void getimage(Rtext *t, Www *w){
+	int pfd[2];
+	Action *ap;
+	Url url;
+	Image *b;
+	int fd;
+	char err[512];
+	Pix *p;
+
+	ap=t->user;
+	crackurl(&url, ap->image, w->base);
+	for(p=w->pix;p!=nil; p=p->next)
+		if(strcmp(ap->image, p->name)==0 && ap->width==p->width && ap->height==p->height){
+			storebitmap(t, p->b);
+			free(ap->image);
+			ap->image=0;
+			w->changed=1;
+			return;
+		}
+	fd=urlopen(&url, GET, 0);
+	if(fd==-1){
+	Err:
+		snprint(err, sizeof(err), "[%s: %r]", url.fullname);
+		free(t->text);
+		t->text=strdup(err);
+		free(ap->image);
+		ap->image=0;
+		w->changed=1;
+		close(fd);
+		return;
+	}
+	if(url.type!=GIF
+	&& url.type!=JPEG
+	&& url.type!=PNG
+	&& url.type!=PIC
+	&& url.type!=TIFF
+	&& url.type!=XBM){
+		werrstr("unknown image type");
+		goto Err;
+	}
+
+	if((fd = pipeline(pixcmd[url.type], fd)) < 0)
+		goto Err;
+	if(ap->width>0 || ap->height>0){
+		char buf[80];
+		char *p;
+
+		p = buf;
+		p += sprint(p, "resize");
+		if(ap->width>0)
+			p += sprint(p, " -x %d", ap->width);
+		if(ap->height>0)
+			p += sprint(p, " -y %d", ap->height);
+		if((fd = pipeline(buf, fd)) < 0)
+			goto Err;
+	}
+	b=readimage(display, fd, 1);
+	if(b==0){
+		werrstr("can't read image");
+		goto Err;
+	}
+	close(fd);
+	p = emallocz(sizeof(Pix), 1);
+	strncpy(p->name, ap->image, sizeof(p->name));
+	p->b=b;
+	p->width=ap->width;
+	p->height=ap->height;
+	p->next=w->pix;
+	w->pix=p;
+	storebitmap(t, b);
+	free(ap->image);
+	ap->image=0;
+	w->changed=1;
+}
+
+void getpix(Rtext *t, Www *w){
+	Action *ap;
+
+	for(;t!=0;t=t->next){
+		ap=t->user;
+		if(ap && ap->image)
+			getimage(t, w);
+	}
+}
+
+void freepix(void *p)
+{
+	Pix *x, *xx;
+	xx = p;
+	while(x = xx){
+		xx = x->next;
+		freeimage(x->b);
+		free(x);
+	}
+}
--- /dev/null
+++ b/sys/src/cmd/mothra/gopher.c
@@ -1,0 +1,38 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <panel.h>
+#include "mothra.h"
+void httpheader(Url *, char *);
+/*
+ * Given a url, return a file descriptor on which caller can
+ * read a gopher document.
+ */
+int gopher(Url *url){
+	int pfd[2];
+	char port[30];
+	if(pipe(pfd)==-1) return -1;
+	switch(rfork(RFFDG|RFPROC|RFNOWAIT)){
+	case -1:
+		close(pfd[0]);
+		close(pfd[1]);
+		return -1;
+	case 0:
+		dup(pfd[1], 1);
+		close(pfd[0]);
+		close(pfd[1]);
+		sprint(port, "%d", url->port);
+		execl("/bin/aux/gopher2html",
+			"gopher2html", url->ipaddr, port, url->reltext+1, 0);
+		fprint(2, "Can't exec aux/gopher2html!\n");
+		print("<head><title>Mothra error</title></head>\n");
+		print("<body><h1>Mothra error</h1>\n");
+		print("Can't exec aux/gopher2html!</body>\n");
+		exits("no exec");
+	default:
+		close(pfd[1]);
+		url->type=HTML;
+		return pfd[0];
+	}
+}
--- /dev/null
+++ b/sys/src/cmd/mothra/gopher2html.c
@@ -1,0 +1,230 @@
+/*
+ * Reads gopher output from a TCP port, outputs
+ * html on standard output.
+ * Usage: gopher2html gopher-string
+ *	where gopher-string is the string sent to
+ *	the gopher server to get the document.
+ *
+ * Gopher protocol is described in rfc1436
+ */
+#include <u.h>
+#include <libc.h>
+char *cmd;
+int ifd;
+void errexit(char *s, ...){
+	static char buf[1024];
+	char *out;
+	va_list args;
+	va_start(args, s);
+	out = doprint(buf, buf+sizeof(buf), s, args);
+	va_end(args);
+	*out='\0';
+	print("<head><title>%s error</title></head>\n", cmd);
+	print("<body><h1>%s error</h1>\n", cmd);
+	print("%s</body>\n", buf);
+	exits("gopher error");
+}
+void wtext(char *buf, char *ebuf){
+	char *bp;
+	for(bp=buf;bp!=ebuf;bp++){
+		if(*bp=='<' || *bp=='>' || *bp=='&' || *bp=='"'){
+			if(bp!=buf) write(1, buf, bp-buf);
+			buf=bp+1;
+			switch(*bp){
+			case '<': print("&lt;"); break;
+			case '>': print("&gt;"); break;
+			case '&': print("&amp;"); break;
+			case '"': print("&quot;"); break;
+			}
+		}
+	}
+	if(bp!=buf) write(1, buf, bp-buf);
+}
+void savefile(char *name, char *type){
+	int fd, n;
+	char save[30], buf[1024];
+	for(n=1;;n++){
+		if(n==100) errexit("can't save binary file %s: %r", name);
+		sprint(save, "gopher.save.%d", n);
+		fd=create(save, OWRITE, 0444);
+		if(fd!=-1) break;
+	}
+	print("<head><title>%s</title></head\n", name);
+	print("<body><h1>%s</h1><p>\n", name);
+	print("Saving %s file %s in <tt>%s</tt>...\n", type, name, save);
+	while((n=read(ifd, buf, sizeof buf))>0) write(fd, buf, n);
+	close(fd);
+	print("done</body>\n");
+}
+void copyfile(char *title){
+	char buf[1024];
+	int n;
+	print("<head><title>%s</title></head>\n", title);
+	print("<body><h1>%s</h1><pre>\n", title);
+	while((n=read(ifd, buf, sizeof buf))>0) wtext(buf, buf+n);
+	print("</pre></body>\n");
+}
+/*
+ * A directory entry contains
+ *	type name selector host port
+ * all tab separated, except type and name (type is one character)
+ */
+char ibuf[1024], *ibp, *eibuf;
+#define	EOF	(-1)
+int get(void){
+	int n;
+Again:
+	if(ibp==eibuf){
+		n=read(ifd, ibuf, sizeof(ibuf));
+		if(n<=0) return EOF;
+		eibuf=ibuf+n;
+		ibp=ibuf;
+	}
+	if(*ibp=='\r'){
+		ibp++;
+		goto Again;
+	}
+	return *ibp++&255;
+}
+char *escape(char *in){
+	static char out[516];
+	char *op, *eop;
+	eop=out+512;
+	op=out;
+	for(;*in;in++){
+		if(op<eop){
+			if(strchr("/$-_@.&!*'(),", *in)
+			|| 'a'<=*in && *in<='z'
+			|| 'A'<=*in && *in<='Z'
+			|| '0'<=*in && *in<='9')
+				*op++=*in;
+			else{
+				sprint(op, "%%%.2X", *in&255);
+				op+=3;
+			}
+		}
+	}
+	*op='\0';
+	return out;
+}
+void copydir(char *title){
+	int type, c;
+	char name[513], *ename;
+	char selector[513];
+	char host[513];
+	char port[513];
+	char *bp;
+	print("<head><title>%s</title></head>\n", title);
+	print("<body><h1>%s</h1><ul>\n", title);
+	for(;;){
+		type=get();
+		if(type==EOF || type=='.') break;
+		bp=name;
+		while((c=get())!=EOF && c!='\t') if(bp!=&name[512]) *bp++=c;
+		ename=bp;
+		bp=selector;
+		while((c=get())!=EOF && c!='\t') if(bp!=&selector[512]) *bp++=c;
+		*bp='\0';
+		bp=host;
+		while((c=get())!=EOF && c!='\t') if(bp!=&host[512]) *bp++=c;
+		*bp='\0';
+		bp=port;
+		while((c=get())!=EOF && c!='\t' && c!='\n') if(bp!=&port[512]) *bp++=c;
+		while(c!=EOF && c!='\n') c=get();
+		*bp='\0';
+		switch(type){
+		case '3':
+			print("<li>");
+			wtext(name, ename);
+			break;
+		case '7':
+			print("<li><isindex action=\"gopher://%s:%s/%c%s\">",
+				host, port, type, escape(selector));
+			wtext(name, ename);
+			break;
+		default:
+			print("<li><a href=\"gopher://%s:%s/%c%s\">",
+				host, port, type, escape(selector));
+			wtext(name, ename);
+			print("</a>\n");
+			break;
+		}
+	}
+	print("</ul></body>\n");
+}
+int hexdigit(int c){
+	if('0'<=c && c<='9') return c-'0';
+	if('a'<=c && c<='f') return c-'a'+10;
+	if('A'<=c && c<='F') return c-'A'+10;
+	return -1;
+}
+void unescape(char *s){
+	char *t;
+	int hi, lo;
+	t=s;
+	while(*s){
+		if(*s=='%'
+		&& (hi=hexdigit(s[1]))>=0
+		&& (lo=hexdigit(s[2]))>=0){
+			*t++=hi*16+lo;
+			s+=3;
+		}
+		else *t++=*s++;
+	}
+	*t='\0';
+}
+void main(int argc, char *argv[]){
+	char dialstr[1024];
+	char *name;
+	cmd=argv[0];
+	if(argc!=4) errexit("Usage: %s host port selector", argv[0]);
+	sprint(dialstr, "tcp!%s!%s", argv[1], argv[2]);
+	ifd=dial(dialstr, 0, 0, 0);
+	if(ifd==-1) errexit("can't call %s:%s", argv[1], argv[2]);
+	unescape(argv[3]);
+	switch(argv[3][0]){
+	case '/':
+		fprint(ifd, "\r\n");
+		copydir(argv[3]);
+		break;
+	case '\0':
+		fprint(ifd, "\r\n");
+		copydir(argv[1]);
+		break;
+	case '7':	/* index query */
+		name=strchr(argv[3], '?');
+		if(name!=0){
+			if(name==argv[3]+1){
+				argv[3][1]=argv[3][0];
+				argv[3]++;
+			}
+			else
+				*name='\t';
+			name++;
+		}
+		else
+			name=argv[3];
+		fprint(ifd, "%s\r\n", argv[3]+1);
+		copydir(name);
+		break;
+	default:
+		fprint(ifd, "%s\r\n", argv[3]+1);
+		name=strrchr(argv[3], '/');
+		if(name==0) name=argv[3];
+		else name++;
+		switch(argv[3][0]){
+		default:	errexit("sorry, can't handle %s (type %c)",
+					argv[3]+1, argv[3][0]);
+		case '0':	copyfile(name); break;
+		case '1':	copydir(name); break;
+		case '4':	savefile(name, "Macintosh BINHEX"); break;
+		case '5':	savefile(name, "DOS binary"); break;
+		case '6':	savefile(name, "uuencoded"); break;
+		case '9':	savefile(name, "binary"); break;
+		case 'g':	savefile(name, "GIF"); break;
+		case 'I':	savefile(name, "some sort of image"); break;
+		}
+		break;
+	}
+	exits(0);
+}
--- /dev/null
+++ b/sys/src/cmd/mothra/help.html
@@ -1,0 +1,78 @@
+<html><head><title>Mothra help</title>
+</head>
+<body>
+<H1>Mothra Help</H1>
+<p>
+Mothra is a World-wide Web browser. Its display looks like this:
+<p><img src="file:display.pic" alt="[mothra display]">
+<p>The display's regions, from top to bottom, are:
+<ul>
+<li>Error messages and other information.
+<li>A text input window in which <a href="#commands">commands</a> can be typed.
+<li>A scrollable list of titles of previously visited documents, with the most recent first.
+Pointing at one of these lines with mouse button 1 revisits the document.
+<li>The title of the currently-displayed document.
+<li>The URL of the currently-displayed document.
+<li>The scrollable document display.  Underlined text and
+images surrounded by boxes may be pointed at with button 1 to
+visit the files that they refer to.  Files that are not
+HTML documents (for example images or mailto: urls) cause
+<i>9v</i> or <i>mail</i> to pop up in a new 8&#189; window.
+</ul>
+<h4>Mouse Action</H4>
+<p>Pointing with button
+2 instead of button 1 selects a url without following it;
+the url will be displayed in the selection: area and commands
+will refer to the url, but it will not be drawn in the document display.
+Button 3 pops up a command menu that contains
+<ul>
+<li><b>alt display</b><br>switches to (or from) the alternate display, which shows only
+the scrollable document display area.  This might be useful when running mothra
+in a small window.
+<li><b>snarf url</b><br>copies the selected url into the snarf buffer.
+<li><b>paste</b><br>appends the snarf buffer to the command window.
+<li><b>inline pix</b><br>turn off/on loading of inline images.  Image maps cannot be disabled.
+<li><b>fix cmap</b><br>reload the default plan 9 colormap
+<li><b>save hit</b><br>appends the selected url to file:$home/lib/hit.html
+<li><b>hit list</b><br>displays file:$home/lib/hit.html
+<li><b>exit</b>
+</ul>
+<a name="#commands"><h4>Commands</h4></a>
+<p>The commands you can type are:
+<ul>
+<li>g [url]<br>get the page with the given url (default, the selection.)
+<li>r [url]<br>refresh the display if the URL changes.
+Otherwise, you will probably see a cached version.
+<li>s file<br>save the current page in the given file.
+<li>w file<br>write a bitmap image of the document display area in the given file.
+<li>q<br>exit.
+<li>?<br>get help.
+<li>h<br>get help.
+</ul>
+<p>
+<h4>Configuration</h4>
+Mothra gets configuration information from the environment.
+<ul>
+<li>$url<br>The default <i>url</i> displayed when mothra starts.
+A <i>url</i> given on the command line overrides this.
+The default is <b>/sys/lib/mothra/start.html</b>
+<li>$httpproxy<br>The network address of an http proxy server,
+in the format expected by dial(2).  If $httpproxy is not set
+or is null, no proxy server is used.
+</ul>
+<h4>Command line</h4>
+If the mothra command has an argument, it is the name of a <i>url</i> to visit
+instead of the startup page.  Giving mothra the <b>-i</b> flag disables loading
+of inline images.  The <b>inline pix</b> menu item will reset this option.
+<h4>Files</h4>
+Mothra creates several files in $home/lib/mothra.
+<ul>
+<li>mothra.log<br>a list of all the url's visited
+<li>mothra.err<br>a log of error messages, mostly uninteresting
+<li>hit.html<br>the hit list used by the <b>save hit</b>
+and <b>hit list</b> commands.  Since <b>save hit</b> only
+adds new urls to the end of this file, it is safe to edit it
+to add annotation or sort the saved urls.
+</ul>
+</body>
+</html>
--- /dev/null
+++ b/sys/src/cmd/mothra/html.h
@@ -1,0 +1,201 @@
+/*
+ * Parameters
+ */
+#define	NSTACK	100	/* html grammar is not recursive, so 30 or so should do */
+#define	NHBUF	8192	/* Input buffer size */
+#define	NPEEKC	3	/* Maximum lookahead */
+#define	NTOKEN	1024	/* Maximum token length */
+#define	NATTR	512	/* Maximum number of attributes of a tag */
+typedef struct Pair Pair;
+typedef struct Tag Tag;
+typedef struct Stack Stack;
+typedef struct Hglob Hglob;
+typedef struct Form Form;
+typedef struct Entity Entity;
+struct Pair{
+	char *name;
+	char *value;
+};
+struct Entity{
+	char *name;
+	Rune value;
+};
+struct Tag{
+	char *name;
+	int action;
+};
+struct Stack{
+	int tag;		/* html tag being processed */
+	int pre;		/* in preformatted text? */
+	int font;		/* typeface */
+	int size;		/* point size of text */
+	int margin;		/* left margin position */
+	int indent;		/* extra indent at paragraph start */
+	int number;		/* paragraph number */
+	int ismap;		/* flag of <img> */
+	int width;		/* size of image */
+	int height;
+	int	table;		/* depth of table nesting */
+	char image[NNAME];	/* arg of <img> */
+	char link[NNAME];	/* arg of <a href=...> */
+	char name[NNAME];	/* arg of <a name=...> */
+};
+
+/*
+ * Globals -- these are packed up into a struct that gets passed around
+ * so that multiple parsers can run concurrently
+ */
+struct Hglob{
+	char *tp;		/* pointer in text buffer */
+	char *name;		/* input file name */
+	int hfd;		/* input file descriptor */
+	char hbuf[NHBUF];	/* input buffer */
+	char *hbufp;		/* next character in buffer */
+	char *ehbuf;		/* end of good characters in buffer */
+	int heof;		/* end of file flag */
+	int peekc[NPEEKC];	/* characters to re-read */
+	int npeekc;		/* # of characters to re-read */
+	char token[NTOKEN];	/* if token type is TEXT */
+	Pair attr[NATTR];	/* tag attribute/value pairs */
+	int nsp;		/* # of white-space characters before TEXT token */
+	int spacc;		/* place to accumulate more spaces */
+				/* if negative, won't accumulate! */
+	int tag;		/* if token type is TAG or END */
+	Stack stack[NSTACK];	/* parse stack */
+	Stack *state;		/* parse stack pointer */
+	int lineno;		/* input line number */
+	int linebrk;		/* flag set if we require a line-break in output */
+	int para;		/* flag set if we need an indent at the break */
+	char *text;		/* text buffer */
+	char *etext;		/* end of text buffer */
+	Form *form;		/* data for form under construction */
+	Www *dst;		/* where the text goes */
+	char charset[NNAME];
+};
+
+/*
+ * Token types
+ */
+enum{
+	TAG=1,
+	ENDTAG,
+	TEXT,
+};
+
+/*
+ * Magic characters corresponding to
+ *	literal < followed by / ! or alpha,
+ *	literal > and
+ *	end of file
+ */
+#define STAG	65536
+#define ETAG	65537
+#define EOF	-1
+
+/*
+ * fonts
+ */
+enum{
+	ROMAN,
+	ITALIC,
+	BOLD,
+	CWIDTH,
+};
+
+/*
+ * font sizes
+ */
+enum{
+	SMALL,
+	NORMAL,
+	LARGE,
+	ENORMOUS,
+};
+
+/*
+ * Token names for the html parser.
+ * Tag_end corresponds to </end> tags.
+ * Tag_text tags text not in a tag.
+ * Those two must follow the others.
+ */
+enum{
+	Tag_comment,
+	Tag_a,
+	Tag_address,
+	Tag_b,
+	Tag_base,
+	Tag_blockquot,
+	Tag_body,
+	Tag_br,
+	Tag_center,
+	Tag_cite,
+	Tag_code,
+	Tag_dd,
+	Tag_dfn,
+	Tag_dir,
+	Tag_dl,
+	Tag_dt,
+	Tag_em,
+	Tag_font,
+	Tag_form,
+	Tag_h1,
+	Tag_h2,
+	Tag_h3,
+	Tag_h4,
+	Tag_h5,
+	Tag_h6,
+	Tag_head,
+	Tag_hr,
+	Tag_html,
+	Tag_i,
+	Tag_img,
+	Tag_input,
+	Tag_isindex,
+	Tag_kbd,
+	Tag_key,
+	Tag_li,
+	Tag_link,
+	Tag_listing,
+	Tag_menu,
+	Tag_meta,
+	Tag_nextid,
+	Tag_ol,
+	Tag_option,
+	Tag_p,
+	Tag_plaintext,
+	Tag_pre,
+	Tag_samp,
+	Tag_select,
+	Tag_strong,
+	Tag_textarea,
+	Tag_title,
+	Tag_tt,
+	Tag_u,
+	Tag_ul,
+	Tag_var,
+	Tag_xmp,
+	Tag_frame,	/* rm 5.8.97 */
+	Tag_table,	/* rm 3.8.00 */
+	Tag_td,
+	Tag_tr,
+	Tag_script,
+	Tag_style,
+	Tag_end,	/* also used to indicate unrecognized start tag */
+
+	Tag_text,
+};
+enum{
+	NTAG=Tag_end,
+	END=1,	/* tag must have a matching end tag */
+	NOEND,	/* tag must not have a matching end tag */
+	OPTEND,	/* tag may have a matching end tag */
+	ERR,		/* tag must not occur */
+};
+Tag tag[];
+Entity pl_entity[];
+int pl_entities;
+void rdform(Hglob *);
+void endform(Hglob *);
+char *pl_getattr(Pair *, char *);
+int pl_hasattr(Pair *, char *);
+void pl_htmloutput(Hglob *, int, char *, Field *);
--- /dev/null
+++ b/sys/src/cmd/mothra/html.syntax.c
@@ -1,0 +1,340 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <panel.h>
+#include "mothra.h"
+#include "html.h"
+Tag tag[]={
+[Tag_comment]	"!--",		NOEND,
+[Tag_a]		"a",		END,
+[Tag_address]	"address",	END,
+[Tag_b]		"b",		END,
+[Tag_base]	"base",		NOEND,
+[Tag_blockquot]	"blockquote",	END,
+[Tag_body]	"body",		END,	/* OPTEND */
+[Tag_br]	"br",		NOEND,
+[Tag_center]	"center",	END,
+[Tag_cite]	"cite",		END,
+[Tag_code]	"code",		END,
+[Tag_dd]	"dd",		NOEND,	/* OPTEND */
+[Tag_dfn]	"dfn",		END,
+[Tag_dir]	"dir",		END,
+[Tag_dl]	"dl",		END,
+[Tag_dt]	"dt",		NOEND,	/* OPTEND */
+[Tag_em]	"em",		END,
+[Tag_font]	"font",		END,
+[Tag_form]	"form",		END,
+[Tag_h1]	"h1",		END,
+[Tag_h2]	"h2",		END,
+[Tag_h3]	"h3",		END,
+[Tag_h4]	"h4",		END,
+[Tag_h5]	"h5",		END,
+[Tag_h6]	"h6",		END,
+[Tag_head]	"head",		END,	/* OPTEND */
+[Tag_hr]	"hr",		NOEND,
+[Tag_html]	"html",		END,	/* OPTEND */
+[Tag_i]		"i",		END,
+[Tag_input]	"input",	NOEND,
+[Tag_img]	"img",		NOEND,
+[Tag_isindex]	"isindex",	NOEND,
+[Tag_kbd]	"kbd",		END,
+[Tag_key]	"key",		END,
+[Tag_li]	"li",		NOEND,	/* OPTEND */
+[Tag_link]	"link",		NOEND,
+[Tag_listing]	"listing",	END,
+[Tag_menu]	"menu",		END,
+[Tag_meta]	"meta",		NOEND,
+[Tag_nextid]	"nextid",	NOEND,
+[Tag_ol]	"ol",		END,
+[Tag_option]	"option",	NOEND,	/* OPTEND */
+[Tag_p]		"p",		NOEND,	/* OPTEND */
+[Tag_plaintext]	"plaintext",	NOEND,
+[Tag_pre]	"pre",		END,
+[Tag_samp]	"samp",		END,
+[Tag_script]	"script",	END,
+[Tag_style]	"style",	END,
+[Tag_select]	"select",	END,
+[Tag_strong]	"strong",	END,
+[Tag_table]		"table",	END,
+[Tag_td]		"td",		END,
+[Tag_textarea]	"textarea",	END,
+[Tag_title]	"title",	END,
+[Tag_tr]	"tr",		END,
+[Tag_tt]	"tt",		END,
+[Tag_u]		"u",		END,
+[Tag_ul]	"ul",		END,
+[Tag_var]	"var",		END,
+[Tag_xmp]	"xmp",		END,
+[Tag_frame]	"frame",	NOEND,
+[Tag_end]	0,		ERR,
+};
+Entity pl_entity[]={
+"AElig",	L'Æ',
+"Aacute",	L'Á',
+"Acirc",	L'Â',
+"Agrave",	L'À',
+"Alpha",	L'Α',
+"Aring",	L'Å',
+"Atilde",	L'Ã',
+"Auml",	L'Ä',
+"Beta",	L'Β',
+"Ccedil",	L'Ç',
+"Chi",	L'Χ',
+"Dagger",	L'‡',
+"Delta",	L'Δ',
+"ETH",	L'Ð',
+"Eacute",	L'É',
+"Ecirc",	L'Ê',
+"Egrave",	L'È',
+"Epsilon",	L'Ε',
+"Eta",	L'Η',
+"Euml",	L'Ë',
+"Gamma",	L'Γ',
+"Iacute",	L'Í',
+"Icirc",	L'Î',
+"Igrave",	L'Ì',
+"Iota",	L'Ι',
+"Iuml",	L'Ï',
+"Kappa",	L'Κ',
+"Lambda",	L'Λ',
+"Mu",	L'Μ',
+"Ntilde",	L'Ñ',
+"Nu",	L'Ν',
+"OElig",	L'Œ',
+"Oacute",	L'Ó',
+"Ocirc",	L'Ô',
+"Ograve",	L'Ò',
+"Omega",	L'Ω',
+"Omicron",	L'Ο',
+"Oslash",	L'Ø',
+"Otilde",	L'Õ',
+"Ouml",	L'Ö',
+"Phi",	L'Φ',
+"Pi",	L'Π',
+"Prime",	L'″',
+"Psi",	L'Ψ',
+"Rho",	L'Ρ',
+"Scaron",	L'Š',
+"Sigma",	L'Σ',
+"THORN",	L'Þ',
+"Tau",	L'Τ',
+"Theta",	L'Θ',
+"Uacute",	L'Ú',
+"Ucirc",	L'Û',
+"Ugrave",	L'Ù',
+"Upsilon",	L'Υ',
+"Uuml",	L'Ü',
+"Xi",	L'Ξ',
+"Yacute",	L'Ý',
+"Yuml",	L'Ÿ',
+"Zeta",	L'Ζ',
+"aacute",	L'á',
+"acirc",	L'â',
+"acute",	L'´',
+"aelig",	L'æ',
+"agrave",	L'à',
+"alefsym",	L'ℵ',
+"alpha",	L'α',
+"amp",	L'&',
+"and",	L'∧',
+"ang",	L'∠',
+"aring",	L'å',
+"asymp",	L'≈',
+"atilde",	L'ã',
+"auml",	L'ä',
+"bdquo",	L'„',
+"beta",	L'β',
+"brvbar",	L'¦',
+"bull",	L'•',
+"cap",	L'∩',
+"ccedil",	L'ç',
+"cdots",	L'⋯',
+"cedil",	L'¸',
+"cent",	L'¢',
+"chi",	L'χ',
+"circ",	L'ˆ',
+"clubs",	L'♣',
+"cong",	L'≅',
+"copy",	L'©',
+"crarr",	L'↵',
+"cup",	L'∪',
+"curren",	L'¤',
+"dArr",	L'⇓',
+"dagger",	L'†',
+"darr",	L'↓',
+"ddots",	L'⋱',
+"deg",	L'°',
+"delta",	L'δ',
+"diams",	L'♦',
+"divide",	L'÷',
+"eacute",	L'é',
+"ecirc",	L'ê',
+"egrave",	L'è',
+"emdash",	L'—',
+"empty",	L'∅',
+"emsp",	L' ',
+"endash",	L'–',
+"ensp",	L' ',
+"epsilon",	L'ε',
+"equiv",	L'≡',
+"eta",	L'η',
+"eth",	L'ð',
+"euml",	L'ë',
+"euro",	L'€',
+"exist",	L'∃',
+"fnof",	L'ƒ',
+"forall",	L'∀',
+"frac12",	L'½',
+"frac14",	L'¼',
+"frac34",	L'¾',
+"frasl",	L'⁄',
+"gamma",	L'γ',
+"ge",	L'≥',
+"gt",	L'>',
+"hArr",	L'⇔',
+"harr",	L'↔',
+"hearts",	L'♥',
+"hellip",	L'…',
+"iacute",	L'í',
+"icirc",	L'î',
+"iexcl",	L'¡',
+"igrave",	L'ì',
+"image",	L'ℑ',
+"infin",	L'∞',
+"int",	L'∫',
+"iota",	L'ι',
+"iquest",	L'¿',
+"isin",	L'∈',
+"iuml",	L'ï',
+"kappa",	L'κ',
+"lArr",	L'⇐',
+"lambda",	L'λ',
+"lang",	L'〈',
+"laquo",	L'«',
+"larr",	L'←',
+"lceil",	L'⌈',
+"ldots",	L'…',
+"ldquo",	L'“',
+"le",	L'≤',
+"lfloor",	L'⌊',
+"lowast",	L'∗',
+"loz",	L'◊',
+"lrm",	L'‎',
+"lsaquo",	L'‹',
+"lsquo",	L'‘',
+"lt",	L'<',
+"macr",	L'¯',
+"mdash",	L'—',
+"micro",	L'µ',
+"middot",	L'·',
+"minus",	L'−',
+"mu",	L'μ',
+"nabla",	L'∇',
+"nbsp",	L' ',
+"ndash",	L'–',
+"ne",	L'≠',
+"ni",	L'∋',
+"not",	L'¬',
+"notin",	L'∉',
+"nsub",	L'⊄',
+"ntilde",	L'ñ',
+"nu",	L'ν',
+"oacute",	L'ó',
+"ocirc",	L'ô',
+"oelig",	L'œ',
+"ograve",	L'ò',
+"oline",	L'‾',
+"omega",	L'ω',
+"omicron",	L'ο',
+"oplus",	L'⊕',
+"or",	L'∨',
+"ordf",	L'ª',
+"ordm",	L'º',
+"oslash",	L'ø',
+"otilde",	L'õ',
+"otimes",	L'⊗',
+"ouml",	L'ö',
+"para",	L'¶',
+"part",	L'∂',
+"permil",	L'‰',
+"perp",	L'⊥',
+"phi",	L'φ',
+"pi",	L'π',
+"piv",	L'ϖ',
+"plusmn",	L'±',
+"pound",	L'£',
+"prime",	L'′',
+"prod",	L'∏',
+"prop",	L'∝',
+"psi",	L'ψ',
+"quad",	L' ',
+"quot",	L'"',
+"rArr",	L'⇒',
+"radic",	L'√',
+"rang",	L'〉',
+"raquo",	L'»',
+"rarr",	L'→',
+"rceil",	L'⌉',
+"rdquo",	L'”',
+"real",	L'ℜ',
+"reg",	L'®',
+"rfloor",	L'⌋',
+"rho",	L'ρ',
+"rlm",	L'‏',
+"rsaquo",	L'›',
+"rsquo",	L'’',
+"sbquo",	L'‚',
+"scaron",	L'š',
+"sdot",	L'⋅',
+"sect",	L'§',
+"shy",	L'­',
+"sigma",	L'σ',
+"sigmaf",	L'ς',
+"sim",	L'∼',
+"sp",	L' ',
+"spades",	L'♠',
+"sub",	L'⊂',
+"sube",	L'⊆',
+"sum",	L'∑',
+"sup",	L'⊃',
+"sup1",	L'¹',
+"sup2",	L'²',
+"sup3",	L'³',
+"supe",	L'⊇',
+"szlig",	L'ß',
+"tau",	L'τ',
+"there4",	L'∴',
+"theta",	L'θ',
+"thetasym",	L'ϑ',
+"thinsp",	L' ',
+"thorn",	L'þ',
+"tilde",	L'˜',
+"times",	L'×',
+"trade",	L'™',
+"uArr",	L'⇑',
+"uacute",	L'ú',
+"uarr",	L'↑',
+"ucirc",	L'û',
+"ugrave",	L'ù',
+"uml",	L'¨',
+"upsih",	L'ϒ',
+"upsilon",	L'υ',
+"uuml",	L'ü',
+"varepsilon",	L'∈',
+"varphi",	L'ϕ',
+"varpi",	L'ϖ',
+"varrho",	L'ϱ',
+"vdots",	L'⋮',
+"vsigma",	L'ς',
+"vtheta",	L'ϑ',
+"weierp",	L'℘',
+"xi",	L'ξ',
+"yacute",	L'ý',
+"yen",	L'¥',
+"yuml",	L'ÿ',
+"zeta",	L'ζ',
+"zwj",	L'‍',
+"zwnj",	L'‌',
+};
+int pl_entities = nelem(pl_entity);
--- /dev/null
+++ b/sys/src/cmd/mothra/http.c
@@ -1,0 +1,486 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <panel.h>
+
+#include <libsec.h>		/* tlsClient */
+
+#include "mothra.h"
+typedef struct Cache Cache;
+struct Cache{
+	int fd;			/* file descriptor on which to write cached data */
+	ulong hash;		/* hash of url, used to compute cache file name */
+	int modtime;		/* time at which cache entry was created */
+	int type;		/* url->type of cached entry */
+};
+void httpheader(Url *, char *);
+int httpresponse(char *);
+static char *proxyserver;	/* name of proxy server */
+void exitnow(void*, char*){
+	noted(NDFLT);
+}
+void hashname(char *name, int n, char *stem, Cache *c){
+	snprint(name, n, "/sys/lib/mothra/cache/%s.%.8lux", stem, c->hash);
+}
+// #define	CacheEnabled
+/*
+ * Returns fd of cached file, if found (else -1)
+ * Fills in Cache data structure for caller
+ * If stale is set, caller has determined that the existing
+ * cache entry for this url is stale, so we shouldn't bother re-examining it.
+ */
+int cacheopen(Url *url, Cache *c, int stale){
+#ifdef CacheEnabled
+	int fd, n;
+	char name[NNAME+1], *s, *l;
+	/*
+	 * If we're using a proxy server or the url contains a ? or =,
+	 * don't even bother.
+	 */
+	if(proxyserver || strchr(url->reltext, '?')!=0 || strchr(url->reltext, '=')!=0){
+		c->fd=-1;
+		return -1;
+	}
+	c->hash=0;
+	for(s=url->fullname,n=0;*s;s++,n++) c->hash=c->hash*n+(*s&255);
+	if(stale)
+		fd=-1;
+	else{
+		hashname(name, sizeof(name), "cache", c);
+		fd=open(name, OREAD);
+	}
+	if(fd==-1){
+		hashname(name, sizeof(name), "write", c);
+		c->fd=create(name, OWRITE, 0444);
+		if(c->fd!=-1)
+			fprint(c->fd, "%s %10ld\n", url->fullname, time(0));
+		return -1;
+	}
+	c->fd=-1;
+	for(l=name;l!=&name[NNAME];l+=n){
+		n=&name[NNAME]-l;
+		n=read(fd, l, n);
+		if(n<=0) break;
+	}
+	*l='\0';
+	s=strchr(name, ' ');
+	if(s==0){
+		close(fd);
+		return -1;
+	}
+	*s='\0';
+	if(strcmp(url->fullname, name)!=0){
+		close(fd);
+		return -1;
+	}
+	c->modtime=atol(++s);
+	s=strchr(s, '\n');
+	if(s==0){
+		close(fd);
+		return -1;
+	}
+	s++;
+	if(strncmp(s, "type ", 5)!=0){
+		close(fd);
+		return -1;
+	}
+	c->type=atoi(s+5);
+	s=strchr(s+5, '\n');
+	if(s==0){
+		close(fd);
+		return -1;
+	}
+	
+	seek(fd, s-name+1, 0);
+	return fd;
+#else
+	c->fd=-1;
+	return -1;
+#endif
+}
+/*
+ * Close url->fd and either rename the cache file or
+ * remove it, depending on success
+ */
+void cacheclose(Cache *c, int success){
+	char wname[NNAME+1], cname[NNAME+1], *celem;
+	Dir *wdir;
+	if(c->fd==-1) return;
+	close(c->fd);
+	hashname(wname, sizeof(wname), "write", c);
+	if(!success){
+		remove(wname);
+		return;
+	}
+	if((wdir = dirstat(wname)) == 0)
+		return;
+	hashname(cname, sizeof(cname), "cache", c);
+	if(access(cname, 0) == 0){
+		if(remove(cname)==-1){
+			remove(wname);
+			free(wdir);
+			return;
+		}
+		/*
+		 * This looks implausible, but it's what the mv command does
+		 */
+		do; while(remove(cname)!=-1);
+	}
+	celem=strrchr(cname, '/');
+	if(celem==0) celem=cname;
+	else celem++;
+	strcpy(wdir->name, celem);
+	if(dirwstat(wname, wdir)==-1)
+		remove(wname);
+	free(wdir);
+}
+static char *wkday[]={
+	"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
+};
+static char *month[]={
+	"Jan", "Feb", "Mar", "Apr", "May", "Jun",
+	"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
+};
+/*
+ * Sun, 06 Nov 1994 08:49:38 GMT
+ * 123456789 123456789 123456789
+ */
+char *rfc1123date(long time){
+	static char buf[50];
+	Tm *t;
+	t=gmtime(time);
+	snprint(buf, sizeof(buf), "%s, %2.2d %s %4.4d %2.2d:%2.2d:%2.2d GMT",
+		wkday[t->wday], t->mday, month[t->mon], t->year+1900,
+		t->hour, t->min, t->sec);
+	return buf;
+}
+/*
+ * Given a url, return a file descriptor on which caller can
+ * read an http document.  As a side effect, we parse the
+ * http header and fill in some fields in the url.
+ * The caller is responsible for processing redirection loops.
+ * Method can be either GET or POST.  If method==post, body
+ * is the text to be posted.
+ */
+int http(Url *url, int method, char *body){
+	char *addr, *com;
+	int fd, n, nnl, len;
+	int ncom, m;
+	int pfd[2];
+	char buf[1024], *bp, *ebp;
+	char line[1024+1], *lp, *elp;
+	char authstr[NAUTH], *urlname;
+	int gotresponse;
+	int response;
+	Cache cache;
+	int cfd, cookiefd;
+	static int firsttime=1;
+	static int gotcookies;
+
+	if(firsttime){
+		proxyserver=getenv("httpproxy");
+		gotcookies=(access("/mnt/webcookies/http", AREAD|AWRITE)==0);
+		firsttime=0;
+	}
+	*authstr = 0;
+Authorize:
+	cfd=-1;
+	cookiefd=-1;
+	if(proxyserver && proxyserver[0]!='\0'){
+		addr=strdup(proxyserver);
+		urlname=url->fullname;
+	}
+	else{
+		addr=emalloc(strlen(url->ipaddr)+100);
+		sprint(addr, "tcp!%s!%d", url->ipaddr, url->port);
+		urlname=url->reltext;
+	}
+	fd=dial(addr, 0, 0, 0);
+	free(addr);
+	if(fd==-1) goto ErrReturn;
+	if(url->ssl){
+		int tfd;
+		TLSconn conn;
+
+		memset(&conn, 0, sizeof conn);
+		tfd = tlsClient(fd, &conn);
+		if(tfd < 0){
+			close(fd);
+			goto ErrReturn;
+		}
+		/* BUG: check cert here? */
+		if(conn.cert)
+			free(conn.cert);
+		close(fd);
+		fd = tfd;
+	}
+	ncom=strlen(urlname)+sizeof(buf);
+	com=emalloc(ncom+2);
+	cache.fd=-1;
+	switch(method){
+	case GET:
+		cfd=cacheopen(url, &cache, 0);
+		if(cfd==-1)
+			n=sprint(com,
+				"GET %s HTTP/1.0\r\n%s"
+				"Accept: */*\r\n"
+				"User-agent: mothra/%s\r\n"
+				"Host: %s\r\n",
+				urlname, authstr, version, url->ipaddr);
+		else
+			n=sprint(com,
+				"GET %s HTTP/1.0\r\n%s"
+				"If-Modified-since: %s\r\n"
+				"Accept: */*\r\n"
+				"User-agent: mothra/%s\r\n"
+				"Host: %s\r\n",
+				urlname, authstr, rfc1123date(cache.modtime), version, url->ipaddr);
+		break;
+	case POST:
+		len=strlen(body);
+		n=sprint(com,
+			"POST %s HTTP/1.0\r\n%s"
+			"Content-type: application/x-www-form-urlencoded\r\n"
+			"Content-length: %d\r\n"
+			"User-agent: mothra/%s\r\n",
+			urlname, authstr, len, version);
+		break;
+	}
+	if(gotcookies && (cookiefd=open("/mnt/webcookies/http", ORDWR)) >= 0){
+		if(fprint(cookiefd, "%s", url->fullname) > 0){
+			while((m=read(cookiefd, buf, sizeof buf)) > 0){
+				if(m+n>ncom){
+					if(write(fd, com, n)!= n){
+						free(com);
+						goto fdErrReturn;
+					}
+					n=0;
+					com[0] = '\0';
+				}
+				strncat(com, buf, m);
+				n += m;
+			}
+		}else{
+			close(cookiefd);
+			cookiefd=-1;
+		}
+	}
+	strcat(com, "\r\n");
+	n += 2;
+	switch(method){
+	case GET:
+		if(write(fd, com, n)!=n){
+			free(com);
+			goto fdErrReturn;
+		}
+		break;
+	case POST:
+		if(write(fd, com, n)!=n
+		|| write(fd, body, len)!=len){
+			free(com);
+			goto fdErrReturn;
+		}
+		break;
+	}
+	free(com);
+	if(pipe(pfd)==-1) goto fdErrReturn;
+	n=read(fd, buf, 1024);
+	if(n<=0){
+	EarlyEof:
+		if(n==0){
+			fprint(2, "%s: EOF in header\n", url->fullname);
+			werrstr("EOF in header");
+		}
+	pfdErrReturn:
+		close(pfd[0]);
+		close(pfd[1]);
+	fdErrReturn:
+		close(fd);
+	ErrReturn:
+		if(cookiefd>=0)
+			close(cookiefd);
+		cacheclose(&cache, 0);
+		return -1;
+	}
+	bp=buf;
+	ebp=buf+n;
+	url->type=0;
+	if(strncmp(buf, "HTTP/", 5)==0){	/* hack test for presence of header */
+		SET(response);
+		gotresponse=0;
+		url->redirname[0]='\0';
+		nnl=0;
+		lp=line;
+		elp=line+1024;
+		while(nnl!=2){
+			if(bp==ebp){
+				n=read(fd, buf, 1024);
+				if(n<=0) goto EarlyEof;
+				ebp=buf+n;
+				bp=buf;
+			}
+			if(*bp!='\r'){
+				if(nnl==1 && (!gotresponse || (*bp!=' ' && *bp!='\t'))){
+					*lp='\0';
+					if(gotresponse){
+						if(cookiefd>=0 && cistrncmp(line, "Set-Cookie:", 11) == 0)
+							fprint(cookiefd, "%s\n", line);
+						httpheader(url, line);
+					}else{
+						response=httpresponse(line);
+						gotresponse=1;
+					}
+					lp=line;
+				}
+				if(*bp=='\n') nnl++;
+				else{
+					nnl=0;
+					if(lp!=elp) *lp++=*bp;
+				}
+			}
+			bp++;
+		}
+		if(gotresponse) switch(response){
+		case 200:	/* OK */
+		case 201:	/* Created */
+		case 202:	/* Accepted */
+			break;
+		case 204:	/* No Content */
+			werrstr("URL has no content");
+			goto pfdErrReturn;
+		case 301:	/* Moved Permanently */
+		case 302:	/* Moved Temporarily */
+			if(url->redirname[0]){
+				url->type=FORWARD;
+				werrstr("URL forwarded");
+				goto pfdErrReturn;
+			}
+			break;
+		case 304:	/* Not Modified */
+			if(cfd!=-1){
+				url->type=cache.type;
+				close(pfd[0]);
+				close(pfd[1]);
+				close(fd);
+				if(cookiefd>=0)
+					close(cookiefd);
+				return cfd;
+			}
+			werrstr("Not modified!");
+			goto pfdErrReturn;
+		case 400:	/* Bad Request */
+			werrstr("Bad Request to server");
+			goto pfdErrReturn;
+		case 401:	/* Unauthorized */
+		case 402:	/* ??? */
+			if(*authstr == 0){
+				close(pfd[0]);
+				close(pfd[1]);
+				close(fd);
+				if(auth(url, authstr, sizeof(authstr)) == 0){
+					if(cfd!=-1)
+						close(cfd);
+					goto Authorize;
+				}
+				goto ErrReturn;
+			}
+			break;
+		case 403:	/* Forbidden */
+			werrstr("Forbidden by server");
+			goto pfdErrReturn;
+		case 404:	/* Not Found */
+			werrstr("Not found on server");
+			goto pfdErrReturn;
+		case 500:	/* Internal server error */
+			werrstr("Server choked");
+			goto pfdErrReturn;
+		case 501:	/* Not implemented */
+			werrstr("Server can't do it!");
+			goto pfdErrReturn;
+		case 502:	/* Bad gateway */
+			werrstr("Bad gateway");
+			goto pfdErrReturn;
+		case 503:	/* Service unavailable */
+			werrstr("Service unavailable");
+			goto pfdErrReturn;
+		}
+	}
+	if(cfd!=-1){
+		close(cfd);
+		cfd=cacheopen(url, &cache, 1);
+	}
+	if(cookiefd>=0){
+		close(cookiefd);
+		cookiefd=-1;
+	}
+	if(url->type==0)
+		url->type=suffix2type(url->fullname);
+	if(cache.fd!=-1) fprint(cache.fd, "type %d\n", url->type);
+	switch(rfork(RFFDG|RFPROC|RFNOWAIT)){
+	case -1:
+		werrstr("Can't fork");
+		goto pfdErrReturn;
+	case 0:
+		notify(exitnow); /* otherwise write on closed pipe below may cause havoc */
+		close(pfd[0]);
+		if(bp!=ebp){
+			write(pfd[1], bp, ebp-bp);
+			if(cache.fd!=-1) write(cache.fd, bp, ebp-bp);
+		}
+		while((n=read(fd, buf, 1024))>0){
+			write(pfd[1], buf, n);
+			if(cache.fd!=-1) write(cache.fd, buf, n);
+		}
+		cacheclose(&cache, 1);
+		_exits(0);
+	default:
+		if(cache.fd!=-1) close(cache.fd);
+		close(pfd[1]);
+		close(fd);
+		return pfd[0];
+	}
+}
+/*
+ * Process a header line for this url
+ */
+void httpheader(Url *url, char *line){
+	char *name, *arg, *s, *arg2;
+	name=line;
+	while(*name==' ' || *name=='\t') name++;
+	for(s=name;*s!=':';s++) if(*s=='\0') return;
+	*s++='\0';
+	while(*s==' ' || *s=='\t') s++;
+	arg=s;
+	while(*s!=' ' && *s!='\t' && *s!=';' && *s!='\0') s++;
+	while(*s == ' ' || *s == '\t' || *s == ';')
+		*s++ = '\0';
+	arg2 = s;
+	if(cistrcmp(name, "Content-Type")==0){
+		url->type|=content2type(arg, url->reltext);
+		if(cistrncmp(arg2, "charset=", 8) == 0){
+			strncpy(url->charset, arg2+8, sizeof(url->charset));
+		} else {
+			url->charset[0] = '\0';
+		}
+	}
+	else if(cistrcmp(name, "Content-Encoding")==0)
+		url->type|=encoding2type(arg);
+	else if(cistrcmp(name, "WWW-authenticate")==0){
+		strncpy(url->authtype, arg, sizeof(url->authtype));
+		strncpy(url->autharg, arg2, sizeof(url->autharg));
+	}
+	else if(cistrcmp(name, "URI")==0){
+		if(*arg!='<') return;
+		++arg;
+		for(s=arg;*s!='>';s++) if(*s=='\0') return;
+		*s='\0';
+		strncpy(url->redirname, arg, sizeof(url->redirname));
+	}
+	else if(cistrcmp(name, "Location")==0)
+		strncpy(url->redirname, arg, sizeof(url->redirname));
+}
+int httpresponse(char *line){
+	while(*line!=' ' && *line!='\t' && *line!='\0') line++;
+	return atoi(line);
+}
--- /dev/null
+++ b/sys/src/cmd/mothra/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/sys/src/cmd/mothra/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/sys/src/cmd/mothra/libpanel/draw.c
@@ -1,0 +1,286 @@
+#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=allocimage(display, Rect(0,0,1,1), screen->chan, 1, 0xAAAAAAFF);
+	pl_dark =allocimage(display, Rect(0,0,1,1), screen->chan, 1, 0x555555FF);
+	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);
+*/
+
+	pl_white=allocimage(display, Rect(0,0,1,1), screen->chan, 1, 0xFFFFFFFF);
+	pl_light=allocimagemix(display, DPalebluegreen, DWhite);
+	pl_dark =allocimage(display, Rect(0,0,1,1), screen->chan, 1, DPurpleblue);
+	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);
+		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:
+		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));
+	}
+}
+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;
+	save=b->clipr;
+	replclipr(b, b->repl, r);
+	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;
+	}
+	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) 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);
+	flushimage(display, 1);
+}
+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/sys/src/cmd/mothra/libpanel/edit.c
@@ -1,0 +1,239 @@
+/*
+ * 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"
+typedef struct Edit Edit;
+struct Edit{
+	Point minsize;
+	int sel0, sel1;
+	Textwin *t;
+	void (*hit)(Panel *);
+	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);
+}
+/*
+ * 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;
+	if(m->buttons&7){
+		ep=p->data;
+		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(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;
+	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;
+	t->b=p->b;
+	twhilite(t, ep->sel0, ep->sel1, 0);
+	switch(c){
+	case '\b':
+		if(ep->sel0!=0) --ep->sel0;
+		twreplace(t, ep->sel0, ep->sel1, 0, 0);
+		break;
+	case '\025':	/* ctrl-u */
+		while(ep->sel0!=0 && t->text[ep->sel0-1]!='\n') --ep->sel0;
+		twreplace(t, ep->sel0, ep->sel1, 0, 0);
+		break;
+	case '\027':	/* ctrl-w */
+		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:
+		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 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->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){
+	twscroll(((Edit *)p->data)->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;
+	return t->etext-t->text;
+}
+Rune *pleget(Panel *p){
+	return ((Edit *)p->data)->t->text;
+}
+void plesel(Panel *p, int sel0, int sel1){
+	Edit *ep;
+	ep=p->data;
+	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;
+	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/sys/src/cmd/mothra/libpanel/entry.c
@@ -1,0 +1,130 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <panel.h>
+#include "pldefs.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 */
+void pl_drawentry(Panel *p){
+	Rectangle r;
+	Entry *ep;
+	ep=p->data;
+	r=pl_box(p->b, p->r, p->state);
+	if(stringwidth(font, ep->entry)<=r.max.x-r.min.x)
+		pl_drawicon(p->b, r, PLACEW, 0, ep->entry);
+	else
+		pl_drawicon(p->b, r, PLACEE, 0, ep->entry);
+}
+int pl_hitentry(Panel *p, Mouse *m){
+	int oldstate;
+	oldstate=p->state;
+	if(m->buttons&OUT)
+		p->state=UP;
+	else if(m->buttons&7)
+		p->state=DOWN;
+	else{	/* mouse inside, but no buttons down */
+		if(p->state==DOWN) plgrabkb(p);
+		p->state=UP;
+	}
+	if(p->state!=oldstate) 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 025:	/* ctrl-u */
+		ep->entp=ep->entry;
+		*ep->entp='\0';
+		break;
+	case '\b':
+		while(ep->entp!=ep->entry && !pl_rune1st(ep->entp[-1])) *--ep->entp='\0';
+		if(ep->entp!=ep->entry) *--ep->entp='\0';
+		break;
+	case 027:	/* ctrl-w */
+		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:
+		ep->entp+=runetochar(ep->entp, &c);
+		if(ep->entp>ep->eent){
+			n=ep->entp-ep->entry;
+			ep->entry=realloc(ep->entry, n+100+SLACK);
+			if(ep->entry==0){
+				fprint(2, "can't realloc in pl_typeentry\n");
+				exits("no mem");
+			}
+			ep->entp=ep->entry+n;
+			ep->eent=ep->entp+100;
+		}
+		break;
+	}
+	memset(ep->entp, 0, SLACK);
+
+	/* strcpy(ep->entp, "◀"); */
+	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){
+	free(((Entry *)p->data)->entry);
+}
+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;
+	elen=100;
+	if(str) elen+=strlen(str);
+	ep->entry=pl_emalloc(elen+SLACK);
+	ep->eent=ep->entry+elen;
+	if(str)
+		strcpy(ep->entry, str);
+	else ep->entry[0]='\0';
+	ep->entp=ep->entry+strlen(ep->entry);
+	/* strcat(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/sys/src/cmd/mothra/libpanel/event.c
@@ -1,0 +1,50 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <panel.h>
+#include "pldefs.h"
+Panel *pl_kbfocus;
+void plgrabkb(Panel *g){
+	pl_kbfocus=g;
+}
+void plkeyboard(Rune c){
+	if(pl_kbfocus){
+		pl_kbfocus->type(pl_kbfocus, c);
+		flushimage(display, 1);
+	}
+}
+/*
+ * 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 mouse){
+	Panel *hit, *last;
+	if(g->flags&REMOUSE)
+		hit=g->lastmouse;
+	else{
+		hit=pl_ptinpanel(mouse.xy, g);
+		last=g->lastmouse;
+		if(last && last!=hit){
+			mouse.buttons|=OUT;
+			last->hit(last, &mouse);
+			mouse.buttons&=~OUT;
+		}
+	}
+	if(hit){
+		if(hit->hit(hit, &mouse))
+			g->flags|=REMOUSE;
+		else
+			g->flags&=~REMOUSE;
+		g->lastmouse=hit;
+	}
+	flushimage(display, 1);
+}
--- /dev/null
+++ b/sys/src/cmd/mothra/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/sys/src/cmd/mothra/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/sys/src/cmd/mothra/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/sys/src/cmd/mothra/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/sys/src/cmd/mothra/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<=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/sys/src/cmd/mothra/libpanel/mem.c
@@ -1,0 +1,108 @@
+#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=malloc(n);
+	if(v==0){
+		fprint(2, "Can't malloc!\n");
+		exits("no mem");
+	}
+	return v;
+}
+void pl_unexpected(Panel *g, char *rou){
+	fprint(2, "%s called unexpectedly (%s %lux)\n", rou, g->kind, (ulong)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 %lux\n", parent->kind, (ulong)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;
+	if(ndata)
+		v->data=pl_emalloc(ndata);
+	else
+		v->data=0;
+	return v;
+}
+void plfree(Panel *p){
+	Panel *cp, *ncp;
+	if(p==0)
+		return;
+	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/sys/src/cmd/mothra/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/sys/src/cmd/mothra/libpanel/mkfile
@@ -1,0 +1,33 @@
+</$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
+
+HFILES=panel.h pldefs.h rtext.h
+
+</sys/src/cmd/mklib
--- /dev/null
+++ b/sys/src/cmd/mothra/libpanel/pack.c
@@ -1,0 +1,167 @@
+#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.
+ * Returns 1 if everything fit, 0 otherwise.
+ * For now we punt in the case that the children don't all fit.
+ * Possibly we should shrink all the children's sizereqs to fit,
+ * by the same means used to do EXPAND, except clamping at some minimum size,
+ * but that smacks of AI.
+ */
+Panel *pl_toosmall;
+int 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.y>avail.y){
+		pl_toosmall=p;
+		return 0;	/* not enough space! */
+	}
+	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&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);
+			if(!pl_setrect(c, ul, Pt(space.x, c->sizereq.y))) return 0;
+			break;
+		case PACKW:
+			newul=Pt(ul.x+c->sizereq.x, ul.y);
+			newspace=Pt(space.x-c->sizereq.x, space.y);
+			if(!pl_setrect(c, ul, Pt(c->sizereq.x, space.y))) return 0;
+			break;
+		case PACKS:
+			newul=ul;
+			newspace=Pt(space.x, space.y-c->sizereq.y);
+			if(!pl_setrect(c, Pt(ul.x, ul.y+space.y-c->sizereq.y),
+				Pt(space.x, c->sizereq.y))) return 0;
+			break;
+		case PACKE:
+			newul=ul;
+			newspace=Pt(space.x-c->sizereq.x, space.y);
+			if(!pl_setrect(c, Pt(ul.x+space.x-c->sizereq.x, ul.y),
+				Pt(c->sizereq.x, space.y))) return 0;
+			break;
+		}
+		ul=newul;
+		space=newspace;
+	}
+	return 1;
+}
+int plpack(Panel *p, Rectangle where){
+	pl_sizereq(p);
+	return 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/sys/src/cmd/mothra/libpanel/panel.h
@@ -1,0 +1,174 @@
+//#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 hot;		/* responds to hits? */
+	void *user;		/* user data */
+	int space;		/* how much space before, if no break */
+	int indent;		/* how much space before, after a break */
+	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 */
+};
+/*
+ * Panel flags -- there are more private flags in panelprivate.h
+ * that need to be kept synchronized with these!
+ */
+#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 */
+/*
+ * 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 */
+int plinit(int);			/* initialization */
+int 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, Font *, char *, int, void *);
+Rtext *plrtbitmap(Rtext **, int, int, Image *, int, void *);
+Rtext *plrtpanel(Rtext **, int, int, Panel *, void *);
+void plrtfree(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*);
binary files /dev/null b/sys/src/cmd/mothra/libpanel/panel.pdf differ
--- /dev/null
+++ b/sys/src/cmd/mothra/libpanel/pldefs.h
@@ -1,0 +1,103 @@
+/*
+ * Definitions for internal use only
+ */
+/*
+ * Variable-font text routines
+ * These could make a separate library.
+ */
+int pl_rtfmt(Rtext *, int);
+void pl_rtdraw(Image *, Rectangle, Rtext *, int);
+void pl_rtredraw(Image *, Rectangle, Rtext *, int, int);
+Rtext *pl_rthit(Rtext *, int, 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_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/sys/src/cmd/mothra/libpanel/popup.c
@@ -1,0 +1,113 @@
+/*
+ * 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);
+					flushimage(display, 1);
+					freeimage(pp->save);
+				}
+				pl_invis(p, 1);
+			}
+			g->state=UP;
+		}
+	}
+	plmouse(p, *m);
+	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/sys/src/cmd/mothra/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/sys/src/cmd/mothra/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/sys/src/cmd/mothra/libpanel/rtext.c
@@ -1,0 +1,230 @@
+/*
+ * 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 */
+Rtext *pl_rtnew(Rtext **t, int space, int indent, Image *b, Panel *p, Font *f, char *s, int hot, void *user){
+	Rtext *new;
+	new=malloc(sizeof(Rtext));
+	if(new==0) return 0;
+	new->hot=hot;
+	new->user=user;
+	new->space=space;
+	new->indent=indent;
+	new->b=b;
+	new->p=p;
+	new->font=f;
+	new->text=s;
+	new->next=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, Panel *p, void *user){
+	return pl_rtnew(t, space, indent, 0, p, 0, 0, 1, user);
+}
+Rtext *plrtstr(Rtext **t, int space, int indent, Font *f, char *s, int hot, void *user){
+	return pl_rtnew(t, space, indent, 0, 0, f, s, hot, user);
+}
+Rtext *plrtbitmap(Rtext **t, int space, int indent, Image *b, int hot, void *user){
+	return pl_rtnew(t, space, indent, b, 0, 0, 0, hot, 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 length of the text
+ */
+int pl_rtfmt(Rtext *t, int wid){
+	Rtext *tp, *eline;
+	int ascent, descent, x, space, a, d, w, topy, indent;
+	Point p;
+	p=Pt(0,0);
+	eline=t;
+	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+2;
+				d=0;
+				w=tp->b->r.max.x-tp->b->r.min.x+4;
+			}
+			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);
+			}
+			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(a>ascent) ascent=a;
+			if(d>descent) descent=d;
+			if(tp==t)
+				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;
+			if(t->b){
+				t->r.max.y=p.y;
+				t->r.min.y=p.y-(t->b->r.max.y-t->b->r.min.y);
+				p.x+=t->b->r.max.x-t->b->r.min.x+2;
+				t->r=rectaddpt(t->r, Pt(2, 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;
+			}
+			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);
+		}
+		p.y+=descent+LEAD;
+	}
+	return p.y;
+}
+void pl_rtdraw(Image *b, Rectangle r, Rtext *t, int yoffs){
+	Point offs;
+	Rectangle dr;
+	Rectangle cr;
+	cr=b->clipr;
+	replclipr(b, b->repl, r);
+	pl_clr(b, r);
+	offs=subpt(r.min, Pt(0, yoffs));
+	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){
+			if(t->b){
+//				bitblt(b, dr.min, t->b, t->b->r, S|D);
+				draw(b, Rpt(dr.min, addpt(dr.min, subpt(t->b->r.max, t->b->r.min))), t->b, 0, t->b->r.min);
+				if(t->hot) border(b, insetrect(dr, -2), 1, display->black, ZP);
+			}
+			else if(t->p){
+				plmove(t->p, subpt(dr.min, t->p->r.min));
+				pldraw(t->p, b);
+			}
+			else{
+				string(b, dr.min, display->black, ZP, t->font, t->text);
+				if(t->hot)
+					line(b, Pt(dr.min.x, dr.max.y-1),
+						Pt(dr.max.x, dr.max.y-1),
+						Endsquare, Endsquare, 0,
+						display->black, ZP);
+			}
+		}
+	}
+	replclipr(b, b->repl, cr);
+}
+/*
+ * 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, int yoffs, int oldoffs){
+	int dy, size;
+	dy=oldoffs-yoffs;
+	size=r.max.y-r.min.y;
+	if(dy>=size || -dy>=size)
+		pl_rtdraw(b, r, t, yoffs);
+	else if(dy<0){
+		pl_reposition(t, b, r.min,
+			Rect(r.min.x, r.min.y-dy, r.max.x, r.max.y));
+		pl_rtdraw(b, Rect(r.min.x, r.max.y+dy, r.max.x, r.max.y),
+			t, yoffs+size+dy);
+	}
+	else if(dy>0){
+		pl_reposition(t, b, Pt(r.min.x, r.min.y+dy),
+			Rect(r.min.x, r.min.y, r.max.x, r.max.y-dy));
+		pl_rtdraw(b, Rect(r.min.x, r.min.y, r.max.x, r.min.y+dy), t, yoffs);
+	}
+}
+Rtext *pl_rthit(Rtext *t, int yoffs, Point p, Point ul){
+	if(t==0) return 0;
+	p.x-=ul.x;
+	p.y+=yoffs-ul.y;
+	while(t->nextline && t->nextline->topy<=p.y) t=t->nextline;
+	for(;t!=0;t=t->next){
+		if(t->topy>p.y) return 0;
+		if(ptinrect(p, t->r)) return t;
+	}
+	return 0;
+}
--- /dev/null
+++ b/sys/src/cmd/mothra/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/sys/src/cmd/mothra/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/sys/src/cmd/mothra/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/sys/src/cmd/mothra/libpanel/scrollbar.c
@@ -1,0 +1,148 @@
+#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	15	/* 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(m->buttons&OUT && m->buttons&7){
+		if(m->xy.y<g->r.min.y) m->xy.y=g->r.min.y;
+		if(m->xy.y>=g->r.max.y) m->xy.y=g->r.max.y-1;
+		if(ptinrect(m->xy, g->r))
+			m->buttons&=~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;
+		if(g->r.min.x<=m->xy.x && m->xy.x<g->r.max.x){
+			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;
+	}
+	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/sys/src/cmd/mothra/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/sys/src/cmd/mothra/libpanel/textview.c
@@ -1,0 +1,246 @@
+/*
+ * 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 */
+	int yoffs;			/* offset of top of screen */
+	Rtext *hitword;			/* text to hilite */
+	Image *hitsave;		/* for restoring hilit text */
+	int twid;			/* text width */
+	int thgt;			/* text height */
+	Point minsize;			/* smallest acceptible window size */
+	int buttons;
+};
+void pl_hiliteword(Panel *p, Rtext *w, int on){
+	Point ul, size;
+	Rectangle r;
+	Textview *tp;
+	if(w==0 || (w->b==0 && w->p!=0)) return;
+	tp=p->data;
+	ul=p->r.min;
+	size=subpt(p->r.max, p->r.min);
+	pl_interior(UP, &ul, &size);
+	ul.y-=tp->yoffs;
+	r=rectaddpt(w->r, ul);
+	if(rectclip(&r, p->r)){
+		if(on){
+			if(tp->hitsave) freeimage(tp->hitsave);
+			tp->hitsave = allocimage(display, r, screen->chan, 0, DNofill);
+			if(tp->hitsave) draw(tp->hitsave, r, p->b, 0, r.min);
+			pl_highlight(p->b, r);
+		}else{
+			if(tp->hitsave){
+				draw(p->b, r, tp->hitsave, 0, r.min);
+				freeimage(tp->hitsave);
+				tp->hitsave = 0;
+			}
+		}
+	}
+}
+void pl_stuffbitmap(Panel *p, Image *b){
+	p->b=b;
+	for(p=p->child;p;p=p->next)
+		pl_stuffbitmap(p, b);
+}
+/*
+ * 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_drawnon(Rtext *rp, Image *b){
+	for(;rp!=0;rp=rp->next)
+		if(rp->b==0 && rp->p!=0)
+			pl_stuffbitmap(rp->p, b);
+}
+/*
+ * Mark the hilite and update the scroll bar
+ */
+void pl_fixtextview(Panel *p, Textview *tp, Rectangle r){
+	Panel *sb;
+	int lo, hi;
+	pl_hiliteword(p, tp->hitword, 1);
+	lo=tp->yoffs;
+	hi=lo+r.max.y-r.min.y;	/* wrong? */
+	sb=p->yscroller;
+	if(sb && sb->setscrollbar) sb->setscrollbar(sb, lo, hi, tp->thgt);
+}
+void pl_drawtextview(Panel *p){
+	int twid;
+	Rectangle r;
+	Textview *tp;
+	Image *b;
+	tp=p->data;
+	b=allocimage(display, p->r, screen->chan, 0, DNofill);
+	if(b==0) b=p->b;
+	r=pl_outline(b, p->r, p->state);
+	twid=r.max.x-r.min.x;
+	if(twid!=tp->twid){
+		tp->twid=twid;
+		tp->thgt=pl_rtfmt(tp->text, tp->twid);
+		p->scr.size.y=tp->thgt;
+	}
+	p->scr.pos.y=tp->yoffs;
+	pl_rtdraw(b, r, tp->text, tp->yoffs);
+	if(b!=p->b){
+		draw(p->b, p->r, b, 0, b->r.min);
+		freeimage(b);
+		pl_drawnon(tp->text, p->b);
+	}
+	pl_fixtextview(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;
+	int hitme;
+	Point ul, size;
+	Textview *tp;
+	tp=p->data;
+	oldhitword=tp->hitword;
+	hitme=0;
+	pl_passon(oldhitword, m);
+	if(m->buttons&OUT)
+		p->state=UP;
+	else if(m->buttons&7){
+		tp->buttons=m->buttons;
+		p->state=DOWN;
+		if(oldhitword==0
+		|| oldhitword->b!=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->yoffs, m->xy, ul);
+			if(tp->hitword!=0 && tp->hitword->hot==0) tp->hitword=0;
+		}
+	}
+	else{
+		if(p->state==DOWN) hitme=1;
+		p->state=UP;
+	}
+	if(tp->hitword!=oldhitword){
+		pl_hiliteword(p, oldhitword, 0);
+		pl_hiliteword(p, tp->hitword, 1);
+		pl_passon(tp->hitword, m);
+	}
+	if(hitme && tp->hit && tp->hitword){
+		pl_hiliteword(p, tp->hitword, 0);
+		if(tp->hitword->b!=0 || tp->hitword->p==0)
+			tp->hit(p, tp->buttons, tp->hitword);
+		tp->hitword=0;
+	}
+	return 0;
+}
+void pl_scrolltextview(Panel *p, int dir, int buttons, int num, int den){
+	int yoffs;
+	Point ul, size;
+	Textview *tp;
+	Rectangle r;
+	if(dir!=VERT) return;
+	tp=p->data;
+	ul=p->r.min;
+	size=subpt(p->r.max, p->r.min);
+	pl_interior(p->state, &ul, &size);
+	switch(buttons){
+	default:
+		SET(yoffs);
+		break;
+	case 1:		/* left -- top moves to pointer */
+		yoffs=tp->yoffs-num*size.y/den;
+		if(yoffs<0) yoffs=0;
+		break;
+	case 2:		/* middle -- absolute index of file */
+		yoffs=tp->thgt*num/den;
+		break;
+	case 4:		/* right -- line pointed at moves to top */
+		yoffs=tp->yoffs+num*size.y/den;
+		if(yoffs>tp->thgt) yoffs=tp->thgt;
+		break;
+	}
+	if(yoffs!=tp->yoffs){
+		pl_hiliteword(p, tp->hitword, 0);
+		r=pl_outline(p->b, p->r, p->state);
+		pl_rtredraw(p->b, r, tp->text, yoffs, tp->yoffs);
+		tp->yoffs=yoffs;
+		pl_fixtextview(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->yoffs, 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;
+}
+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->yoffs=0;
+	tp->hitword=0;
+	v->scroll=pl_scrolltextview;
+	tp->twid=-1;
+	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)->yoffs;
+}
+void plsetpostextview(Panel *p, int yoffs){
+	((Textview *)p->data)->yoffs=yoffs;
+	pldraw(p, p->b);
+}
--- /dev/null
+++ b/sys/src/cmd/mothra/libpanel/textwin.c
@@ -1,0 +1,488 @@
+/*
+ * 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"
+/*
+ * 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+100;
+		t->loc=realloc(t->loc, nloc*sizeof(Point));
+		if(t->loc==0){
+			fprint(2, "No mem in tw_storeloc\n");
+			exits("no mem");
+		}
+		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(;;){
+		*m=emouse();
+		if(m->buttons==0) 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=malloc(nbyte);
+	if(srcloc==0) return;
+	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;
+	Rune *ntext;
+	olen=t->etext-t->text;
+	nlen=olen+r0-r1+nins;
+	tlen=t->eslack-t->text;
+	if(nlen>tlen){
+		tlen=nlen+100;
+		ntext=malloc(tlen*sizeof(Rune));
+		memmove(ntext, t->text, r0*sizeof(Rune));
+		memmove(ntext+r0+nins, t->text+r1, (olen-r1)*sizeof(Rune));
+		t->text=ntext;
+		t->eslack=ntext+tlen;
+	}
+	else 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=malloc(sizeof(Textwin));
+	if(t==0) return 0;
+	t->text=malloc((ntext+100)*sizeof(Rune));
+	if(t->text==0){
+		free(t);
+		return 0;
+	}
+	t->loc=malloc(100*sizeof(Point));
+	if(t->loc==0){
+		free(t->text);
+		free(t);
+		return 0;
+	}
+	t->eloc=t->loc+100;
+	t->etext=t->text+ntext;
+	t->eslack=t->etext+100;
+	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/sys/src/cmd/mothra/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/sys/src/cmd/mothra/mkfile
@@ -1,0 +1,35 @@
+</$objtype/mkfile
+
+TARG=mothra
+LIB=libpanel/libpanel.$O.a 
+CFILES= \
+	cistr.c \
+	crackurl.c \
+	file.c \
+	filetype.c \
+	forms.c \
+	ftp.c \
+	getpix.c \
+	gopher.c \
+	html.syntax.c \
+	http.c \
+	mothra.c \
+	rdhtml.c \
+	auth.c \
+
+OFILES=${CFILES:%.c=%.$O} version.$O
+HFILES=mothra.h html.h tcs.h libpanel/panel.h libpanel/rtext.h
+BIN=/$objtype/bin
+</sys/src/cmd/mkone
+
+CFLAGS=-Dplan9 -Ilibpanel
+version.c:	$CFILES
+	date|sed 's/^... //;s/ +/-/g;s/.*/char version[]="&";/' >version.c
+
+$LIB:V:
+	cd libpanel
+	mk
+
+clean nuke:V:
+	@{ cd libpanel; mk $target }
+	rm -f *.[$OS] [$OS].out $TARG
--- /dev/null
+++ b/sys/src/cmd/mothra/mothra.c
@@ -1,0 +1,1072 @@
+/*
+ * Trivial web browser
+ */
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <keyboard.h>
+#include <plumb.h>
+#include <cursor.h>
+#include <panel.h>
+#include "mothra.h"
+#include "rtext.h"
+int verbose=0;		/* -v flag causes html errors to appear in error log */
+int defdisplay=1;	/* is the default (initial) display visible? */
+Panel *root;	/* the whole display */
+Panel *alt;	/* the alternate display */
+Panel *alttext;	/* the alternate text window */
+Panel *cmd;	/* command entry */
+Panel *curttl;	/* label giving the title of the visible text */
+Panel *cururl;	/* label giving the url of the visible text */
+Panel *list;	/* list of previously acquired www pages */
+Panel *msg;	/* message display */
+Panel *menu3;	/* button 3 menu */
+Mouse mouse;	/* current mouse data */
+char helpfile[] = "file:/sys/lib/mothra/help.html";
+char mothra[] = "mothra!";
+Url defurl={
+	"http://plan9.bell-labs.com/",
+	0,
+	"plan9.bell-labs.com",
+	"/",
+	"",
+	"", "", "",
+	80,
+	HTTP,
+	HTML
+};
+Url badurl={
+	"No file loaded",
+	0,
+	"",
+	"/dev/null",
+	"", "", "",
+	"",
+	0,
+	FILE,
+	HTML
+};
+Cursor patientcurs={
+	0, 0,
+	0x01, 0x80, 0x03, 0xC0, 0x07, 0xE0, 0x07, 0xe0,
+	0x07, 0xe0, 0x07, 0xe0, 0x03, 0xc0, 0x0F, 0xF0,
+	0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8,
+	0x0F, 0xF0, 0x1F, 0xF8, 0x3F, 0xFC, 0x3F, 0xFC,
+
+	0x01, 0x80, 0x03, 0xC0, 0x07, 0xE0, 0x04, 0x20,
+	0x04, 0x20, 0x06, 0x60, 0x02, 0x40, 0x0C, 0x30,
+	0x10, 0x08, 0x14, 0x08, 0x14, 0x28, 0x12, 0x28,
+	0x0A, 0x50, 0x16, 0x68, 0x20, 0x04, 0x3F, 0xFC,
+};
+Cursor confirmcurs={
+	0, 0,
+	0x0F, 0xBF, 0x0F, 0xBF, 0xFF, 0xFF, 0xFF, 0xFF,
+	0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFE, 0xFF, 0xFE,
+	0xFF, 0xFE, 0xFF, 0xFF, 0x00, 0x03, 0xFF, 0xFF,
+	0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFC,
+
+	0x00, 0x0E, 0x07, 0x1F, 0x03, 0x17, 0x73, 0x6F,
+	0xFB, 0xCE, 0xDB, 0x8C, 0xDB, 0xC0, 0xFB, 0x6C,
+	0x77, 0xFC, 0x00, 0x00, 0x00, 0x01, 0x00, 0x03,
+	0x94, 0xA6, 0x63, 0x3C, 0x63, 0x18, 0x94, 0x90
+};
+Cursor readingcurs={
+	-10, -3,
+	0x00, 0x00, 0x00, 0x00, 0x0F, 0xF0, 0x0F, 0xF0,
+	0x0F, 0xF0, 0x0F, 0xF0, 0x0F, 0xF0, 0x1F, 0xF0,
+	0x3F, 0xF0, 0x7F, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF,
+	0xFB, 0xFF, 0xF3, 0xFF, 0x00, 0x00, 0x00, 0x00,
+
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xE0,
+	0x07, 0xE0, 0x01, 0xE0, 0x03, 0xE0, 0x07, 0x60,
+	0x0E, 0x60, 0x1C, 0x00, 0x38, 0x00, 0x71, 0xB6,
+	0x61, 0xB6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+};
+Www *current=0;
+Url *selection=0;
+int logfile;
+void docmd(Panel *, char *);
+void doprev(Panel *, int, int);
+void selurl(char *);
+void setcurrent(int, char *);
+char *genwww(Panel *, int);
+void updtext(Www *);
+void dolink(Panel *, int, Rtext *);
+void hit3(int, int);
+char *buttons[]={
+	"alt display",
+	"snarf url",
+	"paste",
+	"save hit",
+	"hit list",
+	"exit",
+	0
+};
+
+int wwwtop=0;
+Www *www(int index){
+	static Www a[1+NWWW];
+	return &a[1+(index % NWWW)];
+}
+int nwww(void){
+	return wwwtop<NWWW ? wwwtop : NWWW;
+}
+
+void err(Display *, char *msg){
+	fprint(2, "err: %s (%r)\n", msg);
+	abort();
+}
+int subpanel(Panel *obj, Panel *subj){
+	if(obj==0) return 0;
+	if(obj==subj) return 1;
+	for(obj=obj->child;obj;obj=obj->next)
+		if(subpanel(obj, subj)) return 1;
+	return 0;
+}
+/*
+ * Make sure that the keyboard focus is on-screen, by adjusting it to
+ * be the cmd entry if necessary.
+ */
+void adjkb(void){
+	Rtext *t;
+	int yoffs;
+	extern Panel *pl_kbfocus;	/* this is a secret panel library name */
+	yoffs=text->r.min.y-plgetpostextview(text);
+	for(t=current->text;t;t=t->next) if(!eqrect(t->r, Rect(0,0,0,0))){
+		if(t->r.max.y+yoffs>text->r.max.y) break;
+		if(t->r.min.y+yoffs>=text->r.min.y
+		&& t->b==0
+		&& subpanel(t->p, pl_kbfocus)) return;
+	}
+	plgrabkb(cmd);
+}
+
+void scrolltext(int dy)
+{
+	Scroll s;
+
+	s = plgetscroll(text);
+	s.pos.y += dy;
+	if(s.pos.y < 0)
+		s.pos.y = 0;
+	if(s.pos.y > s.size.y)
+		s.pos.y = s.size.y;
+	plsetscroll(text, s);
+	pldraw(root, screen);
+}
+
+void mkpanels(void){
+	Panel *p, *bar;
+	menu3=plmenu(0, 0, buttons, PACKN|FILLX, hit3);
+	root=plpopup(root, EXPAND, 0, 0, menu3);
+		p=plgroup(root, PACKN|FILLX);
+			msg=pllabel(p, PACKN|FILLX, mothra);
+			plplacelabel(msg, PLACEW);
+			pllabel(p, PACKW, "Go:");
+			cmd=plentry(p, PACKN|FILLX, 0, "", docmd);
+		p=plgroup(root, PACKN|FILLX);
+			bar=plscrollbar(p, PACKW);
+			list=pllist(p, PACKN|FILLX, genwww, 8, doprev);
+			plscroll(list, 0, bar);
+		p=plgroup(root, PACKN|FILLX);
+			pllabel(p, PACKW, "Title:");
+			curttl=pllabel(p, PACKE|EXPAND, "Initializing");
+			plplacelabel(curttl, PLACEW);
+		p=plgroup(root, PACKN|FILLX);
+			pllabel(p, PACKW, "Url:");
+			cururl=pllabel(p, PACKE|EXPAND, "---");
+			plplacelabel(cururl, PLACEW);
+		p=plgroup(root, PACKN|EXPAND);
+			bar=plscrollbar(p, PACKW);
+			text=pltextview(p, PACKE|EXPAND, Pt(0, 0), 0, dolink);
+			plscroll(text, 0, bar);
+	plgrabkb(cmd);
+	alt=plpopup(0, PACKE|EXPAND, 0, 0, menu3);
+		bar=plscrollbar(alt, PACKW);
+		alttext=pltextview(alt, PACKE|EXPAND, Pt(0, 0), 0, dolink);
+		plscroll(alttext, 0, bar);
+}
+void killcohort(void){
+	int i;
+	for(i=0;i!=3;i++){	/* It's a long way to the kitchen */
+		postnote(PNGROUP, getpid(), "kill\n");
+		sleep(1);
+	}
+}
+void dienow(void*, char*){
+	noted(NDFLT);
+}
+int mkmfile(char *stem, int mode){
+	char *henv;
+	char filename[NNAME];
+	int f;
+	if(home[0]=='\0'){
+		henv=getenv("home");
+		if(henv){
+			sprint(home, "%s/lib", henv);
+			f=create(home, OREAD, DMDIR|0777);
+			if(f!=-1) close(f);
+			sprint(home, "%s/lib/mothra", henv);
+			f=create(home, OREAD, DMDIR|0777);
+			if(f!=-1) close(f);
+		}
+		else
+			strcpy(home, "/tmp");
+	}
+	snprint(filename, sizeof(filename), "%s/%s", home, stem);
+	f=create(filename, OWRITE, mode);
+	if(f==-1)
+		f=create(stem, OWRITE, mode);
+	return f;
+}
+void main(int argc, char *argv[]){
+	Event e;
+	enum { Eplumb = 128 };
+	Plumbmsg *pm;
+	Www *new;
+	char *url;
+	int errfile;
+	int i;
+	ARGBEGIN{
+	case 'd': debug++; break;
+	case 'v': verbose=1; break;
+	default:  goto Usage;
+	}ARGEND
+
+	/*
+	 * so that we can stop all subprocesses with a note,
+	 * and to isolate rendezvous from other processes
+	 */
+	rfork(RFNOTEG|RFNAMEG|RFREND);
+	atexit(killcohort);
+	switch(argc){
+	default:
+	Usage:
+		fprint(2, "Usage: %s [-d] [url]\n", argv[0]);
+		exits("usage");
+	case 0:
+		url=getenv("url");
+		if(url==0 || url[0]=='\0')
+			url="file:/sys/lib/mothra/start.html";
+		break;
+	case 1: url=argv[0]; break;
+	}
+	errfile=mkmfile("mothra.err", 0666);
+	if(errfile!=-1){
+		dup(errfile, 2);
+		close(errfile);
+	}
+	logfile=mkmfile("mothra.log", 0666|DMAPPEND);
+	
+	initdraw(err,0,"mothra");
+	display->locking = 1;
+	chrwidth=stringwidth(font, "0");
+	pltabsize(chrwidth, 8*chrwidth);
+	einit(Emouse|Ekeyboard);
+	eplumb(Eplumb, "web");
+	etimer(0, 1000);
+	plinit(screen->depth);
+	if(debug) notify(dienow);
+	getfonts();
+	hrule=allocimage(display, Rect(0, 0, 2048, 5), screen->chan, 0, DWhite);
+	if(hrule==0){
+		fprint(2, "%s: can't allocimage!\n", argv[0]);
+		exits("no mem");
+	}
+	draw(hrule, Rect(0,1,1280,3), display->black, 0, ZP);
+	linespace=allocimage(display, Rect(0, 0, 2048, 5), screen->chan, 0, DWhite);
+	if(linespace==0){
+		fprint(2, "%s: can't allocimage!\n", argv[0]);
+		exits("no mem");
+	}
+	bullet=allocimage(display, Rect(0,0,25, 8), screen->chan, 0, DWhite);
+	fillellipse(bullet, Pt(4,4), 3, 3, display->black, ZP);
+	new = www(-1);
+	new->url=&badurl;
+	new->base=&badurl;
+	strcpy(new->title, "See error message above");
+	plrtstr(&new->text, 0, 0, font, "See error message above", 0, 0);
+	new->alldone=1;
+	mkpanels();
+
+	unlockdisplay(display);
+	eresized(0);
+	lockdisplay(display);
+
+	geturl(url, GET, 0, 1, 0);
+
+	if(logfile==-1) message("Can't open log file");
+	mouse.buttons=0;
+	for(;;){
+		if(mouse.buttons==0 && current){
+			if(current->finished){
+				updtext(current);
+				current->finished=0;
+				current->changed=0;
+				current->alldone=1;
+				message(mothra);
+				esetcursor(0);
+			}
+			else if(current->changed){
+				updtext(current);
+				current->changed=0;
+			}
+		}
+
+		unlockdisplay(display);
+		i=event(&e);
+		lockdisplay(display);
+
+		switch(i){
+		case Ekeyboard:
+			switch(e.kbdc){
+			default:
+				adjkb();
+				plkeyboard(e.kbdc);
+				break;
+			case Kup:
+				scrolltext(-text->size.y/4);
+				break;
+			case Kdown:
+				scrolltext(text->size.y/4);
+				break;
+			}
+			break;
+		case Emouse:
+			mouse=e.mouse;
+			plmouse(root, e.mouse);
+			break;
+		case Eplumb:
+			pm=e.v;
+			if(pm->ndata > 0)
+				geturl(pm->data, GET, 0, 1, 0);
+			plumbfree(pm);
+			break;
+		}
+	}
+}
+void message(char *s, ...){
+	static char buf[1024];
+	char *out;
+	va_list args;
+	va_start(args, s);
+	out = buf + vsnprint(buf, sizeof(buf), s, args);
+	va_end(args);
+	*out='\0';
+	plinitlabel(msg, PACKN|FILLX, buf);
+	if(defdisplay) pldraw(msg, screen);
+}
+void htmlerror(char *name, int line, char *m, ...){
+	static char buf[1024];
+	char *out;
+	va_list args;
+	if(verbose){
+		va_start(args, m);
+		out=buf+snprint(buf, sizeof(buf), "%s: line %d: ", name, line);
+		out+=vsnprint(out, sizeof(buf)-(out-buf)-1, m, args);
+		va_end(args);
+		*out='\0';
+		fprint(2, "%s\n", buf);
+	}
+}
+void eresized(int new){
+	Rectangle r;
+
+	lockdisplay(display);
+	if(new && getwindow(display, Refnone) == -1) {
+		fprint(2, "getwindow: %r\n");
+		exits("getwindow");
+	}
+	r=screen->r;
+	plinitlabel(curttl, PACKE|EXPAND, "---");
+	plinitlabel(cururl, PACKE|EXPAND, "---");
+	plpack(root, r);
+	if(current){
+		plinitlabel(curttl, PACKE|EXPAND, current->title);
+		plinitlabel(cururl, PACKE|EXPAND, current->url->fullname);
+	}
+	draw(screen, r, display->white, 0, ZP);
+	pldraw(root, screen);
+	unlockdisplay(display);
+}
+void *emalloc(int n){
+	void *v;
+	v=malloc(n);
+	if(v==0){
+		fprint(2, "out of space\n");
+		exits("no mem");
+	}
+	return v;
+}
+void *emallocz(int n, int z){
+	void *v;
+	v = emalloc(n);
+	if(z)
+		memset(v, 0, n);
+	return v;
+}
+
+char *genwww(Panel *, int index){
+	static char buf[1024];
+	int i;
+
+	if(index >= nwww())
+		return 0;
+	i = wwwtop-index-1;
+	snprint(buf, sizeof(buf), "%2d %s", i+1, www(i)->title);
+	return buf;
+}
+
+void donecurs(void){
+	esetcursor(current && current->alldone?0:&readingcurs);
+}
+/*
+ * selected text should be a url.
+ * get the document, scroll to the given tag
+ */
+void setcurrent(int index, char *tag){
+	Www *new;
+	Rtext *tp;
+	Action *ap;
+	int i;
+	new=www(index);
+	if(new==current && (tag==0 || tag[0]==0)) return;
+	if(current)
+		current->yoffs=plgetpostextview(text);
+	current=new;
+	plinitlabel(curttl, PACKE|EXPAND, current->title);
+	if(defdisplay) pldraw(curttl, screen);
+	plinitlabel(cururl, PACKE|EXPAND, current->url->fullname);
+	if(defdisplay) pldraw(cururl, screen);
+	plinittextview(text, PACKE|EXPAND, Pt(0, 0), current->text, dolink);
+	if(tag && tag[0]){
+		for(tp=current->text;tp;tp=tp->next){
+			ap=tp->user;
+			if(ap && ap->name && strcmp(ap->name, tag)==0){
+				current->yoffs=tp->topy;
+				break;
+			}
+		}
+	}
+	plsetpostextview(text, current->yoffs);
+	donecurs();
+	flushimage(display, 1);
+}
+char *arg(char *s){
+	do ++s; while(*s==' ' || *s=='\t');
+	return s;
+}
+void save(Url *url, char *name){
+	int ofd, ifd, n;
+	char buf[4096];
+	ofd=create(name, OWRITE, 0666);
+	if(ofd==-1){
+		message("save: %s: %r", name);
+		return;
+	}
+	esetcursor(&patientcurs);
+	ifd=urlopen(url, GET, 0);
+	donecurs();
+	if(ifd==-1){
+		message("save: %s: %r", selection->fullname);
+		close(ofd);
+	}
+	switch(rfork(RFNOTEG|RFFDG|RFPROC|RFNOWAIT)){
+	case -1:
+		message("Can't fork -- please wait");
+		esetcursor(&patientcurs);
+		while((n=read(ifd, buf, 4096))>0)
+			write(ofd, buf, n);
+		donecurs();
+		break;
+	case 0:
+		while((n=read(ifd, buf, 4096))>0)
+			write(ofd, buf, n);
+		if(n==-1) fprint(2, "save: %s: %r\n", url->fullname);
+		_exits(0);
+	}
+	close(ifd);
+	close(ofd);
+}
+void screendump(char *name, int full){
+	Image *b;
+	int fd;
+	fd=create(name, OWRITE|OTRUNC, 0666);
+	if(fd==-1){
+		message("can't create %s", name);
+		return;
+	}
+	if(full){
+		writeimage(fd, screen, 0);
+	} else {
+		if((b=allocimage(display, text->r, screen->chan, 0, DNofill)) == nil){
+			message("can't allocate image");
+			close(fd);
+			return;
+		}
+		draw(b, b->r, screen, 0, b->r.min);
+		writeimage(fd, b, 0);
+		freeimage(b);
+	}
+	close(fd);
+}
+
+/*
+ * user typed a command.
+ */
+void docmd(Panel *p, char *s){
+	USED(p);
+	while(*s==' ' || *s=='\t') s++;
+	/*
+	 * Non-command does a get on the url
+	 */
+	if(s[0]!='\0' && s[1]!='\0' && s[1]!=' ')
+		geturl(s, GET, 0, 1, 0);
+	else switch(s[0]){
+	default:
+		message("Unknown command %s, type h for help", s);
+		break;
+	case '?':
+	case 'h':
+		geturl(helpfile, GET, 0, 1, 0);
+		break;
+	case 'g':
+		s=arg(s);
+		if(*s=='\0'){
+			if(selection)
+				geturl(selection->fullname, GET, 0, 1, 0);
+			else
+				message("no url selected");
+		}
+		else geturl(s, GET, 0, 1, 0);
+		break;
+	case 'r':
+		s = arg(s);
+		if(*s == '\0')
+			s = selection ? selection->fullname : helpfile;
+		geturl(s, GET, 0, 0, 0);
+		break;
+	case 'W':
+		s=arg(s);
+		if(s=='\0'){
+			message("Usage: W file");
+			break;
+		}
+		screendump(s, 1);
+		break;
+	case 'w':
+		s=arg(s);
+		if(s=='\0'){
+			message("Usage: w file");
+			break;
+		}
+		screendump(s, 0);
+		break;
+	case 's':
+		s=arg(s);
+		if(*s=='\0'){
+			if(selection){
+				s=strrchr(selection->fullname, '/');
+				if(s) s++;
+			}
+			if(s==0 || *s=='\0'){
+				message("Usage: s file");
+				break;
+			}
+		}
+		save(selection, s);
+		break;
+	case 'q':
+		draw(screen, screen->r, display->white, 0, ZP);
+		exits(0);
+	}
+	plinitentry(cmd, EXPAND, 0, "", docmd);
+	if(defdisplay) pldraw(cmd, screen);
+}
+void hiturl(int buttons, char *url, int map){
+	switch(buttons){
+	case 1: geturl(url, GET, 0, 1, map); break;
+	case 2: selurl(url); break;
+	case 4: message("Button 3 hit on url can't happen!"); break;
+	}
+}
+/*
+ * user selected from the list of available pages
+ */
+void doprev(Panel *p, int buttons, int index){
+	int i;
+	USED(p);
+	if(index >= nwww())
+		return;
+	i = wwwtop-index-1;
+	switch(buttons){
+	case 1: setcurrent(i, 0);	/* no break ... */
+	case 2: selurl(www(i)->url->fullname); break;
+	case 4: message("Button 3 hit on page can't happen!"); break;
+	}
+}
+
+/*
+ * Follow an html link
+ */
+void dolink(Panel *p, int buttons, Rtext *word){
+	char mapurl[NNAME];
+	Action *a;
+	Point coord;
+	int yoffs;
+	USED(p);
+	a=word->user;
+	if(a && a->link){
+		if(a->ismap){
+			yoffs=plgetpostextview(p);
+			coord=subpt(subpt(mouse.xy, word->r.min), p->r.min);
+			snprint(mapurl, sizeof(mapurl), "%s?%d,%d", a->link, coord.x, coord.y+yoffs);
+			hiturl(buttons, mapurl, 1);
+		}
+		else
+			hiturl(buttons, a->link, 0);
+	}
+}
+void filter(char *cmd, int fd){
+	flushimage(display, 1);
+	switch(rfork(RFFDG|RFPROC|RFNOWAIT)){
+	case -1:
+		message("Can't fork!");
+		break;
+	case 0:
+		close(0);
+		dup(fd, 0);
+		close(fd);
+		execl("/bin/rc", "rc", "-c", cmd, 0);
+		message("Can't exec /bin/rc!");
+		_exits(0);
+	default:
+		break;
+	}
+	close(fd);
+}
+void gettext(Www *w, int fd, int type){
+	flushimage(display, 1);
+	switch(rfork(RFFDG|RFPROC|RFNOWAIT|RFMEM)){
+	case -1:
+		message("Can't fork, please wait");
+		if(type==HTML)
+			plrdhtml(w->url->fullname, fd, w);
+		else
+			plrdplain(w->url->fullname, fd, w);
+		break;
+	case 0:
+		if(type==HTML)
+			plrdhtml(w->url->fullname, fd, w);
+		else
+			plrdplain(w->url->fullname, fd, w);
+		_exits(0);
+	}
+	close(fd);
+}
+
+void freetext(Rtext *t){
+	Rtext *tt;
+	Action *a;
+
+	tt = t;
+	for(; t!=0; t = t->next){
+		t->b=0;
+		free(t->text);
+		t->text=0;
+		if(a = t->user){
+			t->user=0;
+			free(a->image);
+			free(a->link);
+			free(a->name);
+			free(a);
+		}
+	}
+	plrtfree(tt);
+}
+
+void popwin(char *cmd){
+	flushimage(display, 1);
+	switch(rfork(RFFDG|RFPROC|RFNOWAIT)){
+	case -1:
+		message("sorry, can't fork to %s", cmd);
+		break;
+	case 0:
+		execl("/bin/window", "window", "100 100 800 800", "rc", "-c", cmd, 0);
+		_exits(0);
+	}
+}
+int urlopen(Url *url, int method, char *body){
+	int fd;
+	Url prev;
+	int nredir;
+	Dir *dir;
+	nredir=0;
+Again:
+	if(++nredir==NREDIR){
+		werrstr("redir loop");
+		return -1;
+	}
+	seek(logfile, 0, 2);
+	fprint(logfile, "%s\n", url->fullname);
+	switch(url->access){
+	default:
+		werrstr("unknown access type");
+		return -1;
+	case FTP:
+		url->type = suffix2type(url->reltext);
+		return ftp(url);
+	case HTTP:
+		fd=http(url, method, body);
+		if(url->type==FORWARD){
+			prev=*url;
+			crackurl(url, prev.redirname, &prev);
+
+			/*
+			 * I'm not convinced that the following two lines are right,
+			 * but once I got a redir loop because they were missing.
+			 */
+			method=GET;
+			body=0;
+			goto Again;
+		}
+		return fd;	
+	case FILE:
+		url->type=suffix2type(url->reltext);
+		fd=open(url->reltext, OREAD);
+		if(fd!=-1){
+			dir=dirfstat(fd);
+			if(dir->mode&DMDIR){
+				url->type=HTML;
+				free(dir);
+				return dir2html(url->reltext, fd);
+			}
+			free(dir);
+		}
+		return fd;
+	case GOPHER:
+		return gopher(url);
+	}
+}
+
+int pipeline(char *cmd, int fd)
+{
+	int pfd[2];
+
+	if(pipe(pfd)==-1){
+Err:
+		close(fd);
+		werrstr("pipeline for %s failed: %r", cmd);
+		return -1;
+	}
+	switch(fork()){
+	case -1:
+		close(pfd[0]);
+		close(pfd[1]);
+		goto Err;
+	case 0:
+		dup(fd, 0);
+		dup(pfd[0], 1);
+		close(pfd[0]);
+		close(pfd[1]);
+		execl("/bin/rc", "rc", "-c", cmd, 0);
+		_exits(0);
+	}
+	close(pfd[0]);
+	close(fd);
+	return pfd[1];
+}
+
+/*
+ * select the file at the given url
+ */
+void selurl(char *urlname){
+	Url *cur;
+	static Url url;
+	if(current){
+		cur=current->base;
+		/*
+		 * I believe that the following test should never succeed
+		 */
+		if(cur==0){
+			cur=current->url;
+			if(cur==0){
+				fprint(2, "bad base & url, getting %s\n", urlname);
+				cur=&defurl;
+			}
+			else
+				fprint(2, "bad base, current %s, getting %s\n",
+					current->url->fullname, urlname);
+		}
+	}
+	else cur=&defurl;
+	crackurl(&url, urlname, cur);
+	selection=&url;
+	message("selected: %s", selection->fullname);
+}
+Url *copyurl(Url *u){
+	Url *v;
+	v=emalloc(sizeof(Url));
+	*v=*u;
+	return v;
+}
+void freeurl(Url *u){
+	if(u!=&defurl && u!=&badurl)
+		free(u);
+}
+
+/*
+ * get the file at the given url
+ */
+void geturl(char *urlname, int method, char *body, int cache, int map){
+	int i, fd;
+	char cmd[NNAME];
+	int pfd[2];
+	Www *w;
+
+	selurl(urlname);
+	selection->map=map;
+
+	message("getting %s", selection->fullname);
+	esetcursor(&patientcurs);
+	switch(selection->access){
+	default:
+		message("unknown access %d", selection->access);
+		break;
+	case TELNET:
+		sprint(cmd, "telnet %s", selection->reltext);
+		popwin(cmd);
+		break;
+	case MAILTO:
+		if(body){
+			/*
+			 * Undocumented Mozilla feature
+			 */
+			pipe(pfd);
+			switch(rfork(RFFDG|RFPROC|RFNOWAIT)){
+			case -1:
+				message("Can't fork!");
+				break;
+			case 0:
+				close(0);
+				dup(pfd[1], 0);
+				close(pfd[1]);
+				close(pfd[0]);
+				execl("/bin/upas/send",
+					"sendmail", selection->reltext, 0);
+				message("Can't exec sendmail");
+				_exits(0);
+			default:
+				close(pfd[1]);
+				fprint(pfd[0],
+				    "Content-type: application/x-www-form-urlencoded\n"
+				    "Subject: Form posted from Mothra\n"
+				    "\n"
+				    "%s\n", body);
+				close(pfd[0]);
+				break;
+			}
+		}
+		else{
+			snprint(cmd, sizeof(cmd), "mail %s", selection->reltext);
+			popwin(cmd);
+		}
+		break;
+	case FTP:
+	case HTTP:
+	case FILE:
+	case GOPHER:
+		fd=urlopen(selection, method, body);
+		if(fd==-1){
+			message("%r");
+			setcurrent(-1, 0);
+			break;
+		}
+		if(selection->type&COMPRESS)
+			fd=pipeline("/bin/uncompress", fd);
+		else if(selection->type&GUNZIP)
+			fd=pipeline("/bin/gunzip", fd);
+		switch(selection->type&~COMPRESSION){
+		default:
+			message("Bad type %x in geturl", selection->type);
+			break;
+		case PLAIN:
+		case HTML:
+			w = www(i = wwwtop++);
+			if(i >= NWWW){
+				extern void freeform(void *p);
+				extern void freepix(void *p);
+
+				/* wait for the reader to finish the document */
+				while(!w->finished && !w->alldone){
+					unlockdisplay(display);
+					sleep(10);
+					lockdisplay(display);
+				}
+
+				freetext(w->text);
+				freeform(w->form);
+				freepix(w->pix);
+				if(w->base != w->url)
+					freeurl(w->base);
+				freeurl(w->url);
+				memset(w, 0, sizeof(*w));
+			}
+			if(selection->map){
+				if(current && current->base)	/* always succeeds */
+					w->url=copyurl(current->base);
+				else{
+					fprint(2, "no base for map!\n");
+					w->url=copyurl(selection);
+				}
+			}
+			else
+				w->url=copyurl(selection);
+			w->finished = 0;
+			w->alldone = 0;
+			gettext(w, fd, selection->type&~COMPRESSION);
+			plinitlist(list, PACKN|FILLX, genwww, 8, doprev);
+			if(defdisplay) pldraw(list, screen);
+			setcurrent(i, selection->tag);
+			break;
+		case POSTSCRIPT:
+		case GIF:
+		case JPEG:
+		case PNG:
+		case PDF:
+			filter("page -w", fd);
+			break;
+		case TIFF:
+			filter("/sys/lib/mothra/tiffview", fd);
+			break;
+		case XBM:
+			filter("fb/xbm2pic|fb/9v", fd);
+			break;
+		}
+	}
+	donecurs();
+}
+void updtext(Www *w){
+	Rtext *t;
+	Action *a;
+	for(t=w->text;t;t=t->next){
+		a=t->user;
+		if(a){
+			if(a->field)
+				mkfieldpanel(t);
+			a->field=0;
+		}
+	}
+	w->yoffs=plgetpostextview(text);
+	plinittextview(text, PACKE|EXPAND, Pt(0, 0), w->text, dolink);
+	plsetpostextview(text, w->yoffs);
+	pldraw(root, screen);
+}
+Cursor confirmcursor={
+	0, 0,
+	0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+	0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+	0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+	0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+
+	0x00, 0x0E, 0x07, 0x1F, 0x03, 0x17, 0x73, 0x6F,
+	0xFB, 0xCE, 0xDB, 0x8C, 0xDB, 0xC0, 0xFB, 0x6C,
+	0x77, 0xFC, 0x00, 0x00, 0x00, 0x01, 0x00, 0x03,
+	0x94, 0xA6, 0x63, 0x3C, 0x63, 0x18, 0x94, 0x90,
+};
+int confirm(int b){
+	Mouse down, up;
+	esetcursor(&confirmcursor);
+	do down=emouse(); while(!down.buttons);
+	do up=emouse(); while(up.buttons);
+	donecurs();
+	return down.buttons==(1<<(b-1));
+}
+void snarf(Panel *p){
+	int fd;
+	fd=create("/dev/snarf", OWRITE, 0666);
+	if(fd>=0){
+		fprint(fd, "%s", selection->fullname);
+		close(fd);
+	}
+}
+void paste(Panel *p){
+	char buf[1024];
+	int n, len, fd;
+	fd=open("/dev/snarf", OREAD);
+	strncpy(buf, plentryval(p), sizeof(buf));
+	len=strlen(buf);
+	n=read(fd, buf+len, 1023-len);
+	if(n>0){
+		buf[len+n]='\0';
+		plinitentry(cmd, PACKE|EXPAND, 0, buf, docmd);
+		pldraw(cmd, screen);
+	}
+	close(fd);
+}
+void hit3(int button, int item){
+	char name[100], *home;
+	Panel *swap;
+	int fd;
+	USED(button);
+	switch(item){
+	case 0:
+		swap=root;
+		root=alt;
+		alt=swap;
+		current->yoffs=plgetpostextview(text);
+		swap=text;
+		text=alttext;
+		alttext=swap;
+		defdisplay=!defdisplay;
+		plpack(root, screen->r);
+		plinittextview(text, PACKE|EXPAND, Pt(0, 0), current->text, dolink);
+		plsetpostextview(text, current->yoffs);
+		pldraw(root, screen);
+		break;
+	case 1:
+		snarf(cmd);
+		break;
+	case 2:
+		paste(cmd);
+		break;
+	case 3:
+		home=getenv("home");
+		if(home==0){
+			message("no $home");
+			return;
+		}
+		snprint(name, sizeof(name), "%s/lib/mothra/hit.html", home);
+		fd=open(name, OWRITE);
+		if(fd==-1){
+			fd=create(name, OWRITE, 0666);
+			if(fd==-1){
+				message("can't open %s", name);
+				return;
+			}
+			fprint(fd, "<head><title>Hit List</title></head>\n");
+			fprint(fd, "<body><h1>Hit list</h1>\n");
+		}
+		seek(fd, 0, 2);
+		fprint(fd, "<p><a href=\"%s\">%s</a>\n",
+			selection->fullname, selection->fullname);
+		close(fd);
+		break;
+	case 4:
+		home=getenv("home");
+		if(home==0){
+			message("no $home");
+			return;
+		}
+		snprint(name, sizeof(name), "file:%s/lib/mothra/hit.html", home);
+		geturl(name, GET, 0, 1, 0);
+		break;
+	case 5:
+		if(confirm(3)){
+			draw(screen, screen->r, display->white, 0, ZP);
+			exits(0);
+		}
+		break;
+	}
+}
binary files /dev/null b/sys/src/cmd/mothra/mothra.gif differ
--- /dev/null
+++ b/sys/src/cmd/mothra/mothra.h
@@ -1,0 +1,143 @@
+enum{
+	NWWW=64,	/* # of pages we hold in the log */
+	NNAME=512,
+	NLINE=256,
+	NAUTH=128,
+	NTITLE=81,	/* length of title (including nul at end) */
+	NLABEL=50,	/* length of option name in forms */
+	NREDIR=10,	/* # of redirections we'll tolerate before declaring a loop */
+};
+
+typedef struct Action Action;
+typedef struct Url Url;
+typedef struct Www Www;
+typedef struct Scheme Scheme;
+typedef struct Field Field;
+struct Scheme{
+	char *name;
+	int type;
+	int flags;
+	int port;
+};
+struct Action{
+	char *image;
+	Field *field;
+	char *link;
+	char *name;
+	int ismap;
+	int width;
+	int height;
+};
+struct Url{
+	char fullname[NNAME];
+	Scheme *scheme;
+	char ipaddr[NNAME];
+	char reltext[NNAME];
+	char tag[NNAME];
+	char redirname[NNAME];
+	char autharg[NAUTH];
+	char authtype[NTITLE];
+	char charset[NNAME];
+	int port;
+	int access;
+	int type;
+	int map;			/* is this an image map? */
+	int ssl;
+};
+struct Www{
+	Url *url;
+	Url *base;
+	void *pix;
+	void *form;
+	char title[NTITLE];
+	Rtext *text;
+	int yoffs;
+	int changed;		/* reader sets this every time it updates page */
+	int finished;		/* reader sets this when done */
+	int alldone;		/* page will not change further -- used to adjust cursor */
+};
+
+/*
+ * url reference types -- COMPRESS and GUNZIP are flags that can modify any other type
+ * Changing these in a non-downward compatible way spoils cache entries
+ */
+enum{
+	GIF=1,
+	HTML,
+	JPEG,
+	PIC,
+	TIFF,
+	AUDIO,
+	PLAIN,
+	XBM,
+	POSTSCRIPT,
+	FORWARD,
+	PDF,
+	SUFFIX,
+	ZIP,
+	PNG,
+
+	COMPRESS=16,
+	GUNZIP=32,
+	COMPRESSION=16+32,
+};
+
+/*
+ * url access types
+ */
+enum{
+	HTTP=1,
+	FTP,
+	FILE,
+	TELNET,
+	MAILTO,
+	GOPHER,
+};
+
+/*
+ *  authentication types
+ */
+enum{
+	ANONE,
+	ABASIC,
+};
+
+Image *hrule, *bullet, *linespace;
+char home[512];		/* where to put files */
+int chrwidth;		/* nominal width of characters in font */
+Panel *text;		/* Panel displaying the current www page */
+int debug;		/* command line flag */
+
+/*
+ * HTTP methods
+ */
+enum{
+	GET=1,
+	POST,
+};
+
+void plrdhtml(char *, int, Www *);
+void plrdplain(char *, int, Www *);
+void htmlerror(char *, int, char *, ...);	/* user-supplied routine */
+void crackurl(Url *, char *, Url *);
+void getpix(Rtext *, Www *);
+int pipeline(char *, int);
+int urlopen(Url *, int, char *);
+void getfonts(void);
+void *emalloc(int);
+void *emallocz(int, int);
+void setbitmap(Rtext *);
+void message(char *, ...);
+int ftp(Url *);
+int http(Url *, int, char *);
+int gopher(Url *);
+int cistrcmp(char *, char *);
+int cistrncmp(char *, char *, int);
+int suffix2type(char *);
+int content2type(char *, char *);
+int encoding2type(char *);
+void mkfieldpanel(Rtext *);
+void geturl(char *, int, char *, int, int);
+int dir2html(char *, int);
+int auth(Url*, char*, int);
+char version[];
binary files /dev/null b/sys/src/cmd/mothra/mothracompat.gif differ
binary files /dev/null b/sys/src/cmd/mothra/mothraenhanced.gif differ
--- /dev/null
+++ b/sys/src/cmd/mothra/rdhtml.c
@@ -1,0 +1,1091 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <panel.h>
+#include "mothra.h"
+#include "html.h"
+#include "rtext.h"
+#include "tcs.h"
+
+typedef struct Fontdata Fontdata;
+struct Fontdata{
+	char *name;
+	Font *font;
+	int space;
+}fontlist[4][4]={
+	"lucidasans/unicode.7", 0, 0,
+	"lucidasans/unicode.8", 0, 0,
+	"lucidasans/unicode.10", 0, 0,
+	"lucidasans/unicode.13", 0, 0,
+
+	"lucidasans/italicunicode.7", 0, 0,
+	"lucidasans/italicunicode.8", 0, 0,
+	"lucidasans/italicunicode.10", 0, 0,
+	"lucidasans/italicunicode.13", 0, 0,
+
+	"lucidasans/boldunicode.7", 0, 0,
+	"lucidasans/boldunicode.8", 0, 0,
+	"lucidasans/boldunicode.10", 0, 0,
+	"lucidasans/boldunicode.13", 0, 0,
+
+	"lucidasans/typeunicode.7", 0, 0,
+	"pelm/unicode.8", 0, 0,
+	"lucidasans/typeunicode.12", 0, 0,
+	"lucidasans/typeunicode.16", 0, 0,
+};
+Fontdata *pl_whichfont(int f, int s){
+	char name[100];
+	if(fontlist[f][s].font==0){
+		snprint(name, sizeof(name), "/lib/font/bit/%s.font",
+			fontlist[f][s].name);
+		fontlist[f][s].font=openfont(display, name);
+		if(fontlist[f][s].font==0) fontlist[f][s].font=font;
+		fontlist[f][s].space=stringwidth(fontlist[f][s].font, "0");
+	}
+	return &fontlist[f][s];
+	
+}
+void getfonts(void){
+	int f, s;
+	for(f=0;f!=4;f++)
+		for(s=0;s!=4;s++)
+			pl_whichfont(f, s);
+}
+void pl_pushstate(Hglob *g, int t){
+	++g->state;
+	if(g->state==&g->stack[NSTACK]){
+		htmlerror(g->name, g->lineno, "stack overflow at <%s>", tag[t].name);
+		--g->state;
+	}
+	g->state[0]=g->state[-1];
+	g->state->tag=t;
+}
+void pl_linespace(Hglob *g){
+	plrtbitmap(&g->dst->text, 1000000, 0, linespace, 0, 0);
+	g->para=0;
+	g->linebrk=0;
+}
+enum{
+	HORIZ,
+	VERT,
+};
+int strtolength(Hglob *g, int dir, char *str)
+{
+	double f;
+
+	f = atof(str);
+	if(cistrstr(str, "px"))
+		return floor(f);
+	if(cistrstr(str, "%"))
+		return floor(f*((dir==HORIZ) ? Dx(g->dst->text->r) : Dy(g->dst->text->r))/100);
+	if(cistrstr(str, "em")){
+		Point z;
+		z = stringsize(g->dst->text->font, "M");
+		return floor(f*((dir==HORIZ) ? z.x : z.y));
+	}
+	return floor(f);
+}
+
+void pl_htmloutput(Hglob *g, int nsp, char *s, Field *field){
+	Fontdata *f;
+	int space, indent;
+	Action *ap;
+	if(g->state->tag==Tag_title
+/*	|| g->state->tag==Tag_textarea */
+	|| g->state->tag==Tag_select){
+		if(s){
+			if(g->tp!=g->text && g->tp!=g->etext && g->tp[-1]!=' ')
+				*g->tp++=' ';
+			while(g->tp!=g->etext && *s) *g->tp++=*s++;
+			if(g->state->tag==Tag_title) g->dst->changed=1;
+			*g->tp='\0';
+		}
+		return;
+	}
+	f=pl_whichfont(g->state->font, g->state->size);
+	space=f->space;
+	indent=g->state->margin;
+	if(g->para){
+		space=1000000;
+		indent+=g->state->indent;
+	}
+	else if(g->linebrk)
+		space=1000000;
+	else if(nsp<=0)
+		space=0;
+	if(g->state->image[0]==0 && g->state->link[0]==0 && g->state->name[0]==0 && field==0)
+		ap=0;
+	else{
+		ap=mallocz(sizeof(Action), 1);
+		if(ap!=0){
+			if(g->state->image[0])
+				ap->image = strdup(g->state->image);
+			if(g->state->link[0])
+				ap->link = strdup(g->state->link);
+			if(g->state->name[0])
+				ap->name = strdup(g->state->name);
+			ap->ismap=g->state->ismap;
+			ap->width=g->state->width;
+			ap->height=g->state->height;
+			ap->field=field;
+		}
+	}
+	if(space<0) space=0;
+	if(indent<0) indent=0;
+	if(g->state->pre && s[0]=='\t'){
+		space=0;
+		while(s[0]=='\t'){
+			space++;
+			s++;
+		}
+		space=PL_TAB|space;
+		if(g->linebrk){
+			indent=space;
+			space=1000000;
+		}
+	}
+	plrtstr(&g->dst->text, space, indent, f->font, strdup(s), g->state->link[0]!=0, ap);
+	g->para=0;
+	g->linebrk=0;
+	g->dst->changed=1;
+}
+
+void pl_applycharset(Hglob *g)
+{
+	int fd, pfd[2], n;
+	char buf[NHBUF];
+	char **cs, *charset;
+
+	charset = nil;
+	for(cs = tcs; *cs; cs += 2){
+		if(cistrcmp(cs[0], g->charset) == 0){
+			charset = cs[1];
+			break;
+		}
+	}
+	/* make sure we dont convet multiple times */
+	g->charset[0]=0;
+
+	/* no match, dont convert */
+	if(charset == nil)
+		return;
+
+	fd = g->hfd;
+	n = g->ehbuf - g->hbufp;
+	memcpy(buf, g->hbufp, n);
+
+	if(pipe(pfd)==-1)
+		return;
+	switch(rfork(RFFDG|RFPROC|RFNOWAIT)){
+	case -1:
+		close(pfd[0]);
+		close(pfd[1]);
+		return;
+	case 0:
+		dup(fd, 0);
+		dup(pfd[1], 1);
+		close(pfd[0]);
+		close(pfd[1]);
+		close(fd);
+
+		write(1, buf, n);
+		while((n=read(0, buf, sizeof(buf)))>0)
+			write(1, buf, n);
+		_exits("no exec!");
+	}
+	dup(pfd[0], fd);
+	close(pfd[0]);
+	close(pfd[1]);
+	g->hbufp = g->ehbuf;
+	snprint(buf, sizeof(buf), "tcs -s -f %s -t utf", charset);
+	if((fd=pipeline(buf, fd)) >= 0)
+		g->hfd = fd;
+}
+
+/*
+ * Buffered read, no translation
+ * Save in cache.
+ */
+int pl_bread(Hglob *g){
+	int n, c;
+	char err[1024];
+	if(g->hbufp==g->ehbuf){
+		n=read(g->hfd, g->hbuf, NHBUF);
+		if(n<=0){
+			if(n<0){
+				snprint(err, sizeof(err), "%r reading %s", g->name);
+				pl_htmloutput(g, 1, err, 0);
+			}
+			g->heof=1;
+			return EOF;
+		}
+		g->hbufp=g->hbuf;
+		g->ehbuf=g->hbuf+n;
+	}
+	c=*g->hbufp++&255;
+	if(c=='\n') g->lineno++;
+	return c;
+}
+/*
+ * Read a character, translating \r\n, \n\r, \r and \n into \n
+ */
+int pl_readc(Hglob *g){
+	int c;
+	static int peek=-1;
+	if(peek!=-1){
+		c=peek;
+		peek=-1;
+	}
+	else
+		c=pl_bread(g);
+	if(c=='\r'){
+		c=pl_bread(g);
+		if(c!='\n') peek=c;
+		return '\n';
+	}
+	if(c=='\n'){
+		c=pl_bread(g);
+		if(c!='\r') peek=c;
+		return '\n';
+	}
+	return c;
+}
+void pl_putback(Hglob *g, int c){
+	if(g->npeekc==NPEEKC) htmlerror(g->name, g->lineno, "too much putback!");
+	else if(c!=EOF) g->peekc[g->npeekc++]=c;
+}
+int pl_nextc(Hglob *g){
+	int c;
+	int n;
+	Rune r;
+	char crune[4];
+	if(g->heof) return EOF;
+	if(g->npeekc!=0) return g->peekc[--g->npeekc];
+	c=pl_readc(g);
+	if(c=='<'){
+		c=pl_readc(g);
+		if(c=='/'){
+			c=pl_readc(g);
+			pl_putback(g, c);
+			pl_putback(g, '/');
+			if('a'<=c && c<='z' || 'A'<=c && c<='Z') return STAG;
+			return '<';
+		}
+		pl_putback(g, c);
+		if(c=='!' || 'a'<=c && c<='z' || 'A'<=c && c<='Z' || c=='?') return STAG;
+		return '<';
+	}
+	if(c=='>') return ETAG;
+	if(c==EOF) return c;
+	n=0;
+	for (;;){
+		crune[n++]=c;
+		if(fullrune(crune, n)){
+			chartorune(&r, crune);
+			return r;
+		}
+		c=pl_readc(g);
+		if(c==EOF)
+			return EOF;
+	}
+	return c;
+}
+int entchar(int c){
+	return c=='#' || 'a'<=c && c<='z' || 'A'<=c && c<='Z' || '0'<=c && c<='9';
+}
+Entity *entsearch(char *s){
+	int i, m, n, r;
+	i=0;
+	n=pl_entities;
+	while ((n-i) > 0) {
+		m=i+(n-i)/2;
+		r=strcmp(s, pl_entity[m].name);
+		if (r > 0)
+			i=m+1;
+		else if (r < 0)
+			n=m;
+		else
+			return &pl_entity[m];
+	}
+	return 0;
+}
+/*
+ * remove entity references, in place.
+ * Potential bug:
+ *	This doesn't work if removing an entity reference can lengthen the string!
+ *	Fortunately, this doesn't happen.
+ */
+void pl_rmentities(Hglob *g, char *s){
+	char *t, *u, c, svc;
+	Entity *ep;
+	Rune r;
+	t=s;
+	do{
+		c=*s++;
+		if(c=='&'
+		&& ((*s=='#' && strchr("0123456789Xx", s[1]))
+		  || 'a'<=*s && *s<='z'
+		  || 'A'<=*s && *s<='Z')){
+			u=s;
+			while(entchar(*s)) s++;
+			svc=*s;
+			if(svc!=';')
+				htmlerror(g->name, g->lineno, "entity syntax error");
+			*s++='\0';
+			if(*u=='#'){
+				if (u[1]=='X' || u[1]=='x')
+					r=strtol(u+2, 0, 16);
+				else
+					r=atoi(u+1);
+				t+=runetochar(t, &r);
+				if(svc!=';') *--s=svc;
+			}
+			else{
+				ep=entsearch(u);
+				if(ep && ep->name){
+					t+=runetochar(t, &ep->value);
+					if(svc!=';') *--s=svc;
+				}
+				else{
+					htmlerror(g->name, g->lineno,
+						"unknown entity %s", u);
+					s[-1]=svc;
+					s=u;
+					*t++='&';
+				}
+			}
+		}	
+		else *t++=c;
+	}while(c);
+}
+/*
+ * Skip over white space
+ */
+char *pl_white(char *s){
+	while(*s==' ' || *s=='\t' || *s=='\n' || *s=='\r') s++;
+	return s;
+}
+/*
+ * Skip over HTML word
+ */
+char *pl_word(char *s){
+	if ('a'<=*s && *s<='z' || 'A'<=*s && *s<='Z') {
+		s++;
+		while('a'<=*s && *s<='z' || 'A'<=*s && *s<='Z' || '0'<=*s && *s<='9' || *s=='-' || *s=='.') s++;
+	}
+	return s;
+}
+/*
+ * Skip to matching quote
+ */
+char *pl_quote(char *s){
+	char q;
+	q=*s++;
+	while(*s!=q && *s!='\0') s++;
+	return s;
+}
+void pl_dnl(char *s){
+	char *t;
+	for(t=s;*s;s++) if(*s!='\r' && *s!='\n') *t++=*s;
+	*t='\0';
+}
+void pl_tagparse(Hglob *g, char *str){
+	char *s, *t, *name, c;
+	Pair *ap;
+	Tag *tagp;
+	g->tag=Tag_end;
+	ap=g->attr;
+	if(str[0]=='!'){	/* test should be strncmp(str, "!--", 3)==0 */
+		g->tag=Tag_comment;
+		ap->name=0;
+		return;
+	}
+	if(str[0]=='/') str++;
+	name=str;
+	s=pl_word(str);
+	if(*s!=' ' && *s!='\n' && *s!='\t' && *s!='\0'){
+		htmlerror(g->name, g->lineno, "bad tag name in %s", str);
+		ap->name=0;
+		return;
+	}
+	if(*s!='\0') *s++='\0';
+	for(t=name;t!=s;t++) if('A'<=*t && *t<='Z') *t+='a'-'A';
+	/*
+	 * Binary search would be faster here
+	 */
+	for(tagp=tag;tagp->name;tagp++) if(strcmp(name, tagp->name)==0) break;
+	g->tag=tagp-tag;
+	if(g->tag==Tag_end) htmlerror(g->name, g->lineno, "no tag %s", name);
+	for(;;){
+		s=pl_white(s);
+		if(*s=='\0'){
+			ap->name=0;
+			return;
+		}
+		ap->name=s;
+		s=pl_word(s);
+		t=pl_white(s);
+		c=*t;
+		*s='\0';
+		for(s=ap->name;*s;s++) if('A'<=*s && *s<='Z') *s+='a'-'A';
+		if(c=='='){
+			s=pl_white(t+1);
+			if(*s=='\'' || *s=='"'){
+				ap->value=s+1;
+				s=pl_quote(s);
+				if(*s=='\0'){
+					htmlerror(g->name, g->lineno,
+						"No terminating quote in rhs of attribute %s",
+						ap->name);
+					ap->name=0;
+					return;
+				}
+				*s++='\0';
+				pl_dnl(ap->value);
+			}
+			else{
+				/* read up to white space or > */
+				ap->value=s;
+				while(*s!=' ' && *s!='\t' && *s!='\n' && *s!='\0') s++;
+				if(*s!='\0') *s++='\0';
+			}
+			pl_rmentities(g, ap->value);
+		}
+		else{
+			if(c!='\0') s++;
+			ap->value="";
+		}
+		if(ap==&g->attr[NATTR-1])
+			htmlerror(g->name, g->lineno, "too many attributes!");
+		else ap++;
+	}
+}
+int pl_getcomment(Hglob *g){
+	int c;
+	if((c=pl_nextc(g))=='-' && (c=pl_nextc(g))=='-'){
+		/* <!-- eats everything until --> or EOF */
+		for(;;){
+			while((c=pl_nextc(g))!='-' && c!=EOF)
+				;
+			if(c==EOF)
+				break;
+			if((c=pl_nextc(g))=='-'){
+				while((c=pl_nextc(g))=='-')
+					;
+				if(c==ETAG || c==EOF)
+					break;
+			}
+		}
+	} else {
+		/* <! eats everything until > or EOF */
+		while(c!=ETAG && c!=EOF)
+			c=pl_nextc(g);
+	}
+	if(c==EOF)
+		htmlerror(g->name, g->lineno, "EOF in comment");
+	g->tag=Tag_comment;
+	g->attr->name=0;
+	g->token[0]='\0';
+	return TAG;
+}
+int lrunetochar(char *p, int v)
+{
+	Rune r;
+
+	r=v;
+	return runetochar(p, &r);
+}
+
+/*
+ * Read a start or end tag -- the caller has read the initial <
+ */
+int pl_gettag(Hglob *g){
+	char *tokp;
+	int c;
+	tokp=g->token;
+	if((c=pl_nextc(g))=='!' || c=='?')
+		return pl_getcomment(g);
+	pl_putback(g, c);
+	while((c=pl_nextc(g))!=ETAG && c!=EOF)
+		if(tokp!=&g->token[NTOKEN-3]) tokp += lrunetochar(tokp, c);
+	*tokp='\0';
+	if(c==EOF) htmlerror(g->name, g->lineno, "EOF in tag");
+	pl_tagparse(g, g->token);
+	if(g->token[0]!='/') return TAG;
+	if(g->attr[0].name!=0)
+		htmlerror(g->name, g->lineno, "end tag should not have attributes");
+	return ENDTAG;
+}
+/*
+ * The next token is a tag, an end tag or a sequence of
+ * non-white characters.
+ * If inside <pre>, newlines are converted to <br> and spaces are preserved.
+ * Otherwise, spaces and newlines are noted and discarded.
+ */
+int pl_gettoken(Hglob *g){
+	char *tokp;
+	int c;
+	if(g->state->pre) switch(c=pl_nextc(g)){
+	case STAG: return pl_gettag(g);
+	case EOF: return EOF;
+	case '\n':
+		pl_tagparse(g, "br");
+		return TAG;
+	default:
+		tokp=g->token;
+		while(c=='\t'){
+			if(tokp!=&g->token[NTOKEN-3]) tokp += lrunetochar(tokp, c);
+			c=pl_nextc(g);
+		}
+		while(c!='\t' && c!='\n' && c!=STAG && c!=EOF){
+			if(c==ETAG) c='>';
+			if(tokp!=&g->token[NTOKEN-3]) tokp += lrunetochar(tokp, c);
+			c=pl_nextc(g);
+		}
+		*tokp='\0';
+		pl_rmentities(g, g->token);
+		pl_putback(g, c);
+		g->nsp=0;
+		g->spacc=0;
+		return TEXT;
+	}
+	while((c=pl_nextc(g))==' ' || c=='\t' || c=='\n')
+		if(g->spacc!=-1)
+			g->spacc++;
+	switch(c){
+	case STAG: return pl_gettag(g);
+	case EOF: return EOF;
+	default:
+		tokp=g->token;
+		do{
+			if(c==ETAG) c='>';
+			if(tokp!=&g->token[NTOKEN-3]) tokp += lrunetochar(tokp, c);
+			c=pl_nextc(g);
+		}while(c!=' ' && c!='\t' && c!='\n' && c!=STAG && c!=EOF);
+		*tokp='\0';
+		pl_rmentities(g, g->token);
+		pl_putback(g, c);
+		g->nsp=g->spacc;
+		g->spacc=0;
+		return TEXT;
+	}
+}
+char *pl_getattr(Pair *attr, char *name){
+	for(;attr->name;attr++)
+		if(strcmp(attr->name, name)==0)
+			return attr->value;
+	return 0;
+}
+int pl_hasattr(Pair *attr, char *name){
+	for(;attr->name;attr++)
+		if(strcmp(attr->name, name)==0)
+			return 1;
+	return 0;
+}
+void plaintext(Hglob *g){
+	char line[NLINE];
+	char *lp, *elp;
+	int c;
+	g->state->font=CWIDTH;
+	g->state->size=NORMAL;
+	elp=&line[NLINE+1];
+	lp=line;
+	for(;;){
+		c=pl_readc(g);
+		if(c==EOF) break;
+		if(c=='\n' || lp==elp){
+			*lp='\0';
+			g->linebrk=1;
+			pl_htmloutput(g, 0, line, 0);
+			lp=line;
+		}
+		if(c=='\t'){
+			do *lp++=' '; while(lp!=elp && utfnlen(line, lp-line)%8!=0);
+		}
+		else if(c!='\n')
+			lp += lrunetochar(lp, c);
+	}
+	if(lp!=line){
+		*lp='\0';
+		g->linebrk=1;
+		pl_htmloutput(g, 0, line, 0);
+	}
+}
+void plrdplain(char *name, int fd, Www *dst){
+	Hglob g;
+	g.state=g.stack;
+	g.state->tag=Tag_html;
+	g.state->font=CWIDTH;
+	g.state->size=NORMAL;
+	g.state->pre=0;
+	g.state->image[0]=0;
+	g.state->link[0]=0;
+	g.state->name[0]=0;
+	g.state->margin=0;
+	g.state->indent=20;
+	g.state->ismap=0;
+	g.state->table=0;
+	g.dst=dst;
+	g.hfd=fd;
+	g.name=name;
+	g.ehbuf=g.hbufp=g.hbuf;
+	g.npeekc=0;
+	g.heof=0;
+	g.lineno=1;
+	g.linebrk=1;
+	g.para=0;
+	g.text=dst->title;
+	g.tp=g.text;
+	g.etext=g.text+NTITLE-1;
+	g.spacc=0;
+	g.form=0;
+	strncpy(g.text, name, NTITLE);
+	pl_applycharset(&g);
+	plaintext(&g);
+	dst->finished=1;
+}
+void plrdhtml(char *name, int fd, Www *dst){
+	Stack *sp;
+	char buf[20];
+	char *str;
+	Hglob g;
+	int t;
+	int tagerr;
+	g.state=g.stack;
+	g.state->tag=Tag_html;
+	g.state->font=ROMAN;
+	g.state->size=NORMAL;
+	g.state->pre=0;
+	g.state->image[0]=0;
+	g.state->link[0]=0;
+	g.state->name[0]=0;
+	g.state->margin=0;
+	g.state->indent=25;
+	g.state->ismap=0;
+	g.state->width=0;
+	g.state->height=0;
+	g.state->table=0;
+	g.dst=dst;
+	g.hfd=fd;
+	g.name=name;
+	g.ehbuf=g.hbufp=g.hbuf;
+	g.npeekc=0;
+	g.heof=0;
+	g.lineno=1;
+	g.linebrk=1;
+	g.para=0;
+	g.text=dst->title;
+	g.tp=g.text;
+	g.etext=g.text+NTITLE-1;
+	dst->title[0]='\0';
+	dst->base=dst->url;
+	g.spacc=0;
+	g.form=0;
+	g.charset[0] = '\0';
+	strncpy(g.charset, dst->url->charset, sizeof(g.charset));
+
+	for(;;) switch(pl_gettoken(&g)){
+	case TAG:
+		switch(tag[g.tag].action){
+		case OPTEND:
+			for(sp=g.state;sp!=g.stack && sp->tag!=g.tag;--sp);
+			if(sp->tag!=g.tag)
+				pl_pushstate(&g, g.tag);
+			else
+				for(;g.state!=sp;--g.state)
+					if(tag[g.state->tag].action!=OPTEND)
+						htmlerror(g.name, g.lineno,
+							"end tag </%s> missing",
+							tag[g.state->tag].name);
+			break;
+		case END:
+			pl_pushstate(&g, g.tag);
+			break;
+		}
+		switch(g.tag){
+		default:
+			htmlerror(g.name, g.lineno,
+				"unimplemented tag <%s>", tag[g.tag].name);
+			break;
+		case Tag_end:	/* unrecognized start tag */
+			break;
+		case Tag_meta:
+			if((str=pl_getattr(g.attr, "http-equiv")) &&
+			   (cistrcmp(str, "content-type"))==0 &&
+			   (str=pl_getattr(g.attr, "content")) &&
+			   (str=cistrstr(str, "charset="))){
+				strncpy(g.charset, str+8, sizeof(g.charset));
+				pl_applycharset(&g);
+			}
+			break;
+		case Tag_img:
+			if(str=pl_getattr(g.attr, "src"))
+				strncpy(g.state->image, str, sizeof(g.state->image));
+			g.state->ismap=pl_hasattr(g.attr, "ismap");
+			if(str=pl_getattr(g.attr, "width"))
+				g.state->width = strtolength(&g, HORIZ, str);
+			if(str=pl_getattr(g.attr, "height"))
+				g.state->height = strtolength(&g, VERT, str);
+			str=pl_getattr(g.attr, "alt");
+			if(str==0){
+				if(g.state->image[0])
+					str=g.state->image;
+				else
+					str="[[image]]";
+			}
+			pl_htmloutput(&g, 0, str, 0);
+			g.state->image[0]=0;
+			g.state->ismap=0;
+			g.state->width=0;
+			g.state->height=0;
+			break;
+		case Tag_plaintext:
+			g.spacc=0;
+			plaintext(&g);
+			break;
+		case Tag_comment:
+		case Tag_html:
+		case Tag_link:
+		case Tag_nextid:
+			break;
+		case Tag_table:
+			g.state->table++;
+			break;
+		case Tag_tr:
+			if(g.state->table==1){
+				g.spacc=0;
+				g.linebrk=1;
+			} else
+				g.spacc++;
+			break;
+		case Tag_td:
+			g.spacc++;
+			break;
+		case Tag_a:
+			if(str=pl_getattr(g.attr, "href"))
+				strncpy(g.state->link, str, sizeof(g.state->link));
+			if(str=pl_getattr(g.attr, "name")){
+				strncpy(g.state->name, str, sizeof(g.state->name));
+				pl_htmloutput(&g, 0, "", 0);
+			}
+			break;
+		case Tag_frame:
+			pl_htmloutput(&g, 0, "FRAME: ", 0);
+			if(str=pl_getattr(g.attr, "src"))
+				strncpy(g.state->link, str, sizeof(g.state->link));
+			if(str=pl_getattr(g.attr, "name"))
+				strncpy(g.state->name, str, sizeof(g.state->name));
+			else
+				str = g.state->link;
+			pl_htmloutput(&g, 0, str, 0);
+			g.state->link[0]=0;
+			g.state->name[0] =0;
+			g.spacc=0;
+			g.linebrk=1;
+			break;
+		case Tag_address:
+			g.spacc=0;
+			g.linebrk=1;
+			g.state->font=ROMAN;
+			g.state->size=NORMAL;
+			g.state->margin=300;
+			g.state->indent=50;
+			break;
+		case Tag_b:
+		case Tag_strong:
+			g.state->font=BOLD;
+			break;
+		case Tag_base:
+			if(str=pl_getattr(g.attr, "href")){
+				dst->base=emalloc(sizeof(Url));
+				crackurl(dst->base, str, dst->url);
+			}
+			break;
+		case Tag_blockquot:
+			g.spacc=0;
+			g.linebrk=1;
+			g.state->margin+=50;
+			g.state->indent=20;
+			break;
+		case Tag_body:
+			pl_applycharset(&g);
+		case Tag_head:
+			g.state->font=ROMAN;
+			g.state->size=NORMAL;
+			g.state->margin=0;
+			g.state->indent=20;
+			g.spacc=0;
+			break;
+		case Tag_br:
+			g.spacc=0;
+			g.linebrk=1;
+			break;
+		case Tag_center:
+			/* more to come */
+			break;
+		case Tag_cite:
+			g.state->font=ITALIC;
+			g.state->size=NORMAL;
+			break;
+		case Tag_code:
+			g.state->font=CWIDTH;
+			g.state->size=NORMAL;
+			break;
+		case Tag_dd:
+			g.linebrk=1;
+			g.state->indent=0;
+			g.state->font=ROMAN;
+			g.spacc=0;
+			break;
+		case Tag_dfn:
+			htmlerror(g.name, g.lineno, "<dfn> deprecated");
+			g.state->font=BOLD;
+			g.state->size=NORMAL;
+			break;
+		case Tag_dl:
+			g.state->font=BOLD;
+			g.state->size=NORMAL;
+			g.state->margin+=40;
+			g.spacc=0;
+			break;
+		case Tag_dt:
+			g.para=1;
+			g.state->indent=-40;
+			g.state->font=BOLD;
+			g.spacc=0;
+			break;
+		case Tag_font:
+			/* more to come */
+			break;
+		case Tag_u:
+			htmlerror(g.name, g.lineno, "<u> deprecated");
+		case Tag_em:
+		case Tag_i:
+		case Tag_var:
+			g.state->font=ITALIC;
+			break;
+		case Tag_h1:
+			g.linebrk=1;
+			g.state->font=BOLD;
+			g.state->size=ENORMOUS;
+			g.state->margin+=100;
+			g.spacc=0;
+			break;
+		case Tag_h2:
+			pl_linespace(&g);
+			g.state->font=BOLD;
+			g.state->size=ENORMOUS;
+			g.spacc=0;
+			break;
+		case Tag_h3:
+			g.linebrk=1;
+			pl_linespace(&g);
+			g.state->font=ITALIC;
+			g.state->size=ENORMOUS;
+			g.state->margin+=20;
+			g.spacc=0;
+			break;
+		case Tag_h4:
+			pl_linespace(&g);
+			g.state->font=BOLD;
+			g.state->size=LARGE;
+			g.state->margin+=10;
+			g.spacc=0;
+			break;
+		case Tag_h5:
+			pl_linespace(&g);
+			g.state->font=ITALIC;
+			g.state->size=LARGE;
+			g.state->margin+=10;
+			g.spacc=0;
+			break;
+		case Tag_h6:
+			pl_linespace(&g);
+			g.state->font=BOLD;
+			g.state->size=LARGE;
+			g.spacc=0;
+			break;
+		case Tag_hr:
+			g.spacc=0;
+			plrtbitmap(&g.dst->text, 1000000, g.state->margin, hrule, 0, 0);
+			break;
+		case Tag_key:
+			htmlerror(g.name, g.lineno, "<key> deprecated");
+		case Tag_kbd:
+			g.state->font=CWIDTH;
+			break;
+		case Tag_dir:
+		case Tag_menu:
+		case Tag_ol:
+		case Tag_ul:
+			g.state->number=0;
+			g.linebrk=1;
+			g.state->margin+=25;
+			g.state->indent=-25;
+			g.spacc=0;
+			break;
+		case Tag_li:
+			g.spacc=0;
+			switch(g.state->tag){
+			default:
+				htmlerror(g.name, g.lineno, "can't have <li> in <%s>",
+					tag[g.state->tag].name);
+			case Tag_dir:	/* supposed to be multi-columns, can't do! */
+			case Tag_menu:
+				g.linebrk=1;
+				break;
+			case Tag_ol:
+				g.para=1;
+				snprint(buf, sizeof(buf), "%2d  ", ++g.state->number);
+				pl_htmloutput(&g, 0, buf, 0);
+				break;
+			case Tag_ul:
+				g.para=0;
+				g.linebrk=0;
+				g.spacc=-1;
+				plrtbitmap(&g.dst->text, 100000,
+					g.state->margin+g.state->indent, bullet, 0, 0);
+				break;
+			}
+			break;
+		case Tag_p:
+			g.para=1;
+			g.spacc=0;
+			break;
+		case Tag_listing:
+		case Tag_xmp:
+			htmlerror(g.name, g.lineno, "<%s> deprecated", tag[g.tag].name);
+		case Tag_pre:
+		case Tag_samp:
+			g.state->indent=0;
+			g.state->pre=1;
+			g.state->font=CWIDTH;
+			g.state->size=NORMAL;
+			pl_linespace(&g);
+			break;
+		case Tag_tt:
+			g.state->font=CWIDTH;
+			g.state->size=NORMAL;
+			break;
+		case Tag_title:
+			g.text=dst->title+strlen(dst->title);
+			g.tp=g.text;
+			g.etext=dst->title+NTITLE-1;
+			break;
+		case Tag_form:
+		case Tag_input:
+		case Tag_select:
+		case Tag_option:
+		case Tag_textarea:
+		case Tag_isindex:
+			rdform(&g);
+			break;
+		case Tag_script:
+		case Tag_style:
+			/*
+			 * ignore the content of these tags, eat tokens until we
+			 * reach a matching endtag.
+			 */
+			t = g.tag;
+			for(;;){
+				switch(pl_gettoken(&g)){
+				default:
+					continue;
+				case ENDTAG:
+					if(g.tag != t)
+						continue;
+				case EOF:
+					break;
+				}
+				break;
+			}
+			break;
+		}
+		break;
+
+	case ENDTAG:
+		/*
+		 * If the end tag doesn't match the top, we try to uncover a match
+		 * on the stack.
+		 */
+		if(g.state->tag!=g.tag){
+			tagerr=0;
+			for(sp=g.state;sp!=g.stack;--sp){
+				if(sp->tag==g.tag)
+					break;
+				if(tag[g.state->tag].action!=OPTEND) tagerr++;
+			}
+			if(sp==g.stack){
+				if(tagerr)
+					htmlerror(g.name, g.lineno,
+						"end tag mismatch <%s>...</%s>, ignored",
+						tag[g.state->tag].name, tag[g.tag].name);
+			}
+			else{
+				if(tagerr)
+					htmlerror(g.name, g.lineno,
+						"end tag mismatch <%s>...</%s>, "
+						"intervening tags popped",
+						tag[g.state->tag].name, tag[g.tag].name);
+				g.state=sp-1;
+			}
+		}
+		else if(g.state==g.stack)
+			htmlerror(g.name, g.lineno, "end tag </%s> at stack bottom",
+				tag[g.tag].name);
+		else
+			--g.state;
+		switch(g.tag){
+		case Tag_select:
+		case Tag_form:
+		case Tag_textarea:
+			endform(&g);
+			break;
+		case Tag_h1:
+		case Tag_h2:
+		case Tag_h3:
+		case Tag_h4:
+			pl_linespace(&g);
+			break;
+		case Tag_address:
+		case Tag_blockquot:
+		case Tag_body:
+		case Tag_dir:
+		case Tag_dl:
+		case Tag_dt:
+		case Tag_h5:
+		case Tag_h6:
+		case Tag_listing:
+		case Tag_menu:
+		case Tag_ol:
+		case Tag_samp:
+		case Tag_title:
+		case Tag_ul:
+		case Tag_xmp:
+			g.linebrk=1;
+			break;
+		case Tag_table:
+			if(g.state->table==0)
+				g.linebrk=1;
+			break;
+		case Tag_pre:
+			pl_linespace(&g);
+			break;
+		}
+		break;
+	case TEXT:
+		pl_htmloutput(&g, g.nsp, g.token, 0);
+		break;
+	case EOF:
+		for(;g.state!=g.stack;--g.state)
+			if(tag[g.state->tag].action!=OPTEND)
+				htmlerror(g.name, g.lineno,
+					"missing </%s> at EOF", tag[g.state->tag].name);
+		*g.tp='\0';
+		dst->changed=1;
+		getpix(dst->text, dst);
+		dst->finished=1;
+		return;
+	}
+}
--- /dev/null
+++ b/sys/src/cmd/mothra/tcs.h
@@ -1,0 +1,172 @@
+/* mapping from web charset to tcs */
+char *tcs[] ={
+"iso_8859-1:1987", "8859-1",
+"iso-ir-100", "8859-1",
+"iso_8859-1", "8859-1",
+"iso-8859-1", "8859-1",
+"latin1", "8859-1",
+"l1", "8859-1",
+"ibm819", "8859-1",
+"cp819", "8859-1",
+"csisolatin1", "8859-1",
+"iso_8859-2:1987", "8859-2",
+"iso-ir-101", "8859-2",
+"iso_8859-2", "8859-2",
+"iso-8859-2", "8859-2",
+"latin2", "8859-2",
+"l2", "8859-2",
+"csisolatin2", "8859-2",
+"iso_8859-3:1988", "8859-3",
+"iso-ir-109", "8859-3",
+"iso_8859-3", "8859-3",
+"iso-8859-3", "8859-3",
+"latin3", "8859-3",
+"l3", "8859-3",
+"csisolatin3", "8859-3",
+"iso_8859-4:1988", "8859-4",
+"iso-ir-110", "8859-4",
+"iso_8859-4", "8859-4",
+"iso-8859-4", "8859-4",
+"latin4", "8859-4",
+"l4", "8859-4",
+"csisolatin4", "8859-4",
+"iso_8859-5:1988", "8859-5",
+"iso-ir-144", "8859-5",
+"iso_8859-5", "8859-5",
+"iso-8859-5", "8859-5",
+"cyrillic", "8859-5",
+"csisolatincyrillic", "8859-5",
+"iso_8859-6:1987", "8859-6",
+"iso-ir-127", "8859-6",
+"iso_8859-6", "8859-6",
+"iso-8859-6", "8859-6",
+"ecma-114", "8859-6",
+"asmo-708", "8859-6",
+"arabic", "8859-6",
+"csisolatinarabic", "8859-6",
+"iso_8859-7:1987", "8859-7",
+"iso-ir-126", "8859-7",
+"iso_8859-7", "8859-7",
+"iso-8859-7", "8859-7",
+"elot_928", "8859-7",
+"ecma-118", "8859-7",
+"greek", "8859-7",
+"greek8", "8859-7",
+"csisolatingreek", "8859-7",
+"iso_8859-8:1988", "8859-8",
+"iso-ir-138", "8859-8",
+"iso_8859-8", "8859-8",
+"iso-8859-8", "8859-8",
+"hebrew", "8859-8",
+"csisolatinhebrew", "8859-8",
+"iso_8859-9:1989", "8859-9",
+"iso-ir-148", "8859-9",
+"iso_8859-9", "8859-9",
+"iso-8859-9", "8859-9",
+"latin5", "8859-9",
+"l5", "8859-9",
+"csisolatin5", "8859-9",
+"iso-8859-15", "8859-15",
+"iso_8859-15", "8859-15",
+"latin-9", "8859-15",
+"ansi_x3.4-1968", "ascii",
+"iso-ir-6", "ascii",
+"ansi_x3.4-1986", "ascii",
+"iso_646.irv:1991", "ascii",
+"ascii", "ascii",
+"iso646-us", "ascii",
+"us-ascii", "ascii",
+"us", "ascii",
+"ibm367", "ascii",
+"cp367", "ascii",
+"csascii", "ascii",
+"big5", "big5",
+"csbig5", "big5",
+"ibm437", "ibm437",
+"cp437", "ibm437",
+"437", "ibm437",
+"cspc8codepage437", "ibm437",
+"ibm850", "ibm850",
+"cp850", "ibm850",
+"850", "ibm850",
+"cspc850multilingual", "ibm850",
+"ibm852", "ibm852",
+"cp852", "ibm852",
+"852", "ibm852",
+"cspcp852", "ibm852",
+"ibm855", "ibm855",
+"cp855", "ibm855",
+"855", "ibm855",
+"csibm855", "ibm855",
+"ibm857", "ibm857",
+"cp857", "ibm857",
+"857", "ibm857",
+"csibm857", "ibm857",
+"ibm862", "ibm862",
+"cp862", "ibm862",
+"862", "ibm862",
+"cspc862latinhebrew", "ibm862",
+"ibm866", "ibm866",
+"cp866", "ibm866",
+"866", "ibm866",
+"csibm866", "ibm866",
+"windows-1250", "windows-1250",
+"windows-1251", "windows-1251",
+"windows-1252", "windows-1252",
+"windows-1253", "windows-1253",
+"windows-1254", "windows-1254",
+"windows-1255", "windows-1255",
+"windows-1256", "windows-1256",
+"windows-1257", "windows-1257",
+"windows-1258", "windows-1258",
+"ks_c_5601-1987", "euc-k",
+"iso-ir-149", "euc-k",
+"ks_c_5601-1989", "euc-k",
+"ksc_5601", "euc-k",
+"korean", "euc-k",
+"csksc56011987", "euc-k",
+"euc-kr", "euc-k",
+"cseuckr", "euc-k",
+"gb2312", "gb",
+"gbk", "gbk",
+"csgb2312", "gb",
+"gb_2312-80", "gb",
+"iso-ir-58", "gb",
+"chinese", "gb",
+"csiso58gb231280", "gb",
+"iso-2022-jp", "jis-kanji",
+"csiso2022jp", "jis-kanji",
+"koi8-r", "koi8",
+"cskoi8r", "koi8",
+"macintosh", "macrom",
+"mac", "macrom",
+"csmacintosh", "macrom",
+"ibm865", "msdos2",
+"cp865", "msdos2",
+"865", "msdos2",
+"csibm865", "msdos2",
+"shift_jis", "ms-kanji",
+"ms_kanji", "ms-kanji",
+"csshiftjis", "ms-kanji",
+"sen_850200_b", "sf1",
+"iso-ir-10", "sf1",
+"fi", "sf1",
+"iso646-fi", "sf1",
+"iso646-se", "sf1",
+"se", "sf1",
+"csiso10swedish", "sf1",
+"sen_850200_c", "sf2",
+"iso-ir-11", "sf2",
+"iso646-se2", "sf2",
+"se2", "sf2",
+"csiso11swedishfornames", "sf2",
+"tis-620", "tis",
+"extended_unix_code_packed_format_for_japanese", "ujis",
+"cseucpkdfmtjapanese", "ujis",
+"euc-jp", "ujis",
+"iso-10646-utf-1", "utf1",
+"csiso10646utf1", "utf1",
+"viscii", "viscii",
+"csviscii", "viscii",
+nil, nil,
+};
--- /dev/null
+++ b/sys/src/cmd/mothra/urlcanon.c
@@ -1,0 +1,70 @@
+#include <u.h>
+#include <libc.h>
+void *emalloc(int n){
+	void *p;
+	p=malloc(n);
+	if(p==0){
+		fprint(2, "can't malloc\n");
+		exits("no mem");
+	}
+	return p;
+}
+void urlcanon(char *name){
+	char *s, *t;
+	char **comp, **p, **q;
+	int rooted;
+	rooted=name[0]=='/';
+	/*
+	 * Break the name into a list of components
+	 */
+	comp=emalloc(strlen(name)*sizeof(char *));
+	p=comp;
+	*p++=name;
+	for(s=name;;s++){
+		if(*s=='/'){
+			*p++=s+1;
+			*s='\0';
+		}
+		else if(*s=='\0')
+			break;
+	}
+	*p=0;
+	/*
+	 * go through the component list, deleting components that are empty (except
+	 * the last component) or ., and any .. and its non-.. predecessor.
+	 */
+	p=q=comp;
+	while(*p){
+		if(strcmp(*p, "")==0 && p[1]!=0
+		|| strcmp(*p, ".")==0)
+			p++;
+		else if(strcmp(*p, "..")==0 && q!=comp && strcmp(q[-1], "..")!=0){
+			--q;
+			p++;
+		}
+		else
+			*q++=*p++;
+	}
+	*q=0;
+	/*
+	 * rebuild the path name
+	 */
+	s=name;
+	if(rooted) *s++='/';
+	for(p=comp;*p;p++){
+		t=*p;
+		while(*t) *s++=*t++;
+		if(p[1]!=0) *s++='/';
+	}
+	*s='\0';
+	free(comp);
+}
+void main(int argc, char *argv[]){
+	int i;
+	for(i=1;i!=argc;i++){
+		print("%s: ", argv[i]);
+		urlcanon(argv[i]);
+		print("%s\n", argv[i]);
+	}
+	exits(0);
+}
--- /dev/null
+++ b/sys/src/cmd/mothra/version.c
@@ -1,0 +1,1 @@
+char version[]="Sep-4-19:07:30-CET-2011";
--