shithub: riscv

ref: c00c60d327168c25e56077c8310cf2380833e9be
dir: /sys/src/cmd/page.c/

View raw version
#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 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;

Image *frame, *paper, *ground;

char pagespool[] = "/tmp/pagespool.";

enum {
	NPROC = 4,
	NBUF = 8*1024,
	NPATH = 1024,
};

enum {
	Corigsize,
	Czoomin,
	Czoomout,
	Cfitwidth,
	Cfitheight,
	Crotate90,
	Cupsidedown,
	Cdummy1,
	Cnext,
	Cprev,
	Csnarf,
	Czerox,
	Cwrite,
	Cext,
	Cdummy2,
	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,
	[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,
	[Cdummy2]	"",		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 *);

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 -f1",
	"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
loadpage(Page *p)
{
	int fd;

	qlock(&lru);
	llinkhead(p);
	qunlock(&lru);

	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);
			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(p);
		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);
		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);
		}
	}
}

/*
 * 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, frame, ZP);
	gendrawdiff(screen, screen->r, insetrect(r, -Borderwidth), ground, 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, paper, 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, paper, nil, ZP);
		string(screen, r.min, display->black, 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), paper, 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;
	switch(rfork(RFPROC|RFMEM)){
	case -1:
		sysfatal("rfork: %r");
	case 0:
		loadpages(p, oviewgen);
		exits(nil);
	}
	if(++nproc >= NPROC)
		if(waitpid() > 0)
			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");
	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;

	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 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 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");
	paper = display->white;
	frame = display->black;
	ground = allocimage(display, Rect(0,0,1,1), screen->chan, 1, 0x777777FF);
	display->locking = 1;
	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);

	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;
		}
	}
}