shithub: fork

Download patch

ref: eea1eebb1304de23bca3b3e9759702c3d391bdd1
parent: 19c9042e451e61f1d71f79eea86209e9369d1944
author: qwx <[email protected]>
date: Sun Aug 13 23:30:07 EDT 2023

add mothra: theme, font

diff: cannot open b/sys/src/cmd/mothra/libpanel//null: file does not exist: 'b/sys/src/cmd/mothra/libpanel//null' diff: cannot open b/sys/src/cmd/mothra//null: file does not exist: 'b/sys/src/cmd/mothra//null'
--- /dev/null
+++ b/sys/src/cmd/mothra/libpanel/draw.c
@@ -1,0 +1,283 @@
+#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	1	/* width of frame relief */
+#define	SPACE	2	/* 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 Image *pl_light, *pl_dark, *pl_scrl, *pl_tick, *pl_hilit;
+Image *pl_blue, *pl_white, *pl_black, *pl_txt;
+int pl_drawinit(void){
+	enum{
+		Cwhite,
+		Clight,
+		Cdark,
+		Cblack,
+		Cblue,
+		Ctxt,
+		Cscrl,
+		Ncols,
+	};
+	Theme th[Ncols] = {
+		[Cwhite] { "rioback",	DWhite },
+		[Clight] { "back", 	DWhite },
+		[Cdark] { "menubar",	0x777777FF },
+		[Cblack] { "menuhigh", 	DBlack },
+		[Cblue] { "hold",	0x0000FFFF },
+		[Ctxt] { "text",	DBlack },
+		[Cscrl] { "border",	0x999999FF },
+	};
+	readtheme(th, nelem(th), nil);
+	pl_white=allocimage(display, Rect(0,0,1,1), screen->chan, 1, th[Cwhite].c);
+	pl_light=allocimage(display, Rect(0,0,1,1), screen->chan, 1, th[Clight].c);
+	pl_dark=allocimage(display, Rect(0,0,1,1), screen->chan, 1, th[Cdark].c);
+	pl_black=allocimage(display, Rect(0,0,1,1), screen->chan, 1, th[Cblack].c);
+	pl_blue=allocimage(display, Rect(0,0,1,1), screen->chan, 1, th[Cblue].c);
+	pl_txt=allocimage(display, Rect(0,0,1,1), screen->chan, 1, th[Ctxt].c);
+	pl_scrl=allocimage(display, Rect(0,0,1,1), screen->chan, 1, th[Cscrl].c);
+	pl_hilit=allocimage(display, Rect(0,0,1,1), CHAN1(CAlpha,8), 1, 0x80);
+	if((pl_tick = allocimage(display, Rect(0, 0, TICKW, font->height), screen->chan, 0x00ff00, DNofill)) != nil){
+		draw(pl_tick, pl_tick->r, pl_white, nil, ZP);
+		draw(pl_tick, Rect(TICKW/2, 0, TICKW/2+1, font->height), pl_black, nil, ZP);
+		draw(pl_tick, Rect(0, 0, TICKW, TICKW), pl_black, nil, ZP);
+		draw(pl_tick, Rect(0, font->height-TICKW, TICKW, font->height), pl_black, nil, ZP);
+	}
+	if(pl_white==0 || pl_light==0 || pl_black==0 || pl_dark==0 || pl_scrl==0 || pl_blue==0 || pl_tick==0 || pl_txt==0) sysfatal("allocimage: %r");
+	return 1;
+}
+
+Rectangle pl_boxoutline(Image *b, Rectangle r, int style, int fill){
+	int doborder;
+	
+	doborder = (style & BORDER) != 0;
+	switch(style & ~BORDER){
+	case SUP:
+	case TUP:
+		if(fill) draw(b, r, pl_light, 0, ZP);
+		else border(b, r, BWID+SPACE, pl_white, ZP);
+		if(doborder) border(b, r, BWID, pl_black, ZP);
+		r=insetrect(r, BWID);
+		break;
+	case UP:
+		if(fill) draw(b, r, pl_light, 0, ZP);
+		else border(b, r, BWID+SPACE, pl_white, ZP);
+		if(doborder) border(b, r, BWID, pl_black, ZP);
+		r=insetrect(r, BWID);
+		break;
+	case DOWN:
+	case DOWN1:
+	case DOWN2:
+	case DOWN3:
+		if(fill) draw(b, r, pl_dark, 0, ZP);
+		else border(b, r, BWID+SPACE, pl_dark, ZP);
+		if(doborder) border(b, r, BWID, pl_black, ZP);
+		r=insetrect(r, BWID);
+		break;
+	case PASSIVE:
+		if(fill) draw(b, r, pl_light, 0, ZP);
+		else border(b, r, PWID+SPACE, pl_white, ZP);
+		if(doborder) border(b, r, BWID, pl_black, ZP);
+		r=insetrect(r, PWID);
+		break;
+	case FRAME:
+		border(b, r, FWID, pl_black, ZP);
+		r=insetrect(r, FWID);
+		if(fill) draw(b, r, pl_light, 0, ZP);
+		else border(b, r, SPACE, pl_white, ZP);
+		break;
+	}
+	switch(style){
+	case SUP: return insetrect(r, SPACE-SPACE);
+	default: 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(2*FWID+2*SPACE, 2*FWID+2*SPACE));
+	}
+	return Pt(0, 0);
+}
+void pl_interior(int state, Point *ul, Point *size){
+	switch(state){
+	case UP:
+	case DOWN:
+	case DOWN1:
+	case DOWN2:
+	case DOWN3:
+		*ul=addpt(*ul, Pt(BWID+SPACE, BWID+SPACE));
+		*size=subpt(*size, Pt(2*(BWID+SPACE), 2*(BWID+SPACE)));
+		break;
+	case PASSIVE:
+		*ul=addpt(*ul, Pt(PWID+SPACE, PWID+SPACE));
+		*size=subpt(*size, Pt(2*(PWID+SPACE), 2*(PWID+SPACE)));
+		break;
+	case FRAME:
+		*ul=addpt(*ul, Pt(FWID+SPACE, FWID+SPACE));
+		*size=subpt(*size, Pt(2*FWID+2*SPACE, 2*FWID+2*SPACE));
+	}
+}
+
+void pl_drawicon(Image *b, Rectangle r, int stick, int flags, Icon *s){
+	Rectangle save;
+	Point ul, offs;
+	ul=r.min;
+	offs=subpt(subpt(r.max, r.min), pl_iconsize(flags, s));
+	switch(stick){
+	case PLACENW:	                                break;
+	case PLACEN:	ul.x+=offs.x/2;                 break;
+	case PLACENE:	ul.x+=offs.x;                   break;
+	case PLACEW:	                ul.y+=offs.y/2; break;
+	case PLACECEN:	ul.x+=offs.x/2; ul.y+=offs.y/2; break;
+	case PLACEE:	ul.x+=offs.x;                   break;
+	case PLACESW:	                ul.y+=offs.y;   break;
+	case PLACES:	ul.x+=offs.x/2; ul.y+=offs.y;   break;
+	case PLACESE:	ul.x+=offs.x;   ul.y+=offs.y;   break;
+	}
+	save=b->clipr;
+	if(!rectclip(&r, save))
+		return;
+	replclipr(b, b->repl, r);
+	if(flags&BITMAP) draw(b, Rpt(ul, addpt(ul, pl_iconsize(flags, s))), s, 0, ZP);
+	else string(b, ul, pl_black, ZP, font, s);
+	replclipr(b, b->repl, save);
+}
+/*
+ * Place a check mark at the left end of r.  Return the unused space.
+ * Caller must guarantee that r.max.x-r.min.x>=r.max.y-r.min.y!
+ */
+Rectangle pl_radio(Image *b, Rectangle r, int val){
+	Rectangle remainder;
+	remainder=r;
+	r.max.x=r.min.x+r.max.y-r.min.y;
+	remainder.min.x=r.max.x;
+	r=insetrect(r, CKINSET);
+	border(b, r, CKWID, pl_white, ZP);
+	r=insetrect(r, CKWID);
+	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);
+	border(b, r, CKWID, pl_white, ZP);
+	r=insetrect(r, CKWID);
+	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_scrollupd(Image *b, Rectangle r, int lo, int hi)
+{
+	Rectangle sr;
+	if(lo<0) lo=0;
+	if(hi<=lo) hi=lo+1;
+	sr=r;
+	sr.min.y+=lo;
+	sr.max.x-=1;
+	sr.max.y=sr.min.y+hi;
+	if(sr.max.y>r.max.y) sr.max.y=r.max.y;
+	draw(b, r, pl_scrl, 0, ZP);
+	draw(b, sr, pl_light, 0, ZP);
+}
+void pl_draw1(Panel *p, Image *b);
+void pl_drawall(Panel *p, Image *b){
+	if(p->flags&INVIS || p->flags&IGNORE) return;
+	p->b=b;
+	p->draw(p);
+	for(p=p->child;p;p=p->next) pl_draw1(p, b);
+}
+void pl_draw1(Panel *p, Image *b){
+	if(b!=0)
+		pl_drawall(p, b);
+}
+void pldraw(Panel *p, Image *b){
+	pl_draw1(p, b);
+}
+void pl_invis(Panel *p, int v){
+	for(;p;p=p->next){
+		if(v) p->flags|=INVIS; else p->flags&=~INVIS;
+		pl_invis(p->child, v);
+	}
+}
+Point pl_iconsize(int flags, Icon *p){
+	if(flags&BITMAP) return subpt(((Image *)p)->r.max, ((Image *)p)->r.min);
+	return stringsize(font, (char *)p);
+}
+void pl_highlight(Image *b, Rectangle r){
+	draw(b, r, pl_dark, pl_hilit, ZP);
+}
+void pl_drawtick(Image *b, Rectangle r){
+	draw(b, r, pl_tick, nil, ZP);
+}
+void pl_clr(Image *b, Rectangle r){
+	draw(b, r, pl_white, 0, ZP);
+}
+void pl_fill(Image *b, Rectangle r){
+	draw(b, r, 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/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, pl_txt, 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/pldefs.h
@@ -1,0 +1,113 @@
+/*
+ * Definitions for internal use only
+ */
+/*
+ * Variable-font text routines
+ * These could make a separate library.
+ */
+Point pl_rtfmt(Rtext *, int);
+void pl_rtdraw(Image *, Rectangle, Rtext *, Point);
+void pl_rtredraw(Image *, Rectangle, Rtext *, Point, Point, int);
+Rtext *pl_rthit(Rtext *, Point, Point, Point);
+#define	HITME	0x08000		/* tells ptinpanel not to look at children */
+#define	LEAF	0x10000		/* newpanel will refuse to attach children */
+#define	INVIS	0x20000		/* don't draw this */
+#define	REMOUSE	0x40000		/* send next mouse event here, even if not inside */
+#define	TICKW	3			/* tick width */
+/*
+ * States, also styles
+ */
+enum{
+	SUP,	// scrollbar
+	TUP,	// textview
+	UP,	// deprecated
+	DOWN1,
+	DOWN2,
+	DOWN3,
+	DOWN,
+	PASSIVE,
+	FRAME,
+	BORDER = 1<<8,
+};
+/*
+ * Scroll flags
+ */
+enum{
+	SCROLLUP,
+	SCROLLDOWN,
+	SCROLLABSY,
+	SCROLLLEFT,
+	SCROLLRIGHT,
+	SCROLLABSX,
+};
+
+extern Image *pl_blue, *pl_white, *pl_black, *pl_txt;
+
+/*
+ * Scrollbar, slider orientations
+ */
+enum{
+	HORIZ,
+	VERT
+};
+Panel *pl_newpanel(Panel *, int);	/* make a new Panel, given parent & data size */
+void *pl_emalloc(int);			/* allocate some space, exit on error */
+void *pl_erealloc(void*,int);		/* reallocate some space, exit on error */
+void pl_print(Panel *);			/* print a Panel tree */
+Panel *pl_ptinpanel(Point, Panel *);	/* highest-priority subpanel containing point */
+/*
+ * Drawing primitives
+ */
+int pl_drawinit(void);
+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_scrollupd(Image *, Rectangle, int, int);
+void pl_invis(Panel *, int);
+Point pl_iconsize(int, Icon *);
+void pl_highlight(Image *, Rectangle);
+void pl_drawtick(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/rtext.c
@@ -1,0 +1,375 @@
+/*
+ * Rich text with images.
+ * Should there be an offset field, to do subscripts & kerning?
+ */
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <panel.h>
+#include "pldefs.h"
+#include "rtext.h"
+
+#define LEAD	4	/* extra space between lines */
+#define BORD	2	/* extra border for images */
+
+Rtext *pl_rtnew(Rtext **t, int space, int indent, int voff, Image *b, Panel *p, Font *f, char *s, int flags, void *user){
+	Rtext *new;
+	new=pl_emalloc(sizeof(Rtext));
+	new->flags=flags;
+	new->user=user;
+	new->space=space;
+	new->indent=indent;
+	new->voff=voff;
+	new->b=b;
+	new->p=p;
+	new->font=f;
+	new->text=s;
+	new->next=0;
+	new->nextline=0;
+	new->r=Rect(0,0,0,0);
+	if(*t)
+		(*t)->last->next=new;
+	else
+		*t=new;
+	(*t)->last=new;
+	return new;
+}
+Rtext *plrtpanel(Rtext **t, int space, int indent, int voff, Panel *p, void *user){
+	return pl_rtnew(t, space, indent, voff, 0, p, 0, 0, 1, user);
+}
+Rtext *plrtstr(Rtext **t, int space, int indent, int voff, Font *f, char *s, int flags, void *user){
+	return pl_rtnew(t, space, indent, voff, 0, 0, f, s, flags, user);
+}
+Rtext *plrtbitmap(Rtext **t, int space, int indent, int voff, Image *b, int flags, void *user){
+	return pl_rtnew(t, space, indent, voff, b, 0, 0, 0, flags, user);
+}
+void plrtfree(Rtext *t){
+	Rtext *next;
+	while(t){
+		next=t->next;
+		free(t);
+		t=next;
+	}
+}
+int pl_tabmin, pl_tabsize;
+void pltabsize(int min, int size){
+	pl_tabmin=min;
+	pl_tabsize=size;
+}
+int pl_space(int space, int pos, int indent){
+	if(space>=0) return space;
+	switch(PL_OP(space)){
+	default:
+		return 0;
+	case PL_TAB:
+		return ((pos-indent+pl_tabmin)/pl_tabsize+PL_ARG(space))*pl_tabsize+indent-pos;
+	}
+}
+/*
+ * initialize rectangles & nextlines of text starting at t,
+ * galley width is wid.  Returns the total width/height of the text
+ */
+Point pl_rtfmt(Rtext *t, int wid){
+	Rtext *tp, *eline;
+	int ascent, descent, x, space, a, d, w, topy, indent, maxwid;
+	Point p;
+
+	p=Pt(0,0);
+	eline=t;
+	maxwid=0;
+	while(t){
+		ascent=0;
+		descent=0;
+		indent=space=pl_space(t->indent, 0, 0);
+		x=0;
+		tp=t;
+		for(;;){
+			if(tp->b){
+				a=tp->b->r.max.y-tp->b->r.min.y+BORD;
+				d=BORD;
+				w=tp->b->repl?wid-x:tp->b->r.max.x-tp->b->r.min.x+BORD*2;
+			}
+			else if(tp->p){
+				/* what if plpack fails? */
+				plpack(tp->p, Rect(0,0,wid,wid));
+				plmove(tp->p, subpt(Pt(0,0), tp->p->r.min));
+				a=tp->p->r.max.y-tp->p->r.min.y;
+				d=0;
+				w=tp->p->r.max.x-tp->p->r.min.x;
+			}
+			else{
+				a=tp->font->ascent;
+				d=tp->font->height-a;
+				w=tp->wid=stringwidth(tp->font, tp->text);
+			}
+			a-=tp->voff,d+=tp->voff;
+			if(x+w+space>wid) break;
+			if(a>ascent) ascent=a;
+			if(d>descent) descent=d;
+			x+=w+space;
+			tp=tp->next;
+			if(tp==0){
+				eline=0;
+				break;
+			}
+			space=pl_space(tp->space, x, indent);
+			if(space) eline=tp;
+		}
+		if(eline==t){	/* No progress!  Force fit the first block! */
+			if(tp==t){
+				if(a>ascent) ascent=a;
+				if(d>descent) descent=d;
+				eline=tp->next;
+			}else
+				eline=tp;
+		}
+		topy=p.y;
+		p.y+=ascent;
+		p.x=indent=pl_space(t->indent, 0, 0);
+		for(;;){
+			t->topy=topy;
+			t->r.min.x=p.x;
+			p.y+=t->voff;
+			if(t->b){
+				t->r.max.y=p.y+BORD;
+				t->r.min.y=p.y-(t->b->r.max.y-t->b->r.min.y)-BORD;
+				p.x+=t->b->repl?wid-p.x:(t->b->r.max.x-t->b->r.min.x)+BORD*2;
+			}
+			else if(t->p){
+				t->r.max.y=p.y;
+				t->r.min.y=p.y-t->p->r.max.y;
+				p.x+=t->p->r.max.x;
+			}
+			else{
+				t->r.min.y=p.y-t->font->ascent;
+				t->r.max.y=t->r.min.y+t->font->height;
+				p.x+=t->wid;
+			}
+			p.y-=t->voff;
+			t->r.max.x=p.x;
+			t->nextline=eline;
+			t=t->next;
+			if(t==eline) break;
+			p.x+=pl_space(t->space, p.x, indent);
+		}
+		if(p.x>maxwid) maxwid=p.x;
+		p.y+=descent+LEAD;
+	}
+	return Pt(maxwid, p.y);
+}
+
+/*
+ * If we draw the text in a backup bitmap and copy it onto the screen,
+ * the bitmap pointers in all the subpanels point to the wrong bitmap.
+ * This code fixes them.
+ */
+void pl_stuffbitmap(Panel *p, Image *b){
+	p->b=b;
+	for(p=p->child;p;p=p->next)
+		pl_stuffbitmap(p, b);
+}
+
+void pl_rtdraw(Image *b, Rectangle r, Rtext *t, Point offs){
+	static Image *backup;
+	Point lp, sp;
+	Rectangle dr;
+	Image *bb;
+
+	bb = b;
+	if(backup==0 || backup->chan!=b->chan || rectinrect(r, backup->r)==0){
+		freeimage(backup);
+		backup=allocimage(display, bb->r, bb->chan, 0, DNofill);
+	}
+	if(backup)
+		b=backup;
+	pl_clr(b, r);
+	lp=ZP;
+	sp=ZP;
+	offs=subpt(r.min, offs);
+	for(;t;t=t->next) if(!eqrect(t->r, Rect(0,0,0,0))){
+		dr=rectaddpt(t->r, offs);
+		if(dr.max.y>r.min.y
+		&& dr.min.y<r.max.y
+		&& dr.max.x>r.min.x
+		&& dr.min.x<r.max.x){
+			if(t->b){
+				if(t->flags&PL_HOT) border(b, dr, 1, pl_blue, ZP);
+				draw(b, insetrect(dr, BORD), t->b, 0, t->b->r.min);
+				if(t->flags&PL_STR) {
+					line(b, Pt(dr.min.x, dr.min.y), Pt(dr.max.x, dr.max.y),
+						Endsquare, Endsquare, 0,
+						pl_txt, ZP);
+					line(b, Pt(dr.min.x, dr.max.y), Pt(dr.max.x, dr.min.y),
+						Endsquare, Endsquare, 0,
+						pl_txt, ZP);
+				}
+				if(t->flags&PL_SEL)
+					pl_highlight(b, dr);
+			}
+			else if(t->p){
+				plmove(t->p, subpt(dr.min, t->p->r.min));
+				pldraw(t->p, b);
+				if(b!=bb)
+					pl_stuffbitmap(t->p, bb);
+			}
+			else{
+				if(t->flags&PL_HOT)
+					string(b, dr.min, pl_blue, ZP, t->font, t->text);
+				else
+					string(b, dr.min, pl_txt, ZP, t->font, t->text);
+				if(t->flags&PL_SEL)
+					pl_highlight(b, dr);
+				if(t->flags&PL_STR){
+					int y = dr.max.y - t->font->height/2;
+					if(sp.y != y)
+						sp = Pt(dr.min.x, y);
+					line(b, sp, Pt(dr.max.x, y),
+						Endsquare, Endsquare, 0,
+						pl_txt, ZP);
+					sp = Pt(dr.max.x, y);
+				} else
+					sp = ZP;
+				if(t->flags&PL_HOT){
+					int y = dr.max.y - 1;
+					if(lp.y != y)
+						lp = Pt(dr.min.x, y);
+					line(b, lp, Pt(dr.max.x, y),
+						Endsquare, Endsquare, 0,
+						pl_blue, ZP);
+					lp = Pt(dr.max.x, y);
+				} else
+					lp = ZP;
+				continue;
+			}
+			lp = ZP;
+			sp = ZP;
+		}
+	}
+	if(b!=bb)
+		draw(bb, r, b, 0, r.min);
+}
+/*
+ * Reposition text already drawn in the window.
+ * We just move the pixels and update the positions of any
+ * enclosed panels
+ */
+void pl_reposition(Rtext *t, Image *b, Point p, Rectangle r){
+	Point offs;
+	pl_cpy(b, p, r);
+	offs=subpt(p, r.min);
+	for(;t;t=t->next)
+		if(!eqrect(t->r, Rect(0,0,0,0)) && !t->b && t->p)
+			plmove(t->p, offs);
+}
+/*
+ * Rectangle r of Image b contains an image of Rtext t, offset by oldoffs.
+ * Redraw the text to have offset yoffs.
+ */
+void pl_rtredraw(Image *b, Rectangle r, Rtext *t, Point offs, Point oldoffs, int dir){
+	int d, size;
+
+	if(dir==VERT){
+		d=oldoffs.y-offs.y;
+		size=r.max.y-r.min.y;
+		if(d>=size || -d>=size) /* move more than screenful */
+			pl_rtdraw(b, r, t, offs);
+		else if(d<0){ /* down */
+			pl_reposition(t, b, r.min,
+				Rect(r.min.x, r.min.y-d, r.max.x, r.max.y));
+			pl_rtdraw(b, Rect(r.min.x, r.max.y+d, r.max.x, r.max.y),
+				t, Pt(offs.x, offs.y+size+d));
+		}
+		else if(d>0){ /* up */
+			pl_reposition(t, b, Pt(r.min.x, r.min.y+d),
+				Rect(r.min.x, r.min.y, r.max.x, r.max.y-d));
+			pl_rtdraw(b, Rect(r.min.x, r.min.y, r.max.x, r.min.y+d),
+				t, offs);
+		}
+	}else{ /* dir==HORIZ */
+		d=oldoffs.x-offs.x;
+		size=r.max.x-r.min.x;
+		if(d>=size || -d>=size) /* move more than screenful */
+			pl_rtdraw(b, r, t, offs);
+		else if(d<0){ /* right */
+			pl_reposition(t, b, r.min,
+				Rect(r.min.x-d, r.min.y, r.max.x, r.max.y));
+			pl_rtdraw(b, Rect(r.max.x+d, r.min.y, r.max.x, r.max.y),
+				t, Pt(offs.x+size+d, offs.y));
+		}
+		else if(d>0){ /* left */
+			pl_reposition(t, b, Pt(r.min.x+d, r.min.y),
+				Rect(r.min.x, r.min.y, r.max.x-d, r.max.y));
+			pl_rtdraw(b, Rect(r.min.x, r.min.y, r.min.x+d, r.max.y),
+				t, offs);
+		}		
+	}
+}
+Rtext *pl_rthit(Rtext *t, Point offs, Point p, Point ul){
+	Rectangle r;
+	Point lp;
+	if(t==0) return 0;
+	p.x+=offs.x-ul.x;
+	p.y+=offs.y-ul.y;
+	while(t->nextline && t->nextline->topy<=p.y) t=t->nextline;
+	lp=ZP;
+	for(;t!=0;t=t->next){
+		if(t->topy>p.y) return 0;
+		r = t->r;
+		if((t->flags&PL_HOT) != 0 && t->b == nil && t->p == nil){
+			if(lp.y == r.max.y && lp.x < r.min.x)
+				r.min.x=lp.x;
+			lp=r.max;
+		} else
+			lp=ZP;
+		if(ptinrect(p, r)) return t;
+	}
+	return 0;
+}
+
+void plrtseltext(Rtext *t, Rtext *s, Rtext *e){
+	while(t){
+		t->flags &= ~PL_SEL;
+		t = t->next;
+	}
+	if(s==0 || e==0)
+		return;
+	for(t=s; t!=0 && t!=e; t=t->next)
+		;
+	if(t==e){
+		for(t=s; t!=e; t=t->next)
+			t->flags |= PL_SEL;
+	}else{
+		for(t=e; t!=s; t=t->next)
+			t->flags |= PL_SEL;
+	}
+	t->flags |= PL_SEL;
+}
+
+char *plrtsnarftext(Rtext *w){
+	char *b, *p, *e, *t;
+	int n;
+
+	b=p=e=0;
+	for(; w; w = w->next){
+		if((w->flags&PL_SEL)==0 || w->text==0)
+			continue;
+		n = strlen(w->text)+64;
+		if(p+n >= e){
+			n = (p+n+64)-b;
+			t = pl_erealloc(b, n);
+			p = t+(p-b);
+			e = t+n;
+			b = t;
+		}
+		if(w->space == 0)
+			p += sprint(p, "%s", w->text);
+		else if(w->space > 0)
+			p += sprint(p, " %s", w->text);
+		else if(PL_OP(w->space) == PL_TAB)
+			p += sprint(p, "\t%s", w->text);
+		if(w->nextline == w->next)
+			p += sprint(p, "\n");
+	}
+	return b;
+}
--- /dev/null
+++ b/sys/src/cmd/mothra/libpanel/textwin.c
@@ -1,0 +1,474 @@
+/*
+ * Text windows
+ *	void twhilite(Textwin *t, int sel0, int sel1, int on)
+ *		hilite (on=1) or unhilite (on=0) a range of characters
+ *	void twselect(Textwin *t, Mouse *m)
+ *		set t->sel0, t->sel1 from mouse input.
+ *		Also hilites selection.
+ *		Caller should first unhilite previous selection.
+ *	void twreplace(Textwin *t, int r0, int r1, Rune *ins, int nins)
+ *		Replace the given range of characters with the given insertion.
+ *		Caller should unhilite selection while this is called.
+ *	void twscroll(Textwin *t, int top)
+ *		Character with index top moves to the top line of the screen.
+ *	int twpt2rune(Textwin *t, Point p)
+ *		which character is displayed at point p?
+ *	void twreshape(Textwin *t, Rectangle r)
+ *		save r and redraw the text
+ *	Textwin *twnew(Bitmap *b, Font *f, Rune *text, int ntext)
+ *		create a new text window
+ *	void twfree(Textwin *t)
+ *		get rid of a surplus Textwin
+ */
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <panel.h>
+#include "pldefs.h"
+
+#define SLACK 100
+
+/*
+ * Is text at point a before or after that at point b?
+ */
+int tw_before(Textwin *t, Point a, Point b){
+	return a.y<b.y || a.y<b.y+t->hgt && a.x<b.x;
+}
+/*
+ * Return the character index indicated by point p, or -1
+ * if its off-screen.  The screen must be up-to-date.
+ *
+ * Linear search should be binary search.
+ */
+int twpt2rune(Textwin *t, Point p){
+	Point *el, *lp;
+	el=t->loc+(t->bot-t->top);
+	for(lp=t->loc;lp!=el;lp++)
+		if(tw_before(t, p, *lp)){
+			if(lp==t->loc) return t->top;
+			return lp-t->loc+t->top-1;
+		}
+	return t->bot;
+}
+/*
+ * Return ul corner of the character with the given index
+ */
+Point tw_rune2pt(Textwin *t, int i){
+	if(i<t->top) return t->r.min;
+	if(i>t->bot) return t->r.max;
+	return t->loc[i-t->top];
+}
+/*
+ * Store p at t->loc[l], extending t->loc if necessary
+ */
+void tw_storeloc(Textwin *t, int l, Point p){
+	int nloc;
+	if(l>=t->eloc-t->loc){
+		nloc=l+SLACK;
+		t->loc=pl_erealloc(t->loc, nloc*sizeof(Point));
+		t->eloc=t->loc+nloc;
+	}
+	t->loc[l]=p;
+}
+/*
+ * Set the locations at which the given runes should appear.
+ * Returns the index of the first rune not set, which might not
+ * be last because we reached the bottom of the window.
+ *
+ * N.B. this zaps the loc of r[last], so that value should be saved first,
+ * if it's important.
+ */
+int tw_setloc(Textwin *t, int first, int last, Point ul){
+	Rune *r, *er;
+	int x, dt, lp;
+	char buf[UTFmax+1];
+	er=t->text+last;
+	for(r=t->text+first,lp=first-t->top;r!=er && ul.y+t->hgt<=t->r.max.y;r++,lp++){
+		tw_storeloc(t, lp, ul);
+		switch(*r){
+		case '\n':
+			ul.x=t->r.min.x;
+			ul.y+=t->hgt;
+			break;
+		case '\t':
+			x=ul.x-t->r.min.x+t->mintab+t->tabstop;
+			x-=x%t->tabstop;
+			ul.x=x+t->r.min.x;
+			if(ul.x>t->r.max.x){
+				ul.x=t->r.min.x;
+				ul.y+=t->hgt;
+				tw_storeloc(t, lp, ul);
+				if(ul.y+t->hgt>t->r.max.y) return r-t->text;
+				ul.x+=+t->tabstop;
+			}
+			break;
+		default:
+			buf[runetochar(buf, r)]='\0';
+			dt=stringwidth(t->font, buf);
+			ul.x+=dt;
+			if(ul.x>t->r.max.x){
+				ul.x=t->r.min.x;
+				ul.y+=t->hgt;
+				tw_storeloc(t, lp, ul);
+				if(ul.y+t->hgt>t->r.max.y) return r-t->text;
+				ul.x+=dt;
+			}
+			break;
+		}
+	}
+	tw_storeloc(t, lp, ul);
+	return r-t->text;
+}
+/*
+ * Draw the given runes at their locations.
+ * Bug -- saving up multiple characters would
+ * reduce the number of calls to string,
+ * and probably make this a lot faster.
+ */
+void tw_draw(Textwin *t, int first, int last){
+	Rune *r, *er;
+	Point *lp, ul, ur;
+	char buf[UTFmax+1];
+	if(first<t->top) first=t->top;
+	if(last>t->bot) last=t->bot;
+	if(last<=first) return;
+	er=t->text+last;
+	for(r=t->text+first,lp=t->loc+(first-t->top);r!=er;r++,lp++){
+		if(lp->y+t->hgt>t->r.max.y){
+			fprint(2, "chr %C, index %zd 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, pl_txt, ZP, t->font, buf);
+			break;
+		}
+		if(lp[1].y!=lp[0].y)
+	/***/		pl_clr(t->b, Rpt(ur, Pt(t->r.max.x, ur.y+t->hgt)));
+	}
+}
+/*
+ * Hilight the characters with tops between ul and ur
+ */
+void tw_hilitep(Textwin *t, Point ul, Point ur){
+	Point swap;
+	int y;
+	if(tw_before(t, ur, ul)){ swap=ul; ul=ur; ur=swap;}
+	y=ul.y+t->hgt;
+	if(y>t->r.max.y) y=t->r.max.y;
+	if(ul.y==ur.y)
+		pl_highlight(t->b, Rpt(ul, Pt(ur.x, y)));
+	else{
+		pl_highlight(t->b, Rpt(ul, Pt(t->r.max.x, y)));
+		ul=Pt(t->r.min.x, y);
+		pl_highlight(t->b, Rpt(ul, Pt(t->r.max.x, ur.y)));
+		ul=Pt(t->r.min.x, ur.y);
+		y=ur.y+t->hgt;
+		if(y>t->r.max.y) y=t->r.max.y;
+		pl_highlight(t->b, Rpt(ul, Pt(ur.x, y)));
+	}
+}
+/*
+ * Hilite/unhilite the given range of characters
+ */
+void twhilite(Textwin *t, int sel0, int sel1, int on){
+	Point ul, ur;
+	int swap, y;
+	if(sel1<sel0){ swap=sel0; sel0=sel1; sel1=swap; }
+	if(sel1<t->top || t->bot<sel0) return;
+	if(sel0<t->top) sel0=t->top;
+	if(sel1>t->bot) sel1=t->bot;
+	if(!on){
+		if(sel1==sel0){
+			ul=t->loc[sel0-t->top];
+			y=ul.y+t->hgt;
+			if(y>t->r.max.y) y=t->r.max.y;
+			pl_clr(t->b, Rpt(ul, Pt(ul.x+1, y)));
+		}else
+			tw_draw(t, sel0, sel1);
+		return;
+	}
+	ul=t->loc[sel0-t->top];
+	if(sel1==sel0)
+		ur=addpt(ul, Pt(1, 0));
+	else
+		ur=t->loc[sel1-t->top];
+	tw_hilitep(t, ul, ur);
+}
+/*
+ * Set t->sel[01] from mouse input.
+ * Also hilites the selection.
+ * Caller should unhilite the previous
+ * selection before calling this.
+ */
+void twselect(Textwin *t, Mouse *m){
+	int sel0, sel1, newsel;
+	Point p0, p1, newp;
+	sel0=sel1=twpt2rune(t, m->xy);
+	p0=tw_rune2pt(t, sel0);
+	p1=addpt(p0, Pt(1, 0));
+	twhilite(t, sel0, sel1, 1);
+	for(;;){
+		if(display->bufp > display->buf)
+			flushimage(display, 1);
+		*m=emouse();
+		if((m->buttons&7)!=1) break;
+		newsel=twpt2rune(t, m->xy);
+		newp=tw_rune2pt(t, newsel);
+		if(eqpt(newp, p0)) newp=addpt(newp, Pt(1, 0));
+		if(!eqpt(newp, p1)){
+			if((sel0<=sel1 && sel1<newsel) || (newsel<sel1 && sel1<sel0))
+				tw_hilitep(t, p1, newp);
+			else if((sel0<=newsel && newsel<sel1) || (sel1<newsel && newsel<=sel0)){
+				twhilite(t, sel1, newsel, 0);
+				if(newsel==sel0)
+					tw_hilitep(t, p0, newp);
+			}else if((newsel<sel0 && sel0<=sel1) || (sel1<sel0 && sel0<=newsel)){
+				twhilite(t, sel0, sel1, 0);
+				tw_hilitep(t, p0, newp);
+			}
+			sel1=newsel;
+			p1=newp;
+		}
+	}
+	if(sel0<=sel1){
+		t->sel0=sel0;
+		t->sel1=sel1;
+	}
+	else{
+		t->sel0=sel1;
+		t->sel1=sel0;
+	}
+}
+/*
+ * Clear the area following the last displayed character
+ */
+void tw_clrend(Textwin *t){
+	Point ul;
+	int y;
+	ul=t->loc[t->bot-t->top];
+	y=ul.y+t->hgt;
+	if(y>t->r.max.y) y=t->r.max.y;
+	pl_clr(t->b, Rpt(ul, Pt(t->r.max.x, y)));
+	ul=Pt(t->r.min.x, y);
+	pl_clr(t->b, Rpt(ul, t->r.max));
+}
+/*
+ * Move part of a line of text, truncating the source or padding
+ * the destination on the right if necessary.
+ */
+void tw_moverect(Textwin *t, Point uld, Point urd, Point uls, Point urs){
+	int sw, dw, d;
+	if(urs.y!=uls.y) urs=Pt(t->r.max.x, uls.y);
+	if(urd.y!=uld.y) urd=Pt(t->r.max.x, uld.y);
+	sw=uls.x-urs.x;
+	dw=uld.x-urd.x;
+	if(dw>sw){
+		d=dw-sw;
+		pl_clr(t->b, Rect(urd.x-d, urd.y, urd.x, urd.y+t->hgt));
+		dw=sw;
+	}
+	pl_cpy(t->b, uld, Rpt(uls, Pt(uls.x+dw, uls.y+t->hgt)));
+}
+/*
+ * Move a block of characters up or to the left:
+ *	Identify contiguous runs of characters whose width doesn't change, and
+ *	move them in one bitblt per run.
+ *	If we get to a point where source and destination are x-aligned,
+ *	they will remain x-aligned for the rest of the block.
+ *	Then, if they are y-aligned, they're already in the right place.
+ *	Otherwise, we can move them in three bitblts; one if all the
+ *	remaining characters are on one line.
+ */
+void tw_moveup(Textwin *t, Point *dp, Point *sp, Point *esp){
+	Point uld, uls;			/* upper left of destination/source */
+	int y;
+	while(sp!=esp && sp->x!=dp->x){
+		uld=*dp;
+		uls=*sp;
+		while(sp!=esp && sp->y==uls.y && dp->y==uld.y && sp->x-uls.x==dp->x-uld.x){
+			sp++;
+			dp++;
+		}
+		tw_moverect(t, uld, *dp, uls, *sp);
+	}
+	if(sp==esp || esp->y==dp->y) return;
+	if(esp->y==sp->y){	/* one line only */
+		pl_cpy(t->b, *dp, Rpt(*sp, Pt(esp->x, sp->y+t->hgt)));
+		return;
+	}
+	y=sp->y+t->hgt;
+	pl_cpy(t->b, *dp, Rpt(*sp, Pt(t->r.max.x, y)));
+	pl_cpy(t->b, Pt(t->r.min.x, dp->y+t->hgt),
+		Rect(t->r.min.x, y, t->r.max.x, esp->y));
+	y=dp->y+esp->y-sp->y;
+	pl_cpy(t->b, Pt(t->r.min.x, y),
+		Rect(t->r.min.x, esp->y, esp->x, esp->y+t->hgt));
+}
+/*
+ * Same as above, but moving down and in reverse order, so as not to overwrite stuff
+ * not moved yet.
+ */
+void tw_movedn(Textwin *t, Point *dp, Point *bsp, Point *esp){
+	Point *sp, urs, urd;
+	int dy;
+	dp+=esp-bsp;
+	sp=esp;
+	dy=dp->y-sp->y;
+	while(sp!=bsp && dp[-1].x==sp[-1].x){
+		--dp;
+		--sp;
+	}
+	if(dy!=0){
+		if(sp->y==esp->y)
+			pl_cpy(t->b, *dp, Rect(sp->x, sp->y, esp->x, esp->y+t->hgt));
+		else{
+			pl_cpy(t->b, Pt(t->r.min.x, sp->x+dy),
+				Rect(t->r.min.x, sp->y, esp->x, esp->y+t->hgt));
+			pl_cpy(t->b, Pt(t->r.min.x, dp->y+t->hgt),
+				Rect(t->r.min.x, sp->y+t->hgt, t->r.max.x, esp->y));
+			pl_cpy(t->b, *dp,
+				Rect(sp->x, sp->y, t->r.max.x, sp->y+t->hgt));
+		}
+	}
+	while(sp!=bsp){
+		urd=*dp;
+		urs=*sp;
+		while(sp!=bsp && sp[-1].y==sp[0].y && dp[-1].y==dp[0].y
+		   && sp[-1].x-sp[0].x==dp[-1].x-dp[0].x){
+			--sp;
+			--dp;
+		}
+		tw_moverect(t, *dp, urd, *sp, urs);
+	}
+}
+/*
+ * Move the given range of characters, already drawn on
+ * the given textwin, to the given location.
+ * Start and end must both index characters that are initially on-screen.
+ */
+void tw_relocate(Textwin *t, int first, int last, Point dst){
+	Point *srcloc;
+	int nbyte;
+	if(first<t->top || last<first || t->bot<last) return;
+	nbyte=(last-first+1)*sizeof(Point);
+	srcloc=pl_emalloc(nbyte);
+	memmove(srcloc, &t->loc[first-t->top], nbyte);
+	tw_setloc(t, first, last, dst);
+	if(tw_before(t, dst, srcloc[0]))
+		tw_moveup(t, t->loc+first-t->top, srcloc, srcloc+(last-first));
+	else
+		tw_movedn(t, t->loc+first-t->top, srcloc, srcloc+(last-first));
+}
+/*
+ * Replace the runes with indices from r0 to r1-1 with the text
+ * pointed to by text, and with length ntext.
+ *	Open up a hole in t->text, t->loc.
+ *	Insert new text, calculate their locs (save the extra loc that's overwritten first)
+ *	(swap saved & overwritten locs)
+ *	move tail.
+ *	calc locs and draw new text after tail, if necessary.
+ *	draw new text, if necessary
+ */
+void twreplace(Textwin *t, int r0, int r1, Rune *ins, int nins){
+	int olen, nlen, tlen, dtop;
+	olen=t->etext-t->text;
+	nlen=olen+nins-(r1-r0);
+	tlen=t->eslack-t->text;
+	if(nlen>tlen){
+		tlen=nlen+SLACK;
+		t->text=pl_erealloc(t->text, tlen*sizeof(Rune));
+		t->eslack=t->text+tlen;
+	}
+	if(olen!=nlen)
+		memmove(t->text+r0+nins, t->text+r1, (olen-r1)*sizeof(Rune));
+	if(nins!=0)	/* ins can be 0 if nins==0 */
+		memmove(t->text+r0, ins, nins*sizeof(Rune));
+	t->etext=t->text+nlen;
+	if(r0>t->bot)		/* insertion is completely below visible text */
+		return;
+	if(r1<t->top){		/* insertion is completely above visible text */
+		dtop=nlen-olen;
+		t->top+=dtop;
+		t->bot+=dtop;
+		return;
+	}
+	if(1 || t->bot<=r0+nins){	/* no useful text on screen below r0 */
+		if(r0<=t->top)	/* no useful text above, either */
+			t->top=r0;
+		t->bot=tw_setloc(t, r0, nlen, t->loc[r0-t->top]);
+		tw_draw(t, r0, t->bot);
+		tw_clrend(t);
+		return;
+	}
+	/*
+	 * code for case where there is useful text below is missing (see `1 ||' above)
+	 */
+}
+/*
+ * This works but is stupid.
+ */
+void twscroll(Textwin *t, int top){
+	while(top!=0 && t->text[top-1]!='\n') --top;
+	t->top=top;
+	t->bot=tw_setloc(t, top, t->etext-t->text, t->r.min);
+	tw_draw(t, t->top, t->bot);
+	tw_clrend(t);
+}
+void twreshape(Textwin *t, Rectangle r){
+	t->r=r;
+	t->bot=tw_setloc(t, t->top, t->etext-t->text, t->r.min);
+	tw_draw(t, t->top, t->bot);
+	tw_clrend(t);
+}
+Textwin *twnew(Image *b, Font *f, Rune *text, int ntext){
+	Textwin *t;
+	t=pl_emalloc(sizeof(Textwin));
+	t->text=pl_emalloc((ntext+SLACK)*sizeof(Rune));
+	t->loc=pl_emalloc(SLACK*sizeof(Point));
+	t->eloc=t->loc+SLACK;
+	t->etext=t->text+ntext;
+	t->eslack=t->etext+SLACK;
+	if(ntext) memmove(t->text, text, ntext*sizeof(Rune));
+	t->top=0;
+	t->bot=0;
+	t->sel0=0;
+	t->sel1=0;
+	t->b=b;
+	t->font=f;
+	t->hgt=f->height;
+	t->mintab=stringwidth(f, "0");
+	t->tabstop=8*t->mintab;
+	return t;
+}
+void twfree(Textwin *t){
+	free(t->loc);
+	free(t->text);
+	free(t);
+}
+/*
+ * Correct the character locations in a textwin after the panel is moved.
+ * This horrid hack would not be necessary if loc values were relative
+ * to the panel, rather than absolute.
+ */
+void twmove(Textwin *t, Point d){
+	Point *lp;
+	t->r = rectaddpt(t->r, d);
+	for(lp=t->loc; lp<t->eloc; lp++)
+		*lp = addpt(*lp, d);
+}
--- /dev/null
+++ b/sys/src/cmd/mothra/mothra.c
@@ -1,0 +1,1282 @@
+/*
+ * 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 <regexp.h>
+#include "mothra.h"
+#include "rtext.h"
+#include "pldefs.h"
+int debug=0;
+int verbose=0;		/* -v flag causes html errors to be written to file-descriptor 2 */
+int killimgs=0;	/* should mothra kill images? */
+int defdisplay=1;	/* is the default (initial) display visible? */
+int visxbar=0;	/* horizontal scrollbar visible? */
+int topxbar=0;	/* horizontal scrollbar at top? */
+Panel *root;	/* the whole display */
+Panel *alt;	/* the alternate display */
+Panel *alttext;	/* the alternate text window */
+Panel *cmd;	/* command entry */
+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 */
+char mothra[] = "mothra!";
+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 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,
+};
+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,
+};
+Cursor mothcurs={
+	{-7, -7},
+	{0x00, 0x00, 0x60, 0x06, 0xf8, 0x1f, 0xfc, 0x3f, 
+	 0xfe, 0x7f, 0xff, 0xff, 0x7f, 0xfe, 0x7f, 0xfe, 
+	 0x7f, 0xfe, 0x3f, 0xfc, 0x3f, 0xfc, 0x1f, 0xf8, 
+	 0x1f, 0xf8, 0x0e, 0x70, 0x0c, 0x30, 0x00, 0x00, },
+	{0x00, 0x00, 0x00, 0x00, 0x60, 0x06, 0x58, 0x1a, 
+	 0x5c, 0x3a, 0x64, 0x26, 0x27, 0xe4, 0x37, 0xec, 
+	 0x37, 0xec, 0x17, 0xe8, 0x1b, 0xd8, 0x0e, 0x70, 
+	 0x0c, 0x30, 0x04, 0x20, 0x00, 0x00, 0x00, 0x00, }
+};
+
+Www *current=0;
+Url *selection=0;
+int mothmode;
+int kickpipe[2];
+
+void docmd(Panel *, char *);
+void doprev(Panel *, int, int);
+char *urlstr(Url *);
+void setcurrent(int, char *);
+char *genwww(Panel *, int);
+void updtext(Www *);
+void dolink(Panel *, int, Rtext *);
+void hit3(int, int);
+void mothon(Www *, int);
+void killpix(Www *w);
+char *buttons[]={
+	"alt display",
+	"moth mode",
+	"snarf",
+	"paste",
+	"plumb",
+	"search",
+	"save hit",
+	"hit list",
+	"exit",
+	0
+};
+
+int wwwtop=0;
+Www *www(int index){
+	static Www a[NWWW];
+	return &a[index % NWWW];
+}
+int nwww(void){
+	return wwwtop<NWWW ? wwwtop : NWWW;
+}
+
+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.
+ */
+int adjkb(void){
+	Rtext *t;
+	int yoffs;
+	if(current){
+		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.min.y
+			&& t->r.min.y+yoffs<text->r.max.y
+			&& t->b==0
+			&& subpanel(t->p, plkbfocus))
+				return 1;
+		}
+	}
+	plgrabkb(cmd);
+	return 0;
+}
+
+void scrollpanel(Panel *p, int dy, int whence)
+{
+	Scroll s;
+
+	s = plgetscroll(p);
+	switch(whence){
+	case 0:
+		s.pos.y = dy;
+		break;
+	case 1:
+		s.pos.y += dy;
+		break;
+	case 2:
+		s.pos.y = s.size.y+dy;
+		break;
+	}
+	if(s.pos.y > s.size.y)
+		s.pos.y = s.size.y;
+	if(s.pos.y < 0)
+		s.pos.y = 0;
+	plsetscroll(p, s);
+}
+
+void sidescroll(int dx, int whence)
+{
+	Scroll s;
+
+	s = plgetscroll(text);
+	switch(whence){
+	case 0:
+		s.pos.x = dx;
+		break;
+	case 1:
+		s.pos.x += dx;
+		break;
+	case 2:
+		s.pos.x = s.size.x+dx;
+		break;
+	}
+	if(s.pos.x > s.size.x - text->size.x + 5)
+		s.pos.x = s.size.x - text->size.x + 5;
+	if(s.pos.x < 0)
+		s.pos.x = 0;
+	plsetscroll(text, s);
+}
+
+void mkpanels(void){
+	Panel *p, *xbar, *ybar, *swap;
+	int xflags;
+
+	if(topxbar)
+		xflags=PACKN|USERFL;
+	else
+		xflags=PACKS|USERFL;
+	if(!visxbar)
+		xflags|=IGNORE;
+	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);
+			ybar=plscrollbar(p, PACKW);
+			list=pllist(p, PACKN|FILLX, genwww, 8, doprev);
+			plscroll(list, 0, ybar);
+		p=plgroup(root, PACKN|FILLX);
+			pllabel(p, PACKW, "Url:");
+			cururl=pllabel(p, PACKE|EXPAND, "---");
+			plplacelabel(cururl, PLACEW);
+		p=plgroup(root, PACKN|EXPAND);
+			ybar=plscrollbar(p, PACKW|USERFL);
+			xbar=plscrollbar(p, xflags);
+			text=pltextview(p, PACKE|EXPAND, Pt(0, 0), 0, dolink);
+			plscroll(text, xbar, ybar);
+	plgrabkb(cmd);
+	alt=plpopup(0, PACKE|EXPAND, 0, 0, menu3);
+		ybar=plscrollbar(alt, PACKW|USERFL);
+		xbar=plscrollbar(alt, xflags);
+		alttext=pltextview(alt, PACKE|EXPAND, Pt(0, 0), 0, dolink);
+		plscroll(alttext, xbar, ybar);
+	if(!defdisplay){
+		swap=root;
+		root=alt;
+		alt=swap;
+		swap=text;
+		text=alttext;
+		alttext=swap;
+	}
+}
+int cohort = -1;
+void killcohort(void){
+	int i;
+	for(i=0;i!=3;i++){	/* It's a long way to the kitchen */
+		postnote(PNGROUP, cohort, "kill\n");
+		sleep(1);
+	}
+}
+void catch(void*, char*){
+	noted(NCONT);
+}
+void dienow(void*, char*){
+	noted(NDFLT);
+}
+
+char* mkhome(void){
+	static char *home;		/* where to put files */
+	char *henv, *tmp;
+	int f;
+
+	if(home == nil){
+		henv=getenv("home");
+		if(henv){
+			tmp = smprint("%s/lib", henv);
+			f=create(tmp, OREAD, DMDIR|0777);
+			if(f!=-1) close(f);
+			free(tmp);
+
+			home = smprint("%s/lib/mothra", henv);
+			f=create(home, OREAD, DMDIR|0777);
+			if(f!=-1) close(f);
+			free(henv);
+		}
+		else
+			home = strdup("/tmp");
+	}
+	return home;
+}
+
+void donecurs(void){
+	if(current && current->alldone==0)
+		esetcursor(&readingcurs);
+	else if(mothmode)
+		esetcursor(&mothcurs);
+	else
+		esetcursor(0);
+}
+
+void drawlock(int dolock){
+	static int ref = 0;
+	if(dolock){
+		if(ref++ == 0)
+			lockdisplay(display);
+	} else {
+		if(--ref == 0)
+			unlockdisplay(display);
+	}
+}
+
+void scrollto(char *tag);
+void search(void);
+
+extern char *mtpt; /* url */
+
+void main(int argc, char *argv[]){
+	Event e;
+	enum { Eplumb = 128, Ekick = 256 };
+	Plumbmsg *pm;
+	char *url;
+	int i;
+
+	quotefmtinstall();
+	fmtinstall('U', Ufmt);
+
+	ARGBEGIN{
+	case 'd': debug=1; break;
+	case 'v': verbose=1; break;
+	case 'k': killimgs=1; break;
+	case 'm':
+		if(mtpt = ARGF())
+			break;
+	case 'a': defdisplay=0; break;
+	default:  goto Usage;
+	}ARGEND
+
+	/*
+	 * so that we can stop all subprocesses with a note,
+	 * and to isolate rendezvous from other processes
+	 */
+	if(cohort=rfork(RFPROC|RFNOTEG|RFNAMEG|RFREND)){
+		atexit(killcohort);
+		notify(catch);
+		waitpid();
+		exits(0);
+	}
+	cohort = getpid();
+	atexit(killcohort);
+
+	switch(argc){
+	default:
+	Usage:
+		fprint(2, "usage: %s [-dvak] [-m mtpt] [url]\n", argv0);
+		exits("usage");
+	case 0:
+		url=getenv("url");
+		break;
+	case 1: url=argv[0]; break;
+	}
+	if(initdraw(0, 0, mothra) < 0)
+		sysfatal("initdraw: %r");
+	display->locking = 1;
+	chrwidth=stringwidth(font, "0");
+	pltabsize(chrwidth, 8*chrwidth);
+	einit(Emouse|Ekeyboard);
+	eplumb(Eplumb, "web");
+	if(pipe(kickpipe) < 0)
+		sysfatal("pipe: %r");
+	estart(Ekick, kickpipe[0], 256);
+	plinit();
+	if(debug) notify(dienow);
+	getfonts();
+	hrule=allocimage(display, Rect(0, 0, 1, 5), screen->chan, 1, DWhite);
+	if(hrule==0)
+		sysfatal("can't allocimage!");
+	draw(hrule, Rect(0,1,1,3), pl_txt, 0, ZP);
+	linespace=display->black;
+	bullet=allocimage(display, Rect(0,0,25, 8), screen->chan, 0, DBlack);
+	fillellipse(bullet, Pt(4,4), 3, 3, pl_txt, ZP);
+	mkpanels();
+	unlockdisplay(display);
+	eresized(0);
+	drawlock(1);
+
+	if(url && url[0])
+		geturl(url, -1, 1, 0);
+
+	mouse.buttons=0;
+	for(;;){
+		if(mouse.buttons==0 && current){
+			if(current->finished){
+				updtext(current);
+				if(current->url->tag[0])
+					scrollto(current->url->tag);
+				current->finished=0;
+				current->changed=0;
+				current->alldone=1;
+				message(mothra);
+				donecurs();
+			}
+		}
+
+		drawlock(0);
+		i=event(&e);
+		drawlock(1);
+
+		switch(i){
+		case Ekick:
+			if(mouse.buttons==0 && current && current->changed){
+				if(!current->finished)
+					updtext(current);
+				current->changed=0;
+			}
+			break;
+		case Ekeyboard:
+			switch(e.kbdc){
+			default:
+Plkey:
+				adjkb();
+				plkeyboard(e.kbdc);
+				break;
+			case Khome:
+				scrollpanel(text, 0, 0);
+				break;
+			case Kup:
+				scrollpanel(text, -text->size.y/4, 1);
+				break;
+			case Kpgup:
+				scrollpanel(text, -text->size.y/2, 1);
+				break;
+			case Kdown:
+				scrollpanel(text, text->size.y/4, 1);
+				break;
+			case Kpgdown:
+				scrollpanel(text, text->size.y/2, 1);
+				break;
+			case Kend:
+				scrollpanel(text, -text->size.y, 2);
+				break;
+			case Kack:
+				search();
+				break;
+			case Kright:
+				if(plkbfocus)
+					goto Plkey;
+				sidescroll(text->size.x/4, 1);
+				break;
+			case Kleft:
+				if(plkbfocus)
+					goto Plkey;
+				sidescroll(-text->size.x/4, 1);
+				break;
+			}
+			break;
+		case Emouse:
+			mouse=e.mouse;
+			if(mouse.buttons & (8|16) && ptinrect(mouse.xy, list->r) && defdisplay){
+				if(mouse.buttons & 8)
+					scrollpanel(list, list->r.min.y - mouse.xy.y, 1);
+				else
+					scrollpanel(list, mouse.xy.y - list->r.min.y, 1);
+				break;
+			}
+			if(mouse.buttons & (8|16) && ptinrect(mouse.xy, text->r)){
+				if(mouse.buttons & 8)
+					scrollpanel(text, text->r.min.y - mouse.xy.y, 1);
+				else
+					scrollpanel(text, mouse.xy.y - text->r.min.y, 1);
+				break;
+			}
+			plmouse(root, &mouse);
+			if(mouse.buttons == 1 && root->lastmouse == root)
+				plgrabkb(nil);
+			break;
+		case Eplumb:
+			pm=e.v;
+			if(pm->ndata > 0)
+				geturl(pm->data, -1, 1, 0);
+			plumbfree(pm);
+			break;
+		}
+	}
+}
+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 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;
+
+	drawlock(1);
+	if(new && getwindow(display, Refnone) == -1) {
+		fprint(2, "getwindow: %r\n");
+		exits("getwindow");
+	}
+	r=screen->r;
+	plpack(root, r);
+	plpack(alt, r);
+	pldraw(cmd, screen);	/* put cmd box on screen for alt display */
+	pldraw(root, screen);
+	flushimage(display, 1);
+	drawlock(0);
+}
+void *emalloc(int n){
+	void *v;
+	v=malloc(n);
+	if(v==0)
+		sysfatal("out of memory");
+	memset(v, 0, n);
+	setmalloctag(v, getcallerpc(&n));
+	return v;
+}
+void nstrcpy(char *to, char *from, int len){
+	strncpy(to, from, len);
+	to[len-1] = 0;
+}
+
+char *genwww(Panel *, int index){
+	static char buf[1024];
+	Www *w;
+	int i;
+
+	if(index >= nwww())
+		return 0;
+	i = wwwtop-index-1;
+	w = www(i);
+	if(!w->url)
+		return 0;
+	if(w->title[0]!='\0'){
+		w->gottitle=1;
+		snprint(buf, sizeof(buf), "%2d %s", i+1, w->title);
+	} else
+		snprint(buf, sizeof(buf), "%2d %s", i+1, urlstr(w->url));
+	return buf;
+}
+
+void scrollto(char *tag){
+	Rtext *tp;
+	Action *ap;
+	if(current == nil || text == nil)
+		return;
+	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);
+}
+
+/*
+ * selected text should be a url.
+ */
+void setcurrent(int index, char *tag){
+	Www *new;
+	int i;
+	new=www(index);
+	if(new==current && (tag==0 || tag[0]==0)) return;
+	if(current)
+		current->yoffs=plgetpostextview(text);
+	current=new;
+	plinitlabel(cururl, PACKE|EXPAND, current->url->fullname);
+	if(defdisplay) pldraw(cururl, screen);
+	plinittextview(text, PACKE|EXPAND, Pt(0, 0), current->text, dolink);
+	scrollto(tag);
+	if((i = open("/dev/label", OWRITE)) >= 0){
+		fprint(i, "%s %s", mothra, current->url->fullname);
+		close(i);
+	}
+	donecurs();
+}
+char *arg(char *s){
+	do ++s; while(*s==' ' || *s=='\t');
+	return s;
+}
+void save(int ifd, char *name){
+	char buf[NNAME+64];
+	int ofd;
+	if(ifd < 0){
+		message("save: %s: %r", name);
+		return;
+	}
+	ofd=create(name, OWRITE, 0666);
+	if(ofd < 0){
+		message("save: %s: %r", name);
+		return;
+	}
+	switch(rfork(RFNOTEG|RFNAMEG|RFFDG|RFMEM|RFPROC|RFNOWAIT)){
+	case -1:
+		message("Can't fork: %r");
+		break;
+	case 0:
+		dup(ifd, 0);
+		close(ifd);
+		dup(ofd, 1);
+		close(ofd);
+
+		snprint(buf, sizeof(buf),
+			"{tput -p || cat} |[2] {aux/statusmsg -k %q >/dev/null || cat >/dev/null}", name);
+		execl("/bin/rc", "rc", "-c", buf, nil);
+		exits("exec");
+	}
+	close(ifd);
+	close(ofd);
+	donecurs();
+}
+void screendump(char *name, int full){
+	Image *b;
+	int fd;
+	fd=create(name, OWRITE, 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);
+}
+
+/*
+ * convert a url into a local file name.
+ */
+char *urltofile(Url *url){
+	char *name, *slash;
+	if(url == nil)
+		return nil;
+	name = urlstr(url);
+	if(name == nil || name[0] == 0)
+		name = "/";
+	if(slash = strrchr(name, '/'))
+		name = slash+1;
+	if(name[0] == 0)
+		name = "index";
+	return name;
+}
+
+/*
+ * user typed a command.
+ */
+void docmd(Panel *p, char *s){
+	char buf[NNAME];
+	int c;
+
+	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, -1, 0, 0);
+	else switch(c = s[0]){
+	default:
+		message("Unknown command %s", s);
+		break;
+	case 'a':
+		s = arg(s);
+		if(*s=='\0' && selection)
+			hit3(3, 0);
+		break;
+	case 'd':
+		s = arg(s);
+		if(*s){
+			s = smprint("https://lite.duckduckgo.com/lite/?q=%U&kd=-1", s);
+			if(s != nil)
+				geturl(s, -1, 0, 0);
+			free(s);
+		}else
+			message("Usage: d text");
+		break;
+	case 'g':
+		s = arg(s);
+		if(*s=='\0'){
+	case 'r':
+			if(selection)
+				s = urlstr(selection);
+			else
+				message("no url selected");
+		}
+		geturl(s, -1, 0, 0);
+		break;
+	case 'j':
+		s = arg(s);
+		if(*s)
+			doprev(nil, 1, wwwtop-atoi(s));
+		else
+			message("Usage: j index");
+		break;
+	case 'm':
+		mothon(current, !mothmode);
+		break;
+	case 'k':
+		killimgs = !killimgs;
+		if (killimgs)
+			killpix(current);
+		break;
+	case 'w':
+	case 'W':
+		s = arg(s);
+		if(s==0 || *s=='\0'){
+			snprint(buf, sizeof(buf), "dump.bit");
+			if(eenter("Screendump to", buf, sizeof(buf), &mouse) <= 0)
+				break;
+			s = buf;
+		}
+		screendump(s, c == 'W');
+		break;
+	case 's':
+		s = arg(s);
+		if(!selection){
+			message("no url selected");
+			break;
+		}
+		if(s==0 || *s=='\0'){
+			snprint(buf, sizeof(buf), "%s", urltofile(selection));
+			if(eenter("Save to", buf, sizeof(buf), &mouse) <= 0)
+				break;
+			s = buf;
+		}
+		save(urlget(selection, -1), s);
+		break;
+	case 'q':
+		exits(0);
+	}
+	plinitentry(cmd, EXPAND, 0, "", docmd);
+	pldraw(root, screen);
+}
+
+void regerror(char *msg)
+{
+	werrstr("regerror: %s", msg);
+}
+
+void search(void){
+	static char last[256];
+	char buf[256];
+	Reprog *re;
+	Rtext *tp;
+
+	for(;;){
+		if(current == nil || current->text == nil || text == nil)
+			return;
+		strncpy(buf, last, sizeof(buf)-1);
+		if(eenter("Search for", buf, sizeof(buf), &mouse) <= 0)
+			return;
+		strncpy(last, buf, sizeof(buf)-1);
+		re = regcompnl(buf);
+		if(re == nil){
+			message("%r");
+			continue;
+		}
+		for(tp=current->text;tp;tp=tp->next)
+			if(tp->flags & PL_SEL)
+				break;
+		if(tp == nil)
+			tp = current->text;
+		else {
+			tp->flags &= ~PL_SEL;
+			tp = tp->next;
+		}
+		while(tp != nil){
+			tp->flags &= ~PL_SEL;
+			if(tp->text && *tp->text)
+			if(regexec(re, tp->text, nil, 0)){
+				tp->flags |= PL_SEL;
+				plsetpostextview(text, tp->topy);
+				break;
+			}
+			tp = tp->next;
+		}
+		free(re);
+		updtext(current);
+	}
+}
+
+void hiturl(int buttons, char *url, int map){
+	switch(buttons){
+	case 1: geturl(url, -1, 0, map); break;
+	case 2: urlresolve(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 < 0 || 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){
+	Action *a;
+
+	a=word->user;
+	if(a == nil || (a->link == nil && a->image == nil))
+		return;
+	if(mothmode)
+		hiturl(buttons, a->image ? a->image : a->link, 0);
+	else if(a->link){
+		if(a->ismap){
+			char mapurl[NNAME];
+			Point coord;
+			int yoffs;
+
+			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(int fd, char *cmd){
+	switch(rfork(RFFDG|RFPROC|RFMEM|RFREND|RFNOWAIT|RFNOTEG)){
+	case -1:
+		message("Can't fork!");
+		break;
+	case 0:
+		dupfds(fd, 1, 2, -1);
+		execl("/bin/rc", "rc", "-c", cmd, nil);
+		_exits(0);
+	}
+	close(fd);
+}
+void gettext(Www *w, int fd, int type){
+	switch(rfork(RFFDG|RFPROC|RFMEM|RFNOWAIT)){
+	case -1:
+		message("Can't fork, please wait");
+		break;
+	case 0:
+		if(type==HTML)
+			plrdhtml(w->url->fullname, fd, w, killimgs);
+		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
+dupfds(int fd, ...)
+{
+	int mfd, n, i;
+	va_list arg;
+	Dir *dir;
+
+	va_start(arg, fd);
+	for(mfd = 0; fd >= 0; fd = va_arg(arg, int), mfd++)
+		if(fd != mfd)
+			if(dup(fd, mfd) < 0)
+				sysfatal("dup: %r");
+	va_end(arg);
+	if((fd = open("/fd", OREAD)) < 0)
+		sysfatal("open: %r");
+	n = dirreadall(fd, &dir);
+	for(i=0; i<n; i++){
+		if(strstr(dir[i].name, "ctl"))
+			continue;
+		fd = atoi(dir[i].name);
+		if(fd >= mfd)
+			close(fd);
+	}
+	free(dir);
+}
+
+int pipeline(int fd, char *fmt, ...)
+{
+	char buf[80], *argv[4];
+	va_list arg;
+	int pfd[2];
+
+	va_start(arg, fmt);
+	vsnprint(buf, sizeof buf, fmt, arg);
+	va_end(arg);
+
+	if(pipe(pfd) < 0){
+	Err:
+		close(fd);
+		werrstr("pipeline for %s failed: %r", buf);
+		return -1;
+	}
+	switch(rfork(RFPROC|RFMEM|RFFDG|RFREND|RFNOWAIT)){
+	case -1:
+		close(pfd[0]);
+		close(pfd[1]);
+		goto Err;
+	case 0:
+		dupfds(fd, pfd[1], 2, -1);
+		argv[0] = "rc";
+		argv[1] = "-c";
+		argv[2] = buf;
+		argv[3] = nil;
+		exec("/bin/rc", argv);
+		_exits(0);
+	}
+	close(fd);
+	close(pfd[1]);
+	return pfd[0];
+}
+
+char*
+urlstr(Url *url){
+	if(url->fullname[0])
+		return url->fullname;
+	return url->reltext;
+}
+
+Url *copyurl(Url *u){
+	Url *v;
+	v=emalloc(sizeof(Url));
+	*v=*u;
+	v->reltext = strdup(u->reltext);
+	v->basename = strdup(u->basename);
+	return v;
+}
+
+void freeurl(Url *u){
+	free(u->reltext);
+	free(u->basename);
+	free(u);
+}
+
+void seturl(Url *url, char *urlname, char *base){
+	url->reltext = strdup(urlname);
+	url->basename = strdup(base);
+	url->fullname[0] = 0;
+	url->tag[0] = 0;
+	url->map = 0;
+}
+
+Url* selurl(char *urlname){
+	Url *last;
+
+	last=selection;
+	selection=emalloc(sizeof(Url));
+	seturl(selection, urlname, current ? current->url->fullname : "");
+	if(last) freeurl(last);
+	message("selected: %s", urlstr(selection));
+	plgrabkb(cmd);		/* for snarf */
+	return selection;
+}
+
+/*
+ * get the file at the given url
+ */
+void geturl(char *urlname, int post, int plumb, int map){
+	int i, fd, typ;
+	char cmd[NNAME];
+	ulong n;
+	Www *w;
+
+	if(*urlname == '#' && post < 0){
+		scrollto(urlname+1);
+		return;
+	}
+
+	selurl(urlname);
+	selection->map=map;
+
+	message("getting %s", urlstr(selection));
+	esetcursor(&patientcurs);
+	for(;;){
+		if((fd=urlget(selection, post)) < 0){
+			message("%r");
+			break;
+		}
+		message("getting %s", selection->fullname);
+		if(mothmode && !plumb)
+			typ = -1;
+		else if((typ = mimetotype(selection->contenttype)) < 0)
+			typ = snooptype(fd);
+
+		switch(typ){
+		default:
+			if(plumb){
+				message("unknown file type");
+				close(fd);
+				break;
+			}
+			snprint(cmd, sizeof(cmd), "%s", urltofile(selection));
+			if(eenter("Save to", cmd, sizeof(cmd), &mouse) <= 0){
+				close(fd);
+				break;
+			}
+			save(fd, cmd);
+			break;
+		case HTML:
+			fd = pipeline(fd, "exec uhtml");
+		case PLAIN:
+			n=0; 
+			for(i=wwwtop-1; i>=0 && i!=(wwwtop-NWWW-1); i--){
+				w = www(i);
+				n += countpix(w->pix);
+				if(n >= NPIXMB*1024*1024)
+					killpix(w);
+			}
+			w = www(i = wwwtop++);
+			if(i >= NWWW){
+				/* wait for the reader to finish the document */
+				while(!w->finished && !w->alldone){
+					drawlock(0);
+					sleep(10);
+					drawlock(1);
+				}
+				freetext(w->text);
+				freeform(w->form);
+				freepix(w->pix);
+				freeurl(w->url);
+				memset(w, 0, sizeof(*w));
+			}
+			if(selection->map)
+				w->url=copyurl(current->url);
+			else
+				w->url=copyurl(selection);
+			w->finished = 0;
+			w->alldone = 0;
+			gettext(w, fd, typ);
+			if(rfork(RFPROC|RFMEM|RFNOWAIT) == 0){
+				for(;;){
+					sleep(1000);
+					if(w->finished || w->alldone)
+						break;
+					if(w->changed)
+						write(kickpipe[1], "C", 1);
+				}
+				_exits(0);
+			}
+			plinitlist(list, PACKN|FILLX, genwww, 8, doprev);
+			if(defdisplay) pldraw(list, screen);
+			setcurrent(i, selection->tag);
+			break;
+		case GIF:
+		case JPEG:
+		case PNG:
+		case BMP:
+		case PAGE:
+			filter(fd, "exec page -w");
+			break;
+		}
+		break;
+	}
+	donecurs();
+}
+void updtext(Www *w){
+	Rtext *t;
+	Action *a;
+	if(defdisplay && w->gottitle==0 && w->title[0]!='\0')
+		pldraw(list, screen);
+	for(t=w->text;t;t=t->next){
+		a=t->user;
+		if(a){
+			if(a->field)
+				mkfieldpanel(t);
+			a->field=0;
+		}
+	}
+	if(w != current)
+		return;
+	w->yoffs=plgetpostextview(text);
+	plinittextview(text, PACKE|EXPAND, Pt(0, 0), w->text, dolink);
+	plsetpostextview(text, w->yoffs);
+	pldraw(text, screen);
+}
+
+void finish(Www *w){
+	w->finished = 1;
+	write(kickpipe[1], "F", 1);
+}
+
+void
+mothon(Www *w, int on)
+{
+	Rtext *t, *x;
+	Action *a, *ap;
+
+	if(w==0 || mothmode==on)
+		return;
+	if(mothmode = on)
+		message("moth mode!");
+	else
+		message(mothra);
+	/*
+	 * insert or remove artificial links to the href for 
+	 * images that are also links
+	 */
+	for(t=w->text;t;t=t->next){
+		a=t->user;
+		if(a == nil || a->image == nil)
+			continue;
+		if(a->link == nil){
+			if(on)
+				t->flags |= PL_HOT;
+			else
+				t->flags &= ~PL_HOT;
+			continue;
+		}
+		x = t->next;
+		if(on){
+			t->next = nil;
+			ap=emalloc(sizeof(Action));
+			ap->link = strdup(a->link);
+			plrtstr(&t->next, 0, 0, 0, t->font, strdup("->"), PL_HOT, ap);
+			t->next->next = x;
+		} else {
+			if(x) {
+				t->next = x->next;
+				x->next = nil;
+				freetext(x);
+			}
+		}
+	}
+	updtext(w);
+	donecurs();
+}
+
+void killpix(Www *w){
+	Rtext *t;
+
+	if(w==0 || !w->finished && !w->alldone)
+		return;
+	for(t=w->text; t; t=t->next)
+		if(t->b && t->user)
+			t->b=0;
+	freepix(w->pix);
+	w->pix=0;
+	updtext(w);
+}
+void snarf(Panel *p){
+	if(p==0 || p==cmd){
+		if(selection){
+			plputsnarf(urlstr(selection));
+			plsnarf(text);
+		}else
+			message("no url selected");
+	}else
+		plsnarf(p);
+}
+void paste(Panel *p){
+	if(p==0) p=cmd;
+	plpaste(p);
+}
+void hit3(int button, int item){
+	char buf[1024];
+	char name[NNAME];
+	char *s;
+	Panel *swap;
+	int fd;
+	USED(button);
+	switch(item){
+	case 0:
+		swap=root;
+		root=alt;
+		alt=swap;
+		if(current)
+			current->yoffs=plgetpostextview(text);
+		swap=text;
+		text=alttext;
+		alttext=swap;
+		defdisplay=!defdisplay;
+		plpack(root, screen->r);
+		if(current){
+			plinittextview(text, PACKE|EXPAND, Pt(0, 0), current->text, dolink);
+			plsetpostextview(text, current->yoffs);
+		}
+		pldraw(root, screen);
+		break;
+	case 1:
+		mothon(current, !mothmode);
+		break;
+	case 2:
+		snarf(plkbfocus);
+		break;
+	case 3:
+		paste(plkbfocus);
+		break;
+	case 4:
+		if(plkbfocus==nil || plkbfocus==cmd){
+			if(text==nil || text->snarf==nil || selection==nil)
+				return;
+			if((s=text->snarf(text))==nil)
+				s=smprint("%s", urlstr(selection));
+		}else
+			if((s=plkbfocus->snarf(plkbfocus))==nil)
+				return;
+		if((fd=plumbopen("send", OWRITE))<0){
+			message("can't plumb");
+			free(s);
+			return;
+		}
+		plumbsendtext(fd, "mothra", nil, getwd(buf, sizeof buf), s);
+		close(fd);
+		free(s);
+		break;
+	case 5:
+		search();
+		break;
+	case 6:
+		if(!selection){
+			message("no url selected");
+			break;
+		}
+		snprint(name, sizeof(name), "%s/hit.html", mkhome());
+		fd=open(name, OWRITE);
+		if(fd==-1){
+			fd=create(name, OWRITE, 0666);
+			if(fd==-1){
+				message("can't open %s", name);
+				return;
+			}
+			fprint(fd, "<html><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", urlstr(selection), urlstr(selection));
+		close(fd);
+		break;
+	case 7:
+		snprint(name, sizeof(name), "file:%s/hit.html", mkhome());
+		geturl(name, -1, 1, 0);
+		break;
+	case 8:
+		if(confirm(3))
+			exits(0);
+		break;
+	}
+}
--- /dev/null
+++ b/sys/src/cmd/mothra/rdhtml.c
@@ -1,0 +1,1249 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <panel.h>
+#include "mothra.h"
+#include "html.h"
+#include "rtext.h"
+
+typedef struct Fontdata Fontdata;
+struct Fontdata{
+	char *name;
+	Font *font;
+	int space;
+}fontlist[4][4]={
+/* original */
+	"dejavusans/unicode.12", 0, 0,
+	"dejavusans/unicode.12", 0, 0,
+	"dejavusans/unicode.14", 0, 0,
+	"dejavusans/unicode.16", 0, 0,
+
+	"dejavusansit/unicode.12", 0, 0,
+	"dejavusansit/unicode.12", 0, 0,
+	"dejavusansit/unicode.14", 0, 0,
+	"dejavusansit/unicode.16", 0, 0,
+
+	"dejavusansbd/unicode.12", 0, 0,
+	"dejavusansbd/unicode.12", 0, 0,
+	"dejavusansbd/unicode.14", 0, 0,
+	"dejavusansbd/unicode.16", 0, 0,
+
+	"terminus/unicode.12", 0, 0,
+	"terminus/unicode.12", 0, 0,
+	"terminus/unicode.14", 0, 0,
+	"terminus/unicode.16", 0, 0,
+};
+
+static struct{
+	char *prefix;
+	int len;
+}links[]={
+	{"http://", 7},
+	{"https://", 8},
+	{"gemini://", 9},
+	{"ftp://", 6},
+};
+
+Font *pl_whichfont(int f, int s, int *space){
+	char name[NNAME];
+
+	assert(f >= 0 && f < 4);
+	assert(s >= 0 && s < 4);
+
+	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");
+	}
+	if(space)
+		*space = fontlist[f][s].space;
+	return fontlist[f][s].font;
+}
+
+void getfonts(void){
+	int f, s;
+	for(f=0;f!=4;f++)
+		for(s=0;s!=4;s++)
+			pl_whichfont(f, s, nil);
+}
+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;
+
+	if(g->state->name)
+		g->state->name = strdup(g->state->name);
+	if(g->state->link)
+		g->state->link = strdup(g->state->link);
+	if(g->state->image)
+		g->state->image = strdup(g->state->image);
+}
+void pl_popstate(Stack *state){
+	free(state->name);
+	state->name=0;
+	free(state->link);
+	state->link=0;
+	free(state->image);
+	state->image=0;
+}
+
+void pl_linespace(Hglob *g){
+	plrtbitmap(&g->dst->text, 1000000, 0, 0, linespace, 0, 0);
+	g->para=0;
+	g->linebrk=0;
+}
+
+int strtolength(Hglob *g, int dir, char *str){
+	double f;
+	Point p;
+
+	f = atof(str);
+	if(cistrstr(str, "%"))
+		return 0;
+	if(cistrstr(str, "em")){
+		p=stringsize(pl_whichfont(g->state->font, g->state->size, nil), "M");
+		return floor(f*((dir==HORIZ) ? p.x : p.y));
+	}
+	return floor(f);
+}
+
+void pl_htmloutput(Hglob *g, int nsp, char *s, Field *field){
+	Font *f;
+	int space, indent, flags, voff;
+	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;
+	}
+	voff = 0;
+	f=pl_whichfont(g->state->font, g->state->size, &space);
+	if(g->state->sub){
+		voff = g->state->sub * f->ascent / 2;
+		g->state->size = SMALL;
+		f=pl_whichfont(g->state->font, g->state->size, &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 && g->state->link==0 && g->state->name==0 && field==0)
+		ap=0;
+	else{
+		ap=emalloc(sizeof(Action));
+		if(g->state->image)
+			ap->image = strdup(g->state->image);
+		if(g->state->link)
+			ap->link = strdup(g->state->link);
+		if(g->state->name)
+			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;
+		}
+	}
+	flags = 0;
+	if(g->state->link)
+		flags |= PL_HOT;
+	if(g->state->strike)
+		flags |= PL_STR;
+	plrtstr(&g->dst->text, space, indent, voff, f, strdup(s), flags, ap);
+	g->para=0;
+	g->linebrk=0;
+	g->dst->changed=1;
+}
+
+/*
+ * 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
+ * convert to runes.
+ */
+int pl_readc(Hglob *g){
+	static int peek=-1;
+	char crune[UTFmax+1];
+	int c, n;
+	Rune r;
+
+	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';
+	}
+
+	if(c < Runeself)
+		return c;
+
+	crune[0]=c;
+	for (n=1; n<=sizeof(crune); n++){
+		if(fullrune(crune, n)){
+			chartorune(&r, crune);
+			return r;
+		}
+		c=pl_bread(g);
+		if(c==EOF)
+			return EOF;
+		crune[n]=c;
+	}
+	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;
+
+	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;
+	return c;
+}
+
+char *unquot(char *src){
+	char *e, *dst;
+	int len;
+
+	e=0;
+	while(*src && strchr(" \t\r\n", *src))
+		src++;
+	if(*src=='\'' || *src=='"'){
+		e=strrchr(src+1, *src);
+		src++;
+	}
+	if(e==0) e=strchr(src, 0);
+	len=e-src;
+	dst = emalloc(len+1);
+	memmove(dst, src, len);
+	dst[len]=0;
+	return dst;
+}
+int alnumchar(int c){
+	return 'a'<=c && c<='z' || 'A'<=c && c<='Z' || '0'<=c && c<='9';
+}
+int entchar(int c){
+	return c=='#' || alnumchar(c);
+}
+
+/* return url if text token looks like a hyperlink */
+char *linkify(char *s){
+	int i;
+	if(s == 0 && s[0] == 0)
+		return 0;
+	for(i = 0; i < nelem(links); i++)
+		if(!cistrncmp(s, links[i].prefix, links[i].len))
+			return strdup(s);
+	if(!cistrncmp(s, "www.", 4)){
+		int d, i;
+
+		d = 1;
+		for(i=4; s[i]; i++){
+			if(s[i] == '.'){
+				if(s[i-1] == '.')
+					return 0;
+				d++;
+			} else if(!alnumchar(s[i]))
+				break;
+		}
+		if(d >= 2)
+			return smprint("http://%s", s);
+	}
+	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 *, char *s){
+	char *t, *u, c, svc;
+	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;
+			*s = 0;
+			if(svc==';') s++;
+			if(strcmp(u, "lt") == 0)
+				*t++='<';
+			else if(strcmp(u, "gt") == 0)
+				*t++='>';
+			else if(strcmp(u, "quot") == 0)
+				*t++='"';
+			else if(strcmp(u, "apos") == 0)
+				*t++='\'';
+			else if(strcmp(u, "amp") == 0)
+				*t++='&';
+			else {
+				if(svc==';') s--;
+				*s=svc;
+				*t++='&';
+				while(u<s)
+					*t++=*u++;
+			}
+		}
+		else if((uchar)c == 0xc2 && (uchar)*s == 0xad)
+			s++; /* ignore soft hyphens */
+		else
+			*t++=c;
+	}while(c);
+}
+/*
+ * Skip over white space
+ */
+char *pl_whitespace(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==':') 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!=' ' && *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_whitespace(s);
+		if(*s=='\0'){
+			ap->name=0;
+			return;
+		}
+		ap->name=s;
+		s=pl_word(s);
+		t=pl_whitespace(s);
+		c=*t;
+		*s='\0';
+		for(s=ap->name;*s;s++) if('A'<=*s && *s<='Z') *s+='a'-'A';
+		if(c=='='){
+			s=pl_whitespace(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(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);
+}
+
+int pl_getscript(Hglob *g){
+	char *tokp, *t;
+	int c;
+	tokp = g->token;
+	*tokp++ = '<';
+	while((c=pl_nextc(g)) != EOF){
+		if(c==STAG || c==' ' || c=='\t' || c=='\n'){
+			pl_putback(g, c);
+			break;
+		}
+		if(c==ETAG) c='>';
+		tokp += lrunetochar(tokp, c);
+		if(c==0 || c=='>' || tokp >= &g->token[NTOKEN-UTFmax-1])
+			break;
+	}
+	*tokp = '\0';
+	t = tag[g->state->tag].name;
+	if(g->token[1] == '/' && cistrncmp(g->token+2, t, strlen(t)) == 0){
+		g->tag=g->state->tag;
+		g->attr->name=0;
+		return ENDTAG;
+	}
+	pl_rmentities(g, g->token);
+	g->nsp=g->spacc;
+	g->spacc=0;
+	return TEXT;
+}
+
+/*
+ * Read a start or end tag -- the caller has read the initial <
+ */
+int pl_gettag(Hglob *g){
+	char *tokp;
+	int c, q;
+	if(g->state->isscript)
+		return pl_getscript(g);
+	if((c=pl_nextc(g))=='!' || c=='?')
+		return pl_getcomment(g);
+	pl_putback(g, c);
+	q = 0;
+	tokp=g->token;
+	while((c=pl_nextc(g))!=EOF){
+		if(c == '=' && q == 0)
+			q = '=';
+		else if(c == '\'' || c == '"'){
+			if(q == '=')
+				q = c;
+			else if(q == c)
+				q = 0;
+		}
+		else if(c == ETAG && q != '\'' && q != '"')
+			break;
+		else if(q == '=' && c != ' ' && c != '\t' && c != '\n')
+			q = 0;
+		if(tokp < &g->token[NTOKEN-UTFmax-1])
+			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>, single newlines are converted to <br>,
+ * double newlines are converted to <p> 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':
+		switch(c=pl_nextc(g)){
+		case '\n':
+			pl_tagparse(g, "p");
+			return TAG;
+		default:
+			pl_tagparse(g, "br");
+			pl_putback(g, c);
+			return TAG;
+		}
+	default:
+		tokp=g->token;
+		while(c=='\t'){
+			if(tokp < &g->token[NTOKEN-UTFmax-1]) 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-UTFmax-1]) 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-UTFmax-1]) 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;
+	g->state->sub = 0;
+	elp=&line[NLINE-UTFmax-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->sub=0;
+	g.state->pre=0;
+	g.state->image=0;
+	g.state->link=0;
+	g.state->name=0;
+	g.state->margin=0;
+	g.state->indent=20;
+	g.state->ismap=0;
+	g.state->isscript=0;
+	g.state->strike=0;
+	g.state->width=0;
+	g.state->height=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;
+	nstrcpy(g.text, name, NTITLE);
+	plaintext(&g);
+	finish(dst);
+}
+void plrdhtml(char *name, int fd, Www *dst, int killimgs){
+	int tagerr;
+	Stack *sp;
+	char buf[20];
+	char *str;
+	Hglob g;
+
+	g.state=g.stack;
+	g.state->tag=Tag_html;
+	g.state->font=ROMAN;
+	g.state->size=NORMAL;
+	g.state->sub=0;
+	g.state->pre=0;
+	g.state->image=0;
+	g.state->link=0;
+	g.state->name=0;
+	g.state->margin=0;
+	g.state->indent=25;
+	g.state->ismap=0;
+	g.state->isscript=0;
+	g.state->strike=0;
+	g.state->width=0;
+	g.state->height=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';
+	g.spacc=0;
+	g.form=0;
+
+	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);
+					pl_popstate(g.state);
+				}
+			break;
+		case END:
+			pl_pushstate(&g, g.tag);
+			break;
+		}
+		str=pl_getattr(g.attr, "id");
+		if(str && *str){
+			char *swap;
+
+			swap = g.state->name;
+			g.state->name = str;
+			pl_htmloutput(&g, 0, "", 0);
+			g.state->name = swap;
+		}
+		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_img:
+		case Tag_image:
+			str=pl_getattr(g.attr, "src");
+			if(str && *str){
+				free(g.state->image);
+				g.state->image = strdup(str);
+			} else {
+				Pair *a;
+
+				/*
+				 * hack to emulate javascript that rewrites some attribute
+				 * into src= after page got loaded. just look for some
+				 * attribute that looks like a url.
+				 */
+				for(a = g.attr; a->name; a++){
+					if(strcmp(a->name, "longdesc") == 0)
+						continue;
+					if(str = linkify(a->value)){
+						free(g.state->image);
+						g.state->image = str;
+						break;
+					}
+				}
+			}
+			g.state->ismap=pl_hasattr(g.attr, "ismap");
+			str=pl_getattr(g.attr, "width");
+			if(str && *str)
+				g.state->width=strtolength(&g, HORIZ, str);
+			str=pl_getattr(g.attr, "height");
+			if(str && *str)
+				g.state->height=strtolength(&g, VERT, str);
+			str=pl_getattr(g.attr, "alt");
+			if(str==0 || *str == 0){
+				if(g.state->image)
+					str=g.state->image;
+				else
+					str="[[image]]";
+			}
+			pl_htmloutput(&g, 0, str, 0);
+			free(g.state->image);
+			g.state->image=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:
+		case Tag_table:
+			break;
+		case Tag_tr:
+			g.spacc=0;
+			g.linebrk=1;
+			break;
+		case Tag_th:
+			g.state->font=BOLD;
+		case Tag_td:
+			g.spacc++;
+			break;
+		case Tag_base:
+			str=pl_getattr(g.attr, "href");
+			if(str && *str){
+				seturl(g.dst->url, str, g.dst->url->fullname);
+				nstrcpy(g.dst->url->fullname, str, sizeof(g.dst->url->fullname));
+				/* base should be a full url, but it often isnt so have to resolve */
+				urlresolve(g.dst->url);
+			}
+			break;
+		case Tag_a:
+			str=pl_getattr(g.attr, "name");
+			if(str && *str){
+				free(g.state->name);
+				g.state->name = strdup(str);
+			}
+			pl_htmloutput(&g, 0, "", 0);
+			str=pl_getattr(g.attr, "href");
+			if(str && *str){
+				free(g.state->link);
+				g.state->link = strdup(str);
+			}
+			break;
+		case Tag_meta:
+			if((str=pl_getattr(g.attr, "http-equiv"))==0)
+				break;
+			if(cistrcmp(str, "refresh"))
+				break;
+			if((str=pl_getattr(g.attr, "content"))==0)
+				break;
+			if((str=strchr(str, '='))==0)
+				break;
+			str++;
+			pl_htmloutput(&g, 0, "[refresh: ", 0);
+			free(g.state->link);
+			g.state->link=unquot(str);
+			pl_htmloutput(&g, 0, g.state->link, 0);
+			free(g.state->link);
+			g.state->link=0;
+			pl_htmloutput(&g, 0, "]", 0);
+			g.linebrk=1;
+			g.spacc=0;
+			break;
+		case Tag_source:
+		case Tag_video:
+		case Tag_audio:
+		case Tag_embed:
+		case Tag_frame:
+		case Tag_iframe:
+			snprint(buf, sizeof(buf), "[%s: ", tag[g.tag].name);
+			pl_htmloutput(&g, 0, buf, 0);
+			str=pl_getattr(g.attr, "src");
+			if(str && *str){
+				free(g.state->link);
+				g.state->link = strdup(str);
+			}
+			str=pl_getattr(g.attr, "name");
+			if(str && *str){
+				free(g.state->name);
+				g.state->name = strdup(str);
+			} else if(g.state->link)
+				str = g.state->link;
+			else
+				str = "";
+			pl_htmloutput(&g, 0, str, 0);
+			free(g.state->link);
+			g.state->link=0;
+			free(g.state->name);
+			g.state->name=0;
+			pl_htmloutput(&g, 0, "]", 0);
+			g.linebrk=1;
+			g.spacc=0;
+			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_s:
+		case Tag_strike:
+		case Tag_del:
+			g.state->strike=1;
+			break;
+		case Tag_sub:
+			g.state->sub++;
+			break;
+		case Tag_sup:
+			g.state->sub--;
+			break;
+		case Tag_blockquot:
+			g.spacc=0;
+			g.linebrk=1;
+			g.state->margin+=50;
+			g.state->indent=20;
+			break;
+		case Tag_body:
+			break;
+		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_div:
+			g.spacc=0;
+			break;
+		case Tag_br:
+		case Tag_wbr:
+			g.spacc=0;
+			g.linebrk=1;
+			break;
+		case Tag_span:
+		case Tag_center:
+			/* more to come */
+			break;
+		case Tag_cite:
+		case Tag_acronym:
+			g.state->font=ITALIC;
+			g.state->size=NORMAL;
+			break;
+		case Tag_code:
+		case Tag_samp:
+			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");
+		case Tag_abbr:
+			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_figcaption:
+			g.linebrk=1;
+			break;
+		case Tag_font:
+			/* more to come */
+			break;
+		case Tag_u:
+			htmlerror(g.name, g.lineno, "<u> deprecated");
+		case Tag_ins:
+		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, 0, 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, 0, bullet, 0, 0);
+				break;
+			}
+			break;
+		case Tag_p:
+			pl_linespace(&g);
+			g.linebrk=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:
+			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_button:
+		case Tag_select:
+		case Tag_option:
+		case Tag_textarea:
+		case Tag_isindex:
+			rdform(&g);
+			break;
+		case Tag_script:
+		case Tag_style:
+			g.state->isscript=1;
+			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);
+
+				for(--sp; g.state!=sp; --g.state)
+					pl_popstate(g.state);
+			}
+		}
+		else if(g.state==g.stack)
+			htmlerror(g.name, g.lineno, "end tag </%s> at stack bottom",
+				tag[g.tag].name);
+		else
+			pl_popstate(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_div:
+		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_title:
+		case Tag_ul:
+		case Tag_xmp:
+		case Tag_table:
+			g.linebrk=1;
+			break;
+		case Tag_article:
+		case Tag_pre:
+			pl_linespace(&g);
+			break;
+		}
+		break;
+	case TEXT:
+		if(g.state->isscript)
+			continue;
+		if(g.state->link==0 && (str = linkify(g.token))){
+			g.state->link=str; 
+			pl_htmloutput(&g, g.nsp, g.token, 0);
+			free(g.state->link);
+			g.state->link=0;
+		} else
+			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);
+			pl_popstate(g.state);
+		}
+		pl_popstate(g.state);
+		*g.tp='\0';
+		if (!killimgs)
+			getpix(dst->text, dst);
+		finish(dst);
+		return;
+	}
+}