shithub: mongrel

Download patch

ref: 70c799c0e786e3c55633003c26c889ea9921fb54
author: phil9 <telephil9@gmail.com>
date: Thu Feb 3 13:05:04 EST 2022

initial import

--- /dev/null
+++ b/LICENSE
@@ -1,0 +1,21 @@
+MIT License
+
+Copyright (c) 2022 phil9 <telephil9@gmail.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
--- /dev/null
+++ b/README.md
@@ -1,0 +1,31 @@
+# mongrel
+An opinionated mail reader for plan9 inspired by mutt.  
+mongrel only provides reading functionality, writing is done using nedmail.  
+
+![mongrel](mongrel.png)
+
+If available, mongrel will use your current [theme](https://ftrv.se/14).
+
+## Quick start
+```sh
+% mk install
+% mongrel -m mbox
+```
+
+## Usage
+mongrel has two components:
+- the index which shows the list of messages
+- the pager which shows an individual mail
+
+Navigation in the index is done using the mouse, the arrow keys, page up/down, home and end. A left click on a given message will display its content. Right-clicking or pressing `enter` will display the message content (in this case the pager will open if not displayed already).
+Scrolling in the pager can be achieved with either the mouse or with a combination of pressing `alt` and the arrow keys or page up/down.  
+The pager displays any attachments the message may have below the message headers. Right-clicking an individual attachment will send it to the plumber.  
+`q` will hide the pager if it is open or quit mongrel if in the index.
+`Del` exit mongrel.
+
+## License
+MIT
+
+## Bugs
+This is work in progress and I already know of quite many of them.
+
--- /dev/null
+++ b/a.h
@@ -1,0 +1,97 @@
+typedef struct Mailbox Mailbox;
+typedef struct Message Message;
+typedef struct Mlist Mlist;
+typedef struct Mailevent Mailevent;
+
+struct Mailbox
+{
+	Lock;
+	char	*name;
+	char	*path;
+	int		count;
+	int		unseen;
+	Mlist*	list;
+};
+
+struct Message
+{
+	int id;
+	char *path;
+	char *info;
+	char *from;
+	char *to;
+	char *cc;
+	char *sender;
+	char *subject;
+	char *date;
+	long time;
+	int flags;
+	char *type;
+	char *filename;
+	char *body;
+	Mlist *parts;
+};
+
+struct Mlist
+{
+	Message**	elts;
+	usize		nelts;
+	usize		size;
+};
+
+enum
+{
+	Fanswered	= 1<<0,
+	Fdeleted	= 1<<1,
+	Fdraft		= 1<<2,
+	Fflagged	= 1<<3,
+	Frecent		= 1<<4,
+	Fseen		= 1<<5,
+	Fstored		= 1<<6,
+};
+
+struct Mailevent
+{
+	int	type;
+	char 	*path;
+};
+
+enum
+{
+	Enew,
+	Edelete,
+	Emodify,
+};
+
+Mailbox* loadmbox(char *name);
+void mesgloadbody(Message*);
+int mesgmarkseen(Mailbox*, Message*);
+void mboxadd(Mailbox *mbox, char *path);
+int  mboxmod(Mailbox *mbox, char *path);
+void mboxdel(Mailbox *mbox, char *path);
+void mesgdel(Mailbox *mbox, Message *m);
+void seemailproc(void *v);
+
+Mlist* mkmlist(usize cap);
+int mladd(Mlist*, Message*);
+int mlins(Mlist*, usize, Message*);
+Message* mldel(Mlist*, usize);
+
+/* index */
+void indexinit(Channel*, Channel*, Theme*);
+Rectangle indexresize(Rectangle, int);
+void indexdraw(void);
+void indexmouse(Mouse);
+void indexkey(Rune);
+void indexresetsel(void);
+void indexswitch(Mailbox*);
+
+/* pager */
+void pagerinit(Mousectl*, Theme*);
+void pagerresize(Rectangle);
+void pagerdraw(void);
+void pagermouse(Mouse);
+void pagerkey(Rune);
+void pagershow(Message*);
+
+
--- /dev/null
+++ b/index.c
@@ -1,0 +1,337 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <thread.h>
+#include "theme.h"
+#include "a.h"
+
+enum
+{
+	Padding = 4,
+	Scrollwidth = 12,
+	Scrollgap = 2,
+	Scrollminh = 5,
+	Collapsedlines = 10,
+};
+
+enum
+{
+	BACK,
+	TEXT,
+	HIGH,
+	SCRL,
+	NCOLS,
+};
+
+enum
+{
+	Mreply,
+	Mreplyall,
+	Mforward,
+	Mdelete,
+};
+
+char *menustr[] =
+{
+	"reply",
+	"reply all",
+	"forward",
+	"delete",
+	nil
+};
+
+Menu menu =
+{
+	menustr
+};
+
+Channel *showc;
+Channel *selc;
+Mailbox *mbox;
+static Image *cols[NCOLS];
+static Rectangle viewr;
+static Rectangle listr;
+static Rectangle scrollr;
+static int nlines;
+static int offset;
+static int sel;
+static int lineh;
+
+void
+indexresetsel(void)
+{
+	sel = 0;
+	offset = 0;
+}
+
+void
+indexswitch(Mailbox *mb)
+{
+	indexresetsel();
+	mbox = mb;
+}
+
+
+Message*
+messageat(int index)
+{
+	index = mbox->count - index - 1;
+	return mbox->list->elts[index];
+}
+
+void
+drawmessage(Message *m, Point p, int selected)
+{
+	const Rune *ellipsis = L"…";
+	Image *fg, *bg;
+	char *s, buf[9];
+	char n, r;
+	Tm t;
+	Rune rn;
+	int i, w;
+	Point pe;
+
+	bg = cols[HIGH];
+	fg = cols[TEXT];
+	if(selected)
+		draw(screen, Rect(p.x, p.y-Padding/2, p.x+Dx(viewr), p.y+lineh-Padding/2), bg, nil, ZP);
+	n = m->flags&Fseen?' ':'N';
+	r = m->flags&Fanswered ? 'R':' ';
+	snprint(buf, sizeof buf, "[%c%c] ", n, r);
+	p = string(screen, p, fg, ZP, font, buf);
+	tmtime(&t, m->time, nil);
+	snprint(buf, sizeof buf, "%τ", tmfmt(&t, "DD/MM/YY"));
+	p = string(screen, p, fg, ZP, font, buf);
+	p = string(screen, p, fg, ZP, font, "  ");
+	s = m->sender;
+	pe = addpt(p, Pt(20*stringwidth(font, " "), 0));
+	for(i = 0; i < 20; i++){
+		if(*s == '@')
+			s = "";
+		if(*s && i == 19){
+			p = runestringn(screen, p, fg, ZP, font, ellipsis, 1);
+			break;
+		}else if(*s){
+			s += chartorune(&rn, s);
+			p = runestringn(screen, p, fg, ZP, font, &rn, 1);
+		}else
+			p = stringn(screen, p, fg, ZP, font, " ", 1);
+	}
+	p = string(screen, pe, fg, ZP, font, "  ");
+	s = m->subject;
+	while(s && *s){
+		s += chartorune(&rn, s);
+		w = runestringnwidth(font, &rn, 1);
+		if(p.x + w + 2*Padding > viewr.max.x){
+			runestringn(screen, p, fg, ZP, font, ellipsis, 1);
+			break;
+		}
+		p = runestringn(screen, p, fg, ZP, font, &rn, 1);
+	}
+}
+
+void
+indexdraw(void)
+{
+	Rectangle scrposr;
+	Point p;
+	int i, h, y;
+
+	draw(screen, viewr, cols[BACK], nil, ZP);
+	draw(screen, scrollr, cols[SCRL], nil, ZP);
+	h = ((double)nlines/mbox->count) * Dy(scrollr);
+	y = ((double)offset/mbox->count) * Dy(scrollr);
+	if(h < Scrollminh)
+		h = Scrollminh;
+	scrposr = Rpt(addpt(scrollr.min, Pt(0,y)), addpt(scrollr.min, Pt(Dx(scrollr)-1, y+h)));
+	draw(screen, scrposr, cols[BACK], nil, ZP);
+	p = addpt(listr.min, Pt(Padding, Padding));
+	for(i = offset; i < offset + nlines; i++){
+		if(i >= mbox->list->nelts)
+			break;
+		drawmessage(messageat(i), p, i == sel);
+		p.y += lineh;
+	}
+}
+
+void
+indexdrawsync(void)
+{
+	indexdraw();
+	flushimage(display, 1);
+}
+
+Rectangle
+indexresize(Rectangle r, int collapsed)
+{
+	lineh = font->height + Padding;
+	viewr = r;
+	if(collapsed)
+		viewr.max.y = viewr.min.y + Collapsedlines * lineh + Padding;
+	scrollr = viewr;
+	scrollr.max.x = scrollr.min.x + Scrollwidth + Scrollgap;
+	scrollr = insetrect(scrollr, 1);
+	listr = viewr;
+	listr.min.x += Scrollwidth + Scrollgap;
+	listr.max.x -= Padding;
+	nlines = Dy(viewr) / lineh;
+	return viewr;
+}
+
+void
+indexinit(Channel *c0, Channel *c1, Theme *theme)
+{
+	Rectangle r;
+
+	sel = 0;
+	offset = 0;
+	showc = c0;
+	selc = c1;
+	if(theme != nil){
+		cols[BACK] = theme->back;
+		cols[TEXT] = theme->text;
+		cols[HIGH] = theme->border;
+		cols[SCRL] = theme->border;
+	}else{
+		r = Rect(0, 0, 1, 1);
+		cols[BACK] = display->white;
+		cols[TEXT] = display->black;
+		cols[HIGH] = allocimage(display, r, screen->chan, 1, 0xCCCCCCFF);
+		cols[SCRL] = allocimage(display, r, screen->chan, 1, 0x999999FF);
+		/*
+		cols[BACK] = allocimage(display, r, screen->chan, 1, 0x282828FF);
+		cols[TEXT] = allocimage(display, r, screen->chan, 1, 0xA89984FF);
+		cols[HIGH] = allocimage(display, r, screen->chan, 1, 0x3C3836FF);
+		cols[SCRL] = allocimage(display, r, screen->chan, 1, 0x504945FF);
+		*/
+	}
+}
+
+void
+scroll(int Δ, int ssel)
+{
+	int nelts;
+
+	nelts = mbox->list->nelts;
+	if(nelts <= nlines)
+		return;
+	if(Δ < 0 && offset == 0)
+		return;
+	if(Δ > 0 && offset + nlines >= nelts)
+		return;
+	offset += Δ;
+	if(offset < 0)
+		offset = 0;
+	if(offset + nelts%nlines >= nelts)
+		offset = nelts - nelts%nlines;
+	if(ssel){
+		if(Δ > 0)
+			sel = 0;
+		else
+			sel = nlines - 1;
+	}
+	indexdrawsync();
+}
+
+void
+changesel(int Δ)
+{
+	if(Δ < 0 && sel == 0)
+		return;
+	if(Δ > 0 && sel == mbox->count - 1)
+		return;
+	sel += Δ;
+	indexdrawsync();
+}
+
+void
+setsel(Point p)
+{
+	int n;
+
+	n = (p.y-listr.min.y)/lineh;
+	sel = n+offset;
+	indexdrawsync();
+}
+
+void
+indexmouse(Mouse m)
+{
+	int sl;
+
+	if(!ptinrect(m.xy, viewr))
+		return;
+	if(m.buttons & 1){
+		setsel(m.xy);
+		sendp(selc, messageat(sel));
+	}else if(m.buttons & 2){
+		/* TODO: menu */
+	}else if(m.buttons & 4){
+		setsel(m.xy);
+		sendp(showc, messageat(sel));
+	}else if(m.buttons & 8){
+		sl = mousescrollsize(nlines);
+		scroll(-sl, 0);
+	}else if(m.buttons & 16){
+		sl = mousescrollsize(nlines);
+		scroll(sl, 0);
+	}
+}
+
+void
+indexkey(Rune k)
+{
+	switch(k){
+		case Kup:
+			if(sel == offset)
+				scroll(-nlines, 1);
+			else if(sel > offset)
+				changesel(-1);
+			sendp(selc, messageat(sel));
+			break;
+		case Kdown:
+			if(sel < (mbox->count - 1)){
+				if(sel == offset + nlines - 1){
+					sel = offset + nlines;
+					scroll(nlines, 0);
+				}else
+					changesel(1);
+				sendp(selc, messageat(sel));
+			}
+			break;
+		case '\n':
+			sendp(showc, messageat(sel));
+			break;
+		case Kpgup:
+			if(sel > 0){
+				sel -= nlines;
+				if(sel < 0)
+					sel = 0;
+				scroll(-nlines, 0);
+				sendp(selc, messageat(sel));
+			}
+			break;
+		case Kpgdown:
+			if(sel < (mbox->count - 1)){
+				sel += nlines;
+				if(sel >= mbox->count)
+					sel = mbox->count - 1;
+				scroll(nlines, 0);
+				sendp(selc, messageat(sel));
+			}
+			break;
+		case Khome:
+			sel = 0;
+			scroll(-mbox->count, 0);
+			sendp(selc, messageat(sel));
+			break;
+		case Kend:
+			sel = mbox->count - 1;
+			scroll(mbox->count, 0);
+			sendp(selc, messageat(sel));
+			break;
+	}
+}
+
--- /dev/null
+++ b/kbd.c
@@ -1,0 +1,113 @@
+#include <u.h>
+#include <libc.h>
+#include <thread.h>
+#include <keyboard.h>
+#include "kbd.h"
+
+void
+kbdproc(void *v)
+{
+	Kbdctl *kc;
+	char buf[128], buf2[128], *s;
+	long n;
+	Rune r;
+	Key k;
+	int mods = 0;
+
+	kc = v;
+	threadsetname("kbdproc");
+	buf[0] = 0;
+	buf2[0] = 0;
+	buf2[1] = 0;
+	while(kc->fd >= 0){
+		if(buf[0] != 0){
+			n = strlen(buf)+1;
+			memmove(buf, buf+n, sizeof(buf)-n);
+		}
+		if (buf[0] == 0) {
+			n = read(kc->fd, buf, sizeof(buf)-1);
+			if (n <= 0){
+				yield();
+				if(kc->fd < 0)
+					threadexits(nil);
+				break;
+			}
+			buf[n-1] = 0;
+			buf[n] = 0;
+		}
+		switch(buf[0]){
+		case 'c':
+			if(chartorune(&r, buf+1) > 0 && r != Runeerror){
+				k = (Key){ r, mods };
+				nbsend(kc->c, &k);
+			}
+		default:
+			continue;
+		case 'k':
+			s = buf+1;
+			while(*s){
+				s += chartorune(&r, s);
+				if(utfrune(buf2+1, r) == nil){
+					if(r == Kctl)
+						mods |= Mctrl;
+					else if(r == Kalt)
+						mods |= Malt;
+					else if(r == Kshift)
+						mods |= Mshift;
+					else{
+						k = (Key){r, mods};
+						nbsend(kc->c, &k);
+					}
+				}
+			}
+			break;
+		case 'K':
+			s = buf2+1;
+			while(*s){
+				s += chartorune(&r, s);
+				if(utfrune(buf+1, r) == nil) {
+					if(r == Kctl)
+						mods ^= Mctrl;
+					else if(r == Kalt)
+						mods ^= Malt;
+					else if(r == Kshift)
+						mods ^= Mshift;
+				}
+			}
+			break;
+		}
+		strcpy(buf2, buf);
+	}
+}
+
+Kbdctl*
+initkbd(void)
+{
+	Kbdctl *kc;
+
+	kc = malloc(sizeof *kc);
+	if(kc == nil)
+		return nil;
+	kc->fd = open("/dev/kbd", OREAD);
+	if(kc->fd < 0){
+		free(kc);
+		return nil;
+	}
+	kc->c = chancreate(sizeof(Key), 0);
+	if(kc->c == nil){
+		close(kc->fd);
+		free(kc);
+		return nil;
+	}
+	kc->pid = proccreate(kbdproc, kc, 8192);
+	return kc;
+}
+
+void
+closekbd(Kbdctl *kc)
+{
+	close(kc->fd);
+	kc->fd = -1;
+	threadint(kc->pid);
+}
+
--- /dev/null
+++ b/kbd.h
@@ -1,0 +1,25 @@
+typedef struct Kbdctl Kbdctl;
+typedef struct Key Key;
+
+struct Kbdctl
+{
+	int fd;
+	int pid;
+	Channel *c;
+};
+
+struct Key
+{
+	Rune k;
+	ushort mods;
+};
+
+enum
+{
+	Mctrl	= 1<<0,
+	Malt	= 1<<1,
+	Mshift	= 1<<2,
+};
+
+Kbdctl *initkbd(void);
+void closekbd(Kbdctl*);
--- /dev/null
+++ b/main.c
@@ -1,0 +1,318 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <draw.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <thread.h>
+#include <plumb.h>
+#include "theme.h"
+#include "a.h"
+#include "kbd.h"
+
+enum
+{
+	Padding = 4,
+};
+
+enum
+{
+	Emouse,
+	Eresize,
+	Ekeyboard,
+	Eseemail,
+	Eshowmesg,
+	Eselmesg,
+};
+
+enum
+{
+	BACK,
+	TEXT,
+	BORD,
+	NCOLS,
+};
+
+Mousectl *mctl;
+Kbdctl *kctl;
+Channel *showc;
+Channel *selc;
+Channel *eventc;
+Mailbox *mboxes[16];
+int nmboxes;
+Mailbox *mbox;
+static Image *cols[NCOLS];
+Rectangle headr;
+Rectangle indexr;
+Rectangle pagerr;
+int collapsed = 0;
+
+void
+drawheader(void)
+{
+	char buf[255] = {0};
+	Point p;
+
+	draw(screen, headr, cols[BACK], nil, ZP);
+	p = headr.min;
+	p.x += Padding;
+	p.y += Padding / 2;
+	if(mbox->unseen > 0)
+		snprint(buf, sizeof buf, "» %s [total:%d - new:%d]", mbox->name, mbox->count, mbox->unseen);
+	else
+		snprint(buf, sizeof buf, "» %s [total:%d]", mbox->name, mbox->count);
+	string(screen, p, cols[TEXT], ZP, font, buf);
+	line(screen, Pt(headr.min.x, headr.max.y), headr.max, 0, 0, 0, cols[BORD], ZP);
+}
+
+void
+redraw(void)
+{
+	draw(screen, screen->r, cols[BACK], nil, ZP);
+	drawheader();
+	indexdraw();
+	if(collapsed){
+		line(screen, Pt(indexr.min.x, indexr.max.y), indexr.max, 0, 0, 0, cols[BORD], ZP);
+		pagerdraw();
+	}
+	flushimage(display, 1);
+}
+
+void
+resize(void)
+{
+	headr = screen->r;
+	headr.max.y = headr.min.y+Padding+font->height;
+	indexr = screen->r;
+	indexr.min.y = headr.max.y + 1;
+	indexr = indexresize(indexr, collapsed);
+	if(collapsed){
+		pagerr = screen->r;
+		pagerr.min.y = indexr.max.y + 1;
+		pagerresize(pagerr);
+	}
+	redraw();
+}
+
+void
+mouse(Mouse m)
+{
+	indexmouse(m);
+	pagermouse(m);
+}
+
+void
+key(Key k)
+{
+	if(k.k == Kdel)
+		threadexitsall(nil);
+	else if(k.k == 'q'){
+		if(collapsed){
+			collapsed = 0;
+			resize();
+		}else
+			threadexitsall(nil);
+	}else if(k.mods == Malt){
+		if(collapsed)
+			pagerkey(k.k);
+	}else
+		indexkey(k.k);
+}
+
+void
+seemail(Mailevent e)
+{
+	Mailbox *mb;
+	char *s;
+	int i;
+
+	for(mb = nil, i = 0; i < nmboxes; i++){
+		if(strncmp(mboxes[i]->path, e.path, strlen(mboxes[i]->path))==0){
+			mb = mboxes[i];
+			break;
+		}
+	}
+	if(mb==nil)
+		return;
+	s = e.path;
+	switch(e.type){
+		case Enew:
+			mboxadd(mb, s);
+			if(mb==mbox){
+				indexresetsel();
+				redraw();
+			}
+			break;
+		case Edelete:
+			mboxdel(mb, s);
+			if(mb==mbox){
+				indexresetsel();
+				redraw();
+			}
+			break;
+		case Emodify:
+			if(mboxmod(mb, s) && mb==mbox)
+				redraw();
+			break;
+	}
+	free(s);
+}
+
+void
+init(Channel *selc)
+{
+	Theme *theme;
+	Rectangle r;
+
+	theme = loadtheme();
+	if(theme != nil){
+		cols[BACK] = theme->back;
+		cols[TEXT] = theme->text;
+		cols[BORD] = theme->title;
+	}else{
+		r = Rect(0, 0, 1, 1);
+		cols[BACK] = allocimage(display, r, screen->chan, 1, 0xFFFFFFFF);
+		cols[TEXT] = allocimage(display, r, screen->chan, 1, 0x000000FF);
+		cols[BORD] = allocimage(display, r, screen->chan, 1, DGreygreen);
+		/*
+		cols[BACK] = allocimage(display, r, screen->chan, 1, 0x282828FF);
+		cols[TEXT] = allocimage(display, r, screen->chan, 1, 0xA89984FF);
+		cols[BORD] = allocimage(display, r, screen->chan, 1, 0x98971AFF);
+		*/
+	}
+	indexinit(showc, selc, theme);
+	pagerinit(mctl, theme);
+}
+
+void
+switchmbox(int n)
+{
+	if(mbox==mboxes[n])
+		return;
+	mbox = mboxes[n];
+	indexswitch(mbox);
+}
+
+void
+plumbmsg(Message *m)
+{
+	int fd;
+
+	fd = plumbopen("send", OWRITE|OCEXEC);
+	if(fd<0)
+		return;
+	plumbsendtext(fd, "mongrel", nil, nil, m->path);
+	close(fd);
+}
+
+void
+showmsg(Message *m)
+{
+	if(collapsed == 0){
+		collapsed = 1;
+		resize();
+	}
+	pagershow(m);
+	if(mesgmarkseen(mbox, m))
+		redraw();
+}
+
+void
+selchanged(Message *m)
+{
+	if(collapsed == 0)
+		return;
+	showmsg(m);
+}
+
+
+void
+usage(void)
+{
+	fprint(2, "usage: %s [-m maildir]\n", argv0);
+	exits("usage");
+}
+
+void
+threadmain(int argc, char *argv[])
+{
+	Mouse m;
+	Key k;
+	Mailevent e;
+	Message *msg;
+	Alt alts[] = 
+	{
+		{ nil, &m,   CHANRCV },
+		{ nil, nil,  CHANRCV },
+		{ nil, &k,   CHANRCV },
+		{ nil, &e,   CHANRCV },
+		{ nil, &msg, CHANRCV },
+		{ nil, &msg, CHANRCV },
+		{ nil, nil,  CHANEND },
+	};
+	char *s;
+
+	nmboxes = 0;
+	ARGBEGIN{
+	case 'm':
+		s = EARGF(usage());
+		mboxes[nmboxes++] = loadmbox(s);
+		break;
+	default:
+		fprint(2, "unknown flag '%c'\n", ARGC());
+		usage();
+	}ARGEND
+	if(nmboxes==0){
+		fprint(2, "no maildir specified\n");
+		usage();
+	}
+	tmfmtinstall();
+	if(initdraw(nil, nil, "mongrel")<0)
+		sysfatal("initdraw: %r");
+	display->locking = 0;
+	if((mctl = initmouse(nil, screen)) == nil)
+		sysfatal("initmouse: %r");
+	if((kctl = initkbd()) == nil)
+		sysfatal("initkbd: %r");
+	if((eventc = chancreate(sizeof(Mailevent), 0))==nil)
+		sysfatal("chancreate: %r");
+	if((showc = chancreate(sizeof(Message*), 1))==nil)
+		sysfatal("chancreate: %r");
+	if((selc = chancreate(sizeof(Message*), 1))==nil)
+		sysfatal("chancreate: %r");
+	alts[0].c = mctl->c;
+	alts[1].c = mctl->resizec;
+	alts[2].c = kctl->c;
+	alts[3].c = eventc;
+	alts[4].c = showc;
+	alts[5].c = selc;
+	proccreate(seemailproc, eventc, 8192);
+	init(selc);
+	switchmbox(0);
+	resize();
+	for(;;){
+		switch(alt(alts)){
+		case Emouse:
+			mouse(m);
+			break;
+		case Eresize:
+			if(getwindow(display, Refnone)<0)
+				sysfatal("cannot reattach: %r");
+			resize();
+			break;
+		case Ekeyboard:
+			key(k);
+			break;
+		case Eseemail:
+			seemail(e);
+			break;
+		case Eshowmesg:
+			showmsg(msg);
+			break;
+		case Eselmesg:
+			//plumbmsg(msg);
+			selchanged(msg);
+			break;
+		}
+	}
+}
--- /dev/null
+++ b/mbox.c
@@ -1,0 +1,344 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <plumb.h>
+#include <thread.h>
+#include <draw.h>
+#include <mouse.h>
+#include "theme.h"
+#include "a.h"
+
+char*
+slurp(char *path)
+{
+	int fd;
+	long r, n, s;
+	char *buf;
+
+	n = 0;
+	s = 8192;
+	buf = malloc(s);
+	if(buf == nil)
+		return nil;
+	fd = open(path, OREAD);
+	if(fd < 0)
+		return nil;
+	for(;;){
+		r = read(fd, buf + n, s - n);
+		if(r < 0)
+			return nil;
+		if(r == 0)
+			break;
+		n += r;
+		if(n == s){
+			s *= 1.5;
+			buf = realloc(buf, s);
+			if(buf == nil)
+				return nil;
+		}
+	}
+	buf[n] = 0;
+	close(fd);
+	return buf;
+}
+
+
+char*
+readpart(char *path, char *part)
+{
+	Biobuf *bp;
+	char *f, *s;
+
+	f = smprint("%s/%s", path, part);
+	if(f==nil)
+		sysfatal("smprint: %r");
+	bp = Bopen(f, OREAD);
+	if(bp==nil)
+		sysfatal("bopen %s: %r", f);
+	s = Brdstr(bp, '\n', 1);
+	Bterm(bp);
+	return s;
+}
+
+int 
+parseflags(char *s)
+{
+	int f;
+
+	f = 0;
+	if(s[0]!='-')
+		f |= Fanswered;
+	if(s[5]!='-')
+		f |= Fseen;
+	return f;
+}
+
+int
+readflags(char *path)
+{
+	char *f;
+	int i;
+
+	f = readpart(path, "flags");
+	i = parseflags(f);
+	free(f);
+	return i;
+}
+
+#define Datefmt		"?WWW, ?MMM ?DD hh:mm:ss ?Z YYYY"
+
+/* most code stolen from nedmail */
+Message*
+loadmessage(char *path)
+{
+	Message *m;
+	char p[256], *f[20+1];
+	int n;
+	Tm tm;
+
+	snprint(p, sizeof p, "%s/info", path);
+	m = mallocz(sizeof *m, 1);
+	if(m==nil)
+		sysfatal("mallocz: %r");
+	m->path = strdup(path);
+	m->info = slurp(p);
+	if(m->info == nil)
+		sysfatal("read info: %r");
+	n = getfields(m->info, f, nelem(f), 0, "\n");
+	if(n < 17)
+		sysfatal("invalid info file %s: only %d fields", path, n);
+	m->from = f[0];
+	m->to = f[1];
+	m->cc = f[2];
+	m->date = f[4];
+	m->subject = f[5];
+	m->type = f[6];
+	m->filename = f[8];
+	if(n > 17)
+		m->flags = parseflags(f[17]);
+	else
+		m->flags = readflags(path);
+	if(n >= 20 && f[19] != nil && strlen(f[19]) > 0)
+		m->sender = f[19];
+	else
+		m->sender = strdup(m->from);
+	m->time = time(nil);
+	if(tmparse(&tm, Datefmt, m->date, nil, nil) != nil)
+		m->time = tmnorm(&tm);
+	return m;
+}
+
+void
+mesgloadbody(Message *m)
+{
+	char path[255];
+	int i;
+	Dir *d;
+	Message *p;
+
+	if(m->body != nil || m->parts != nil)
+		return;
+	snprint(path, sizeof path, "%s/body", m->path);
+	m->body = slurp(path);
+	if(m->type && strncmp(m->type, "multipart/", 10) == 0){
+		m->parts = mkmlist(8);
+		for(i = 1; ; i++){
+			snprint(path, sizeof path, "%s/%d", m->path, i);
+			d = dirstat(path);
+			if(d == nil)
+				break;
+			if((d->qid.type & QTDIR) != QTDIR){
+				free(d);
+				break;
+			}
+			free(d);
+			p = loadmessage(path);
+			mesgloadbody(p);
+			mladd(m->parts, p);
+		}
+	}
+}
+
+int
+mesgmarkseen(Mailbox *mbox, Message *m)
+{
+	char path[255];
+	int fd;
+
+	if(m->flags & Fseen)
+		return 0;
+	snprint(path, sizeof path, "%s/flags", m->path);
+	fd = open(path, OWRITE);
+	if(fd < 0)
+		return 0;
+	fprint(fd, "+s");
+	close(fd);
+	m->flags |= Fseen;
+	mbox->unseen -= 1;
+	return 1;
+}
+
+int
+dircmp(Dir *a, Dir *b)
+{
+	return atoi(a->name) - atoi(b->name);
+}
+
+Mailbox*
+loadmbox(char *name)
+{
+	Mailbox *mb;
+	Dir *d;
+	int n, fd, i;
+	char buf[256];
+	Message *m;
+
+	mb = mallocz(sizeof(Mailbox), 1);
+	if(mb==nil)
+		sysfatal("malloc: %r");
+	mb->name = strdup(name);
+	mb->path = smprint("/mail/fs/%s", name);
+	fd = open(mb->path, OREAD);
+	if(fd<0)
+		sysfatal("open: %r");
+	n = dirreadall(fd, &d);
+	close(fd);
+	qsort(d, n, sizeof *d, (int(*)(void*,void*))dircmp);
+	mb->list = mkmlist(n*1.5);
+	for(i = 1; i < n; i++){
+		snprint(buf, sizeof buf, "%s/%s", mb->path, d[i].name);
+		if((d[i].qid.type & QTDIR)==0)
+			continue;
+		m = loadmessage(buf);
+		m->id = atoi(d[i].name);
+		mladd(mb->list, m);
+		if((m->flags & Fseen) == 0)
+			++mb->unseen;
+		++mb->count;
+	}
+	free(d);
+	return mb;
+}
+
+void
+mboxadd(Mailbox *mbox, char *path)
+{
+	Message *m;
+	int i, id;
+
+	id = atoi(path+strlen(mbox->path)+1);
+	for(i = 0; i < mbox->list->nelts; i++){
+		m = mbox->list->elts[i];
+		if(m->id == id)
+			return;
+	}
+	m = loadmessage(path);
+	m->id = id;
+	if((m->flags & Fseen) == 0)
+		++mbox->unseen;
+	mladd(mbox->list, m);
+	++mbox->count;
+}
+
+int
+mboxmod(Mailbox *mbox, char *path)
+{
+	Message *m;
+	int i, f;
+
+	m = nil;
+	for(i = 0; i < mbox->list->nelts; i++){
+		m = mbox->list->elts[i];
+		if(strcmp(path, m->path)==0)
+			break;
+	}
+	if(m==nil){
+		fprint(2, "could not find mail '%s'\n", path);
+		return 0;
+	}
+	f = readflags(path);
+	if(m->flags != f){
+		if((m->flags & Fseen) != 0 && (f & Fseen) == 0)
+			++mbox->unseen;
+		else if((m->flags & Fseen) == 0 && (f & Fseen) != 0)
+			--mbox->unseen;
+		m->flags = f;
+		return 1;
+	}
+	return 0;
+}
+
+void
+mboxdel(Mailbox *mbox, char *path)
+{
+	Message *m;
+	int i;
+
+	m = nil;
+	for(i = 0; i < mbox->list->nelts; i++){
+		m = mbox->list->elts[i];
+		if(strcmp(path, m->path)==0){
+			mldel(mbox->list, i);
+			free(m->path);
+			free(m->info);
+			free(m->sender);
+			free(m);
+			--mbox->count;
+			return;
+		}
+	}
+	if(m==nil){
+		fprint(2, "could not find mail '%s'\n", path);
+		return;
+	}
+}
+
+void
+mesgdel(Mailbox *mbox, Message *m)
+{
+	int fd;
+
+	fd = open("/mail/fs/ctl", OWRITE);
+	if(fd<0)
+		sysfatal("open: %r");
+	fprint(fd, "delete %s %s", mbox->name, m->path+strlen(mbox->path)+1);
+	close(fd);
+}
+
+void
+seemailproc(void *v)
+{
+	Channel *c;
+	Mailevent e;
+	Plumbmsg *m;
+	int fd;
+	char *s;
+
+	c = v;
+	threadsetname("seemailproc");
+	fd = plumbopen("seemail", OREAD);
+	if(fd<0)
+		sysfatal("cannot open seemail: %r");
+	for(;;){
+		m = plumbrecv(fd);
+		if(m==nil)
+			sysfatal("seemail plumbrecv: %r");
+		s = plumblookup(m->attr, "filetype");
+		if(s != nil && strcmp(s, "vwhois") == 0){
+			plumbfree(m);
+			continue;
+		}
+		s = plumblookup(m->attr, "mailtype");
+		if(strcmp(s, "modify")==0)
+			e.type = Emodify;
+		else if(strcmp(s, "delete")==0)
+			e.type = Edelete;
+		else if(strcmp(s, "new")==0)
+			e.type = Enew;
+		else
+			fprint(2, "received unknown message type: %s\n", s);
+		e.path = strdup(m->data);
+		send(c, &e);
+		plumbfree(m);
+	}
+}
--- /dev/null
+++ b/mkfile
@@ -1,0 +1,8 @@
+</$objtype/mkfile
+
+BIN=/$objtype/bin
+TARG=mongrel
+OFILES=main.$O index.$O pager.$O mbox.$O utils.$O kbd.$O text.$O theme.$O
+HFILES=a.h w.h kbd.h theme.h
+
+</sys/src/cmd/mkone
binary files /dev/null b/mongrel.png differ
--- /dev/null
+++ b/pager.c
@@ -1,0 +1,277 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <thread.h>
+#include <plumb.h>
+#include "theme.h"
+#include "a.h"
+#include "w.h"
+
+enum
+{
+	Padding = 4,
+};
+
+static Mousectl *mc;
+static Text text;
+static Rectangle viewr;
+static Rectangle headr;
+static Rectangle partsr;
+static Rectangle textr;
+static Image *cols[NCOLS];
+static Image *headercol;
+static Message *mesg;
+static Tzone *tz;
+static int showcc;
+static Message *parts[16];
+static int nparts;
+
+char*
+findtextpart(Message *m)
+{
+	Message *p;
+	int i;
+
+	if(strncmp(m->type, "multipart/", 10) == 0){
+		for(i = 0; i < m->parts->nelts; i++){
+			p = m->parts->elts[i];
+			if(strcmp(p->type, "text/plain") == 0)
+				return p->body;
+			else if(strncmp(p->type, "multipart/", 10) == 0)
+				return findtextpart(p);
+		}
+	}
+	return m->body;
+}
+
+void
+collectparts(Message *m)
+{
+	Message *p;
+	int i;
+
+	if(m->parts == nil)
+		return;
+	for(i = 0; i < m->parts->nelts; i++){
+		p = m->parts->elts[i];
+		if(strncmp(p->type, "multipart/", 10) == 0)
+			collectparts(p);
+		else{
+			if(strcmp(p->type, "text/plain") == 0)
+				continue;
+			parts[nparts++] = p;
+		}
+	}
+}
+
+void
+pagershow(Message *m)
+{
+	char *body;
+	int needresize;
+	int oldnparts;
+
+	oldnparts = nparts;
+	needresize = 0;
+	mesg = m;
+	if(showcc){
+		if(mesg->cc == nil || mesg->cc[0] == 0){
+			showcc = 0;
+			needresize = 1;
+		}
+	}else{
+		if(mesg->cc != nil && mesg->cc[0] != 0){
+			showcc = 1;
+			needresize = 1;
+		}
+	}
+	mesgloadbody(mesg);
+	nparts = 0;
+	collectparts(mesg);
+	if(nparts != oldnparts)
+		needresize = 1;
+	if(needresize)
+		pagerresize(viewr);
+	body = findtextpart(mesg);
+	textset(&text, body, strlen(body));
+	pagerdraw();
+}
+
+static
+Point
+drawheader(Point p, char *h, char *s)
+{
+	Rune r;
+	
+	p = string(screen, p, cols[BORD], ZP, font, h);
+	while(s && *s){
+		s += chartorune(&r, s);
+		p = runestringn(screen, p, cols[TEXT], ZP, font, &r, 1);
+	}
+	return p;
+}
+
+Point
+drawparts(Point p)
+{
+	Point q;
+	int i;
+
+	for(i = 0; i < nparts; i++){
+//		q = string(screen, p, cols[TEXT], ZP, font, "=> ");
+		q = p;
+		if(parts[i]->filename != nil && parts[i]->filename[0] != 0)
+			q = string(screen, q, cols[TEXT], ZP, font, parts[i]->filename);
+		else
+			q = string(screen, q, cols[TEXT], ZP, font, "unnamed");
+		q = string(screen, q, cols[BORD], ZP, font, " [");
+		q = string(screen, q, cols[BORD], ZP, font, parts[i]->type);
+		string(screen, q, cols[BORD], ZP, font, "]");
+		p.y += font->height + Padding;
+	}
+	return p;
+}
+
+void
+pagerdraw(void)
+{
+	Point p, q;
+	char buf[32], *s;
+	Rune r;
+	Tm t;
+	int w;
+
+	draw(screen, viewr, cols[BACK], nil, ZP);
+	if(mesg != nil){
+		p = addpt(headr.min, Pt(Padding, Padding));
+		tmtime(&t, mesg->time, tz);
+		snprint(buf, sizeof buf, "%τ", tmfmt(&t, "DD/MM/YYYY hh:mm"));
+		w = stringwidth(font, buf);
+		string(screen, addpt(p, Pt(Dx(headr) - w - 2*Padding, 0)), cols[TEXT], ZP, font, buf);
+		q = drawheader(p, "   From ", mesg->sender);
+		if(strcmp(mesg->sender, mesg->from) != 0){
+			q = string(screen, q, cols[TEXT], ZP, font, " <");
+			s = mesg->from;
+			while(s && *s){
+				s += chartorune(&r, s);
+				q = runestringn(screen, q, cols[TEXT], ZP, font, &r, 1);
+			}
+			string(screen, q, cols[TEXT], ZP, font, ">");
+		}
+		p.y += font->height + Padding;
+		drawheader(p, "     To ", mesg->to);
+		if(showcc){
+			p.y += font->height + Padding;
+			drawheader(p, "     Cc ", mesg->cc);
+		}
+		p.y += font->height + Padding;
+		drawheader(p, "Subject ", mesg->subject);
+	}
+	line(screen, addpt(headr.min, Pt(0, Dy(headr))), headr.max, 0, 0, 0, headercol, ZP);
+	if(nparts > 0){
+		p = addpt(partsr.min, Pt(Padding, Padding));
+		drawparts(p);
+		line(screen, addpt(partsr.min, Pt(0, Dy(partsr))), partsr.max, 0, 0, 0, headercol, ZP);
+	}
+	textdraw(&text);
+}
+
+void
+pagerresize(Rectangle r)
+{
+	int n;
+
+	n = showcc ? 4 : 3;
+	viewr = r;
+	headr = viewr;
+	headr.max.y = headr.min.y + Padding + n*(font->height + Padding);
+	if(nparts > 0){
+		partsr = viewr;
+		partsr.min.y = headr.max.y + 1;
+		partsr.max.y = partsr.min.y + nparts*(font->height + Padding) + Padding;
+	}
+	textr = viewr;
+	if(nparts > 0)
+		textr.min.y = partsr.max.y + 1;
+	else
+		textr.min.y = headr.max.y + 1;
+	textresize(&text, textr);
+}
+
+void
+partclick(Point p)
+{
+	Message *m;
+	char *dst, buf[1024] = {0};
+	int fd, n;
+
+	fd = plumbopen("send", OWRITE);
+	if(fd < 0)
+		return;
+	n = (p.y - partsr.min.y) / (font->height+Padding);
+	if(n < 0 || n >= nparts){
+		close(fd);
+		return;
+	}
+	m = parts[n];
+	dst = nil;
+	if(strcmp(m->type, "text/html") == 0){
+		snprint(buf, sizeof buf, "file://%s/body.html", m->path);
+		dst = "web";
+	}else if(strncmp(m->type, "image/", 6) == 0){
+		snprint(buf, sizeof buf, "%s/body.%s", m->path, m->type+6);
+		dst = "image";
+	}else{
+		snprint(buf, sizeof buf, "%s/body", m->path);
+	}
+	plumbsendtext(fd, "mongrel", dst, nil, buf);
+	close(fd);
+}
+
+void
+pagermouse(Mouse m)
+{
+	if(!ptinrect(m.xy, viewr))
+		return;
+	if(nparts > 0 && ptinrect(m.xy, partsr) && m.buttons == 4)
+		partclick(m.xy);
+	else if(ptinrect(m.xy, textr))
+		textmouse(&text, mc);
+}
+
+void
+pagerkey(Rune k)
+{
+	textkeyboard(&text, k);
+}
+
+void
+pagerinit(Mousectl *mctl, Theme *theme)
+{
+	Rectangle r;
+
+	mc = mctl;
+	tz = tzload("local");
+	showcc = 0;
+	nparts = 0;
+	if(theme != nil){
+		cols[BACK] = theme->back;
+		cols[BORD] = theme->border;
+		cols[TEXT] = theme->text;
+		cols[HTEXT] = theme->htext;
+		cols[HIGH] = theme->high;
+		headercol = theme->title;
+	}else{
+		r = Rect(0, 0, 1, 1);
+		cols[BACK] = display->white;
+		cols[BORD] = allocimage(display, r, screen->chan, 1, 0x999999FF);
+		cols[TEXT] = allocimage(display, r, screen->chan, 1, 0x000000FF);
+		cols[HTEXT] = allocimage(display, r, screen->chan, 1, 0x000000FF);
+		cols[HIGH] = allocimage(display, r, screen->chan, 1, 0xCCCCCCFF);
+		headercol = allocimage(display, r, screen->chan, 1, DGreygreen);
+	}
+	textinit(&text, screen, screen->r, font, cols);
+}
+
--- /dev/null
+++ b/text.c
@@ -1,0 +1,347 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <thread.h>
+#include <plumb.h>
+#include "w.h"
+
+enum
+{
+	Scrollwidth = 12,
+	Padding = 4,
+};
+
+enum
+{
+	Msnarf,
+	Mplumb,
+};
+char *menu2str[] = {
+	"snarf",
+	"plumb",
+	nil,
+};
+Menu menu2 = { menu2str };
+
+void
+computelines(Text *t)
+{
+	int i, x, w, l, c;
+	Rune r;
+
+	t->lines[0] = 0;
+	t->nlines = 1;
+	w = Dx(t->textr);
+	x = 0;
+	for(i = 0; i < t->ndata; ){
+		c = chartorune(&r, &t->data[i]);
+		if(r == '\n'){
+			if(i + c == t->ndata)
+				break;
+			t->lines[t->nlines++] = i + c;
+			x = 0;
+		}else{
+			l = 0;
+			if(r == '\t'){
+				x += stringwidth(t->font, "    ");
+			}else{
+				l = runestringnwidth(t->font, &r, 1);
+				x += l;
+			}
+			if(x > w){
+				t->lines[t->nlines++] = i;
+				x = l;
+			}
+		}
+		i += c;
+	}
+}
+
+int
+indexat(Text *t, Point p)
+{
+	int line, i, s, e, x, c, l;
+	Rune r;
+
+	if(!ptinrect(p, t->textr))
+		return -1;
+	line = t->offset + ((p.y - t->textr.min.y) / font->height);
+	s = t->lines[line];
+	if(line+1 >= t->nlines)
+		e = t->ndata;
+	else
+		e = t->lines[line+1];
+	x = t->textr.min.x;
+	for(i = s; i <= e; ){
+		c = chartorune(&r, &t->data[i]);
+		if(r == '\t')
+			l = stringwidth(t->font, "    ");
+		else
+			l = runestringnwidth(t->font, &r, 1);
+		if(x <= p.x && p.x <= x+l)
+			break;
+		i += c;
+		x += l;
+	}
+	return i;
+}
+
+void
+textinit(Text *t, Image *b, Rectangle r, Font *f, Image *cols[NCOLS])
+{
+	memset(t, 0, sizeof *t);
+	t->b = b;
+	t->font = f;
+	t->s0 = -1;
+	t->s1 = -1;
+	t->offset = 0;
+	textresize(t, r);
+	memmove(t->cols, cols, sizeof t->cols);
+}
+
+void
+textset(Text *t, char *data, usize ndata)
+{
+	t->data = data;
+	t->ndata = ndata;
+	computelines(t);
+}
+
+void
+textresize(Text *t, Rectangle r)
+{
+	t->r = r;
+	t->vlines = Dy(t->r) / t->font->height;
+	t->scrollr = rectaddpt(Rect(0, 0, Scrollwidth, Dy(r)), r.min);
+	t->textr = r;
+	t->textr.min.x = t->scrollr.max.x + Padding;
+	if(t->nlines > 0)
+		computelines(t);
+}
+
+int
+selected(Text *t, int index)
+{
+	int s0, s1;
+
+	if(t->s0 < 0 || t->s1 < 0)
+		return 0;
+	s0 = t->s0 < t->s1 ? t->s0 : t->s1;
+	s1 = t->s0 > t->s1 ? t->s0 : t->s1;
+	return s0 <= index && index <= s1;
+}
+
+void
+drawline(Text *t, int index)
+{
+	int i, s, e;
+	Point p;
+	Rune r;
+	Image *fg, *bg;
+
+	s = t->lines[t->offset+index];
+	if(t->offset+index+1 >= t->nlines)
+		e = t->ndata;
+	else
+		e = t->lines[t->offset+index+1];
+	p = addpt(t->textr.min, Pt(0, index*font->height));
+	for(i = s; i < e; ){
+		fg = selected(t, i) ? t->cols[HTEXT] : t->cols[TEXT];
+		bg = selected(t, i) ? t->cols[HIGH]  : t->cols[BACK];
+		i += chartorune(&r, &t->data[i]);
+		if(r == '\n')
+			if(s + 1 == e) /* empty line */
+				r = L' ';
+			else
+				continue;
+		if(r == '\t')
+			p = stringbg(t->b, p, fg, ZP, t->font, "    ", bg, ZP);
+		else
+			p = runestringnbg(t->b, p, fg, ZP, t->font, &r, 1, bg, ZP);
+	}
+}
+
+void
+textdraw(Text *t)
+{
+	int i, h, y;
+	Rectangle sr;
+
+	draw(t->b, t->r, t->cols[BACK], nil, ZP);
+	draw(t->b, t->scrollr, t->cols[BORD], nil, ZP);
+	border(t->b, t->scrollr, 0, t->cols[TEXT], ZP);
+	if(t->nlines > 0){
+		h = ((double)t->vlines / t->nlines) * Dy(t->scrollr);
+		y = ((double)t->offset / t->nlines) * Dy(t->scrollr);
+		sr = Rect(t->scrollr.min.x + 1, t->scrollr.min.y + y + 1, t->scrollr.max.x - 1, t->scrollr.min.y + y + h - 1);
+	}else
+		sr = insetrect(t->scrollr, -1);
+	draw(t->b, sr, t->cols[BACK], nil, ZP);
+	for(i = 0; i < t->vlines; i++){
+		if(t->offset+i >= t->nlines)
+			break;
+		drawline(t, i);
+	}
+	flushimage(display, 1);
+}
+
+static
+void
+scroll(Text *t, int lines)
+{
+	if(t->nlines <= t->vlines)
+		return;
+	if(lines < 0 && t->offset == 0)
+		return;
+	if(lines > 0 && t->offset + t->vlines >= t->nlines)
+		return;
+	t->offset += lines;
+	if(t->offset < 0)
+		t->offset = 0;
+	if(t->offset + t->nlines%t->vlines >= t->nlines)
+		t->offset = t->nlines - t->nlines%t->vlines;
+	textdraw(t);
+}
+
+void
+textkeyboard(Text *t, Rune k)
+{
+	switch(k){
+	case Kup:
+		scroll(t, -1);
+		break;
+	case Kdown:
+		scroll(t, 1);
+		break;
+	case Kpgup:
+		scroll(t, -t->vlines);
+		break;
+	case Kpgdown:
+		scroll(t, t->vlines);
+		break;
+	case Khome:
+		scroll(t, -t->nlines);
+		break;
+	case Kend:
+		scroll(t, t->nlines);
+		break;
+	}
+}
+
+void
+snarfsel(Text *t)
+{
+	int fd, s0, s1;
+
+	if(t->s0 < 0 || t->s1 < 0)
+		return;
+	fd = open("/dev/snarf", OWRITE);
+	if(fd < 0)
+		return;
+	s0 = t->s0 < t->s1 ? t->s0 : t->s1;
+	s1 = t->s0 > t->s1 ? t->s0 : t->s1;
+	write(fd, &t->data[s0], s1 - s0 + 1);
+	close(fd);
+}
+
+void
+plumbsel(Text *t)
+{
+	int fd, s0, s1;
+	char *s;
+
+	if(t->s0 < 0 || t->s1 < 0)
+		return;
+	fd = plumbopen("send", OWRITE);
+	if(fd < 0)
+		return;
+	s0 = t->s0 < t->s1 ? t->s0 : t->s1;
+	s1 = t->s0 > t->s1 ? t->s0 : t->s1;
+	s = smprint("%.*s", s1 - s0 + 1, &t->data[s0]);
+	plumbsendtext(fd, argv0, nil, nil, s);
+	free(s);
+	close(fd);
+}
+
+void
+menu2hit(Text *t, Mousectl *mc)
+{
+	int n;
+
+	n = menuhit(2, mc, &menu2, nil);
+	switch(n){
+		case Msnarf:
+			snarfsel(t);
+			break;
+		case Mplumb:
+			plumbsel(t);
+			break;
+	}
+}
+
+void
+textmouse(Text *t, Mousectl *mc)
+{
+	static selecting = 0;
+	Point p;
+	int n;
+
+	if(ptinrect(mc->xy, t->scrollr)){
+		if(mc->buttons == 1){
+			n = (mc->xy.y - t->scrollr.min.y) / font->height;
+			scroll(t, -n);
+		}else if(mc->buttons == 2){
+			t->offset = (mc->xy.y - t->scrollr.min.y) * t->nlines / Dy(t->scrollr);
+			textdraw(t);
+		}else if(mc->buttons == 4){
+			n = (mc->xy.y - t->scrollr.min.y) / font->height;
+			scroll(t, n);
+		}
+	}else if(ptinrect(mc->xy, t->textr)){
+		if(mc->buttons == 0)
+			selecting = 0;
+		if(mc->buttons == 1){
+			if(!selecting){
+				selecting = 1;
+				t->s0 = t->s1 = -1;
+				n = indexat(t, mc->xy);
+				if(n < 0)
+					return;
+				t->s0 = n;
+				t->s1 = -1;
+				textdraw(t);
+			}else{
+				n = indexat(t, mc->xy);
+				if(n < 0)
+					return;
+				t->s1 = n;
+			}
+			for(readmouse(mc); mc->buttons == 1; readmouse(mc)){
+				p = mc->xy;
+				if(p.y <= t->textr.min.y){
+					scroll(t, -1);
+					p.y = t->textr.min.y + 1;
+				}else if(p.y >= t->textr.max.y){
+					scroll(t, 1);
+					p.y = t->textr.max.y - 1;
+				}
+				n = indexat(t, p);
+				if(n < 0)
+					break;
+				t->s1 = n;
+				textdraw(t);
+			}
+		}else if(mc->buttons == 2){
+			menu2hit(t, mc);
+		}else if(mc->buttons == 8){
+			n = mousescrollsize(t->vlines);
+			scroll(t, -n);
+		}else if(mc->buttons == 16){
+			n = mousescrollsize(t->vlines);
+			scroll(t, n);
+		}
+	}
+}
+
--- /dev/null
+++ b/theme.c
@@ -1,0 +1,84 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <draw.h>
+#include "theme.h"
+
+Image*
+ereadcol(char *s)
+{
+	Image *i;
+	char *e;
+	ulong c;
+
+	c = strtoul(s, &e, 16);
+	if(e == nil || e == s)
+		return nil;
+	c = (c << 8) | 0xff;
+	i = allocimage(display, Rect(0, 0, 1, 1), screen->chan, 1, c);
+	if(i == nil)
+		sysfatal("allocimage: %r");
+	return i;
+}
+
+Theme*
+loadtheme(void)
+{
+	Theme *theme;
+	Biobuf *bp;
+	char *s;
+
+	if(access("/dev/theme", AREAD) < 0)
+		return 0;
+	bp = Bopen("/dev/theme", OREAD);
+	if(bp == nil)
+		return 0;
+	theme = malloc(sizeof *theme);
+	if(theme == nil){
+		Bterm(bp);
+		return nil;
+	}
+	for(;;){
+		s = Brdstr(bp, '\n', 1);
+		if(s == nil)
+			break;
+		if(strncmp(s, "back", 4) == 0)
+			theme->back = ereadcol(s+5);
+		else if(strncmp(s, "high", 4) == 0)
+			theme->high = ereadcol(s+5);
+		else if(strncmp(s, "border", 6) == 0)
+			theme->border = ereadcol(s+7);
+		else if(strncmp(s, "text", 4) == 0)
+			theme->text = ereadcol(s+5);
+		else if(strncmp(s, "htext", 5) == 0)
+			theme->htext = ereadcol(s+6);
+		else if(strncmp(s, "title", 5) == 0)
+			theme->title = ereadcol(s+6);
+		else if(strncmp(s, "ltitle", 6) == 0)
+			theme->ltitle = ereadcol(s+7);
+		else if(strncmp(s, "hold", 4) == 0)
+			theme->hold = ereadcol(s+5);
+		else if(strncmp(s, "lhold", 5) == 0)
+			theme->lhold = ereadcol(s+6);
+		else if(strncmp(s, "palehold", 8) == 0)
+			theme->palehold = ereadcol(s+9);
+		else if(strncmp(s, "paletext", 8) == 0)
+			theme->paletext = ereadcol(s+9);
+		else if(strncmp(s, "size", 4) == 0)
+			theme->size = ereadcol(s+5);
+		else if(strncmp(s, "menuback", 8) == 0)
+			theme->menuback = ereadcol(s+9);
+		else if(strncmp(s, "menuhigh", 8) == 0)
+			theme->menuhigh = ereadcol(s+9);
+		else if(strncmp(s, "menubord", 8) == 0)
+			theme->menubord = ereadcol(s+9);
+		else if(strncmp(s, "menutext", 8) == 0)
+			theme->menutext = ereadcol(s+9);
+		else if(strncmp(s, "menuhtext", 5) == 0)
+			theme->menuhtext = ereadcol(s+6);
+		free(s);
+	}
+	Bterm(bp);
+	return theme;
+}
+
--- /dev/null
+++ b/theme.h
@@ -1,0 +1,25 @@
+typedef struct Theme Theme;
+
+struct Theme
+{
+	Image *back;
+	Image *high;
+	Image *border;
+	Image *text;
+	Image *htext;
+	Image *title;
+	Image *ltitle;
+	Image *hold;
+	Image *lhold;
+	Image *palehold;
+	Image *paletext;
+	Image *size;
+	Image *menubar;
+	Image *menuback;
+	Image *menuhigh;
+	Image *menubord;
+	Image *menutext;
+	Image *menuhtext;
+};
+
+Theme* loadtheme(void);
--- /dev/null
+++ b/utils.c
@@ -1,0 +1,83 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <mouse.h>
+#include "theme.h"
+#include "a.h"
+
+void*
+emalloc(ulong size)
+{
+	void *p;
+
+	p = malloc(size);
+	if(p == nil)
+		sysfatal("malloc: %r");
+	return p;
+}
+
+void*
+erealloc(void *p, ulong size)
+{
+	p = realloc(p, size);
+	if(p == nil)
+		sysfatal("realloc: %r");
+	return p;
+}
+
+void
+mlmaybegrow(Mlist *ml)
+{
+	if(ml->nelts < ml->size)
+		return;
+	ml->size *= 1.5;
+	ml->elts = erealloc(ml->elts, ml->size * sizeof(Message*));
+}
+
+Mlist*
+mkmlist(usize cap)
+{
+	Mlist *ml;
+
+	ml = emalloc(sizeof *ml);
+	ml->size = cap;
+	ml->nelts = 0;
+	ml->elts = emalloc(cap * sizeof(Message*));
+	return ml;
+}
+
+int
+mladd(Mlist *ml, Message *m)
+{
+	mlmaybegrow(ml);
+	ml->elts[ml->nelts] = m;
+	ml->nelts += 1;
+	return 0;
+}
+
+int
+mlinsert(Mlist *ml, usize index, Message *m)
+{
+	if(index > ml->nelts || index >= ml->size)
+		return -1;
+	mlmaybegrow(ml);
+	memmove(&ml->elts[index+1], &ml->elts[index], (ml->nelts - index + 1)*sizeof(Message*));
+	ml->elts[index] = m;
+	ml->nelts += 1;
+	return 0;
+}
+
+Message*
+mldel(Mlist *ml, usize index)
+{
+	Message *m;
+
+	if(index >= ml->nelts || index >= ml->size)
+		return nil;
+	m = ml->elts[index];
+	memmove(&ml->elts[index], &ml->elts[index+1], (ml->nelts -index)*sizeof(Message*));
+	ml->nelts -= 1;
+	return m;
+}
+
--- /dev/null
+++ b/w.h
@@ -1,0 +1,42 @@
+typedef struct Text Text;
+
+enum
+{
+	BACK,
+	BORD,
+	TEXT,
+	HTEXT,
+	HIGH,
+	NCOLS,
+};
+
+enum
+{
+	Maxlines = 65535,
+};
+
+struct Text
+{
+	Image *b;
+	Rectangle r;
+	Rectangle scrollr;
+	Rectangle textr;
+	Font *font;
+	Image *cols[NCOLS];
+	int vlines;
+	int offset;
+	char *data;
+	usize ndata;
+	usize lines[Maxlines];
+	int nlines;
+	int s0;
+	int s1;
+};
+
+void textinit(Text *t, Image *b, Rectangle r, Font *f, Image *cols[NCOLS]);
+void textset(Text *t, char *data, usize ndata);
+void textresize(Text *t, Rectangle r);
+void textkeyboard(Text *t, Rune k);
+void textmouse(Text *t, Mousectl *m);
+void textdraw(Text *t);
+