shithub: riscv

ref: fe1eb39db7ae6904924f3ab1f6f9b34416f2eb1b
dir: /sys/src/cmd/tweak.c/

View raw version
#include <u.h>
#include <libc.h>
#include <draw.h>
#include <cursor.h>
#include <event.h>
#include <plumb.h>
#include <bio.h>

#define Eplumb 128

typedef struct	Thing	Thing;

struct Thing
{
	Image	*b;
	Subfont 	*s;
	char		*name;	/* file name */
	int		face;		/* is 48x48 face file or cursor file*/
	Rectangle r;		/* drawing region */
	Rectangle tr;		/* text region */
	Rectangle er;		/* entire region */
	long		c;		/* character number in subfont */
	int		mod;	/* modified */
	int		mag;		/* magnification */
	Rune		off;		/* offset for subfont indices */
	Thing	*parent;	/* thing of which i'm an edit */
	Thing	*next;
};

enum
{
	Border	= 1,
	Up		= 1,
	Down	= 0,
	Mag		= 4,
	Maxmag	= 10,
};

enum
{
	NORMAL	=0,
	FACE	=1,
	CURSOR	=2
};

enum
{
	Mopen,
	Mread,
	Mwrite,
	Mcopy,
	Mchar,
	Mpixels,
	Mclose,
	Mexit,
};

enum
{
	Blue	= 54,
};

char	*menu3str[] = {
	[Mopen]	"open",
	[Mread]	"read",
	[Mwrite]	"write",
	[Mcopy]	"copy",
	[Mchar]	"char",
	[Mpixels]	"pixels",
	[Mclose]	"close",
	[Mexit]	"exit",
	0,
};

Menu	menu3 = {
	menu3str
};

Cursor sweep0 = {
	{-7, -7},
	{0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0,
	 0x03, 0xC0, 0x03, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF,
	 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0xC0, 0x03, 0xC0,
	 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0},
	{0x00, 0x00, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80,
	 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x7F, 0xFE,
	 0x7F, 0xFE, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80,
	 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x00, 0x00}
};

Cursor box = {
	{-7, -7},
	{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
	 0xFF, 0xFF, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F,
	 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xFF, 0xFF,
	 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF},
	{0x00, 0x00, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE,
	 0x70, 0x0E, 0x70, 0x0E, 0x70, 0x0E, 0x70, 0x0E,
	 0x70, 0x0E, 0x70, 0x0E, 0x70, 0x0E, 0x70, 0x0E,
	 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x00, 0x00}
};

Cursor sight = {
	{-7, -7},
	{0x1F, 0xF8, 0x3F, 0xFC, 0x7F, 0xFE, 0xFB, 0xDF,
	 0xF3, 0xCF, 0xE3, 0xC7, 0xFF, 0xFF, 0xFF, 0xFF,
	 0xFF, 0xFF, 0xFF, 0xFF, 0xE3, 0xC7, 0xF3, 0xCF,
	 0x7B, 0xDF, 0x7F, 0xFE, 0x3F, 0xFC, 0x1F, 0xF8,},
	{0x00, 0x00, 0x0F, 0xF0, 0x31, 0x8C, 0x21, 0x84,
	 0x41, 0x82, 0x41, 0x82, 0x41, 0x82, 0x7F, 0xFE,
	 0x7F, 0xFE, 0x41, 0x82, 0x41, 0x82, 0x41, 0x82,
	 0x21, 0x84, 0x31, 0x8C, 0x0F, 0xF0, 0x00, 0x00,}
};

Cursor pixel = {
	{-7, -7},
	{0x1f, 0xf8, 0x3f, 0xfc,  0x7f, 0xfe,  0xf8, 0x1f,
	0xf0, 0x0f,  0xe0, 0x07, 0xe0, 0x07, 0xfe, 0x7f, 
	0xfe, 0x7f, 0xe0, 0x07, 0xe0, 0x07, 0xf0, 0x0f, 
	0x78, 0x1f, 0x7f, 0xfe, 0x3f, 0xfc, 0x1f, 0xf8, },
	{0x00, 0x00, 0x0f, 0xf0, 0x31, 0x8c, 0x21, 0x84, 
	0x41, 0x82, 0x41, 0x82, 0x41, 0x82, 0x40, 0x02, 
	0x40, 0x02, 0x41, 0x82, 0x41, 0x82, 0x41, 0x82, 
	0x21, 0x84, 0x31, 0x8c, 0x0f, 0xf0, 0x00, 0x00, }
};

Cursor busy = {
	{-7, -7},
	{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	 0x00, 0x00, 0x00, 0x0c, 0x00, 0x8e, 0x1d, 0xc7,
	 0xff, 0xe3, 0xff, 0xf3, 0xff, 0xff, 0x7f, 0xfe, 
	 0x3f, 0xf8, 0x17, 0xf0, 0x03, 0xe0, 0x00, 0x00,},
	{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	 0x00, 0x00, 0x00, 0x08, 0x00, 0x04, 0x00, 0x82,
	 0x04, 0x41, 0xff, 0xe1, 0x5f, 0xf1, 0x3f, 0xfe, 
	 0x17, 0xf0, 0x03, 0xe0, 0x00, 0x00, 0x00, 0x00,}
};

Cursor skull = {
	{-7,-7},
	{0x00, 0x00, 0x00, 0x00, 0xc0, 0x03, 0xe7, 0xe7, 
	 0xff, 0xff, 0xff, 0xff, 0x3f, 0xfc, 0x1f, 0xf8, 
	 0x0f, 0xf0, 0x3f, 0xfc, 0xff, 0xff, 0xff, 0xff, 
	 0xef, 0xf7, 0xc7, 0xe3, 0x00, 0x00, 0x00, 0x00,},
	{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x03,
	 0xE7, 0xE7, 0x3F, 0xFC, 0x0F, 0xF0, 0x0D, 0xB0,
	 0x07, 0xE0, 0x06, 0x60, 0x37, 0xEC, 0xE4, 0x27,
	 0xC3, 0xC3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,}
};

Rectangle	cntlr;		/* control region */
Rectangle	editr;		/* editing region */
Rectangle	textr;		/* text region */
Thing		*thing;
Mouse		mouse;
char		hex[] = "0123456789abcdefABCDEF";
jmp_buf		err;
char		*file;
int		mag;
int		but1val = 0;
int		but2val = 255;
int		invert = 0;
int		blockinput = 0;
int		tmpindex = 0;
Image		*values[256];
Image		*greyvalues[256];
uchar		data[8192];

Thing*	tget(char*);
void	mesg(char*, ...);
void	drawthing(Thing*, int);
void	select(void);
void	menu(void);
void	error(Display*, char*);
void	buttons(int);
void	drawall(void);
void	tclose1(Thing*);

void
flushevents(ulong keys)
{
	Event e;
	if(keys == 0)
		return;
	while(ecanread(keys))
		eread(keys, &e);
}

char*
pshowdata(Plumbmsg *pm)
{
	int fd;
	char *file;
	file = smprint("/tmp/tweaktmp.%d.%d", getpid(), tmpindex++);
	if(file == 0)
		sysfatal("malloc failed: %r");
	fd = create(file, OWRITE|OEXCL, 0600);
	if(fd < 0){
		free(file);
		mesg("cannot create tmpfile.");
		return nil;
	}
	if(write(fd, pm->data, pm->ndata) != pm->ndata){
		mesg("error writing tmpfile: %r");
		close(fd);
		free(file);
		return nil;
	}
	close(fd);
	return file;
}

char*
pfilename(Plumbmsg *pm)
{
	char *file;
	if(pm->data[0] == '/')
		file = smprint("%.*s", pm->ndata, pm->data);
	else
		file = smprint("%s/%.*s", pm->wdir, pm->ndata, pm->data);
	return cleanname(file);
}

void
main(int argc, char *argv[])
{
	int i;
	Event e;
	Thing *t;
	Plumbmsg *pm;
	char *s;

	srand(time(0));
	mag = Mag;
	if(initdraw(error, 0, "tweak") < 0){
		fprint(2, "tweak: initdraw failed: %r\n");
		exits("initdraw");
	}
	for(i=0; i<256; i++){
		values[i] = allocimage(display, Rect(0, 0, 1, 1), screen->chan, 1, cmap2rgba(i));
		greyvalues[i] = allocimage(display, Rect(0, 0, 1, 1), screen->chan, 1, (i<<24)|(i<<16)|(i<<8)|0xFF);
		if(values[i] == 0 || greyvalues[i] == 0)
			drawerror(display, "can't allocate image");
	}
	einit(Emouse|Ekeyboard);
	eplumb(Eplumb, "imageedit");
	eresized(0);
	i = 1;
	setjmp(err);
	for(; i<argc; i++){
		file = argv[i];
		t = tget(argv[i]);
		if(t)
			drawthing(t, 1);
		flushimage(display, 1);
	}
	file = 0;
	setjmp(err);
	for(;;)
		switch(event(&e)){
		case Ekeyboard:
			break;
		case Emouse:
			mouse = e.mouse;
			if(mouse.buttons & 3){
				select();
				break;
			}
			if(mouse.buttons & 4)
				menu();
			break;
		case Eplumb:
			pm = e.v;
			if(pm->ndata == 0)
				break;
			s = plumblookup(pm->attr, "action");
			if(s && strcmp(s, "showdata") == 0)
				file = pshowdata(pm);
			else
				file = pfilename(pm);
			if(!file){
				mesg("invalid plumb data");
				plumbfree(pm);
				break;
			}
			plumbfree(pm);
			t = tget(file);
			if(t)
				drawthing(t, 1);
			flushimage(display, 1);
			file = 0;
		}
}

void
cleantmpfiles(void)
{
	char *s;
	for(tmpindex--; tmpindex >= 0; tmpindex--){
		s = smprint("/tmp/tweaktmp.%d.%d", getpid(), tmpindex);
		remove(s);
		free(s);
	}
}

void
error(Display*, char *s)
{
	if(file)
		mesg("can't read %s: %s: %r", file, s);
	else
		mesg("/dev/bitblt error: %s", s);
	if(err[0])
		longjmp(err, 1);
	cleantmpfiles();
	exits(s);
}

void
redraw(Thing *t)
{
	Thing *nt;
	Point p;

	if(thing==0 || thing==t)
		draw(screen, editr, display->white, nil, ZP);
	if(thing == 0)
		return;
	if(thing != t){
		for(nt=thing; nt->next!=t; nt=nt->next)
			;
		draw(screen, Rect(screen->r.min.x, nt->er.max.y, editr.max.x, editr.max.y),
			display->white, nil, ZP);
	}
	for(nt=t; nt; nt=nt->next){
		drawthing(nt, 0);
		if(nt->next == 0){
			p = Pt(editr.min.x, nt->er.max.y);
			draw(screen, Rpt(p, editr.max), display->white, nil, ZP);
		}
	}
	mesg("");
}

void
eresized(int new)
{
	if(new && getwindow(display, Refnone) < 0)
		error(display, "can't reattach to window");
	cntlr = insetrect(screen->clipr, 1);
	editr = cntlr;
	textr = editr;
	textr.min.y = textr.max.y - font->height;
	cntlr.max.y = cntlr.min.y + font->height;
	editr.min.y = cntlr.max.y+1;
	editr.max.y = textr.min.y-1;
	draw(screen, screen->clipr, display->white, nil, ZP);
	draw(screen, Rect(editr.min.x, editr.max.y, editr.max.x+1, editr.max.y+1), display->black, nil, ZP);
	replclipr(screen, 0, editr);
	drawall();
}

void
mesgstr(Point p, int line, char *s)
{
	Rectangle c, r;

	r.min = p;
	r.min.y += line*font->height;
	r.max.y = r.min.y+font->height;
	r.max.x = editr.max.x;
	c = screen->clipr;
	replclipr(screen, 0, r);
	draw(screen, r, values[0xDD], nil, ZP);
	r.min.x++;
	string(screen, r.min, display->black, ZP, font, s);
	replclipr(screen, 0, c);
	flushimage(display, 1);
}

void
mesg(char *fmt, ...)
{
	char buf[1024];
	va_list arg;

	va_start(arg, fmt);
	vseprint(buf, buf+sizeof(buf), fmt, arg);
	va_end(arg);
	mesgstr(textr.min, 0, buf);
}

void
tmesg(Thing *t, int line, char *fmt, ...)
{
	char buf[1024];
	va_list arg;

	va_start(arg, fmt);
	vseprint(buf, buf+sizeof(buf), fmt, arg);
	va_end(arg);
	mesgstr(t->tr.min, line, buf);
}


void
scntl(char *l)
{
	sprint(l, "mag: %d  but1: %d  but2: %d  invert-on-copy: %c", mag, but1val, but2val, "ny"[invert]);
}

void
cntl(void)
{
	char buf[256];

	scntl(buf);
	mesgstr(cntlr.min, 0, buf);
}

void
stext(Thing *t, char *l0, char *l1)
{
	Fontchar *fc;
	char buf[256];

	l1[0] = 0;
	sprint(buf, "depth:%d r:%d %d  %d %d ", 
		t->b->depth, t->b->r.min.x, t->b->r.min.y,
		t->b->r.max.x, t->b->r.max.y);
	if(t->parent)
		sprint(buf+strlen(buf), "mag: %d ", t->mag);
	sprint(l0, "%s file: %s", buf, t->name);
	if(t->c >= 0){
		fc = &t->parent->s->info[t->c];
		sprint(l1, "c(hex): %x c(char): %C x: %d "
			   "top: %d bottom: %d left: %d width: %d iwidth: %d",
			(int)(t->c+t->parent->off), (int)(t->c+t->parent->off),
			fc->x, fc->top, fc->bottom, fc->left,
			fc->width, Dx(t->b->r));
	}else if(t->s)
		sprint(l1, "offset(hex): %ux n:%d  height:%d  ascent:%d",
			t->off, t->s->n, t->s->height, t->s->ascent);
}

void
text(Thing *t)
{
	char l0[256], l1[256];

	stext(t, l0, l1);
	tmesg(t, 0, l0);
	if(l1[0])
		tmesg(t, 1, l1);
}

void
drawall(void)
{
	Thing *t;

	cntl();
	for(t=thing; t; t=t->next)
		drawthing(t, 0);
}

/* imported from libdraw/arith.c to permit an extern log2 function */
static int log2[] = {
	-1, 0, 1, -1, 2, -1, -1, -1, 3, -1, -1, -1, -1, -1, -1, -1, 4,
	-1, -1, -1, -1, -1, -1, -1, 4 /* BUG */, -1, -1, -1, -1, -1, -1, -1, 5
};

int
value(Image *b, int x)
{
	int v, l, w;
	uchar mask;

	w = b->depth;
	if(w > 8){
		mesg("ldepth too large");
		return 0;
	}
	l = log2[w];
	mask = (1<<w)-1;		/* ones at right end of word */
	x -= b->r.min.x&~(7>>l);	/* adjust x relative to first pixel */
	v = data[x>>(3-l)];
	v >>= ((7>>l)<<l) - ((x&(7>>l))<<l);	/* pixel at right end of word */
	v &= mask;			/* pixel at right end of word */
	return v;
}

int
bvalue(int v, int d)
{
	v &= (1<<d)-1;
	if(d > screen->depth)
		v >>= d - screen->depth;
	else
		while(d < screen->depth && d < 8){
			v |= v << d;
			d <<= 1;
		}
	if(v<0 || v>255){
		mesg("internal error: bad color");
		return Blue;
	}
	return v;
}

void
drawthing(Thing *nt, int link)
{
	int nl, nf, i, x, y, sx, sy, fdx, dx, dy, v;
	Thing *t;
	Subfont *s;
	Image *b, *col;
	Point p, p1, p2;

	if(link){
		nt->next = 0;
		if(thing == 0){
			thing = nt;
			y = editr.min.y;
		}else{
			for(t=thing; t->next; t=t->next)
				;
			t->next = nt;
			y = t->er.max.y;
		}
	}else{
		if(thing == nt)
			y = editr.min.y;
		else{
			for(t=thing; t->next!=nt; t=t->next)
				;
			y = t->er.max.y;
		}
	}
	s = nt->s;
	b = nt->b;
	nl = font->height;
	if(s || nt->c>=0)
		nl += font->height;
	fdx = Dx(editr) - 2*Border;
	dx = Dx(b->r);
	dy = Dy(b->r);
	if(nt->mag > 1){
		dx *= nt->mag;
		dy *= nt->mag;
		fdx -= fdx%nt->mag;
	}
	nf = 1 + dx/fdx;
	nt->er.min.y = y;
	nt->er.min.x = editr.min.x;
	nt->er.max.x = nt->er.min.x + Border + dx + Border;
	if(nt->er.max.x > editr.max.x)
		nt->er.max.x = editr.max.x;
	nt->er.max.y = nt->er.min.y + Border + nf*(dy+Border);
	nt->r = insetrect(nt->er, Border);
	nt->er.max.x = editr.max.x;
	draw(screen, nt->er, display->white, nil, ZP);
	for(i=0; i<nf; i++){
		p1 = Pt(nt->r.min.x-1, nt->r.min.y+i*(Border+dy));
		/* draw portion of bitmap */
		p = Pt(p1.x+1, p1.y);
		if(nt->mag == 1)
			draw(screen, Rect(p.x, p.y, p.x+fdx+Dx(b->r), p.y+Dy(b->r)),
				b, nil, Pt(b->r.min.x+i*fdx, b->r.min.y));
		else{
			for(y=b->r.min.y; y<b->r.max.y; y++){
				sy = p.y+(y-b->r.min.y)*nt->mag;
				unloadimage(b, Rect(b->r.min.x, y, b->r.max.x, y+1), data, sizeof data);
				for(x=b->r.min.x+i*(fdx/nt->mag); x<b->r.max.x; x++){
					sx = p.x+(x-i*(fdx/nt->mag)-b->r.min.x)*nt->mag;
					if(sx >= nt->r.max.x)
						break;
					v = bvalue(value(b, x), b->depth);
					if(v == 255)
						continue;
					if(b->chan == GREY8)
						draw(screen, Rect(sx, sy, sx+nt->mag, sy+nt->mag),
							greyvalues[v], nil, ZP);
					else
						draw(screen, Rect(sx, sy, sx+nt->mag, sy+nt->mag),
							values[v], nil, ZP);
				}

			}
		}
		/* line down left */
		if(i == 0)
			col = display->black;
		else
			col = display->white;
		draw(screen, Rect(p1.x, p1.y, p1.x+1, p1.y+dy+Border), col, nil, ZP);
		/* line across top */
		draw(screen, Rect(p1.x, p1.y-1, nt->r.max.x+Border, p1.y), display->black, nil, ZP);
		p2 = p1;
		if(i == nf-1){
			p2.x += 1 + dx%fdx;
			col = display->black;
		}else{
			p2.x = nt->r.max.x;
			col = display->white;
		}
		/* line down right */
		draw(screen, Rect(p2.x, p2.y, p2.x+1, p2.y+dy+Border), col, nil, ZP);
		/* line across bottom */
		if(i == nf-1){
			p1.y += Border+dy;
			draw(screen, Rect(p1.x, p1.y-1, p2.x,p1.y), display->black, nil, ZP);
		}
	}
	nt->tr.min.x = editr.min.x;
	nt->tr.max.x = editr.max.x;
	nt->tr.min.y = nt->er.max.y + Border;
	nt->tr.max.y = nt->tr.min.y + nl;
	nt->er.max.y = nt->tr.max.y + Border;
	text(nt);

	mesg("");
	if(blockinput){
		flushevents(Emouse|Ekeyboard);
		blockinput = 0;
	}
}

int
tohex(int c)
{
	if('0'<=c && c<='9')
		return c - '0';
	if('a'<=c && c<='f')
		return 10 + (c - 'a');
	if('A'<=c && c<='F')
		return 10 + (c - 'A');
	return 0;
}

Thing*
tget(char *file)
{
	int i, j, fd, face, x, y, c, chan;
	Image *b;
	Subfont *s;
	Thing *t;
	Dir *d;
	jmp_buf oerr;
	uchar buf[256];
	char *data;

	mesg("reading %s", file);
	blockinput = 1;

	buf[0] = '\0';
	errstr((char*)buf, sizeof buf);	/* flush pending error message */
	memmove(oerr, err, sizeof err);
	d = nil;
	if(setjmp(err)){
   Err:
		free(d);
		memmove(err, oerr, sizeof err);
		return 0;
	}
	fd = open(file, OREAD);
	if(fd < 0){
		mesg("can't open %s: %r", file);
		goto Err;
	}
	d = dirfstat(fd);
	if(d == nil){
		mesg("can't stat bitmap file %s: %r", file);
		close(fd);
		goto Err;
	}
	if(read(fd, buf, 11) != 11){
		mesg("can't read %s: %r", file);
		close(fd);
		goto Err;
	}
	seek(fd, 0, 0);
	data = (char*)buf;
	if(*data == '{')
		data++;
	if(memcmp(data, "0x", 2)==0 && data[4]==','){
		/*
		 * cursor file
		 */
		face = CURSOR;
		s = 0;
		data = malloc(d->length+1);
		if(data == 0){
			mesg("can't malloc buffer: %r");
			close(fd);
			goto Err;
		}
		data[d->length] = 0;
		if(read(fd, data, d->length) != d->length){
			mesg("can't read cursor file %s: %r", file);
			close(fd);
			goto Err;
		}
		b = allocimage(display, Rect(0, 0, 16, 32), GREY1, 0, DNofill);
		if(b == 0){
			mesg("image alloc failed file %s: %r", file);
			free(data);
			close(fd);
			goto Err;
		}
		i = 0;
		for(x=0;x<64; ){
			if((c=data[i]) == '\0')
				goto ill;
			if(c=='0' && data[i+1] == 'x'){
				i += 2;
				continue;
			}
			if(strchr(hex, c)){
				buf[x++] = (tohex(c)<<4) | tohex(data[i+1]);
				i += 2;
				continue;
			}
			i++;
		}
		loadimage(b, Rect(0, 0, 16, 32), buf, sizeof buf);
		free(data);
	}else if(memcmp(buf, "0x", 2)==0){
		/*
		 * face file
		 */
		face = FACE;
		s = 0;
		data = malloc(d->length+1);
		if(data == 0){
			mesg("can't malloc buffer: %r");
			close(fd);
			goto Err;
		}
		data[d->length] = 0;
		if(read(fd, data, d->length) != d->length){
			mesg("can't read bitmap file %s: %r", file);
			close(fd);
			goto Err;
		}
		for(y=0,i=0; i<d->length; i++)
			if(data[i] == '\n')
				y++;
		if(y == 0){
	ill:
			mesg("ill-formed face file %s", file);
			close(fd);
			free(data);
			goto Err;
		}
		for(x=0,i=0; (c=data[i])!='\n'; ){
			if(c==',' || c==' ' || c=='\t'){
				i++;
				continue;
			}
			if(c=='0' && data[i+1] == 'x'){
				i += 2;
				continue;
			}
			if(strchr(hex, c)){
				x += 4;
				i++;
				continue;
			}
			goto ill;
		}
		if(x % y)
			goto ill;
		switch(x / y){
		default:
			goto ill;
		case 1:
			chan = GREY1;
			break;
		case 2:
			chan = GREY2;
			break;
		case 4:
			chan = GREY4;
			break;
		case 8:
			chan = CMAP8;
			break;
		}
		b = allocimage(display, Rect(0, 0, y, y), chan, 0, -1);
		if(b == 0){
			mesg("image alloc failed file %s: %r", file);
			free(data);
			close(fd);
			goto Err;
		}
		i = 0;
		for(j=0; j<y; j++){
			for(x=0; (c=data[i])!='\n'; ){
				if(c=='0' && data[i+1] == 'x'){
					i += 2;
					continue;
				}
				if(strchr(hex, c)){
					buf[x++] = ~((tohex(c)<<4) | tohex(data[i+1]));
					i += 2;
					continue;
				}
				i++;
			}
			i++;
			loadimage(b, Rect(0, j, y, j+1), buf, sizeof buf);
		}
		free(data);
	}else{
		face = NORMAL;
		s = 0;
		b = readimage(display, fd, 0);
		if(b == 0){
			mesg("can't read bitmap file %s: %r", file);
			close(fd);
			goto Err;
		}
		if(seek(fd, 0, 1) < d->length)
			s = readsubfonti(display, file, fd, b, 0);
	}
	close(fd);
	t = malloc(sizeof(Thing));
	if(t == 0){
   nomem:
		mesg("malloc failed: %r");
		if(s)
			freesubfont(s);
		else
			freeimage(b);
		goto Err;
	}
	t->name = strdup(file);
	if(t->name == 0){
		free(t);
		goto nomem;
	}
	t->b = b;
	t->s = s;
	t->face = face;
	t->mod = 0;
	t->parent = 0;
	t->c = -1;
	t->mag = 1;
	t->off = 0;
	memmove(err, oerr, sizeof err);
	return t;
}

int
atline(int x, Point p, char *line, char *buf)
{
	char *s, *c, *word, *hit;
	int w, wasblank;
	Rune r;

	wasblank = 1;
	hit = 0;
	word = 0;
	for(s=line; *s; s+=w){
		w = chartorune(&r, s);
		x += runestringnwidth(font, &r, 1);
		if(wasblank && r!=' ')
			word = s;
		wasblank = 0;
		if(r == ' '){
			if(x >= p.x)
				break;
			wasblank = 1;
		}
		if(r == ':')
			hit = word;
	}
	if(x < p.x)
		return 0;
	c = utfrune(hit, ':');
	strncpy(buf, hit, c-hit);
	buf[c-hit] = 0;
	return 1;
}

int
attext(Thing *t, Point p, char *buf)
{
	char l0[256], l1[256];

	if(!ptinrect(p, t->tr))
		return 0;
	stext(t, l0, l1);
	if(p.y < t->tr.min.y+font->height)
		return atline(t->r.min.x, p, l0, buf);
	else
		return atline(t->r.min.x, p, l1, buf);
}

int
type(char *buf, int nbuf, char *tag)
{
	Rune r;
	char *p, *e;

	esetcursor(&busy);
	p = buf;
	e = buf + nbuf-UTFmax-1;
	for(;;){
		*p = 0;
		mesg("%s: %s", tag, buf);
		r = ekbd();
		switch(r){
		case '\n':
			mesg("");
			esetcursor(0);
			return p-buf;
		case 0x15:	/* control-U */
			p = buf;
			break;
		case '\b':
			if(p > buf)
				--p;
			break;
		default:
			if(p < e)
				p += runetochar(p, &r);
		}
	}
}

void
textedit(Thing *t, char *tag)
{
	char buf[256];
	char *s;
	Image *b;
	Subfont *f;
	Fontchar *fc, *nfc;
	Rectangle r;
	ulong chan;
	int i, ld, d, w, c, doredraw, fdx, x;
	Thing *nt;

	buttons(Up);
	if(type(buf, sizeof(buf), tag) == 0)
		return;
	if(strcmp(tag, "file") == 0){
		for(s=buf; *s; s++)
			if(*s <= ' '){
				mesg("illegal file name");
				return;
			}
		if(strcmp(t->name, buf) != 0){
			if(t->parent)
				t->parent->mod = 1;
			else
				t->mod = 1;
		}
		for(nt=thing; nt; nt=nt->next)
			if(t==nt || t->parent==nt || nt->parent==t){
				free(nt->name);
				nt->name = strdup(buf);
				if(nt->name == 0){
					mesg("malloc failed: %r");
					return;
				}
				text(nt);
			}
		return;
	}
	if(strcmp(tag, "depth") == 0){
		if(buf[0]<'0' || '9'<buf[0] || (d=atoi(buf))<0 || d>8 || log2[d]<0){
			mesg("illegal ldepth");
			return;
		}
		if(d == t->b->depth)
			return;
		if(t->parent)
			t->parent->mod = 1;
		else
			t->mod = 1;
		if(d == 8)
			chan = CMAP8;
		else
			chan = CHAN1(CGrey, d);
		for(nt=thing; nt; nt=nt->next){
			if(nt!=t && nt!=t->parent && nt->parent!=t)
				continue;
			b = allocimage(display, nt->b->r, chan, 0, 0);
			if(b == 0){
	nobmem:
				mesg("image alloc failed: %r");
				return;
			}
			draw(b, b->r, nt->b, nil, nt->b->r.min);
			freeimage(nt->b);
			nt->b = b;
			if(nt->s){
				b = allocimage(display, nt->b->r, chan, 0, -1);
				if(b == 0)
					goto nobmem;
				draw(b, b->r, nt->b, nil, nt->b->r.min);
				f = allocsubfont(t->name, nt->s->n, nt->s->height, nt->s->ascent, nt->s->info, b);
				if(f == 0){
	nofmem:
					freeimage(b);
					mesg("can't make subfont: %r");
					return;
				}
				nt->s->info = 0;	/* prevent it being freed */
				nt->s->bits = 0;
				freesubfont(nt->s);
				nt->s = f;
			}
			drawthing(nt, 0);
		}
		return;
	}
	if(strcmp(tag, "mag") == 0){
		if(buf[0]<'0' || '9'<buf[0] || (ld=atoi(buf))<=0 || ld>Maxmag){
			mesg("illegal magnification");
			return;
		}
		if(t->mag == ld)
			return;
		t->mag = ld;
		redraw(t);
		return;
	}
	if(strcmp(tag, "r") == 0){
		if(t->s){
			mesg("can't change rectangle of subfont\n");
			return;
		}
		s = buf;
		r.min.x = strtoul(s, &s, 0);
		r.min.y = strtoul(s, &s, 0);
		r.max.x = strtoul(s, &s, 0);
		r.max.y = strtoul(s, &s, 0);
		if(Dx(r)<=0 || Dy(r)<=0){
			mesg("illegal rectangle");
			return;
		}
		if(t->parent)
			t = t->parent;
		for(nt=thing; nt; nt=nt->next){
			if(nt->parent==t && !rectinrect(nt->b->r, r))
				tclose1(nt);
		}
		b = allocimage(display, r, t->b->chan, 0, 0);
		if(b == 0)
			goto nobmem;
		draw(b, r, t->b, nil, r.min);
		freeimage(t->b);
		t->b = b;
		b = allocimage(display, r, t->b->chan, 0, 0);
		if(b == 0)
			goto nobmem;
		redraw(t);
		t->mod = 1;
		return;
	}
	if(strcmp(tag, "ascent") == 0){
		if(buf[0]<'0' || '9'<buf[0] || (ld=atoi(buf))<0 || ld>t->s->height){
			mesg("illegal ascent");
			return;
		}
		if(t->s->ascent == ld)
			return;
		t->s->ascent = ld;
		text(t);
		t->mod = 1;
		return;
	}
	if(strcmp(tag, "height") == 0){
		if(buf[0]<'0' || '9'<buf[0] || (ld=atoi(buf))<0){
			mesg("illegal height");
			return;
		}
		if(t->s->height == ld)
			return;
		t->s->height = ld;
		text(t);
		t->mod = 1;
		return;
	}
	if(strcmp(tag, "left")==0 || strcmp(tag, "width") == 0){
		if(buf[0]<'0' || '9'<buf[0] || (ld=atoi(buf))<0){
			mesg("illegal value");
			return;
		}
		fc = &t->parent->s->info[t->c];
		if(strcmp(tag, "left")==0){
			if(fc->left == ld)
				return;
			fc->left = ld;
		}else{
			if(fc->width == ld)
				return;
			fc->width = ld;
		}
		text(t);
		t->parent->mod = 1;
		return;
	}
	if(strcmp(tag, "offset(hex)") == 0){
		if(!strchr(hex, buf[0])){
	illoff:
			mesg("illegal offset");
			return;
		}
		s = 0;
		ld = strtoul(buf, &s, 16);
		if(*s)
			goto illoff;
		t->off = ld;
		text(t);
		for(nt=thing; nt; nt=nt->next)
			if(nt->parent == t)
				text(nt);
		return;
	}
	if(strcmp(tag, "n") == 0){
		if(buf[0]<'0' || '9'<buf[0] || (w=atoi(buf))<=0){
			mesg("illegal n");
			return;
		}
		f = t->s;
		if(w == f->n)
			return;
		doredraw = 0;
	again:
		for(nt=thing; nt; nt=nt->next)
			if(nt->parent == t){
				doredraw = 1;
				tclose1(nt);
				goto again;
			}
		r = t->b->r;
		if(w < f->n)
			r.max.x = f->info[w].x;
		b = allocimage(display, r, t->b->chan, 0, 0);
		if(b == 0)
			goto nobmem;
		draw(b, b->r, t->b, nil, r.min);
		fdx = Dx(editr) - 2*Border;
		if(Dx(t->b->r)/fdx != Dx(b->r)/fdx)
			doredraw = 1;
		freeimage(t->b);
		t->b = b;
		b = allocimage(display, r, t->b->chan, 0, 0);
		if(b == 0)
			goto nobmem;
		draw(b, b->r, t->b, nil, r.min);
		nfc = malloc((w+1)*sizeof(Fontchar));
		if(nfc == 0){
			mesg("malloc failed");
			freeimage(b);
			return;
		}
		fc = f->info;
		for(i=0; i<=w && i<=f->n; i++)
			nfc[i] = fc[i];
		if(w+1 < i)
			memset(nfc+i, 0, ((w+1)-i)*sizeof(Fontchar));
		x = fc[f->n].x;
		for(; i<=w; i++)
			nfc[i].x = x;
		f = allocsubfont(t->name, w, f->height, f->ascent, nfc, b);
		if(f == 0)
			goto nofmem;
		t->s->bits = nil;	/* don't free it */
		freesubfont(t->s);
		f->info = nfc;
		t->s = f;
		if(doredraw)
			redraw(thing);
		else
			drawthing(t, 0);
		t->mod = 1;
		return;
	}
	if(strcmp(tag, "iwidth") == 0){
		if(buf[0]<'0' || '9'<buf[0] || (w=atoi(buf))<0){
			mesg("illegal iwidth");
			return;
		}
		w -= Dx(t->b->r);
		if(w == 0)
			return;
		r = t->parent->b->r;
		r.max.x += w;
		c = t->c;
		t = t->parent;
		f = t->s;
		b = allocimage(display, r, t->b->chan, 0, 0);
		if(b == 0)
			goto nobmem;
		fc = &f->info[c];
		draw(b, Rect(b->r.min.x, b->r.min.y,
				b->r.min.x+(fc[1].x-t->b->r.min.x), b->r.min.y+Dy(t->b->r)),
				t->b, nil, t->b->r.min);
		draw(b, Rect(fc[1].x+w, b->r.min.y, w+t->b->r.max.x, b->r.min.y+Dy(t->b->r)),
			t->b, nil, Pt(fc[1].x, t->b->r.min.y));
		fdx = Dx(editr) - 2*Border;
		doredraw = 0;
		if(Dx(t->b->r)/fdx != Dx(b->r)/fdx)
			doredraw = 1;
		freeimage(t->b);
		t->b = b;
		b = allocimage(display, r, t->b->chan, 0, 0);
		if(b == 0)
			goto nobmem;
		draw(b, b->r, t->b, nil, t->b->r.min);
		fc = &f->info[c+1];
		for(i=c+1; i<=f->n; i++, fc++)
			fc->x += w;
		f = allocsubfont(t->name, f->n, f->height, f->ascent,
			f->info, b);
		if(f == 0)
			goto nofmem;
		/* t->s and f share info; free carefully */
		fc = f->info;
		t->s->bits = nil;
		t->s->info = 0;
		freesubfont(t->s);
		f->info = fc;
		t->s = f;
		if(doredraw)
			redraw(t);
		else
			drawthing(t, 0);
		/* redraw all affected chars */
		for(nt=thing; nt; nt=nt->next){
			if(nt->parent!=t || nt->c<c)
				continue;
			fc = &f->info[nt->c];
			r.min.x = fc[0].x;
			r.min.y = nt->b->r.min.y;
			r.max.x = fc[1].x;
			r.max.y = nt->b->r.max.y;
			b = allocimage(display, r, nt->b->chan, 0, 0);
			if(b == 0)
				goto nobmem;
			draw(b, r, t->b, nil, r.min);
			doredraw = 0;
			if(Dx(nt->b->r)/fdx != Dx(b->r)/fdx)
				doredraw = 1;
			freeimage(nt->b);
			nt->b = b;
			if(c != nt->c)
				text(nt);
			else{
				if(doredraw)
					redraw(nt);
				else
					drawthing(nt, 0);
			}
		}
		t->mod = 1;
		return;
	}
	mesg("cannot edit %s in file %s", tag, t->name);
}

void
cntledit(char *tag)
{
	char buf[256];
	long l;

	buttons(Up);
	if(type(buf, sizeof(buf), tag) == 0)
		return;
	if(strcmp(tag, "mag") == 0){
		if(buf[0]<'0' || '9'<buf[0] || (l=atoi(buf))<=0 || l>Maxmag){
			mesg("illegal magnification");
			return;
		}
		mag = l;
		cntl();
		return;
	}
	if(strcmp(tag, "but1")==0
	|| strcmp(tag, "but2")==0){
		if(buf[0]<'0' || '9'<buf[0] || (l=atoi(buf))<0 || l>255){
			mesg("illegal value");
			return;
		}
		if(strcmp(tag, "but1") == 0)
			but1val = l;
		else if(strcmp(tag, "but2") == 0)
			but2val = l;
		cntl();
		return;
	}
	if(strcmp(tag, "invert-on-copy")==0){
		if(buf[0]=='y' || buf[0]=='1')
			invert = 1;
		else if(buf[0]=='n' || buf[0]=='0')
			invert = 0;
		else{
			mesg("illegal value");
			return;
		}
		cntl();
		return;
	}
	mesg("cannot edit %s", tag);
}

void
buttons(int ud)
{
	while((mouse.buttons==0) != ud)
		mouse = emouse();
}

Point
screenpt(Thing *t, Point realp)
{
	int fdx, n;
	Point p;

	fdx = Dx(editr)-2*Border;
	if(t->mag > 1)
		fdx -= fdx%t->mag;
	p = mulpt(subpt(realp, t->b->r.min), t->mag);
	if(fdx < Dx(t->b->r)*t->mag){
		n = p.x/fdx;
		p.y += n * (Dy(t->b->r)*t->mag+Border);
		p.x -= n * fdx;
	}
	p = addpt(p, t->r.min);
	return p;
}

Point
realpt(Thing *t, Point screenp)
{
	int fdx, n, dy;
	Point p;

	fdx = (Dx(editr)-2*Border);
	if(t->mag > 1)
		fdx -= fdx%t->mag;
	p.y = screenp.y-t->r.min.y;
	p.x = 0;
	if(fdx < Dx(t->b->r)*t->mag){
		dy = Dy(t->b->r)*t->mag+Border;
		n = (p.y/dy);
		p.x = n * fdx;
		p.y -= n * dy;
	}
	p.x += screenp.x-t->r.min.x;
	p = addpt(divpt(p, t->mag), t->b->r.min);
	return p;
}

int
sweep(int but, Rectangle *r)
{
	Thing *t;
	Point p, q, lastq;

	esetcursor(&sweep0);
	buttons(Down);
	if(mouse.buttons != (1<<(but-1))){
		buttons(Up);
		esetcursor(0);
		return 0;
	}
	p = mouse.xy;
	for(t=thing; t; t=t->next)
		if(ptinrect(p, t->r))
			break;
	if(t)
		p = screenpt(t, realpt(t, p));
	r->min = p;
	r->max = p;
	esetcursor(&box);
	lastq = ZP;
	while(mouse.buttons == (1<<(but-1))){
		edrawgetrect(insetrect(*r, -Borderwidth), 1);
		mouse = emouse();
		edrawgetrect(insetrect(*r, -Borderwidth), 0);
		q = mouse.xy;
		if(t)
			q = screenpt(t, realpt(t, q));
		if(eqpt(q, lastq))
			continue;
		*r = canonrect(Rpt(p, q));
		lastq = q;
	}
	esetcursor(0);
	if(mouse.buttons){
		buttons(Up);
		return 0;
	}
	return 1;
}

void
openedit(Thing *t, Point pt, int c)
{
	int x, y;
	Point p;
	Rectangle r;
	Rectangle br;
	Fontchar *fc;
	Thing *nt;

	if(t->b->depth > 8){
		mesg("image has depth %d; can't handle >8", t->b->depth);
		return;
	}
	br = t->b->r;
	if(t->s == 0){
		c = -1; 
		/* if big enough to bother, sweep box */
		if(Dx(br)<=16 && Dy(br)<=16)
			r = br;
		else{
			if(!sweep(1, &r))
				return;
			r = rectaddpt(r, subpt(br.min, t->r.min));
			if(!rectclip(&r, br))
				return;
			if(Dx(br) <= 8){
				r.min.x = br.min.x;
				r.max.x = br.max.x;
			}else if(Dx(r) < 4){
	    toosmall:
				mesg("rectangle too small");
				return;
			}
			if(Dy(br) <= 8){
				r.min.y = br.min.y;
				r.max.y = br.max.y;
			}else if(Dy(r) < 4)
				goto toosmall;
		}
	}else if(c >= 0){
		fc = &t->s->info[c];
		r.min.x = fc[0].x;
		r.min.y = br.min.y;
		r.max.x = fc[1].x;
		r.max.y = br.min.y + Dy(br);
	}else{
		/* just point at character */
		fc = t->s->info;
		p = addpt(pt, subpt(br.min, t->r.min));
		x = br.min.x;
		y = br.min.y;
		for(c=0; c<t->s->n; c++,fc++){
	    again:
			r.min.x = x;
			r.min.y = y;
			r.max.x = x + fc[1].x - fc[0].x;
			r.max.y = y + Dy(br);
			if(ptinrect(p, r))
				goto found;
			if(r.max.x >= br.min.x+Dx(t->r)){
				x -= Dx(t->r);
				y += t->s->height;
				if(fc[1].x > fc[0].x)
					goto again;
			}
			x += fc[1].x - fc[0].x;
		}
		return;
	   found:
		r = br;
		r.min.x = fc[0].x;
		r.max.x = fc[1].x;
	}
	nt = malloc(sizeof(Thing));
	if(nt == 0){
   nomem:
		mesg("can't allocate: %r");
		return;
	}
	memset(nt, 0, sizeof(Thing));
	nt->c = c;
	nt->b = allocimage(display, r, t->b->chan, 0, DNofill);
	if(nt->b == 0){
		free(nt);
		goto nomem;
	}
	draw(nt->b, r, t->b, nil, r.min);
	nt->name = strdup(t->name);
	if(nt->name == 0){
		freeimage(nt->b);
		free(nt);
		goto nomem;
	}
	nt->parent = t;
	nt->mag = mag;
	drawthing(nt, 1);
}

void
ckinfo(Thing *t, Rectangle mod)
{
	int i, j, k, top, bot, n, zero;
	Fontchar *fc;
	Rectangle r;
	Image *b;
	Thing *nt;

	if(t->parent)
		t = t->parent;
	if(t->s==0 || Dy(t->b->r)==0)
		return;
	b = 0;
	/* check bounding boxes */
	fc = &t->s->info[0];
	r.min.y = t->b->r.min.y;
	r.max.y = t->b->r.max.y;
	for(i=0; i<t->s->n; i++, fc++){
		r.min.x = fc[0].x;
		r.max.x = fc[1].x;
		if(!rectXrect(mod, r))
			continue;
		if(b==0 || Dx(b->r)<Dx(r)){
			if(b)
				freeimage(b);
			b = allocimage(display, rectsubpt(r, r.min), t->b->chan, 0, 0);
			if(b == 0){
				mesg("can't alloc image");
				break;
			}
		}
		draw(b, b->r, display->white, nil, ZP);
		draw(b, b->r, t->b, nil, r.min);
		top = 100000;
		bot = 0;
		n = 2+((Dx(r)/8)*t->b->depth);
		for(j=0; j<b->r.max.y; j++){
			memset(data, 0, n);
			unloadimage(b, Rect(b->r.min.x, j, b->r.max.x, j+1), data, sizeof data);
			zero = 1;
			for(k=0; k<n; k++)
				if(data[k]){
					zero = 0;
					break;
				}
			if(!zero){
				if(top > j)
					top = j;
				bot = j+1;
			}
		}
		if(top > j)
			top = 0;
		if(top!=fc->top || bot!=fc->bottom){
			fc->top = top;
			fc->bottom = bot;
			for(nt=thing; nt; nt=nt->next)
				if(nt->parent==t && nt->c==i)
					text(nt);
		}
	}
	if(b)
		freeimage(b);
}

void
twidpix(Thing *t, Point p, int set)
{
	Image *b, *v;
	int c;

	b = t->b;
	if(!ptinrect(p, b->r))
		return;
	if(set)
		c = but1val;
	else
		c = but2val;
	if(b->chan == GREY8)
		v = greyvalues[c];
	else
		v = values[c];
	draw(b, Rect(p.x, p.y, p.x+1, p.y+1), v, nil, ZP);
	p = screenpt(t, p);
	draw(screen, Rect(p.x, p.y, p.x+t->mag, p.y+t->mag), v, nil, ZP);
}

void
twiddle(Thing *t)
{
	int set;
	Point p, lastp;
	Image *b;
	Thing *nt;
	Rectangle mod;

	if(mouse.buttons!=1 && mouse.buttons!=2){
		buttons(Up);
		return;
	}
	set = mouse.buttons==1;
	b = t->b;
	lastp = addpt(b->r.min, Pt(-1, -1));
	mod = Rpt(addpt(b->r.max, Pt(1, 1)), lastp);
	while(mouse.buttons){
		p = realpt(t, mouse.xy);
		if(!eqpt(p, lastp)){
			lastp = p;
			if(ptinrect(p, b->r)){
				for(nt=thing; nt; nt=nt->next)
					if(nt->parent==t->parent || nt==t->parent)
						twidpix(nt, p, set);
				if(t->parent)
					t->parent->mod = 1;
				else
					t->mod = 1;
				if(p.x < mod.min.x)
					mod.min.x = p.x;
				if(p.y < mod.min.y)
					mod.min.y = p.y;
				if(p.x >= mod.max.x)
					mod.max.x = p.x+1;
				if(p.y >= mod.max.y)
					mod.max.y = p.y+1;
			}
		}
		mouse = emouse();
	}
	ckinfo(t, mod);
}

void
select(void)
{
	Thing *t;
	char line[128], buf[128];
	Point p;

	if(ptinrect(mouse.xy, cntlr)){
		scntl(line);
		if(atline(cntlr.min.x, mouse.xy, line, buf)){
			if(mouse.buttons == 1)
				cntledit(buf);
			else
				buttons(Up);
			return;
		}
		return;
	}
	for(t=thing; t; t=t->next){
		if(attext(t, mouse.xy, buf)){
			if(mouse.buttons == 1)
				textedit(t, buf);
			else
				buttons(Up);
			return;
		}
		if(ptinrect(mouse.xy, t->r)){
			if(t->parent == 0){
				if(mouse.buttons == 1){
					p = mouse.xy;
					buttons(Up);
					openedit(t, p, -1);
				}else
					buttons(Up);
				return;
			}
			twiddle(t);
			return;
		}
	}
}

void
twrite(Thing *t)
{
	int i, j, x, y, fd, ws, ld;
	Biobuf buf;
	Rectangle r;

	if(t->parent)
		t = t->parent;
	esetcursor(&busy);
	fd = create(t->name, OWRITE, 0666);
	if(fd < 0){
		mesg("can't write %s: %r", t->name);
		return;
	}
	if(t->face && t->b->depth <= 4){
		r = t->b->r;
		ld = log2[t->b->depth];
		/* This heuristic reflects peculiarly different formats */
		ws = 4;
		if(t->face == 2)	/* cursor file */
			ws = 1;
		else if(Dx(r)<32 || ld==0)
			ws = 2;
		Binit(&buf, fd, OWRITE);
		if(t->face == CURSOR)
			Bprint(&buf, "{");
		for(y=r.min.y; y<r.max.y; y++){
			unloadimage(t->b, Rect(r.min.x, y, r.max.x, y+1), data, sizeof data);
			j = 0;
			for(x=r.min.x; x<r.max.x; j+=ws,x+=ws*8>>ld){
				Bprint(&buf, "0x");
				for(i=0; i<ws; i++)
					Bprint(&buf, "%.2x", data[i+j]);
				Bprint(&buf, ", ");
			}
			if(t->face == CURSOR){
				switch(y){
				case 3: case 7: case 11: case 19: case 23: case 27:
					Bprint(&buf, "\n ");
					break;
				case 15:
					Bprint(&buf, "},\n{");
					break;
				case 31:
					Bprint(&buf, "}\n");
					break;
				}
			}else
				Bprint(&buf, "\n");
		}
		Bterm(&buf);
	}else
		if(writeimage(fd, t->b, 0)<0 || (t->s && writesubfont(fd, t->s)<0)){
			close(fd);
			mesg("can't write %s: %r", t->name);
			return;
		}
	t->mod = 0;
	close(fd);
	mesg("wrote %s", t->name);
}

void
tpixels(void)
{
	Thing *t;
	Point p, lastp;

	esetcursor(&pixel);
	for(;;){
		buttons(Down);
		if(mouse.buttons != 4)
			break;
		for(t=thing; t; t=t->next){
			lastp = Pt(-1, -1);
			if(ptinrect(mouse.xy, t->r)){
				while(ptinrect(mouse.xy, t->r) && mouse.buttons==4){
					p = realpt(t, mouse.xy);
					if(!eqpt(p, lastp)){
						if(p.y != lastp.y)
							unloadimage(t->b, Rect(t->b->r.min.x, p.y, t->b->r.max.x, p.y+1), data, sizeof data);
						mesg("[%d,%d] = %d=0x%ux", p.x, p.y, value(t->b, p.x), value(t->b, p.x));
						lastp = p;
					}
					mouse = emouse();
				}
				goto Continue;
			}
		}
		mouse = emouse();
    Continue:;
	}
	buttons(Up);
	esetcursor(0);
}

void
tclose1(Thing *t)
{
	Thing *nt;

	if(t == thing)
		thing = t->next;
	else{
		for(nt=thing; nt->next!=t; nt=nt->next)
			;
		nt->next = t->next;
	}
	do
		for(nt=thing; nt; nt=nt->next)
			if(nt->parent == t){
				tclose1(nt);
				break;
			}
	while(nt);
	if(t->s)
		freesubfont(t->s);
	else
		freeimage(t->b);
	free(t->name);
	free(t);
}

void
tclose(Thing *t)
{
	Thing *ct;

	if(t->mod){
		mesg("%s modified", t->name);
		t->mod = 0;
		return;
	}
	/* fiddle to save redrawing unmoved things */
	if(t == thing)
		ct = 0;
	else
		for(ct=thing; ct; ct=ct->next)
			if(ct->next==t || ct->next->parent==t)
				break;
	tclose1(t);
	if(ct)
		ct = ct->next;
	else
		ct = thing;
	redraw(ct);
}

void
tread(Thing *t)
{
	Thing *nt, *new;
	Fontchar *i;
	Rectangle r;
	int nclosed;

	if(t->parent)
		t = t->parent;
	new = tget(t->name);
	if(new == 0)
		return;
	nclosed = 0;
    again:
	for(nt=thing; nt; nt=nt->next)
		if(nt->parent == t){
			if(!rectinrect(nt->b->r, new->b->r)
			|| new->b->depth!=nt->b->depth){
    closeit:
				nclosed++;
				nt->parent = 0;
				tclose1(nt);
				goto again;
			}
			if((t->s==0) != (new->s==0))
				goto closeit;
			if((t->face==0) != (new->face==0))
				goto closeit;
			if(t->s){	/* check same char */
				if(nt->c >= new->s->n)
					goto closeit;
				i = &new->s->info[nt->c];
				r.min.x = i[0].x;
				r.max.x = i[1].x;
				r.min.y = new->b->r.min.y;
				r.max.y = new->b->r.max.y;
				if(!eqrect(r, nt->b->r))
					goto closeit;
			}
			nt->parent = new;
			draw(nt->b, nt->b->r, new->b, nil, nt->b->r.min);
		}
	new->next = t->next;
	if(t == thing)
		thing = new;
	else{
		for(nt=thing; nt->next!=t; nt=nt->next)
			;
		nt->next = new;
	}
	if(t->s)
		freesubfont(t->s);
	else
		freeimage(t->b);
	free(t->name);
	free(t);
	for(nt=thing; nt; nt=nt->next)
		if(nt==new || nt->parent==new)
			if(nclosed == 0)
				drawthing(nt, 0);	/* can draw in place */
			else{
				redraw(nt);	/* must redraw all below */
				break;
			}
}

void
tchar(Thing *t)
{
	char buf[256], *p;
	Rune r;
	ulong c, d;

	if(t->s == 0){
		t = t->parent;
		if(t==0 || t->s==0){
			mesg("not a subfont");
			return;
		}
	}
	if(type(buf, sizeof(buf), "char (hex or character or hex-hex)") == 0)
		return;
	if(utflen(buf) == 1){
		chartorune(&r, buf);
		c = r;
		d = r;
	}else{
		if(!strchr(hex, buf[0])){
			mesg("illegal hex character");
			return;
		}
		c = strtoul(buf, 0, 16);
		d = c;
		p = utfrune(buf, '-');
		if(p){
			d = strtoul(p+1, 0, 16);
			if(d < c){
				mesg("invalid range");
				return;
			}
		}
	}
	c -= t->off;
	d -= t->off;
	while(c <= d){
		if(c>=t->s->n){
			mesg("0x%lux not in font %s", c+t->off, t->name);
			return;
		}
		openedit(t, Pt(0, 0), c);
		c++;
	}
}

void
apply(void (*f)(Thing*))
{
	Thing *t;

	esetcursor(&sight);
	buttons(Down);
	if(mouse.buttons == 4)
		for(t=thing; t; t=t->next)
			if(ptinrect(mouse.xy, t->er)){
				buttons(Up);
				f(t);
				break;
			}
	buttons(Up);
	esetcursor(0);
}

int
complement(Image *t)
{
	int i, n;
	uchar *buf;

	n = Dy(t->r)*bytesperline(t->r, t->depth);
	buf = malloc(n);
	if(buf == 0)
		return 0;
	unloadimage(t, t->r, buf, n);
	for(i=0; i<n; i++)
		buf[i] = ~buf[i];
	loadimage(t, t->r, buf, n);
	free(buf);
	return 1;
}

void
copy(void)
{
	Thing *st, *dt, *nt;
	Rectangle sr, dr, fr;
	Image *tmp;
	Point p1, p2;
	int but, up;

	if(!sweep(3, &sr))
		return;
	for(st=thing; st; st=st->next)
		if(rectXrect(sr, st->r))
			break;
	if(st == 0)
		return;
	/* click gives full rectangle */
	if(Dx(sr)<4 && Dy(sr)<4)
		sr = st->r;
	rectclip(&sr, st->r);
	p1 = realpt(st, sr.min);
	p2 = realpt(st, Pt(sr.min.x, sr.max.y));
	up = 0;
	if(p1.x != p2.x){	/* swept across a fold */
   onafold:
		mesg("sweep spans a fold");
		goto Return;
	}
	p2 = realpt(st, sr.max);
	sr.min = p1;
	sr.max = p2;
	fr.min = screenpt(st, sr.min);
	fr.max = screenpt(st, sr.max);
	p1 = subpt(p2, p1);	/* diagonal */
	if(p1.x==0 || p1.y==0)
		return;
	border(screen, fr, -1, values[Blue], ZP);
	esetcursor(&box);
	for(; mouse.buttons==0; mouse=emouse()){
		for(dt=thing; dt; dt=dt->next)
			if(ptinrect(mouse.xy, dt->er))
				break;
		if(up)
			edrawgetrect(insetrect(dr, -Borderwidth), 0);
		up = 0;
		if(dt == 0)
			continue;
		dr.max = screenpt(dt, realpt(dt, mouse.xy));
		dr.min = subpt(dr.max, mulpt(p1, dt->mag));
		if(!rectXrect(dr, dt->r))
			continue;
		edrawgetrect(insetrect(dr, -Borderwidth), 1);
		up = 1;
	}
	/* if up==1, we had a hit */
	esetcursor(0);
	if(up)
		edrawgetrect(insetrect(dr, -Borderwidth), 0);
	but = mouse.buttons;
	buttons(Up);
	if(!up || but!=4)
		goto Return;
	dt = 0;
	for(nt=thing; nt; nt=nt->next)
		if(rectXrect(dr, nt->r)){
			if(dt){
				mesg("ambiguous sweep");
				return;
			}
			dt = nt;
		}
	if(dt == 0)
		goto Return;
	p1 = realpt(dt, dr.min);
	p2 = realpt(dt, Pt(dr.min.x, dr.max.y));
	if(p1.x != p2.x)
		goto onafold;
	p2 = realpt(dt, dr.max);
	dr.min = p1;
	dr.max = p2;

	if(invert){
		tmp = allocimage(display, dr, dt->b->chan, 0, 255);
		if(tmp == 0){
    nomem:
			mesg("can't allocate temporary");
			goto Return;
		}
		draw(tmp, dr, st->b, nil, sr.min);
		if(!complement(tmp))
			goto nomem;
		draw(dt->b, dr, tmp, nil, dr.min);
		freeimage(tmp);
	}else
		draw(dt->b, dr, st->b, nil, sr.min);
	if(dt->parent){
		draw(dt->parent->b, dr, dt->b, nil, dr.min);
		dt = dt->parent;
	}
	drawthing(dt, 0);
	for(nt=thing; nt; nt=nt->next)
		if(nt->parent==dt && rectXrect(dr, nt->b->r)){
			draw(nt->b, dr, dt->b, nil, dr.min);
			drawthing(nt, 0);
		}
	ckinfo(dt, dr);
	dt->mod = 1;

Return:
	/* clear blue box */
	drawthing(st, 0);
}

void
menu(void)
{
	Thing *t;
	char *mod;
	int sel;
	char buf[256];

	sel = emenuhit(3, &mouse, &menu3);
	switch(sel){
	case Mopen:
		if(type(buf, sizeof(buf), "file")){
			t = tget(buf);
			if(t)
				drawthing(t, 1);
		}
		break;
	case Mwrite:
		apply(twrite);
		break;
	case Mread:
		apply(tread);
		break;
	case Mchar:
		apply(tchar);
		break;
	case Mcopy:
		copy();
		break;
	case Mpixels:
		tpixels();
		break;
	case Mclose:
		apply(tclose);
		break;
	case Mexit:
		mod = 0;
		for(t=thing; t; t=t->next)
			if(t->mod){
				mod = t->name;
				t->mod = 0;
			}
		if(mod){
			mesg("%s modified", mod);
			break;
		}
		esetcursor(&skull);
		buttons(Down);
		if(mouse.buttons == 4){
			buttons(Up);
			cleantmpfiles();
			exits(0);
		}
		buttons(Up);
		esetcursor(0);
		break;
	}
}