shithub: riscv

Download patch

ref: 712fd30652d29dc9e936f11d7837d1cb079575fc
parent: 555a05018b60b28bdfd6ada0310848c68fe20e48
author: aiju <devnull@localhost>
date: Wed Jul 30 11:57:14 EDT 2014

added sprite editor spred

diff: cannot open b/sys/src/cmd/spred//null: file does not exist: 'b/sys/src/cmd/spred//null'
--- /dev/null
+++ b/sys/man/1/spred
@@ -1,0 +1,58 @@
+b.TH SPRED 1
+.SH NAME
+spred \- sprite editor
+.SH SYNOPSIS
+.B spred
+.SH DESCRIPTION
+.I Spred
+is an editor for small images using a limited palette.
+It uses a window system mimicking
+.IR samterm (1).
+There is a command window which uses a command language described below.
+There is also an arbitrary number of palette and sprite windows.
+Each open sprite file has an associated palette file.
+.PP
+A left click on a color in a palette window selects that color.
+Colors in different palettes can be selected indepedently.
+A left click on a pixel in a sprite window sets that pixel to the selected color.
+.PP
+A right click brings up the global menu to create windows etc.
+It also lists all currently open files, including those that are not open in any window.
+A middle click brings up the menu for the local window, if applicable. Available commands there are:
+.TP
+.I pal
+The \fIpal\fR command sets the palette for the current sprite window. The palette is selected with a middle click.
+.PP
+The command language is a very stripped down version of
+.IR rc (1),
+currently only supporting "simple" commands consisting of a name and an arbitrary number of arguments separated by spaces. Quoting works just like with
+.IR rc (1).
+Available commands are:
+.TP
+.B q
+Quits the program. If any files have unsaved changes, it will fail on the first attempt to quit.
+.TP
+.BI pal file
+.TP
+.BI spr file
+Open a palette (\fIpal\fR) or sprite (\fIspr\fR) file named \fIfile\fR.
+If the file does not exist it is created.
+.TP
+.BI w [file]
+Write the currently selected file to \fIfile\fR. If \fIfile\fR is not specified, the name specified to the command opening the file is used.
+.TP
+.BI size sz
+Sets the size of the current file to \fIsz\fR.
+\fISz\fR should be of the form \fIn\fR for palettes or \fIn\fB*\fIm\fR for sprites where \fIn\fR and \fIm\fR are integers.
+.TP
+.B set 0x\fIrrggbb\fR
+Sets the currently selected color to the rgb color \fI(rr,gg,bb)\fR where
+.IR rr , gg and bb
+are in hexadecimal notation.
+.TP
+.B zoom n
+Sets the current zoom factor to \fIn\fR.
+.SH SOURCE
+.B /sys/src/cmd/spred
+.SH SEE ALSO
+.IR sam (1)
--- /dev/null
+++ b/sys/src/cmd/spred/cmd.c
@@ -1,0 +1,188 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include <draw.h>
+#include <mouse.h>
+#include <frame.h>
+#include "dat.h"
+#include "fns.h"
+
+extern Mousectl *mc;
+
+static void
+dopal(int, char **argv)
+{
+	Pal *p;
+	
+	p = findpal("", argv[1], 2);
+	if(p == nil){
+		cmdprint("?%r\n");
+		p = newpal(argv[1]);
+	}
+	if(newwinsel(PAL, mc, p) == nil){
+		if(p->ref == 0)
+			putfil(p);
+		return;
+	}
+}
+
+static void
+dosize(int, char **argv)
+{
+	char *p;
+	int n, m;
+
+	if(actf == nil)
+		goto err;
+	switch(actf->type){
+	case PAL:
+		n = strtol(argv[1], &p, 0);
+		if(*p != 0 || n < 0)
+			goto err;
+		palsize((Pal *) actf->f, n);
+		return;
+	case SPR:
+		n = strtol(argv[1], &p, 0);
+		if(*p != '*' || n < 0)
+			goto err;
+		m = strtol(++p, &p, 0);
+		if(*p != 0 || m < 0)
+			goto err;
+		sprsize((Spr *) actf->f, n, m);
+		return;
+	}
+err:
+	cmdprint("?\n");
+}
+
+static void
+doset(int, char **argv)
+{
+	int n;
+	char *p;
+	Pal *q;
+
+	if(actf == nil)
+		goto err;
+	switch(actf->type){
+	case PAL:
+		n = strtol(argv[1], &p, 0);
+		if(*p != 0 || n < 0 || n > 0xffffff)
+			goto err;
+		q = (Pal *) actf->f;
+		palset(q, q->sel, n);
+		return;
+	}
+err:
+	cmdprint("?\n");
+}
+
+static void
+dozoom(int, char **argv)
+{
+	int n;
+	char *p;
+	
+	if(actf == nil)
+		goto err;
+	n = strtol(argv[1], &p, 0);
+	if(*p != 0 || n <= 0)
+		goto err;
+	actf->zoom = n;
+	actf->tab->draw(actf);
+	return;
+err:
+	cmdprint("?\n");
+}
+
+static void
+dospr(int, char **argv)
+{
+	Win *w;
+	Spr *s;
+	Biobuf *bp;
+	
+	s = newspr(argv[1]);
+	bp = Bopen(argv[1], OREAD);
+	if(bp == nil)
+		cmdprint("?%r\n");
+	else{
+		if(readspr(s, bp) < 0)
+			cmdprint("?%r\n");
+		Bterm(bp);
+	}
+	w = newwinsel(SPR, mc, s);
+	if(w == nil){
+		putfil(s);
+		return;
+	}
+	if(s->palfile != nil){
+		s->pal = findpal(argv[1], s->palfile, 1);
+		if(s->pal == nil)
+			cmdprint("?palette: %r\n");
+		else{
+			incref(s->pal);
+			w->tab->draw(w);
+		}
+	}
+}
+
+static void
+dowrite(int argc, char **argv)
+{
+	char *f;
+	
+	if(argc > 2)
+		cmdprint("?\n");
+	if(argc == 2)
+		f = argv[1];
+	else
+		f = nil;
+	if(actf == nil)
+		cmdprint("?\n");
+	winwrite(actf, f);
+}
+
+static void
+doquit(int, char **)
+{
+	if(quit() < 0)
+		threadexitsall(nil);
+}
+
+static struct cmd {
+	char *name;
+	int argc;
+	void (*f)(int, char **);
+} cmds[] = {
+	{"pal", 2, dopal},
+	{"size", 2, dosize},
+	{"set", 2, doset},
+	{"spr", 2, dospr},
+	{"w", 0, dowrite},
+	{"q", 1, doquit},
+	{"zoom", 2, dozoom},
+	{nil, nil}
+};
+
+void
+docmd(char *s)
+{
+	char *t[32];
+	int nt;
+	struct cmd *c;
+
+	nt = tokenize(s, t, nelem(t));
+	if(nt == 0)
+		return;
+	for(c = cmds; c->name != 0; c++)
+		if(strcmp(t[0], c->name) == 0){
+			if(c->argc != 0 && c->argc != nt)
+				cmdprint("?\n");
+			else
+				c->f(nt, t);
+			return;
+		}
+	cmdprint("?\n");
+}
--- /dev/null
+++ b/sys/src/cmd/spred/cmdw.c
@@ -1,0 +1,199 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include <draw.h>
+#include <mouse.h>
+#include <frame.h>
+#include "dat.h"
+#include "fns.h"
+
+static int
+cmdinit(Win *)
+{
+	return 0;
+}
+
+static void
+scrollbar(Win *w)
+{
+	int h, t0, t1;
+
+	h = Dy(w->inner);
+	draw(w->im, rectaddpt(Rect(0, 0, SCRBSIZ+1, h), w->inner.min), w->tab->cols[BORD], nil, ZP);
+	t0 = w->toprune * h;
+	t1 = (w->toprune + w->fr.nchars) * h;
+	if(w->nrunes == 0){
+		 t0 = 0;
+		 t1 = h;
+	}else{
+		t0 /= w->nrunes;
+		t1 /= w->nrunes;
+	}
+	draw(w->im, rectaddpt(Rect(0, t0, SCRBSIZ, t1), w->inner.min), w->tab->cols[BACK], nil, ZP);
+}
+
+static void
+cmddraw(Win *w)
+{
+	Rectangle r;
+	
+	frclear(&w->fr, 0);
+	r = insetrect(w->inner, 1);
+	r.min.x += SCRTSIZ;
+	scrollbar(w);
+	frinit(&w->fr, r, display->defaultfont, w->im, w->tab->cols);
+	frinsert(&w->fr, w->runes + w->toprune, w->runes + w->nrunes, 0);
+}
+
+void
+cmdscroll(Win *w, int l)
+{
+	int r;
+	
+	if(l == 0)
+		return;
+	if(l > 0){
+		for(r = w->toprune; r < w->nrunes && l != 0; r++)
+			if(w->runes[r] == '\n')
+				l--;
+		frdelete(&w->fr, 0, r - w->toprune);
+		w->toprune = r;
+	}else{
+		for(r = w->toprune; r > 0; r--)
+			if(w->runes[r] == '\n' && --l == 0)
+				break;
+		frinsert(&w->fr, w->runes + r, w->runes + w->toprune, 0);
+		w->toprune = r;
+	}
+	scrollbar(w);
+}
+
+static void
+cmdclick(Win *w, Mousectl *mc)
+{
+	if(mc->xy.x <= w->inner.min.x + SCRBSIZ){
+		cmdscroll(w, -5);
+		return;
+	}
+	frselect(&w->fr, mc);
+}
+
+static int
+cmdrmb(Win *w, Mousectl *mc)
+{
+	if(mc->xy.x > w->inner.min.x + SCRBSIZ)
+		return -1;
+	cmdscroll(w, 5);
+	return 0;
+}
+
+void
+cmdinsert(Win *w, Rune *r, int nr, int rp)
+{
+	Rune *s;
+
+	if(nr < 0)
+		for(nr = 0, s = r; *s++ != 0; nr++)
+			;
+	if(rp < 0 || rp > w->nrunes)
+		rp = w->nrunes;
+	if(w->nrunes + nr > w->arunes){
+		w->runes = realloc(w->runes, w->arunes = w->arunes + (nr + RUNEBLK - 1) & ~(RUNEBLK - 1));
+		if(w->runes == nil)
+			sysfatal("realloc: %r");
+	}
+	if(rp != w->nrunes)
+		memmove(w->runes + rp, w->runes + rp + nr, (w->nrunes - rp) * sizeof(Rune));
+	memmove(w->runes + rp, r, nr * sizeof(Rune));
+	w->nrunes += nr;
+	if(w->toprune > rp)
+		w->toprune += nr;
+	else{
+		frinsert(&w->fr, w->runes + rp, w->runes + rp + nr, rp - w->toprune);
+		if(rp == w->nrunes - nr){
+			if(w->fr.lastlinefull)
+				cmdscroll(w, 1);
+		}
+	}
+}
+
+static void
+cmddel(Win *w, int a, int b)
+{
+	if(a >= b)
+		return;
+	memmove(w->runes + a, w->runes + b, w->nrunes - b);
+	w->nrunes -= b - a;
+	if(w->toprune >= b)
+		w->toprune -= b - a;
+	else{
+		frdelete(&w->fr, a - w->toprune, b - w->toprune);
+		if(w->toprune >= a)
+			w->toprune = a;
+	}
+}
+
+static void
+cmdkey(Win *w, Rune r)
+{
+	static char buf[4096];
+	char *p;
+	Rune *q;
+
+	if(w->fr.p0 < w->fr.p1)
+		cmddel(w, w->toprune + w->fr.p0, w->toprune + w->fr.p1);
+	switch(r){
+	case 0x00:
+	case 0x1b:
+		break;
+	case '\b':
+		if(w->fr.p0 > 0)
+			cmddel(w, w->toprune + w->fr.p0 - 1, w->toprune + w->fr.p0);
+		break;
+	case '\n':
+		cmdinsert(w, &r, 1, w->fr.p0 + w->toprune);
+		if(w->toprune + w->fr.p0 == w->nrunes){
+			q = w->runes + w->toprune + w->fr.p0 - 1;
+			p = buf;
+			while(*--q != 0xa && q > w->runes)
+				;
+			if(*q == 0xa)
+				q++;
+			while(q < w->runes + w->nrunes && p < buf + nelem(buf) + 1 && *q != 0xa)
+				p += runetochar(p, q++);
+			*p = 0;
+			docmd(buf);
+		}
+		break;
+	default:
+		cmdinsert(w, &r, 1, w->fr.p0 + w->toprune);
+	}
+}
+
+void
+cmdprint(char *fmt, ...)
+{
+	Rune *r;
+	va_list va;
+	
+	va_start(va, fmt);
+	r = runevsmprint(fmt, va);
+	va_end(va);
+	if(r != nil)
+		cmdinsert(cmdw, r, -1, -1);
+}
+
+Wintab cmdtab = {
+	.init = cmdinit,
+	.draw = cmddraw,
+	.click = cmdclick,
+	.rmb = cmdrmb,
+	.key = cmdkey,
+	.hexcols = {
+		[BORD] DPurpleblue,
+		[DISB] 0xCCCCEEFF,
+		[BACK] 0xCCFFFFFF,
+		[HIGH] DPalegreygreen
+	}
+};
--- /dev/null
+++ b/sys/src/cmd/spred/dat.h
@@ -1,0 +1,98 @@
+typedef struct Ident Ident;
+typedef struct Win Win;
+typedef struct Wintab Wintab;
+typedef struct Pal Pal;
+typedef struct Spr Spr;
+typedef struct File File;
+
+enum {
+	BORDSIZ = 5,
+	MINSIZ = 3 * BORDSIZ,
+	SELSIZ = 2,
+	SCRBSIZ = 11,
+	SCRTSIZ = 14,
+	RUNEBLK = 4096,
+};
+
+enum {
+	DISB = NCOL,
+	NCOLS
+};
+
+enum {
+	CMD,
+	PAL,
+	SPR,
+	NTYPES
+};
+
+struct Wintab {
+	int (*init)(Win *);
+	void (*die)(Win *);
+	void (*click)(Win *, Mousectl *);
+	void (*menu)(Win *, Mousectl *);
+	int (*rmb)(Win *, Mousectl *);
+	void (*key)(Win *, Rune);
+	void (*draw)(Win *);
+	void (*zerox)(Win *, Win *);
+	u32int hexcols[NCOLS];
+	Image *cols[NCOLS];
+};
+
+struct Win {
+	Rectangle entire;
+	Rectangle inner;
+	Image *im;
+	Win *next, *prev;
+	Win *wnext, *wprev;
+	int type;
+	Wintab *tab;
+	
+	Frame fr;
+	Rune *runes;
+	int nrunes, arunes;
+	int toprune;
+	
+	int zoom;
+	Point scr;
+	File *f;
+	Rectangle sprr;
+};
+
+struct Ident {
+	uint type, dev;
+	Qid;
+};
+
+struct File {
+	int type;
+	Ref;
+	File *next, *prev;
+	char *name;
+	int change;
+	Ident id;
+	Win wins;
+};
+
+struct Pal {
+	File;
+	int ncol;
+	u32int *cols;
+	Image **ims;
+	int sel;
+};
+
+struct Spr {	
+	File;
+	Pal *pal;
+	int w, h;
+	u32int *data;
+	char *palfile;
+};
+
+extern Win wlist;
+extern File flist;
+extern Win *actw, *actf, *cmdw;
+extern Screen *scr;
+extern Image *invcol;
+extern int quitok;
--- /dev/null
+++ b/sys/src/cmd/spred/fil.c
@@ -1,0 +1,157 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include <draw.h>
+#include <mouse.h>
+#include <frame.h>
+#include "dat.h"
+#include "fns.h"
+
+int
+tline(Biobuf *bp, char **str, char **args, int max)
+{
+	char *s, *p;
+	int q, dq, rc;
+
+	do{
+		s = Brdstr(bp, '\n', 10);
+		if(s == nil)
+			return -1;
+		q = dq = 0;
+		for(p = s; *p != 0; p++)
+			if(*p == '\'')
+				dq = !dq;
+			else{
+				if(dq){
+					q = !q;
+					dq = 0;
+				}
+				if(*p == '#' && !q){
+					*p = 0;
+					break;
+				}
+			}
+		rc = tokenize(s, args, max);
+	}while(rc == 0 && (free(s), 1));
+	*str = s;
+	return rc;
+}
+
+Ident
+getident(int fd)
+{
+	Dir *d;
+	Ident i;
+	
+	d = dirfstat(fd);
+	if(d == nil)
+		return (Ident){-1, -1, (Qid){0, 0, 0}};
+	i = (Ident){d->type, d->dev, d->qid};
+	free(d);
+	return i;
+}
+
+void
+putident(Ident)
+{
+}
+
+int
+identcmp(Ident *a, Ident *b)
+{
+	return a->type != b->type || a->dev != b->dev || a->path != b->path;
+}
+
+int
+filcmp(File *f, File *g)
+{
+	if(f->type != g->type)
+		return f->type - g->type;
+	if(f->name == nil || g->name == nil)
+		return -1;
+	return strcmp(f->name, g->name);
+}
+
+void
+filinit(File *f, char *t)
+{
+	File *g;
+
+	f->wins.wnext = f->wins.wprev = &f->wins;
+	f->name = strdup(t);
+	for(g = flist.next; g != &flist && filcmp(g, f) < 0; g = g->next)
+		;
+	f->prev = g->prev;
+	f->next = g;
+	g->prev->next = f;
+	g->prev = f;
+}
+
+void
+putfil(File *f)
+{
+	switch(f->type){
+	case PAL: putpal((Pal *) f); break;
+	case SPR: putspr((Spr *) f); break;
+	}
+	f->prev->next = f->next;
+	f->next->prev = f->prev;
+	free(f->name);
+	free(f);
+}
+
+static char phasetitle[] = "??? phase error ???";
+
+int
+filtitlelen(File *f)
+{
+	if(f->name != nil)
+		return utflen(f->name) + 4;
+	return strlen(phasetitle);
+}
+
+char *
+filtitle(File *f, char *s, char *e)
+{
+	if(f->name == nil)
+		return strecpy(s, e, phasetitle);
+	*s++ = f->change ? '\'' : ' ';
+	if(f->wins.wnext != &f->wins)
+		if(f->wins.wnext->wnext != &f->wins)
+			*s++ = '*';
+		else
+			*s++ = '+';
+	else
+		*s++ = '-';
+	*s++ = actf != nil && f == actf->f ? '.' : ' ';
+	*s++ = ' ';
+	return strecpy(s, e, f->name);
+}
+
+void
+winwrite(Win *w, char *f)
+{
+	if(w->f == nil){
+		cmdprint("?\n");
+		return;
+	}
+	switch(w->type){
+	case PAL:
+		writepal((Pal *) w->f, f);
+		return;
+	case SPR:
+		writespr((Spr *) w->f, f);
+		return;
+	}
+	cmdprint("?\n");
+}
+
+void
+filredraw(File *f)
+{
+	Win *w;
+	
+	for(w = f->wins.wnext; w != &f->wins; w = w->wnext)
+		w->tab->draw(w);
+}
--- /dev/null
+++ b/sys/src/cmd/spred/fns.h
@@ -1,0 +1,37 @@
+void	cmdprint(char *, ...);
+void	docmd(char *);
+void*	emalloc(ulong);
+void	filinit(File *, char *);
+void	filredraw(File *);
+char*	filtitle(File *, char *, char *);
+int	filtitlelen(File *);
+Pal*	findpal(char *, char *, int);
+Ident	getident(int);
+int	identcmp(Ident*, Ident *);
+void	initwin(void);
+Pal*	newpal(char *);
+Spr*	newspr(char *);
+Win*	newwin(int, Rectangle, File *);
+Win*	newwinsel(int, Mousectl *, File *);
+void	paldraw(Win *);
+void	palset(Pal *, int, u32int);
+void	palsize(Pal *, int);
+void	putfil(File *);
+void	putident(Ident);
+void	putpal(Pal *);
+void	putspr(Spr *);
+int	quit(void);
+int	readpal(Pal *, Biobuf *);
+int	readspr(Spr *, Biobuf *);
+void	resize(void);
+void	setfocus(Win *);
+void	sprsize(Spr *, int, int);
+int	tline(Biobuf *, char **, char **, int);
+void	winclick(Mousectl *);
+void	winclose(Win *);
+void	winresize(Win *, Mousectl *);
+Win*	winsel(Mousectl*, int);
+void	winwrite(Win *, char *);
+void	winzerox(Win *, Mousectl *);
+int	writepal(Pal *, char *);
+int	writespr(Spr *, char *);
--- /dev/null
+++ b/sys/src/cmd/spred/mkfile
@@ -1,0 +1,16 @@
+</$objtype/mkfile
+
+TARG=spred
+BIN=/$objtype/bin
+OFILES=\
+	spred.$O\
+	win.$O\
+	cmd.$O\
+	pal.$O\
+	cmdw.$O\
+	spr.$O\
+	fil.$O\
+
+HFILES=dat.h fns.h
+
+</sys/src/cmd/mkone
--- /dev/null
+++ b/sys/src/cmd/spred/pal.c
@@ -1,0 +1,280 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include <draw.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <frame.h>
+#include "dat.h"
+#include "fns.h"
+
+Pal *
+newpal(char *f)
+{
+	Pal *p;
+	
+	p = emalloc(sizeof(*p));
+	p->type = PAL;
+	p->sel = -1;
+	filinit(p, f);
+	return p;
+}
+
+void
+putpal(Pal *p)
+{
+	int i;
+
+	for(i = 0; i < p->ncol; i++)
+		freeimage(p->ims[i]);
+	free(p->cols);
+	free(p->ims);
+}
+
+int
+readpal(Pal *p, Biobuf *bp)
+{
+	char *s, *sp;
+	char *args[8];
+	int nc, i, c;
+	
+	s = nil;
+	if(tline(bp, &s, args, nelem(args)) != 2)
+		goto err;
+	if(strcmp(args[0], "pal") != 0)
+		goto err;
+	nc = strtol(args[1], &sp, 0);
+	if(*sp != 0 || nc < 0)
+		goto err;
+	free(s);
+	s = nil;
+	p->ncol = nc;
+	p->cols = emalloc(nc * sizeof(*p->cols));
+	p->ims = emalloc(nc * sizeof(*p->ims));
+	for(i = 0; i < nc; i++){
+		if(tline(bp, &s, args, nelem(args)) != 1)
+			goto err;
+		c = strtol(args[0], &sp, 0);
+		if(*sp != 0 || c < 0 || c > 0xffffff)
+			goto err;
+		p->cols[i] = c;
+		free(s);
+		s = nil;
+	}
+	for(i = 0; i < nc; i++)
+		p->ims[i] = allocimage(display, Rect(0, 0, 1, 1), screen->chan, 1, p->cols[i] << 8 | 0xff);
+	p->id = getident(bp->fid);
+	return 0;
+err:
+	if(s != nil)
+		free(s);
+	werrstr("invalid format");
+	return -1;
+}
+
+int
+writepal(Pal *p, char *f)
+{
+	Biobuf *bp;
+	int i, rc, n;
+
+	if(f == nil)
+		f = p->name;
+	bp = Bopen(f, OWRITE);
+	if(bp == nil){
+		cmdprint("?%r\n");
+		return -1;
+	}
+	n = 0;
+	rc = Bprint(bp, "pal %d\n", p->ncol);
+	if(rc < 0) goto err;
+	n += rc;
+	for(i = 0; i < p->ncol; i++){
+		rc = Bprint(bp, "%#.6x\n", p->cols[i]);
+		if(rc < 0) goto err;
+		n += rc;
+	}
+	if(Bterm(bp) < 0){
+		cmdprint("?%r\n");
+		return -1;
+	}
+	p->change = 0;
+	cmdprint("%s: #%d\n", f, n);
+	return 0;
+err:
+	cmdprint("?%r\n");
+	Bterm(bp);
+	return -1;
+}
+
+Pal *
+findpal(char *sf, char *fn, int op)
+{
+	File *f;
+	char *s, *q;
+	Ident i;
+	int fd;
+	Biobuf *bp;
+	Pal *p;
+	
+	if(sf == nil)
+		sf = "";
+	s = emalloc(strlen(sf) + strlen(fn) + 2);
+	strcpy(s, sf);
+	q = strrchr(s, '/');
+	if(q != nil)
+		*++q = 0;
+	else
+		*s = 0;
+	strcpy(s, fn);
+	fd = open(s, OREAD);
+	if(fd < 0){
+		free(s);
+		return nil;
+	}
+	i = getident(fd);
+	if(i.type == (uint)-1){
+		close(fd);
+		return nil;
+	}
+	for(f = flist.next; f != &flist; f = f->next)
+		if(f->type == PAL && identcmp(&f->id, &i) == 0){
+			close(fd);
+			putident(i);
+			return (Pal *) f;
+		}
+	putident(i);
+	if(op == 0){
+		close(fd);
+		return nil;
+	}
+	bp = emalloc(sizeof(*bp));
+	Binit(bp, fd, OREAD);
+	p = newpal(s);
+	if(readpal(p, bp) < 0){
+		putfil(p);
+		p = nil;
+		goto end;
+	}
+end:
+	Bterm(bp);
+	close(fd);
+	free(bp);
+	free(s);
+	return p;
+}
+
+static void
+palredraw(Pal *p)
+{
+	File *f;
+
+	filredraw(p);
+	for(f = flist.next; f != &flist; f = f->next)
+		if(f->type == SPR && ((Spr *) f)->pal == p)
+			filredraw(f);
+}
+
+void
+palsize(Pal *p, int sz)
+{
+	int i;
+
+	if(sz == p->ncol)
+		return;
+	p->cols = realloc(p->cols, sz * sizeof(*p->cols));
+	p->ims = realloc(p->ims, sz * sizeof(*p->ims));
+	if(sz > p->ncol){
+		memset(p->cols + p->ncol, 0, sz);
+		for(i = p->ncol; i < sz; i++)
+			p->ims[i] = allocimage(display, Rect(0, 0, 1, 1), screen->chan, 1, 0);
+	}
+	p->ncol = sz;
+	p->change = 1;
+	quitok = 0;
+	palredraw(p);
+}
+
+void
+paldraw(Win *w)
+{
+	Pal *p;
+	int n, i;
+	Rectangle r;
+	
+	if(w->type != PAL || w->f == nil)
+		sysfatal("paldraw: phase error");
+	p = (Pal *) w->f;
+	n = Dx(w->inner) / w->zoom;
+	draw(w->im, w->inner, w->tab->cols[BACK], nil, ZP);
+	for(i = 0; i < p->ncol; i++){
+		r.min = addpt(w->inner.min, mulpt(Pt(i%n, i/n), w->zoom));
+		r.max.x = r.min.x + w->zoom;
+		r.max.y = r.min.y + w->zoom;
+		draw(w->im, r, p->ims[i], nil, ZP);
+		if(p->sel == i)
+			border(w->im, r, SELSIZ, display->white, ZP);
+	}
+}
+
+void
+palset(Pal *p, int s, u32int c)
+{
+	if(s < 0 || s >= p->ncol || p->cols[s] == c)
+		return;
+	p->cols[s] = c;
+	freeimage(p->ims[s]);
+	p->ims[s] = allocimage(display, Rect(0, 0, 1, 1), screen->chan, 1, c << 8 | 0xff);
+	p->change = 1;
+	quitok = 0;
+	palredraw(p);
+}
+
+static int
+palinit(Win *w)
+{
+	w->zoom = 32;
+	return 0;
+}
+
+static void
+palzerox(Win *w, Win *v)
+{
+	v->zoom = w->zoom;
+}
+
+static void
+palclick(Win *w, Mousectl *mc)
+{
+	int n, i;
+	Point pt;
+	Pal *p;
+	
+	if(!ptinrect(mc->xy, w->inner))
+		return;
+	if(w->f == nil)
+		sysfatal("palclick: phase error");
+	p = (Pal *) w->f;
+	n = Dx(w->inner) / w->zoom;
+	pt = subpt(mc->xy, w->inner.min);
+	if(pt.x >= n * w->zoom)
+		return;
+	i = pt.x / w->zoom + pt.y / w->zoom * n;
+	if(i >= p->ncol)
+		return;
+	p->sel = i;
+	palredraw(p);
+}
+
+Wintab paltab = {
+	.init = palinit,
+	.click = palclick,
+	.draw = paldraw,
+	.zerox = palzerox,
+	.hexcols = {
+		[BORD] 0xAA0000FF,
+		[DISB] 0xCC8888FF,
+		[BACK] 0xFFCCFFFF,
+	},
+};
--- /dev/null
+++ b/sys/src/cmd/spred/spr.c
@@ -1,0 +1,371 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include <draw.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <frame.h>
+#include "dat.h"
+#include "fns.h"
+
+Spr *
+newspr(char *f)
+{
+	Spr *s;
+	
+	s = emalloc(sizeof(*s));
+	s->type = SPR;
+	filinit(s, f);
+	return s;
+}
+
+void
+putspr(Spr *s)
+{
+	if(s->pal != nil && !decref(s->pal) && s->pal->change <= 0)
+		putfil(s->pal);
+	free(s->palfile);
+	free(s->data);
+}
+
+int
+readspr(Spr *s, Biobuf *bp)
+{
+	char *args0[8], *p, *ss, **args;
+	int n, i, j;
+
+	args = nil;
+	ss = nil;
+	if(tline(bp, &ss, args0, nelem(args0)) != 4)
+		goto err;
+	if(strcmp(args0[0], "sprite") != 0)
+		goto err;
+	n = strtol(args0[1], &p, 0);
+	if(*p != 0 || n < 0)
+		goto err;
+	s->w = n;
+	n = strtol(args0[2], &p, 0);
+	if(*p != 0 || n < 0)
+		goto err;
+	s->h = n;
+	if(*args0[3] != 0)
+		s->palfile = strdup(args0[3]);
+	else
+		s->palfile = nil;
+	free(ss);
+	ss = nil;
+	s->data = emalloc(s->w * s->h * sizeof(u32int));
+	args = emalloc((s->w + 1) * sizeof(char *));
+	for(i = 0; i < s->h; i++){
+		if(tline(bp, &ss, args, s->w + 1) != s->w)
+			goto err;
+		for(j = 0; j < s->w; j++){
+			n = strtol(args[j], &p, 0);
+			if(*p != 0 || n < 0)
+				goto err;
+			s->data[i * s->w + j] = n;
+		}
+		free(ss);
+		ss = nil;
+	}
+	free(args);
+	return 0;
+err:
+	werrstr("invalid format");
+	free(s->data);
+	free(args);
+	s->w = 0;
+	s->h = 0;
+	return -1;
+}
+
+int
+writespr(Spr *s, char *file)
+{
+	Biobuf *bp;
+	int n, rc;
+	int i, j;
+
+	if(file == nil)
+		file = s->name;
+	bp = Bopen(file, OWRITE);
+	if(bp == nil){
+		cmdprint("?%r\n");
+		return -1;
+	}
+	rc = Bprint(bp, "sprite %d %d %q\n", s->w, s->h, s->palfile != nil ? s->palfile : "");
+	if(rc < 0) goto err;
+	n = rc;
+	for(i = 0; i < s->h; i++)
+		for(j = 0; j < s->w; j++){
+			rc = Bprint(bp, "%d%c", s->data[s->w * i + j], j == s->w - 1 ? '\n' : ' ');
+			if(rc < 0) goto err;
+			n += rc;
+		}
+	if(Bterm(bp) < 0){
+		cmdprint("?%r\n");
+		return -1;
+	}
+	s->change = 0;
+	quitok = 0;
+	cmdprint("%s: #%d\n", file, n);
+	return 0;
+err:
+	cmdprint("?%r\n");
+	Bterm(bp);
+	return -1;
+}
+
+int
+sprinit(Win *w)
+{
+	w->zoom = 4;
+	return 0;
+}
+
+static Rectangle
+sprrect(Win *w, Rectangle s)
+{
+	Rectangle r;
+	Point p, q;
+	Spr *t;
+
+	t = (Spr *) w->f;
+	p = Pt(t->w * w->zoom, t->h * w->zoom);
+	q = addpt(divpt(addpt(s.min, s.max), 2), w->scr);
+	r.min = subpt(q, divpt(p, 2));
+	r.max = addpt(r.min, p);
+	return r;
+}
+
+static void
+scrollbars(Win *w)
+{
+	Rectangle r, s;
+	int dx, dy;
+	int t0, t1;
+
+	if(rectinrect(w->sprr, w->inner))
+		return;
+	r = w->inner;
+	dx = Dx(r) - SCRTSIZ;
+	dy = Dy(r) - SCRTSIZ;
+	if(dx <= 0 || dy <= 0)
+		return;
+	s = r;
+	if(!rectclip(&s, w->sprr))
+		return;
+	draw(w->im, Rect(r.min.x, r.max.y - SCRBSIZ, r.max.x - SCRTSIZ, r.max.y), w->tab->cols[BORD], nil, ZP);
+	draw(w->im, Rect(r.max.x - SCRBSIZ, r.min.y, r.max.x, r.max.y - SCRTSIZ), w->tab->cols[BORD], nil, ZP);
+	t0 = (s.min.x - w->sprr.min.x) * dx / Dx(w->sprr) + r.min.x;
+	t1 = (s.max.x - w->sprr.min.x) * dx / Dx(w->sprr) + r.min.x;
+	draw(w->im, Rect(t0, r.max.y - SCRBSIZ + 1, t1, r.max.y), w->tab->cols[BACK], nil, ZP);
+	t0 = (s.min.y - w->sprr.min.y) * dy / Dy(w->sprr) + r.min.y;
+	t1 = (s.max.y - w->sprr.min.y) * dy / Dy(w->sprr) + r.min.y;
+	draw(w->im, Rect(r.max.x - SCRBSIZ, t0, r.max.x, t1), w->tab->cols[BACK], nil, ZP);
+}
+
+void
+sprdraw(Win *w)
+{
+	Rectangle r, t;
+	Spr *s;
+	Pal *p;
+	int i, j;
+	Image *im;
+	u32int *d;
+
+	if(w->type != SPR || w->f == nil)
+		sysfatal("sprdraw: phase error");
+	s = (Spr *) w->f;
+	p = s->pal;
+	draw(w->im, w->inner, w->tab->cols[BACK], nil, ZP);
+	r = sprrect(w, w->inner);
+	w->sprr = r;
+	if(!rectinrect(r, w->inner)){
+		t = w->inner;
+		t.max.x -= SCRTSIZ;
+		t.max.y -= SCRTSIZ;
+		r = sprrect(w, t);
+		w->sprr = r;
+		rectclip(&r, t);
+		scrollbars(w);
+	}
+	d = s->data;
+	for(j = 0; j < s->h; j++)
+		for(i = 0; i < s->w; i++, d++){
+			t.min = addpt(w->sprr.min, Pt(i * w->zoom, j * w->zoom));
+			t.max = addpt(t.min, Pt(w->zoom, w->zoom));
+			if(!rectclip(&t, r))
+				continue;
+			if(p != nil && *d < p->ncol)
+				im = p->ims[*d];
+			else
+				im = invcol;
+			draw(w->im, t, im, nil, ZP);
+		}
+}
+
+static int
+sprbars(Win *w, Mousectl *mc)
+{
+	int d;
+
+	if(rectinrect(w->sprr, w->inner))
+		return -1;
+	if(mc->xy.x >= w->inner.max.x - SCRBSIZ){
+		d = Dy(w->inner) / 5;
+		switch(mc->buttons){
+		case 1: w->scr.y += d; break;
+		case 4: w->scr.y -= d; break;
+		default: return 0;
+		}
+		sprdraw(w);
+		return 0;
+	}
+	if(mc->xy.y >= w->inner.max.y - SCRBSIZ){
+		d = Dx(w->inner) / 5;
+		switch(mc->buttons){
+		case 1: w->scr.x += d; break;
+		case 4: w->scr.x -= d; break;
+		default: return 0;
+		}
+		sprdraw(w);
+		return 0;
+	}
+	return -1;
+}
+
+void
+sprclick(Win *w, Mousectl *mc)
+{
+	Spr *s;
+	Pal *p;
+	Point q;
+	
+	if(w->f == nil)
+		sysfatal("sprclick: phase error");
+	if(sprbars(w, mc) >= 0)
+		return;
+	s = (Spr *) w->f;
+	p = s->pal;
+	if(p == nil || p->sel < 0 || p->sel >= p->ncol)
+		return;
+	do{
+		q = divpt(subpt(mc->xy, w->sprr.min), w->zoom);
+		if(q.x < 0 || q.y < 0 || q.x >= s->w || q.y >= s->h)
+			continue;
+		if(s->data[q.y * s->w + q.x] != p->sel){
+			s->data[q.y * s->w + q.x] = p->sel;
+			s->change = 1;
+			quitok = 0;
+			sprdraw(w);
+		}
+	}while(readmouse(mc) >= 0 && (mc->buttons & 1) != 0);
+}
+
+void
+sprsize(Spr *s, int n, int m)
+{
+	u32int *v;
+	int i, j, w, h;
+	
+	v = s->data;
+	if(s->w == n && s->h == m)
+		return;
+	s->data = emalloc(n * m * sizeof(u32int));
+	w = n < s->w ? n : s->w;
+	h = m < s->h ? m : s->h;
+	for(j = 0; j < h; j++)
+		for(i = 0; i < w; i++)
+			s->data[j * n + i] = v[j * w + i];
+	s->w = n;
+	s->h = m;
+	s->change = 1;
+	quitok = 0;
+	filredraw(s);
+}
+
+static char *
+palfile(char *, char *n)
+{
+	return strdup(n);
+}
+
+static void
+sprmenu(Win *w, Mousectl *mc)
+{
+	enum { MPAL };
+	static char *menus[] = {
+		"pal",
+		nil,
+	};
+	static Menu menu = {menus};
+	Win *wp;
+	Spr *s;
+	
+	if(w->f == nil)
+		sysfatal("sprmenu: phase error");
+	s = (Spr *) w->f;
+	switch(menuhit(2, mc, &menu, scr)){
+	case MPAL:
+		wp = winsel(mc, 2);
+		if(wp == nil || wp->type != PAL)
+			break;
+		if(wp->f == nil)
+			sysfatal("sprmenu: pal phase error");
+		if(s->pal != (Pal *) wp->f){
+			if(s->pal != nil && decref(s->pal) == 0 && s->pal->change <= 0)
+				putfil(s->pal);
+			incref(wp->f);
+			s->pal = (Pal *) wp->f;
+			free(s->palfile);
+			s->palfile = palfile(s->name, s->pal->name);
+			s->change = 1;
+			quitok = 0;
+			filredraw(s);
+		}
+		break;
+	}
+}
+
+static void
+sprzerox(Win *w, Win *v)
+{
+	v->zoom = w->zoom;
+	v->scr = w->scr;
+}
+
+static void
+sprkey(Win *w, Rune r)
+{
+	static char keys[] = "1234567890qwertyuiop";
+	char *p;
+	Spr *s;
+	
+	s = (Spr *) w->f;
+	if(s == nil)
+		sysfatal("sprkey: phase error");
+	if(r < 0x100 && (p = strchr(keys, r)) != nil){
+		if(s->pal == nil || p - keys >= s->pal->ncol)
+			return;
+		s->pal->sel = p - keys;
+		filredraw(s->pal);
+	}
+}
+
+Wintab sprtab = {
+	.init = sprinit,
+	.click = sprclick,
+	.draw = sprdraw,
+	.menu = sprmenu,
+	.rmb = sprbars,
+	.zerox = sprzerox,
+	.key = sprkey,
+	.hexcols = {
+		[BORD] 0x00AA00FF,
+		[DISB] 0x88CC88FF,
+		[BACK] 0xCCFFCCFF,
+	},
+};
--- /dev/null
+++ b/sys/src/cmd/spred/spred.c
@@ -1,0 +1,210 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include <draw.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <frame.h>
+#include <cursor.h>
+#include "dat.h"
+#include "fns.h"
+
+Mousectl *mc;
+Keyboardctl *kc;
+int quitok;
+
+enum {
+	ZEROX,
+	RESIZE,
+	CLOSE,
+	WRITE,
+	QUIT,
+	WIN
+};
+
+int 
+quit(void)
+{
+	File *f;
+	
+	if(!quitok)
+		for(f = flist.next; f != &flist; f = f->next)
+			if(f->change > 0){
+				cmdprint("?\n");
+				quitok = 1;
+				return 0;
+			}
+	return -1;
+}
+
+static char *
+menugen(int n)
+{
+	File *f;
+	static int mw;
+	static char buf[512];
+	int rc;
+	char *p;
+
+	switch(n){
+	case ZEROX: return "zerox";
+	case CLOSE: return "close";
+	case RESIZE: return "resize";
+	case WRITE: return "write";
+	case QUIT: return "quit";
+	}
+	if(n < WIN)
+		sysfatal("menugen: no string for n=%d", n);
+	n -= WIN;
+	if(n == 0){
+		mw = 0;
+		for(f = flist.next; f != &flist; f = f->next){
+			rc = filtitlelen(f);
+			if(rc > mw)
+				mw = rc;
+		}
+		return "~~spred~~";
+	}
+	for(f = flist.next; f != &flist; f = f->next)
+		if(--n == 0){
+			p = filtitle(f, buf, buf + sizeof(buf));
+			rc = mw - utflen(buf);
+			if(p + rc >= buf + sizeof(buf))
+				rc = buf + sizeof(buf) - p - 1;
+			memset(p, ' ', rc);
+			p[rc] = 0;
+			return buf;
+		}
+	return nil;
+	
+}
+
+static int
+rmb(void)
+{
+	static Menu menu = {nil, menugen};
+	int n;
+	Win *w;
+	File *f;
+
+	if(actw != nil && actw->tab->rmb != nil && actw->tab->rmb(actw, mc) >= 0)
+		return 0;
+	n = menuhit(3, mc, &menu, nil);
+	if(n < 0)
+		return 0;
+	switch(n){
+	case ZEROX:
+		w = winsel(mc, 3);
+		if(w != nil)
+			winzerox(w, mc);
+		return 0;
+	case CLOSE:
+		w = winsel(mc, 3);
+		if(w != nil)
+			winclose(w);
+		return 0;
+	case RESIZE:
+		winresize(winsel(mc, 3), mc);
+		return 0;
+	case WRITE:
+		w = winsel(mc, 3);
+		if(w != nil)
+			winwrite(w, nil);
+		return 0;
+	case QUIT:
+		return quit();
+	}
+	if(n < WIN)
+		sysfatal("rmb: no action for n=%d", n);
+	if(n == 0){
+		setfocus(cmdw);
+		return 0;
+	}
+	n -= WIN;
+	for(f = flist.next; f != &flist; f = f->next)
+		if(--n == 0){
+			if(f->wins.wnext == &f->wins){
+				newwinsel(f->type, mc, f);
+				return 0;
+			}
+			for(w = f->wins.wnext; w != &f->wins && w != actw; w = w->wnext)
+				;
+			if(w->wnext == &f->wins)
+				w = w->wnext;
+			setfocus(w->wnext);
+			return 0;
+		}
+	return 0;
+}
+
+static void
+loop(void)
+{
+	Rune r;
+	int n;
+
+	Alt a[] = {
+		{mc->c, &mc->Mouse, CHANRCV},
+		{kc->c, &r, CHANRCV},
+		{mc->resizec, &n, CHANRCV},
+		{nil, nil, CHANEND}
+	};
+	
+	for(;;){
+		flushimage(display, 1);
+		switch(alt(a)){
+		case 0:
+			if((mc->buttons & 1) != 0)
+				winclick(mc);
+			if((mc->buttons & 2) != 0)
+				if(actw != nil && actw->tab->menu != nil)
+					actw->tab->menu(actw, mc);
+			if((mc->buttons & 4) != 0)
+				if(rmb() < 0)
+					return;
+			break;
+		case 1:
+			if(actw != nil && actw->tab->key != nil)
+				actw->tab->key(actw, r);
+			break;
+		case 2:
+			resize();
+			break;
+		}
+	}
+}
+
+void
+threadmain(int argc, char **argv)
+{
+	ARGBEGIN {
+	default:
+		;
+	} ARGEND;
+	
+	quotefmtinstall();
+	if(initdraw(nil, nil, nil) < 0)
+		sysfatal("initdraw: %r");
+	initwin();
+	mc = initmouse(nil, screen);
+	if(mc == nil)
+		sysfatal("initmouse: %r");
+	kc = initkeyboard(nil);
+	if(kc == nil)
+		sysfatal("initkeyboard: %r");
+	loop();
+	threadexitsall(nil);
+}
+
+Cursor crosscursor = {
+	{-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, }
+};
--- /dev/null
+++ b/sys/src/cmd/spred/win.c
@@ -1,0 +1,281 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include <draw.h>
+#include <mouse.h>
+#include <cursor.h>
+#include <frame.h>
+#include "dat.h"
+#include "fns.h"
+
+Screen *scr;
+extern Wintab *tabs[];
+Win wlist;
+File flist;
+Win *actw, *actf, *cmdw;
+Image *invcol;
+
+void*
+emalloc(ulong sz)
+{
+	void *v;
+	
+	v = malloc(sz);
+	if(v == nil)
+		sysfatal("malloc: %r");
+	memset(v, 0, sz);
+	setmalloctag(v, getcallerpc(&sz));
+	return v;
+}
+
+void
+initwin(void)
+{
+	Rectangle r;
+	int i, j;
+
+	scr = allocscreen(screen, display->white, 0);
+	if(scr == nil)
+		sysfatal("allocscreen: %r");
+	for(i = 0; i < NTYPES; i++)
+		for(j = 0; j < NCOLS; j++)
+			tabs[i]->cols[j] = allocimage(display, Rect(0, 0, 1, 1), screen->chan, 1, tabs[i]->hexcols[j]);
+	invcol = allocimage(display, Rect(0, 0, 2, 2), screen->chan, 1, 0);
+	draw(invcol, Rect(1, 0, 2, 1), display->white, nil, ZP);
+	draw(invcol, Rect(0, 1, 1, 2), display->white, nil, ZP);
+	wlist.next = wlist.prev = &wlist;
+	flist.next = flist.prev = &flist;
+	r = screen->r;
+	r.max.y = r.min.y + Dy(r) / 5;
+	cmdw = newwin(CMD, r, nil);
+	if(cmdw == nil)
+		sysfatal("newwin: %r");
+}
+
+Win *
+newwin(int t, Rectangle r, File *f)
+{
+	Win *w;
+	
+	w = emalloc(sizeof(*w));
+	w->next = &wlist;
+	w->prev = wlist.prev;
+	w->next->prev = w;
+	w->prev->next = w;
+	w->type = t;
+	w->tab = tabs[t];
+	w->entire = r;
+	w->inner = insetrect(r, BORDSIZ);
+	w->im = allocwindow(scr, r, Refbackup, 0);
+	draw(w->im, w->inner, w->tab->cols[BACK], nil, ZP);
+	if(f != nil){
+		incref(f);
+		w->wprev = f->wins.wprev;
+		w->wnext = &f->wins;
+		f->wins.wprev->wnext = w;
+		f->wins.wprev = w;
+		w->f = f;
+	}
+	w->tab->init(w);
+	setfocus(w);
+	w->tab->draw(w);
+	return w;
+}
+
+Win *
+newwinsel(int t, Mousectl *mc, File *f)
+{
+	Rectangle u;
+
+	u = getrect(3, mc);
+	if(Dx(u) < MINSIZ || Dy(u) < MINSIZ)
+		return nil;
+	rectclip(&u, screen->r);
+	return newwin(t, u, f);
+}
+
+void
+winzerox(Win *w, Mousectl *mc)
+{
+	Win *v;
+
+	if(w->tab->zerox == nil){
+		cmdprint("?\n");
+		return;
+	}
+	v = newwinsel(w->type, mc, w->f);
+	if(v == nil)
+		return;
+	w->tab->zerox(w, v);
+	v->tab->draw(v);
+}
+
+void
+winclose(Win *w)
+{
+	if(w->f == nil){
+		cmdprint("?\n");
+		return;
+	}
+	if(!decref(w->f)){
+		if(w->f->change > 0){
+			cmdprint("?\n");
+			incref(w->f);
+			w->f->change = -1;
+			return;
+		}
+		putfil(w->f);
+		w->f = nil;
+	}
+	freeimage(w->im);
+	if(w->f != nil){
+		w->wnext->wprev = w->wprev;
+		w->wprev->wnext = w->wnext;
+	}
+	w->next->prev = w->prev;
+	w->prev->next = w->next;
+	if(w == actw)
+		actw = nil;
+	if(w == actf)
+		actf = nil;
+	free(w);
+}
+
+void
+setfocus(Win *w)
+{
+	if(actw != nil)
+		border(actw->im, actw->entire, BORDSIZ, actw->tab->cols[DISB], ZP);
+	actw = w;
+	if(w != cmdw)
+		actf = w;
+	if(w == nil)
+		return;
+	if(w->im == nil)
+		sysfatal("setfocus: phase error");
+	topwindow(w->im);
+	w->prev->next = w->next;
+	w->next->prev = w->prev;
+	w->prev = wlist.prev;
+	w->next = &wlist;
+	w->prev->next = w;
+	w->next->prev = w;
+	border(w->im, w->entire, BORDSIZ, w->tab->cols[BORD], ZP);
+}
+
+static Win *
+winpoint(Point p)
+{
+	Win *w;
+	
+	for(w = wlist.prev; w != &wlist; w = w->prev)
+		if(ptinrect(p, w->entire))
+			return w;
+	return nil;
+}
+
+void
+winclick(Mousectl *mc)
+{
+	Win *w;
+	
+	w = winpoint(mc->xy);
+	if(w != nil){
+		if(w != actw)
+			setfocus(w);
+		w->tab->click(w, mc);
+	}
+	while((mc->buttons & 1) != 0)
+		readmouse(mc);
+}
+
+Win *
+winsel(Mousectl *mc, int but)
+{
+	extern Cursor crosscursor;
+	int m;
+	Win *w;
+	
+	m = 1 << but - 1;
+	setcursor(mc, &crosscursor);
+	for(;;){
+		readmouse(mc);
+		if((mc->buttons & ~m) != 0){
+			w = nil;
+			goto end;
+		}
+		if((mc->buttons & m) != 0)
+			break;
+	}
+	w = winpoint(mc->xy);
+end:
+	while(readmouse(mc), mc->buttons != 0)
+		;
+	setcursor(mc, nil);
+	return w;
+}
+
+void
+winresize(Win *w, Mousectl *mc)
+{
+	Rectangle r;
+	
+	if(w == nil)
+		return;
+	r = getrect(3, mc);
+	if(Dx(r) < MINSIZ || Dy(r) < MINSIZ)
+		return;
+	rectclip(&r, screen->r);
+	freeimage(w->im);
+	w->entire = r;
+	w->inner = insetrect(r, BORDSIZ);
+	w->im = allocwindow(scr, r, Refbackup, 0);
+	draw(w->im, w->inner, w->tab->cols[BACK], nil, ZP);
+	setfocus(w);
+	w->tab->draw(w);
+}
+
+void
+resize(void)
+{
+	Rectangle old, r;
+	int dxo, dyo, dxn, dyn;
+	Win *w;
+	
+	old = screen->r;
+	dxo = Dx(old);
+	dyo = Dy(old);
+	if(getwindow(display, Refnone) < 0)
+		sysfatal("resize failed: %r");
+	dxn = Dx(screen->r);
+	dyn = Dy(screen->r);
+	freescreen(scr);
+	scr = allocscreen(screen, display->white, 0);
+	if(scr == nil)
+		sysfatal("allocscreen: %r");
+	for(w = wlist.next; w != &wlist; w = w->next){
+		r = rectsubpt(w->entire, old.min);
+		r.min.x = muldiv(r.min.x, dxn, dxo);
+		r.max.x = muldiv(r.max.x, dxn, dxo);
+		r.min.y = muldiv(r.min.y, dyn, dyo);
+		r.max.y = muldiv(r.max.y, dyn, dyo);
+		w->entire = rectaddpt(r, screen->r.min);
+		w->inner = insetrect(w->entire, BORDSIZ);
+		freeimage(w->im);
+		w->im = allocwindow(scr, w->entire, Refbackup, 0);
+		if(w->im == nil)
+			sysfatal("allocwindow: %r");
+		draw(w->im, w->inner, w->tab->cols[BACK], nil, ZP);
+		border(w->im, w->entire, BORDSIZ, w->tab->cols[w == actw ? BORD : DISB], ZP);
+		w->tab->draw(w);
+	}
+}
+
+extern Wintab cmdtab, paltab, sprtab;
+
+Wintab *tabs[] = {
+	[CMD] &cmdtab,
+	[PAL] &paltab,
+	[SPR] &sprtab,
+};