shithub: fork

Download patch

ref: db10b4c70cf1c7eeaca2b51d415bdf0f37da23c7
parent: 4026a6866022dc165bebf5de58e60d16c1e11a11
author: qwx <[email protected]>
date: Sat Aug 19 05:12:17 EDT 2023

page: theming, delete and invert commands

--- /dev/null
+++ b/sys/src/cmd/page.c
@@ -1,0 +1,1956 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <cursor.h>
+#include <keyboard.h>
+#include <plumb.h>
+
+typedef struct Page Page;
+struct Page {
+	char	*name;
+	char	*delim;
+
+	QLock;
+	char	*ext;
+	void	*data;
+	int	(*open)(Page *);
+
+	Image	*image;
+	int	fd;
+
+	Page	*up;
+	Page	*next;
+	Page	*down;
+	Page	*tail;
+
+	Page	*lnext;
+	Page	*lprev;
+};
+
+int zoom = 1;
+int ppi = 100;
+int imode;
+int newwin;
+int rotate;
+int invert;
+int viewgen;
+int forward;	/* read ahead direction: >= 0 forwards, < 0 backwards */
+Point resize, pos;
+Page *root, *current;
+Page lru;
+QLock pagelock;
+int nullfd;
+char *pagewalk = nil;
+
+enum {
+	MiB	= 1024*1024,
+};
+
+ulong imemlimit = 16*MiB;
+ulong imemsize;
+
+enum{
+	Cground,
+	Cpaper,
+	Cframe,
+	Ctext,
+	Ncols,
+};
+Image *cols[Ncols];
+
+char pagespool[] = "/tmp/pagespool.";
+
+enum {
+	NPROC = 8,
+	NBUF = 8*1024,
+	NPATH = 1024,
+};
+
+enum {
+	Corigsize,
+	Czoomin,
+	Czoomout,
+	Cfitwidth,
+	Cfitheight,
+	Crotate90,
+	Cupsidedown,
+	Cinvert,
+	Cdummy1,
+	Cnext,
+	Cprev,
+	Csnarf,
+	Czerox,
+	Cwrite,
+	Cext,
+	Cpop,
+	Cdummy2,
+	Cdelete,
+	Cdummy3,
+	Cquit,
+};
+
+struct {
+	char	*m;
+	Rune	k1;
+	Rune	k2;
+	Rune	k3;
+} cmds[] = {
+	[Corigsize]	"orig size",	'o', Kesc, 0,
+	[Czoomin]	"zoom in",	'+', 0, 0,
+	[Czoomout]	"zoom out",	'-', 0, 0,
+	[Cfitwidth]	"fit width",	'f', 0, 0,
+	[Cfitheight]	"fit height",	'h', 0, 0,
+	[Crotate90]	"rotate 90",	'r', 0, 0,
+	[Cupsidedown]	"upside down",	'u', 0, 0,
+	[Cinvert]	"invert",	'i', 0, 0,
+	[Cdummy1]	"",		0, 0, 0,
+	[Cnext]		"next",		Kright, ' ', '\n', 
+	[Cprev]		"prev",		Kleft, Kbs, 0,
+	[Csnarf]	"snarf",	's', 0, 0,
+	[Czerox]	"zerox",	'z', 0, 0,
+	[Cwrite]	"write",	'w', 0, 0,
+	[Cext]		"ext",		'x', 0, 0,
+	[Cpop]		"pop",		'p', 0, 0,
+	[Cdummy2]	"",		0, 0, 0,
+	[Cdelete]	"delete",	'D', 0, 0,
+	[Cdummy3]	"",		0, 0, 0,
+	[Cquit]		"quit",		'q', Kdel, Keof,
+};
+
+char *pagemenugen(int i);
+char *cmdmenugen(int i);
+
+Menu pagemenu = {
+	nil,
+	pagemenugen,
+	-1,
+};
+
+Menu cmdmenu = {
+	nil,
+	cmdmenugen,
+	-1,
+};
+
+Cursor reading = {
+	{-1, -1},
+	{0xff, 0x80, 0xff, 0x80, 0xff, 0x00, 0xfe, 0x00, 
+	 0xff, 0x00, 0xff, 0x80, 0xff, 0xc0, 0xef, 0xe0, 
+	 0xc7, 0xf0, 0x03, 0xf0, 0x01, 0xe0, 0x00, 0xc0, 
+	 0x03, 0xff, 0x03, 0xff, 0x03, 0xff, 0x03, 0xff, },
+	{0x00, 0x00, 0x7f, 0x00, 0x7e, 0x00, 0x7c, 0x00, 
+	 0x7e, 0x00, 0x7f, 0x00, 0x6f, 0x80, 0x47, 0xc0, 
+	 0x03, 0xe0, 0x01, 0xf0, 0x00, 0xe0, 0x00, 0x40, 
+	 0x00, 0x00, 0x01, 0xb6, 0x01, 0xb6, 0x00, 0x00, }
+};
+
+int pagewalk1(Page *p);
+void showpage1(Page *);
+void showpage(Page *);
+void drawpage(Page *);
+Point pagesize(Page *);
+void drawlock(int);
+
+Page*
+addpage(Page *up, char *name, int (*popen)(Page *), void *pdata, int fd)
+{
+	Page *p;
+
+	p = mallocz(sizeof(*p), 1);
+	p->name = strdup(name);
+	p->delim = "!";
+ 	p->image = nil;
+	p->data = pdata;
+	p->open = popen;
+	p->fd = fd;
+
+	qlock(&pagelock);
+	if(p->up = up){
+		if(up->tail == nil)
+			up->down = up->tail = p;
+		else {
+			up->tail->next = p;
+			up->tail = p;
+		}
+	}
+	qunlock(&pagelock);
+
+	if(up && current == up){
+		if(!pagewalk1(p))
+			return p;
+		showpage1(p);
+	}
+	return p;
+}
+
+void
+resizewin(Point size)
+{
+	int wctl;
+
+	if((wctl = open("/dev/wctl", OWRITE)) < 0)
+		return;
+	/* add rio border */
+	size = addpt(size, Pt(Borderwidth*2, Borderwidth*2));
+	if(display->image != nil){
+		Point dsize = subpt(display->image->r.max, display->image->r.min);
+		if(size.x > dsize.x)
+			size.x = dsize.x;
+		if(size.y > dsize.y)
+			size.y = dsize.y;
+		/* can't just conver whole display */
+		if(eqpt(size, dsize))
+			size.y--;
+	}
+	fprint(wctl, "resize -dx %d -dy %d\n", size.x, size.y);
+	close(wctl);
+}
+
+int
+createtmp(char *pfx)
+{
+	static ulong id = 1;
+	char nam[64];
+	snprint(nam, sizeof nam, "%s%s%.12d%.8lux", pagespool, pfx, getpid(), id++);
+	return create(nam, OEXCL|ORCLOSE|ORDWR, 0600);
+}
+
+int
+catchnote(void *, char *msg)
+{
+	if(strstr(msg, "sys: write on closed pipe"))
+		return 1;
+	if(strstr(msg, "hangup"))
+		return 1;
+	if(strstr(msg, "alarm"))
+		return 1;
+	if(strstr(msg, "interrupt"))
+		return 1;
+	if(strstr(msg, "kill"))
+		exits("killed");
+	return 0;
+}
+
+void
+dupfds(int fd, ...)
+{
+	int mfd, n, i;
+	va_list arg;
+	Dir *dir;
+
+	va_start(arg, fd);
+	for(mfd = 0; fd >= 0; fd = va_arg(arg, int), mfd++)
+		if(fd != mfd)
+			if(dup(fd, mfd) < 0)
+				sysfatal("dup: %r");
+	va_end(arg);
+	if((fd = open("/fd", OREAD)) < 0)
+		sysfatal("open: %r");
+	n = dirreadall(fd, &dir);
+	for(i=0; i<n; i++){
+		if(strstr(dir[i].name, "ctl"))
+			continue;
+		fd = atoi(dir[i].name);
+		if(fd >= mfd)
+			close(fd);
+	}
+	free(dir);
+}
+
+void
+pipeline(int fd, char *fmt, ...)
+{
+	char buf[NPATH], *argv[4];
+	va_list arg;
+	int pfd[2];
+
+	if(pipe(pfd) < 0){
+	Err:
+		dup(nullfd, fd);
+		return;
+	}
+	va_start(arg, fmt);
+	vsnprint(buf, sizeof buf, fmt, arg);
+	va_end(arg);
+	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);
+		sysfatal("exec: %r");
+	}
+	close(pfd[1]);
+	dup(pfd[0], fd);
+	close(pfd[0]);
+}
+
+static char*
+shortlabel(char *s)
+{
+	enum { NR=60 };
+	static char buf[NR*UTFmax];
+	int i, k, l;
+	Rune r;
+
+	l = utflen(s);
+	if(l < NR-2)
+		return s;
+	k = i = 0;
+	while(i < NR/2){
+		k += chartorune(&r, s+k);
+		i++;
+	}
+	strncpy(buf, s, k);
+	strcpy(buf+k, "...");
+	while((l-i) >= NR/2-4){
+		k += chartorune(&r, s+k);
+		i++;
+	}
+	strcat(buf, s+k);
+	return buf;
+}
+
+static char*
+pageaddr1(Page *p, char *s, char *e)
+{
+	if(p == nil || p == root)
+		return s;
+	return seprint(pageaddr1(p->up, s, e), e, "%s%s", p->up->delim, p->name);
+}
+
+/*
+ * returns address string of a page in the form:
+ * /dir/filename!page!subpage!...
+ */
+char*
+pageaddr(Page *p, char *buf, int nbuf)
+{
+	buf[0] = 0;
+	pageaddr1(p, buf, buf+nbuf);
+	return buf;
+}
+
+int
+popenfile(Page*);
+
+int
+popenimg(Page *p)
+{
+	char nam[NPATH];
+	int fd;
+
+	if((fd = dup(p->fd, -1)) < 0){
+		close(p->fd);
+		p->fd = -1;
+		return -1;
+	}
+
+	seek(fd, 0, 0);
+	if(p->data){
+		p->ext = p->data;
+		if(strcmp(p->ext, "ico") == 0)
+			pipeline(fd, "exec %s -c", p->ext);
+		else
+			pipeline(fd, "exec %s -t9", p->ext);
+	}
+
+	/*
+	 * dont keep the file descriptor arround if it can simply
+	 * be reopened.
+	 */
+	fd2path(p->fd, nam, sizeof(nam));
+	if(strncmp(nam, pagespool, strlen(pagespool))){
+		close(p->fd);
+		p->fd = -1;
+		p->data = strdup(nam);
+		p->open = popenfile;
+	}
+
+	return fd;
+}
+
+int
+popenfilter(Page *p)
+{
+	seek(p->fd, 0, 0);
+	if(p->data){
+		pipeline(p->fd, "exec %s", (char*)p->data);
+		p->data = nil;
+	}
+	p->open = popenfile;
+	return p->open(p);
+}
+
+int
+popentape(Page *p)
+{
+	char mnt[32], cmd[64], *argv[4];
+
+	seek(p->fd, 0, 0);
+	snprint(mnt, sizeof(mnt), "/n/tapefs.%.12d%.8lux", getpid(), (ulong)(uintptr)p);
+	snprint(cmd, sizeof(cmd), "exec %s -m %s /fd/0", (char*)p->data, mnt);
+	switch(rfork(RFPROC|RFMEM|RFFDG|RFREND)){
+	case -1:
+		close(p->fd);
+		p->fd = -1;
+		return -1;
+	case 0:
+		dupfds(p->fd, 1, 2, -1);
+		argv[0] = "rc";
+		argv[1] = "-c";
+		argv[2] = cmd;
+		argv[3] = nil;
+		exec("/bin/rc", argv);
+		sysfatal("exec: %r");
+	}
+	close(p->fd);
+	waitpid();
+	p->fd = -1;
+	p->data = strdup(mnt);
+	p->open = popenfile;
+	return p->open(p);
+}
+
+int
+popenepub(Page *p)
+{
+	char buf[NPATH], *s, *e;
+	int n, fd;
+
+	fd = p->fd;
+	p->fd = -1;
+	s = buf;
+	e = buf+sizeof(buf)-1;
+	s += snprint(s, e-s, "%s/", (char*)p->data);
+	free(p->data);
+	p->data = nil;
+	pipeline(fd, "awk '/\\<rootfile/{"
+		"if(match($0, /full\\-path\\=\\\"([^\\\"]+)\\\"/)){"
+		"print substr($0, RSTART+11,RLENGTH-12);exit}}'");
+	n = read(fd, s, e - s);
+	close(fd);
+	if(n <= 0)
+		return -1;
+	while(n > 0 && s[n-1] == '\n')
+		n--;
+	s += n;
+	*s = 0;
+	if((fd = open(buf, OREAD)) < 0)
+		return -1;
+	pipeline(fd, "awk '/\\<item/{"
+		"if(match($0, /id\\=\\\"([^\\\"]+)\\\"/)){"
+		"id=substr($0, RSTART+4, RLENGTH-5);"
+		"if(match($0, /href\\=\\\"([^\\\"]+)\\\"/)){"
+		"item[id]=substr($0, RSTART+6, RLENGTH-7)}}};"
+		"/\\<itemref/{"
+		"if(match($0, /idref\\=\\\"([^\\\"]+)\\\"/)){"
+		"ref=substr($0, RSTART+7, RLENGTH-8);"
+		"print item[ref]; fflush}}'");
+	s = strrchr(buf, '/')+1;
+	while((n = read(fd, s, e-s)) > 0){
+		while(n > 0 && s[n-1] == '\n')
+			n--;
+		s[n] = 0;
+		addpage(p, s, popenfile, strdup(buf), -1);
+	}
+	close(fd);
+	return -1;
+}
+
+typedef struct Ghost Ghost;
+struct Ghost
+{
+	QLock;
+
+	int	pin;
+	int	pout;
+	int	pdat;
+};
+
+int
+popenpdf(Page *p)
+{
+	char buf[NBUF];
+	int n, pfd[2];
+	Ghost *gs;
+
+	if(pipe(pfd) < 0)
+		return -1;
+	switch(rfork(RFPROC|RFMEM|RFFDG|RFNOWAIT)){
+	case -1:
+		close(pfd[0]);
+		close(pfd[1]);
+		return -1;
+	case 0:
+		gs = p->data;
+		qlock(gs);
+		dupfds(gs->pdat, gs->pin, pfd[1], -1);
+		fprint(1, "%s DoPDFPage\n"
+			"(/fd/3) (w) file "
+			"dup flushfile "
+			"dup (THIS IS NOT AN INFERNO BITMAP\\n) writestring "
+			"flushfile\n", p->name);
+		while((n = read(0, buf, sizeof buf)) > 0){
+			if(memcmp(buf, "THIS IS NOT AN INFERNO BITMAP\n", 30) == 0)
+				break;
+			write(2, buf, n);
+		}
+		qunlock(gs);
+		exits(nil);
+	}
+	close(pfd[1]);
+	return pfd[0];
+}
+
+int
+infernobithdr(char *buf, int n)
+{
+	if(n >= 11){
+		if(memcmp(buf, "compressed\n", 11) == 0)
+			return 1;
+		if(strtochan((char*)buf))
+			return 1;
+		if(memcmp(buf, "          ", 10) == 0 && 
+			'0' <= buf[10] && buf[10] <= '9' &&
+			buf[11] == ' ')
+			return 1;
+	}
+	return 0;
+}
+
+int
+popengs(Page *p)
+{
+	int n, i, pdf, ifd, ofd, pin[2], pout[2], pdat[2];
+	char buf[NBUF], nam[32], *argv[16];
+
+	pdf = 0;
+	ifd = p->fd;
+	p->fd = -1;
+	p->open = nil;
+	seek(ifd, 0, 0);
+	if(read(ifd, buf, 5) != 5)
+		goto Err0;
+	seek(ifd, 0, 0);
+	if(memcmp(buf, "%PDF-", 5) == 0)
+		pdf = 1;
+	if(pipe(pin) < 0){
+	Err0:
+		close(ifd);
+		return -1;
+	}
+	if(pipe(pout) < 0){
+	Err1:
+		close(pin[0]);
+		close(pin[1]);
+		goto Err0;
+	}
+	if(pipe(pdat) < 0){
+	Err2:
+		close(pdat[0]);
+		close(pdat[1]);
+		goto Err1;
+	}
+
+	argv[0] = (char*)p->data;
+	switch(rfork(RFPROC|RFMEM|RFFDG|RFREND|RFNOWAIT)){
+	case -1:
+		goto Err2;
+	case 0:
+		if(pdf)
+			dupfds(pin[1], pout[1], 2, pdat[1], ifd, -1);
+		else
+			dupfds(nullfd, nullfd, 2, pdat[1], ifd, -1);
+		if(argv[0])
+			pipeline(4, "%s", argv[0]);
+		argv[0] = "gs";
+		argv[1] = "-q";
+		argv[2] = "-sDEVICE=plan9";
+		argv[3] = "-sOutputFile=/fd/3";
+		argv[4] = "-dBATCH";
+		argv[5] = pdf ? "-dDELAYSAFER" : "-dSAFER";
+		argv[6] = "-dQUIET";
+		argv[7] = "-dTextAlphaBits=4";
+		argv[8] = "-dGraphicsAlphaBits=4";
+		snprint(buf, sizeof buf, "-r%d", ppi);
+		argv[9] = buf;
+		argv[10] = "-dDOINTERPOLATE";
+		argv[11] = pdf ? "-" : "/fd/4";
+		argv[12] = nil;
+		exec("/bin/gs", argv);
+		sysfatal("exec: %r");
+	}
+
+	close(pin[1]);
+	close(pout[1]);
+	close(pdat[1]);
+	close(ifd);
+
+	if(pdf){
+		Ghost *gs;
+		char *prolog =
+			"/PAGEOUT (/fd/1) (w) file def\n"
+			"/PAGE== { PAGEOUT exch write==only PAGEOUT (\\n) writestring PAGEOUT flushfile } def\n"
+			"\n"
+			"/Page null def\n"
+			"/Page# 0 def\n"
+			"/PDFSave null def\n"
+			"/DSCPageCount 0 def\n"
+			"/DoPDFPage {dup /Page# exch store pdfgetpage pdfshowpage } def\n"
+			"\n"
+			"GS_PDF_ProcSet begin\n"
+			"pdfdict begin\n"
+			"(/fd/4) (r) file { DELAYSAFER { .setsafe } if } stopped pop pdfopen begin\n"
+			"\n"
+			"pdfpagecount PAGE==\n";
+
+		n = strlen(prolog);
+		if(write(pin[0], prolog, n) != n)
+			goto Out;
+		if((n = read(pout[0], buf, sizeof(buf)-1)) < 0)
+			goto Out;
+		buf[n] = 0;
+		n = atoi(buf);
+		if(n <= 0){
+			werrstr("no pages");
+			goto Out;
+		}
+		gs = mallocz(sizeof(*gs), 1);
+		gs->pin = pin[0];
+		gs->pout = pout[0];
+		gs->pdat = pdat[0];
+		for(i=1; i<=n; i++){
+			snprint(nam, sizeof nam, "%d", i);
+			addpage(p, nam, popenpdf, gs, -1);
+		}
+
+		/* keep ghostscript arround */
+		return -1;
+	} else {
+		i = 0;
+		ofd = -1;
+		while((n = read(pdat[0], buf, sizeof(buf))) >= 0){
+			if(ofd >= 0 && (n <= 0 || infernobithdr(buf, n))){
+				snprint(nam, sizeof nam, "%d", i);
+				addpage(p, nam, popenimg, nil, ofd);
+				ofd = -1;
+			}
+			if(n <= 0)
+				break;
+			if(ofd < 0){
+				snprint(nam, sizeof nam, "%.4d", ++i);
+				if((ofd = createtmp(nam)) < 0)
+					ofd = dup(nullfd, -1);
+			}
+			if(write(ofd, buf, n) != n)
+				break;
+		}
+		if(ofd >= 0)
+			close(ofd);
+	}
+Out:
+	close(pin[0]);
+	close(pout[0]);
+	close(pdat[0]);
+	return -1;
+}
+
+int
+filetype(char *buf, int nbuf, char *typ, int ntyp)
+{
+	int n, ifd[2], ofd[2];
+	char *argv[3];
+
+	if(infernobithdr(buf, nbuf)){
+		strncpy(typ, "image/p9bit", ntyp);
+		return 0;
+	}
+
+	typ[0] = 0;
+	if(pipe(ifd) < 0)
+		return -1;
+	if(pipe(ofd) < 0){
+		close(ifd[0]);
+		close(ifd[1]);
+		return -1;
+	}
+	if(rfork(RFPROC|RFMEM|RFFDG|RFREND|RFNOWAIT) == 0){
+		dupfds(ifd[1], ofd[1], 2, -1);
+		argv[0] = "file";
+		argv[1] = "-m";
+		argv[2] = 0;
+		exec("/bin/file", argv);
+	}
+	close(ifd[1]);
+	close(ofd[1]);
+	if(rfork(RFPROC|RFMEM|RFFDG|RFNOWAIT) == 0){
+		dupfds(ifd[0], -1);
+		write(0, buf, nbuf);
+		exits(nil);
+	}
+	close(ifd[0]);
+	if((n = readn(ofd[0], typ, ntyp-1)) < 0)
+		n = 0;
+	close(ofd[0]);
+	while(n > 0 && typ[n-1] == '\n')
+		n--;
+	typ[n] = 0;
+	return 0;
+}
+
+int
+dircmp(void *p1, void *p2)
+{
+	Dir *d1, *d2;
+
+	d1 = p1;
+	d2 = p2;
+
+	return strcmp(d1->name, d2->name);
+}
+
+int
+popenfile(Page *p)
+{
+	static struct {
+		char	*typ;
+		void	*open;
+		void	*data;
+	} tab[] = {
+	"application/pdf",		popengs,	nil,
+	"application/postscript",	popengs,	nil,
+	"application/troff",		popengs,	"lp -dstdout",
+	"text/plain",			popengs,	"lp -dstdout",
+	"text/html",			popengs,	"uhtml | html2ms | tbl | troff -ms | lp -dstdout",
+	"application/dvi",		popengs,	"dvips -Pps -r0 -q1 /fd/0",
+	"application/doc",		popengs,	"doc2ps",
+	"application/zip",		popentape,	"fs/zipfs",
+	"application/x-tar",		popentape,	"fs/tarfs",
+	"application/x-ustar",		popentape,	"fs/tarfs",
+	"application/x-compress",	popenfilter,	"uncompress",
+	"application/x-gzip",		popenfilter,	"gunzip",
+	"application/x-bzip2",		popenfilter,	"bunzip2",
+	"image/gif",			popenimg,	"gif",
+	"image/jpeg",			popenimg,	"jpg",
+	"image/png",			popenimg,	"png",
+	"image/tiff",			popenimg,	"tif",
+	"image/ppm",			popenimg,	"ppm",
+	"image/bmp",			popenimg,	"bmp",
+	"image/tga",			popenimg,	"tga",
+	"image/x-icon",			popenimg,	"ico",
+	"image/p9bit",			popenimg,	nil,
+	};
+
+	char buf[NBUF], typ[128], *file;
+	int i, n, fd, tfd;
+	Dir *d;
+
+	fd = p->fd;
+	p->fd = -1;
+	p->ext = nil;
+	file = p->data;
+	p->data = nil;
+	p->open = nil;
+	if(fd < 0){
+		if((fd = open(file, OREAD)) < 0){
+		Err0:
+			free(file);
+			return -1;
+		}
+	}
+	seek(fd, 0, 0);
+	if((d = dirfstat(fd)) == nil){
+	Err1:
+		close(fd);
+		goto Err0;
+	}
+	if(d->mode & DMDIR){
+		free(d);
+		d = nil;
+
+		snprint(buf, sizeof(buf), "%s/META-INF/container.xml", file);
+		if((tfd = open(buf, OREAD)) >= 0){
+			close(fd);
+			p->fd = tfd;
+			p->data = file;
+			p->open = popenepub;
+			return p->open(p);
+		}
+		if(strcmp(pageaddr(p, buf, sizeof(buf)), file) == 0)
+			p->delim = "/";
+		if((n = dirreadall(fd, &d)) < 0)
+			goto Err1;
+		qsort(d, n, sizeof d[0], dircmp);
+		for(i = 0; i<n; i++)
+			addpage(p, d[i].name, popenfile, smprint("%s/%s", file, d[i].name), -1);
+		free(d);
+		goto Err1;
+	}
+	free(d);
+
+	memset(buf, 0, NBUF/2);
+	if((n = readn(fd, buf, NBUF/2)) <= 0)
+		goto Err1;
+	filetype(buf, n, typ, sizeof(typ));
+	for(i=0; i<nelem(tab); i++)
+		if(strncmp(typ, tab[i].typ, strlen(tab[i].typ)) == 0)
+			break;
+	if(i == nelem(tab)){
+		werrstr("unknown image format: %s", typ);
+		goto Err1;
+	}
+	p->fd = fd;
+	p->data = tab[i].data;
+	p->open = tab[i].open;
+	if(seek(fd, 0, 0) < 0)
+		goto Noseek;
+	if((i = readn(fd, buf+n, n)) < 0)
+		goto Err1;
+	if(i != n || memcmp(buf, buf+n, i)){
+		n += i;
+	Noseek:
+		if((tfd = createtmp("file")) < 0)
+			goto Err1;
+		while(n > 0){
+			if(write(tfd, buf, n) != n)
+				goto Err2;
+			if((n = read(fd, buf, sizeof(buf))) < 0)
+				goto Err2;
+		}
+		if(dup(tfd, fd) < 0){
+		Err2:
+			close(tfd);
+			goto Err1;
+		}
+		close(tfd);
+	}
+	free(file);
+	return p->open(p);
+}
+
+Page*
+nextpage(Page *p)
+{
+	if(p != nil && p->down != nil)
+		return p->down;
+	while(p != nil){
+		if(p->next != nil)
+			return p->next;
+		p = p->up;
+	}
+	return nil;
+}
+
+Page*
+prevpage(Page *x)
+{
+	Page *p, *t;
+
+	if(x != nil){
+		for(p = root->down; p != nil; p = t)
+			if((t = nextpage(p)) == x)
+				return p;
+	}
+	return nil;
+}
+
+int
+openpage(Page *p)
+{
+	int fd;
+
+	fd = -1;
+	if(p->open == nil || (fd = p->open(p)) < 0)
+		p->open = nil;
+	else {
+		if(rotate)
+			pipeline(fd, "exec rotate -r %d", rotate);
+		if(resize.x)
+			pipeline(fd, "exec resize -x %d", resize.x);
+		else if(resize.y)
+			pipeline(fd, "exec resize -y %d", resize.y);
+	}
+	return fd;
+}
+
+static ulong
+imagesize(Image *i)
+{
+	if(i == nil)
+		return 0;
+	return Dy(i->r)*bytesperline(i->r, i->depth);
+}
+
+static void
+lunlink(Page *p)
+{
+	if(p->lnext == nil || p->lnext == p)
+		return;
+	p->lnext->lprev = p->lprev;
+	p->lprev->lnext = p->lnext;
+	p->lnext = nil;
+	p->lprev = nil;
+}
+
+static void
+llinkhead(Page *p)
+{
+	lunlink(p);
+	p->lnext = lru.lnext;
+	p->lprev = &lru;
+	p->lnext->lprev = p;
+	p->lprev->lnext = p;
+}
+
+void
+invertimage(Image *i)
+{
+	int n, m;
+	uchar *b;
+	uintptr *buf, *p;
+
+	n = imagesize(i);
+	if((buf = malloc(n)) == nil)
+		return;
+	unloadimage(i, i->r, (uchar*)buf, n);
+	m = n;
+	for(p=buf; m>=sizeof *p; m-=sizeof *p, p++)
+		*p = ~*p;
+	for(b=(uchar*)p; m>0; m--, b++)
+		*b = ~*b;
+	loadimage(i, i->r, (uchar*)buf, n);
+	free(buf);
+}
+
+void
+loadpage(Page *p)
+{
+	int fd;
+
+	if(p->open != nil && p->image == nil){
+		fd = openpage(p);
+		if(fd >= 0){
+			if((p->image = readimage(display, fd, 1)) == nil)
+				fprint(2, "readimage: %r\n");
+			close(fd);
+		}
+		if(p->image == nil)
+			p->open = nil;
+		else {
+			lockdisplay(display);
+			if(invert)
+				invertimage(p->image);
+			imemsize += imagesize(p->image);
+			unlockdisplay(display);
+		}
+	}
+}
+
+void
+unloadpage(Page *p)
+{
+	qlock(&lru);
+	lunlink(p);
+	qunlock(&lru);
+
+	if(p->open == nil || p->image == nil)
+		return;
+	lockdisplay(display);
+	imemsize -= imagesize(p->image);
+	freeimage(p->image);
+	unlockdisplay(display);
+	p->image = nil;
+}
+
+void
+unloadpages(ulong limit)
+{
+	Page *p;
+
+	while(imemsize >= limit && (p = lru.lprev) != &lru){
+		qlock(p);
+		unloadpage(p);
+		qunlock(p);
+	}
+}
+
+void
+loadpages(Page *p, int oviewgen)
+{
+	while(p != nil && viewgen == oviewgen){
+		qlock(&lru);
+		llinkhead(p);
+		qunlock(&lru);
+		if(!canqlock(p))
+			goto next;
+		loadpage(p);
+		if(viewgen != oviewgen){
+			unloadpage(p);
+			qunlock(p);
+			break;
+		}
+		if(p == current){
+			Point size;
+
+			esetcursor(nil);
+			size = pagesize(p);
+			if(size.x && size.y && newwin){
+				newwin = 0;
+				resizewin(size);
+			}
+			lockdisplay(display);
+			drawpage(p);
+			unlockdisplay(display);
+		}
+		qunlock(p);
+	next:
+		if(p != current && imemsize >= imemlimit)
+			break;		/* only one page ahead once we reach the limit */
+		if(forward < 0){
+			if(p->up == nil || p->up->down == p)
+				break;
+			p = prevpage(p);
+		} else {
+			if(p->next == nil)
+				break;
+			p = nextpage(p);
+		}
+	}
+}
+
+/* doesn't actually free the page entry or touch links to avoid breakage */
+Page*
+freepage(Page *p, Page *prev)
+{
+	Page *next, *up;
+
+	drawlock(0);
+	unloadpage(p);
+	drawlock(1);
+	if(p->fd >= 0)
+		close(p->fd);
+	p->fd = -1;
+	/* not touching p->data */
+	free(p->name);
+	p->name = nil;
+	p->open = nil;
+	next = nextpage(p);
+	up = p->up;
+	if(up->down == p){
+		if(up->tail != p)
+			up->down = next;
+		else
+			up->down = nil;
+	}else if(up->tail == p){
+		up->tail = prev;
+		prev->next = nil;
+	}else
+		prev->next = next;
+	return next;
+}
+
+Page*
+poppage(Page *p, int del)
+{
+	Page *t, *prev, *next;
+
+	if(p == nil)
+		return nil;
+	if(p == root)
+		return p;
+	if(del){
+		if(!(access(p->name, OREAD) == 0 && remove(p->name) == 0
+		|| p->data != nil && access(p->data, OREAD) == 0 && remove(p->data) == 0)){
+			fprint(2, "remove %s: %r", p->name);
+			return p;
+		}
+	}
+	qlock(&pagelock);
+	for(t = p->down, prev = p; t != nil && t->up != p->up; prev = t, t = next){
+		qlock(t);
+		next = freepage(t, prev);
+		qunlock(t);
+	}
+	p->down = nil;
+	prev = prevpage(p);
+	next = freepage(p, prev);
+	qunlock(&pagelock);
+	qunlock(p);
+	if(next != nil){
+		forward = 1;
+		return next;
+	}
+	forward = -1;
+	return prev;
+}
+
+/*
+ * A draw operation that touches only the area contained in bot but not in top.
+ * mp and sp get aligned with bot.min.
+ */
+static void
+gendrawdiff(Image *dst, Rectangle bot, Rectangle top, 
+	Image *src, Point sp, Image *mask, Point mp, int op)
+{
+	Rectangle r;
+	Point origin;
+	Point delta;
+
+	if(Dx(bot)*Dy(bot) == 0)
+		return;
+
+	/* no points in bot - top */
+	if(rectinrect(bot, top))
+		return;
+
+	/* bot - top ≡ bot */
+	if(Dx(top)*Dy(top)==0 || rectXrect(bot, top)==0){
+		gendrawop(dst, bot, src, sp, mask, mp, op);
+		return;
+	}
+
+	origin = bot.min;
+	/* split bot into rectangles that don't intersect top */
+	/* left side */
+	if(bot.min.x < top.min.x){
+		r = Rect(bot.min.x, bot.min.y, top.min.x, bot.max.y);
+		delta = subpt(r.min, origin);
+		gendrawop(dst, r, src, addpt(sp, delta), mask, addpt(mp, delta), op);
+		bot.min.x = top.min.x;
+	}
+
+	/* right side */
+	if(bot.max.x > top.max.x){
+		r = Rect(top.max.x, bot.min.y, bot.max.x, bot.max.y);
+		delta = subpt(r.min, origin);
+		gendrawop(dst, r, src, addpt(sp, delta), mask, addpt(mp, delta), op);
+		bot.max.x = top.max.x;
+	}
+
+	/* top */
+	if(bot.min.y < top.min.y){
+		r = Rect(bot.min.x, bot.min.y, bot.max.x, top.min.y);
+		delta = subpt(r.min, origin);
+		gendrawop(dst, r, src, addpt(sp, delta), mask, addpt(mp, delta), op);
+		bot.min.y = top.min.y;
+	}
+
+	/* bottom */
+	if(bot.max.y > top.max.y){
+		r = Rect(bot.min.x, top.max.y, bot.max.x, bot.max.y);
+		delta = subpt(r.min, origin);
+		gendrawop(dst, r, src, addpt(sp, delta), mask, addpt(mp, delta), op);
+		bot.max.y = top.max.y;
+	}
+}
+
+int
+alphachan(ulong chan)
+{
+	for(; chan; chan >>= 8)
+		if(TYPE(chan) == CAlpha)
+			return 1;
+	return 0;
+}
+
+void
+zoomdraw(Image *d, Rectangle r, Rectangle top, Image *b, Image *s, Point sp, int f)
+{
+	Rectangle dr;
+	Image *t;
+	Point a;
+	int w;
+
+	a = ZP;
+	if(r.min.x < d->r.min.x){
+		sp.x += (d->r.min.x - r.min.x)/f;
+		a.x = (d->r.min.x - r.min.x)%f;
+		r.min.x = d->r.min.x;
+	}
+	if(r.min.y < d->r.min.y){
+		sp.y += (d->r.min.y - r.min.y)/f;
+		a.y = (d->r.min.y - r.min.y)%f;
+		r.min.y = d->r.min.y;
+	}
+	rectclip(&r, d->r);
+	w = s->r.max.x - sp.x;
+	if(w > Dx(r))
+		w = Dx(r);
+	dr = r;
+	dr.max.x = dr.min.x+w;
+	if(!alphachan(s->chan))
+		b = nil;
+	if(f <= 1){
+		if(b) gendrawdiff(d, dr, top, b, sp, nil, ZP, SoverD);
+		gendrawdiff(d, dr, top, s, sp, nil, ZP, SoverD);
+		return;
+	}
+	if((t = allocimage(display, dr, s->chan, 0, 0)) == nil)
+		return;
+	for(; dr.min.y < r.max.y; dr.min.y++){
+		dr.max.y = dr.min.y+1;
+		draw(t, dr, s, nil, sp);
+		if(++a.y == f){
+			a.y = 0;
+			sp.y++;
+		}
+	}
+	dr = r;
+	for(sp=dr.min; dr.min.x < r.max.x; sp.x++){
+		dr.max.x = dr.min.x+1;
+		if(b != nil) gendrawdiff(d, dr, top, b, sp, nil, ZP, SoverD);
+		gendrawdiff(d, dr, top, t, sp, nil, ZP, SoverD);
+		for(dr.min.x++; ++a.x < f && dr.min.x < r.max.x; dr.min.x++){
+			dr.max.x = dr.min.x+1;
+			gendrawdiff(d, dr, top, d, Pt(dr.min.x-1, dr.min.y), nil, ZP, SoverD);
+		}
+		a.x = 0;
+	}
+	freeimage(t);
+}
+
+Point
+pagesize(Page *p)
+{
+	return p->image != nil ? mulpt(subpt(p->image->r.max, p->image->r.min), zoom) : ZP;
+}
+
+void
+drawframe(Rectangle r)
+{
+	border(screen, r, -Borderwidth, cols[Cframe], ZP);
+	gendrawdiff(screen, screen->r, insetrect(r, -Borderwidth), cols[Cground], ZP, nil, ZP, SoverD);
+	flushimage(display, 1);
+}
+
+void
+drawpage(Page *p)
+{
+	Rectangle r;
+	Image *i;
+
+	if((i = p->image) != nil){
+		r = rectaddpt(Rpt(ZP, pagesize(p)), addpt(pos, screen->r.min));
+		zoomdraw(screen, r, ZR, cols[Cpaper], i, i->r.min, zoom);
+	} else {
+		r = Rpt(ZP, stringsize(font, p->name));
+		r = rectaddpt(r, addpt(subpt(divpt(subpt(screen->r.max, screen->r.min), 2),
+			divpt(r.max, 2)), screen->r.min));
+		draw(screen, r, cols[Cground], nil, ZP);
+		string(screen, r.min, cols[Ctext], ZP, font, p->name);
+	}
+	drawframe(r);
+}
+
+void
+translate(Page *p, Point d)
+{
+	Rectangle r, nr;
+	Image *i;
+
+	i = p->image;
+	if(i==nil || d.x==0 && d.y==0)
+		return;
+	r = rectaddpt(Rpt(ZP, pagesize(p)), addpt(pos, screen->r.min));
+	pos = addpt(pos, d);
+	nr = rectaddpt(r, d);
+	if(rectclip(&r, screen->r))
+		draw(screen, rectaddpt(r, d), screen, nil, r.min);
+	else
+		r = ZR;
+	zoomdraw(screen, nr, rectaddpt(r, d), cols[Cpaper], i, i->r.min, zoom);
+	drawframe(nr);
+}
+
+int
+pagewalk1(Page *p)
+{
+	char *s;
+	int n;
+
+	if((s = pagewalk) == nil || *s == 0)
+		return 1;
+	n = strlen(p->name);
+	if(n == 0 || strncmp(s, p->name, n) != 0)
+		return 0;
+	if(s[n] == 0){
+		pagewalk = nil;
+		return 1;
+	}
+	if(s[n] == '/' || s[n] == '!'){
+		pagewalk = s + n+1;
+		return 1;
+	}
+	return 0;
+}
+
+Page*
+trywalk(char *name, char *addr)
+{
+	static char buf[NPATH];
+	Page *p, *a;
+
+	pagewalk = nil;
+	memset(buf, 0, sizeof(buf));
+	snprint(buf, sizeof(buf), "%s%s%s",
+		name != nil ? name : "",
+		(name != nil && addr != nil) ? "!" : "", 
+		addr != nil ? addr : "");
+	pagewalk = buf;
+
+	a = nil;
+	if(root != nil){
+		p = root->down;
+	Loop:
+		for(; p != nil; p = p->next)
+			if(pagewalk1(p)){
+				a = p;
+				p = p->down;
+				goto Loop;
+			}
+	}
+	return a;
+}
+
+Page*
+findpage(char *name)
+{
+	Page *p;
+	int n;
+
+	if(name == nil)
+		return nil;
+
+	n = strlen(name);
+	/* look in current document */
+	if(current != nil && current->up != nil){
+		for(p = current->up->down; p != nil; p = p->next)
+			if(cistrncmp(p->name, name, n) == 0)
+				return p;
+	}
+	/* look everywhere */
+	if(root != nil){
+		for(p = root->down; p != nil; p = nextpage(p))
+			if(cistrncmp(p->name, name, n) == 0)
+				return p;
+	}
+	/* try bookmark */
+	return trywalk(name, nil);
+}
+
+void
+writeaddr(Page *p, char *file)
+{
+	char buf[NPATH], *s;
+	int fd;
+
+	s = pageaddr(p, buf, sizeof(buf));
+	if((fd = open(file, OWRITE)) >= 0){
+		write(fd, s, strlen(s));
+		close(fd);
+	}
+}
+
+Page*
+pageat(int i)
+{
+	Page *p;
+
+	for(p = root->down; i > 0 && p != nil; p = nextpage(p))
+		i--;
+	return i ? nil : p;
+}
+
+int
+pageindex(Page *x)
+{
+	Page *p;
+	int i;
+
+	for(i = 0, p = root->down; p != nil && p != x; p = nextpage(p))
+		i++;
+	return (p == x) ? i : -1;
+}
+
+char*
+pagemenugen(int i)
+{
+	Page *p;
+
+	if((p = pageat(i)) != nil)
+		return shortlabel(p->name);
+	return nil;
+}
+
+char*
+cmdmenugen(int i)
+{
+	if(i < 0 || i >= nelem(cmds))
+		return nil;
+	return cmds[i].m;
+}
+
+/*
+ * spawn new proc to load a run of pages starting with p
+ * the display should *not* be locked as it gets called
+ * from recursive page load.
+ */
+void
+showpage1(Page *p)
+{
+	static int nproc;
+	int oviewgen;
+
+	if(p == nil)
+		return;
+	esetcursor(&reading);
+	writeaddr(p, "/dev/label");
+	current = p;
+	oviewgen = viewgen;
+	if(nproc >= NPROC)
+		waitpid();
+	switch(rfork(RFPROC|RFMEM)){
+	case -1:
+		sysfatal("rfork: %r");
+	case 0:
+		loadpages(p, oviewgen);
+		nproc--;
+		exits(nil);
+	}
+	nproc++;
+}
+
+/* recursive display lock, called from main proc only */
+void
+drawlock(int dolock){
+	static int ref = 0;
+	if(dolock){
+		if(ref++ == 0)
+			lockdisplay(display);
+	} else {
+		if(--ref == 0)
+			unlockdisplay(display);
+	}
+}
+
+
+void
+showpage(Page *p)
+{
+	if(p == nil)
+		return;
+	drawlock(0);
+	unloadpages(imemlimit);
+	showpage1(p);
+	drawlock(1);
+}
+
+void
+zerox(Page *p)
+{
+	char nam[64], *argv[4];
+	int fd;
+
+	if(p == nil)
+		return;
+	drawlock(0);
+	qlock(p);
+	if((fd = openpage(p)) < 0)
+		goto Out;
+	if(rfork(RFPROC|RFMEM|RFFDG|RFENVG|RFNOTEG|RFNOWAIT) == 0){
+		dupfds(fd, 1, 2, -1);
+		snprint(nam, sizeof nam, "/bin/%s", argv0);
+		argv[0] = argv0;
+		argv[1] = "-w";
+		argv[2] = nil;
+		exec(nam, argv);
+		sysfatal("exec: %r");
+	}
+	close(fd);
+Out:
+	qunlock(p);
+	drawlock(1);
+}
+
+void
+showext(Page *p)
+{
+	char label[64], *argv[4];
+	Point ps;
+	int fd;
+
+	if(p->ext == nil)
+		return;
+	snprint(label, sizeof(label), "%s %s", p->ext, p->name);
+	ps = Pt(0, 0);
+	if(p->image != nil)
+		ps = addpt(subpt(p->image->r.max, p->image->r.min), Pt(24, 24));
+	drawlock(0);
+	if((fd = p->fd) < 0){
+		if(p->open != popenfile)
+			return;
+		fd = open((char*)p->data, OREAD);
+	} else {
+		fd = dup(fd, -1);
+		seek(fd, 0, 0);
+	}
+	if(rfork(RFPROC|RFMEM|RFFDG|RFNOTEG|RFREND|RFNOWAIT) == 0){
+		if(newwindow(nil) != -1){
+			dupfds(fd, open("/dev/cons", OWRITE), open("/dev/cons", OWRITE), -1);
+			if((fd = open("/dev/label", OWRITE)) >= 0){
+				write(fd, label, strlen(label));
+				close(fd);
+			}
+			if(ps.x && ps.y)
+				resizewin(ps);
+			argv[0] = "rc";
+			argv[1] = "-c";
+			argv[2] = p->ext;
+			argv[3] = nil;
+			exec("/bin/rc", argv);
+		}
+		exits(0);
+	}
+	close(fd);
+	drawlock(1);
+}
+
+void
+eresized(int new)
+{
+	Page *p;
+
+	drawlock(1);
+	if(new && getwindow(display, Refnone) == -1)
+		sysfatal("getwindow: %r");
+	draw(screen, screen->r, cols[Cground], nil, ZP);
+	if((p = current) != nil){
+		if(canqlock(p)){
+			drawpage(p);
+			qunlock(p);
+		}
+	}
+	drawlock(0);
+}
+
+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");
+		sleep(1);
+	}
+}
+
+void drawerr(Display *, char *msg)
+{
+	sysfatal("draw: %s", msg);
+}
+
+void
+usage(void)
+{
+	fprint(2, "usage: %s [ -iRw ] [ -m mb ] [ -p ppi ] [ -j addr ] [ file ... ]\n", argv0);
+	exits("usage");
+}
+
+void
+docmd(int i, Mouse *m)
+{
+	char buf[NPATH], *s;
+	Point o;
+	int fd, del;
+	Page *p;
+
+	del = 0;
+	switch(i){
+	case Corigsize:
+		pos = ZP;
+		zoom = 1;
+		resize = ZP;
+		rotate = 0;
+	Unload:
+		viewgen++;
+		drawlock(0);
+		unloadpages(0);
+		showpage1(current);
+		drawlock(1);
+		break;
+	case Cupsidedown:
+		rotate += 90;
+	case Crotate90:
+		rotate += 90;
+		rotate %= 360;
+		goto Unload;
+	case Cfitwidth:
+		pos = ZP;
+		zoom = 1;
+		resize = subpt(screen->r.max, screen->r.min);
+		resize.y = 0;
+		goto Unload;
+	case Cfitheight:
+		pos = ZP;
+		zoom = 1;
+		resize = subpt(screen->r.max, screen->r.min);
+		resize.x = 0;
+		goto Unload;
+	case Cinvert:
+		invert = !invert;
+		goto Unload;
+	case Czoomin:
+	case Czoomout:
+		if(current == nil || !canqlock(current))
+			break;
+		o = subpt(m->xy, screen->r.min);
+		if(i == Czoomin){
+			if(zoom < 0x1000){
+				zoom *= 2;
+				pos =  addpt(mulpt(subpt(pos, o), 2), o);
+			}
+		}else{
+			if(zoom > 1){
+				zoom /= 2;
+				pos =  addpt(divpt(subpt(pos, o), 2), o);
+			}
+		}
+		drawpage(current);
+		qunlock(current);
+		break;
+	case Cwrite:
+		if(current == nil || !canqlock(current))
+			break;
+		if(current->image != nil){
+			s = nil;
+			if(current->up != nil && current->up != root)
+				s = current->up->name;
+			snprint(buf, sizeof(buf), "%s%s%s.bit",
+				s != nil ? s : "",
+				s != nil ? "." : "",
+				current->name);
+			if(eenter("Write", buf, sizeof(buf), m) > 0){
+				if((fd = create(buf, OWRITE, 0666)) < 0){
+					errstr(buf, sizeof(buf));
+					eenter(buf, 0, 0, m);
+				} else {
+					esetcursor(&reading);
+					writeimage(fd, current->image, 0);
+					close(fd);
+					esetcursor(nil);
+				}
+			}
+		}
+		qunlock(current);
+		break;
+	case Cext:
+		if(current == nil || !canqlock(current))
+			break;
+		showext(current);
+		qunlock(current);
+		break;
+	case Csnarf:
+		writeaddr(current, "/dev/snarf");
+		break;
+	case Cdelete:
+		del = 1;
+		/* wet floor */
+	case Cpop:
+		if(current == nil || !canqlock(current))
+			break;
+		if((p = poppage(current, del)) == current){
+			qunlock(current);
+			break;
+		}
+		if((current = p) == nil){
+			drawlock(0);
+			draw(screen, screen->r, cols[Cground], nil, ZP);
+			drawframe(screen->r);
+			drawlock(1);
+			break;
+		}
+		showpage(current);
+		break;
+	case Cnext:
+		forward = 1;
+		showpage(nextpage(current));
+		break;
+	case Cprev:
+		forward = -1;
+		showpage(prevpage(current));
+		break;
+	case Czerox:
+		zerox(current);
+		break;
+	case Cquit:
+		exits(0);
+	}
+}
+
+void
+scroll(int y)
+{
+	Point z;
+	Page *p;
+
+	if(current == nil || !canqlock(current))
+		return;
+	if(y < 0){
+		if(pos.y >= 0){
+			p = prevpage(current);
+			if(p != nil){
+				qunlock(current);
+				z = ZP;
+				if(canqlock(p)){
+					z = pagesize(p);
+					qunlock(p);
+				}
+				if(z.y == 0)
+					z.y = Dy(screen->r);
+				if(pos.y+z.y > Dy(screen->r))
+					pos.y = Dy(screen->r) - z.y;
+				forward = -1;
+				showpage(p);
+				return;
+			}
+			y = 0;
+		}
+	} else {
+		z = pagesize(current);
+		if(pos.y+z.y <= Dy(screen->r)){
+			p = nextpage(current);
+			if(p != nil){
+				qunlock(current);
+				if(pos.y < 0)
+					pos.y = 0;
+				forward = 1;
+				showpage(p);
+				return;
+			}
+			y = 0;
+		}
+	}
+	translate(current, Pt(0, -y));
+	qunlock(current);
+}
+
+void
+main(int argc, char *argv[])
+{
+	enum { Eplumb = 4 };
+	char buf[NPATH];
+	Plumbmsg *pm;
+	Point o;
+	Mouse m;
+	Event e;
+	char *s;
+	int i;
+
+	quotefmtinstall();
+
+	ARGBEGIN {
+	case 'a':
+	case 'v':
+	case 'V':
+	case 'P':
+		break;
+	case 'R':
+		if(newwin == 0)
+			newwin = -1;
+		break;
+	case 'w':
+		newwin = 1;
+		break;
+	case 'i':
+		imode = 1;
+		break;
+	case 'j':
+		trywalk(EARGF(usage()), nil);
+		break;
+	case 'm':
+		imemlimit = atol(EARGF(usage()))*MiB;
+		break;
+	case 'p':
+		ppi = atoi(EARGF(usage()));
+		break;
+	default:
+		usage();
+	} ARGEND;
+
+	if(newwin > 0){
+		if(newwindow(nil) < 0)
+			sysfatal("newwindow: %r");
+	}
+
+	/*
+	 * so that we can stop all subprocesses with a note,
+	 * and to isolate rendezvous from other processes
+	 */
+	atnotify(catchnote, 1);
+	if(cohort = rfork(RFPROC|RFNOTEG|RFNAMEG|RFREND)){
+		atexit(killcohort);
+		waitpid();
+		exits(0);
+	}
+	cohort = getpid();
+	atexit(killcohort);
+	if(initdraw(drawerr, nil, argv0) < 0)
+		sysfatal("initdraw: %r");
+	Theme th[nelem(cols)] = {
+		[Cground] { "back",	0x777777FF },
+		[Cpaper] { "paper",	DWhite },
+		[Cframe] { "border",	DBlack },
+		[Ctext]	{ "text",	DBlack },
+	};
+	readtheme(th, nelem(th), nil);
+	for(i=0; i<nelem(cols); i++)
+ 		cols[i] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, th[i].c);
+	display->locking = 1;
+	draw(screen, screen->r, cols[Cground], nil, ZP);
+	unlockdisplay(display);
+
+	einit(Ekeyboard|Emouse);
+	eplumb(Eplumb, "image");
+	memset(&m, 0, sizeof(m));
+	if((nullfd = open("/dev/null", ORDWR)) < 0)
+		sysfatal("open: %r");
+	dup(nullfd, 1);
+	lru.lprev = &lru;
+	lru.lnext = &lru;
+	current = root = addpage(nil, "", nil, nil, -1);
+	root->delim = "";
+	if(*argv == nil && !imode)
+		addpage(root, "stdin", popenfile, strdup("/fd/0"), -1);
+	for(; *argv; argv++)
+		addpage(root, *argv, popenfile, strdup(*argv), -1);
+	for(i=0; i<NPROC/4; i++)	/* rice */
+		showpage1(current);
+
+	drawlock(1);
+	for(;;){
+		drawlock(0);
+		i=event(&e);
+		drawlock(1);
+
+		switch(i){
+		case Emouse:
+			m = e.mouse;
+			if(m.buttons & 1){
+				if(current &&  canqlock(current)){
+					for(;;) {
+						o = m.xy;
+						m = emouse();
+						if((m.buttons & 1) == 0)
+							break;
+						translate(current, subpt(m.xy, o));
+					}
+					qunlock(current);
+				}
+			} else if(m.buttons & 2){
+				o = m.xy;
+				i = emenuhit(2, &m, &cmdmenu);
+				m.xy = o;
+				docmd(i, &m);
+			} else if(m.buttons & 4){
+				if(root->down){
+					Page *x;
+
+					qlock(&pagelock);
+					pagemenu.lasthit = pageindex(current);
+					x = pageat(emenuhit(3, &m, &pagemenu));
+					qunlock(&pagelock);
+					forward = 0;
+					showpage(x);
+				}
+			} else if(m.buttons & 8){
+				scroll(screen->r.min.y - m.xy.y);
+			} else if(m.buttons & 16){
+				scroll(m.xy.y - screen->r.min.y);
+			}
+			break;
+		case Ekeyboard:
+			switch(e.kbdc){
+			case Kup:
+				scroll(-Dy(screen->r)/3);
+				break;
+			case Kpgup:
+				scroll(-Dy(screen->r)/2);
+				break;
+			case Kdown:
+				scroll(Dy(screen->r)/3);
+				break;
+			case Kpgdown:
+				scroll(Dy(screen->r)/2);
+				break;
+			default:
+				for(i = 0; i<nelem(cmds); i++)
+					if((cmds[i].k1 == e.kbdc) ||
+					   (cmds[i].k2 == e.kbdc) ||
+					   (cmds[i].k3 == e.kbdc))
+						break;
+				if(i < nelem(cmds)){
+					docmd(i, &m);
+					break;
+				}
+				if((e.kbdc < 0x20) || 
+				   (e.kbdc & 0xFF00) == KF || 
+				   (e.kbdc & 0xFF00) == Spec)
+					break;
+				snprint(buf, sizeof(buf), "%C", (Rune)e.kbdc);
+				if(eenter("Go to", buf, sizeof(buf), &m) > 0){
+					forward = 0;
+					showpage(findpage(buf));
+				}
+			}
+			break;
+		case Eplumb:
+			pm = e.v;
+			if(pm && pm->ndata > 0){
+				Page *j;
+				int fd;
+
+				fd = -1;
+				s = plumblookup(pm->attr, "action");
+				if(s && strcmp(s, "quit")==0)
+					exits(0);
+				if(s && strcmp(s, "showdata")==0){
+					if((fd = createtmp("plumb")) < 0){
+						fprint(2, "plumb: createtmp: %r\n");
+						goto Plumbfree;
+					}
+					s = malloc(NPATH);
+					if(fd2path(fd, s, NPATH) < 0){
+						close(fd);
+						goto Plumbfree;
+					}
+					write(fd, pm->data, pm->ndata);
+				}else if(pm->data[0] == '/'){
+					s = strdup(pm->data);
+				}else{
+					s = malloc(strlen(pm->wdir)+1+pm->ndata+1);
+					sprint(s, "%s/%s", pm->wdir, pm->data);
+					cleanname(s);
+				}
+				j = trywalk(s, plumblookup(pm->attr, "addr"));
+				if(j == nil){
+					current = root;
+					drawlock(0);
+					j = addpage(root, s, popenfile, s, fd);
+					drawlock(1);
+				}
+				forward = 0;
+				showpage(j);
+			}
+		Plumbfree:
+			plumbfree(pm);
+			break;
+		}
+	}
+}