shithub: zuke

ref: b185cb042f1cc752ebec2a23e9d462f9a20fd204
dir: /zuke.c/

View raw version
#include "theme.c"
#include <mouse.h>
#include <keyboard.h>
#include <ctype.h>
#include "plist.h"

#define MAX(a,b) ((a)>=(b)?(a):(b))
#define MIN(a,b) ((a)<=(b)?(a):(b))
#define CLAMP(x,min,max) MAX(min, MIN(max, x))

typedef struct Player Player;

enum
{
	Cstart = 1,
	Cstop,
	Ctoggle,
	Cseekrel,

	Everror = 1,
	Evready,

	Seek = 10, /* 10 seconds */
	Seekfast = 60, /* a minute */

	Scrollwidth = 14,
	Scrollheight = 16,

	Bps = 44100*2*2, /* 44100KHz, stereo, s16 for a sample */
	Relbufsz = Bps/5, /* 0.2 second */
};

struct Player
{
	Channel *ctl;
	Channel *ev;
	Channel *img;
	double seek;
	int pcur;
};

int mainstacksize = 32768;

static int audio;
static Meta *pl;
static int plnum;
static char *plraw;
static int plrawsize;
static int volume;
static Player *playernext;
static Player *playercurr;
static vlong byteswritten;
static int pcur, pcurplaying;
static int scroll, scrollsz;
static Font *f;
static Image *cover;
static Channel *ev;
static Mousectl *mctl;
static Keyboardctl *kctl;
static int colwidth[7];
static int mincolwidth[7];
static char *cols = "AatD";
static int *shuffle;
static Rectangle seekbar;
static int seekmx, newseekmx;
static double seekoff; /* ms */
static char *covers[] = {"folder", "cover", "Cover", "scans/CD", "Scans/Front", "Covers/Front"};

static char *menu3i[] = {
	"theme",
	"exit",
	nil
};

static Menu menu3 = {
	.item = menu3i,
};

#pragma varargck type "P" int
static int
positionfmt(Fmt *f)
{
	char *s, tmp[16];
	u64int sec;

	s = tmp;
	sec = va_arg(f->args, int);
	if(sec >= 3600){
		s = seprint(s, tmp+sizeof(tmp), "%02lld:", sec/3600);
		sec %= 3600;
	}
	s = seprint(s, tmp+sizeof(tmp), "%02lld:", sec/60);
	sec %= 60;
	seprint(s, tmp+sizeof(tmp), "%02lld", sec);

	return fmtstrcpy(f, tmp);
}

static char *
getcol(Meta *m, int c)
{
	static char tmp[32];

	switch(c){
	case Palbum: return m->album;
	case Partist: return m->artist[0];
	case Pdate: return m->date;
	case Pduration: snprint(tmp, sizeof(tmp), "%8P", m->duration/1000); return tmp;
	case Ptitle: return m->title;
	case Ptrack: snprint(tmp, sizeof(tmp), "%4s", m->track); return m->track ? tmp : nil;
	case Ppath: return m->path;
	default: sysfatal("invalid column '%c'", c);
	}

	return nil;
}

static void
adjustcolumns(void)
{
	int i, n, x, total, width;

	if(mincolwidth[0] == 0){
		for(i = 0; cols[i] != 0; i++)
			mincolwidth[i] = 1;
		for(n = 0; n < plnum; n++){
			for(i = 0; cols[i] != 0; i++){
				if((x = stringwidth(f, getcol(&pl[n], cols[i]))) > mincolwidth[i])
					mincolwidth[i] = x;
			}
		}
	}

	total = 0;
	n = 0;
	width = Dx(screen->r);
	for(i = 0; cols[i] != 0; i++){
		if(cols[i] == Pduration || cols[i] == Pdate || cols[i] == Ptrack)
			width -= mincolwidth[i] + 8;
		else{
			total += mincolwidth[i];
			n++;
		}
	}
	for(i = 0; cols[i] != 0; i++){
		if(cols[i] == Pduration || cols[i] == Pdate || cols[i] == Ptrack)
			colwidth[i] = mincolwidth[i];
		else
			colwidth[i] = (width - Scrollwidth - n*8) * mincolwidth[i] / total;
	}
}

static Meta *
getmeta(int i)
{
	return &pl[shuffle != nil ? shuffle[i] : i];
}

static void
redraw(int full)
{
	Image *col;
	Point p, sp;
	Rectangle sel, r;
	int i, j, left, right, scrollcenter;
	char tmp[32];

	lockdisplay(display);
	scrollsz = (Dy(screen->r) - f->height - 4) / f->height - 1;
	left = screen->r.min.x;
	if(scrollsz < plnum) /* adjust for scrollbar */
		left += Scrollwidth + 1;

	if(full){
		draw(screen, screen->r, colors[Dback].im, nil, ZP);

		adjustcolumns();
		if(scrollsz < plnum){ /* scrollbar */
			p.x = sp.x = screen->r.min.x + Scrollwidth;
			p.y = screen->r.min.y;
			sp.y = screen->r.max.y;
			line(screen, p, sp, Endsquare, Endsquare, 0, colors[Dfmed].im, ZP);

			r = screen->r;
			r.max.x = r.min.x + Scrollwidth - 1;
			r.min.x += 1;
			if(scroll < 1)
				scrollcenter = 0;
			else
				scrollcenter = (Dy(screen->r)-Scrollheight*5/4)*scroll / (plnum - scrollsz);
			r.min.y += scrollcenter + Scrollheight/4;
			r.max.y = r.min.y + Scrollheight;
			draw(screen, r, colors[Dfmed].im, nil, ZP);
		}

		p.x = sp.x = left;
		p.y = 0;
		sp.y = screen->r.max.y;
		for(i = 0; cols[i+1] != 0; i++){
			p.x += colwidth[i] + 4;
			sp.x = p.x;
			line(screen, p, sp, Endsquare, Endsquare, 0, colors[Dfmed].im, ZP);
			p.x += 4;
		}

		sp.x = sp.y = 0;
		p.x = left + 2;
		p.y = screen->r.min.y + 2;

		for(i = scroll; i < plnum; i++, p.y += f->height){
			if(i < 0)
				continue;
			if(p.y > screen->r.max.y)
				break;

			if(pcur == i){
				sel.min.x = left;
				sel.min.y = p.y;
				sel.max.x = screen->r.max.x;
				sel.max.y = p.y + f->height;
				draw(screen, sel, colors[Dbinv].im, nil, ZP);
				col = colors[Dfinv].im;
			}else{
				col = colors[pcurplaying == i ? Dfhigh : Dfmed].im;
			}

			sel = screen->r;

			p.x = left + 2 + 3;
			for(j = 0; cols[j] != 0; j++){
				sel.max.x = p.x + colwidth[j];
				replclipr(screen, 0, sel);
				string(screen, p, col, sp, f, getcol(getmeta(i), cols[j]));
				p.x += colwidth[j] + 8;
			}
			replclipr(screen, 0, screen->r);

			if(pcurplaying == i){
				Point rightp, leftp;
				leftp.y = rightp.y = p.y - 1;
				leftp.x = left;
				rightp.x = screen->r.max.x;
				line(screen, leftp, rightp, 0, 0, 0, colors[Dfmed].im, sp);
				leftp.y = rightp.y = p.y + f->height;
				line(screen, leftp, rightp, 0, 0, 0, colors[Dfmed].im, sp);
			}
		}
	}

	vlong msec = 0;
	if(pcurplaying >= 0){
		msec = byteswritten*1000/Bps;
		if(getmeta(pcurplaying)->duration != 0){
			msec = MIN(msec, getmeta(pcurplaying)->duration);
			snprint(tmp, sizeof(tmp), "%s%P/%P %d%%",
				shuffle != nil ? "∫ " : "",
				(int)(newseekmx >= 0 ? seekoff : msec)/1000,
				getmeta(pcurplaying)->duration/1000,
				volume);
			seekmx = newseekmx;
		}else{
			snprint(tmp, sizeof(tmp), "%s%P %d%%",
				shuffle != nil ? "∫ " : "",
				(int)msec/1000,
				volume);
		}
	}else
		snprint(tmp, sizeof(tmp), "%s%d%%", shuffle != nil ? "∫ " : "", volume);
	r = screen->r;
	right = r.max.x - stringwidth(f, tmp) - 4;
	r.min.x = left;
	r.min.y = r.max.y - f->height - 4;
	if(pcurplaying < 0 || getmeta(pcurplaying)->duration == 0)
		r.min.x = right;
	draw(screen, r, colors[Dblow].im, nil, ZP);
	r.max.x = right;
	p = addpt(Pt(right, r.min.y), Pt(2, 2));
	string(screen, p, colors[Dfhigh].im, sp, f, tmp);
	sel = r;

	if(cover != nil && full){
		r.max.x = r.min.x;
		r.min.x = screen->r.max.x - cover->r.max.x - 8;
		draw(screen, r, colors[Dblow].im, nil, ZP);
		r = screen->r;
		r.min.x = r.max.x - cover->r.max.x - 8;
		r.min.y = r.max.y - cover->r.max.y - 8 - f->height - 4;
		r.max.y = r.min.y + cover->r.max.y + 8;
		draw(screen, r, colors[Dblow].im, nil, ZP);
		draw(screen, insetrect(r, 4), cover, nil, ZP);
	}

	/* seek bar */
	seekbar = ZR;
	if(pcurplaying >= 0 && getmeta(pcurplaying)->duration != 0){
		r = insetrect(sel, 3);
		draw(screen, r, colors[Dflow].im, nil, ZP);
		seekbar = r;
		r.max.x = r.min.x + Dx(r) * (double)msec / (double)getmeta(pcurplaying)->duration;
		draw(screen, r, colors[Dbinv].im, nil, ZP);
	}

	flushimage(display, 1);
	unlockdisplay(display);
}

void
themechanged(void)
{
	redraw(1);
}

static void
coverload(void *player_)
{
	int p[2], pid, fd, i;
	char *prog, *path, *s, tmp[32];
	Meta *m;
	Channel *ch;
	Player *player;
	Image *newcover;

	threadsetname("cover");
	player = player_;
	m = getmeta(player->pcur);
	pid = -1;
	ch = player->img;
	fd = -1;
	prog = nil;

	if(m->imagefmt != nil && m->imagereader == 0){
		if(strcmp(m->imagefmt, "image/png") == 0)
			prog = "png";
		else if(strcmp(m->imagefmt, "image/jpeg") == 0)
			prog = "jpg";
	}

	if(prog == nil){
		path = strdup(m->path);
		if(path != nil && (s = utfrrune(path, '/')) != nil){
			*s = 0;

			for(i = 0; i < nelem(covers) && prog == nil; i++){
				if((s = smprint("%s/%s.jpg", path, covers[i])) != nil && (fd = open(s, OREAD)) >= 0)
					prog = "jpg";
				free(s);
				s = nil;
				if(fd < 0 && (s = smprint("%s/%s.png", path, covers[i])) != nil && (fd = open(s, OREAD)) >= 0)
					prog = "png";
				free(s);
			}
		}
		free(path);
	}

	if(prog == nil)
		goto done;

	if(fd < 0){
		fd = open(m->path, OREAD);
		seek(fd, m->imageoffset, 0);
	}
	pipe(p);
	if((pid = rfork(RFPROC|RFFDG|RFREND|RFNOTEG)) == 0){
		dup(fd, 0); close(fd);
		dup(p[1], 1); close(p[1]);
		dup(open("/dev/null", OWRITE), 2);
		snprint(tmp, sizeof(tmp), "%s -9t | resample -x128", prog);
		execl("/bin/rc", "rc", "-c", tmp, nil);
		sysfatal("execl: %r");
	}
	close(fd);
	close(p[1]);

	if(pid > 0){
		newcover = readimage(display, p[0], 1);
		sendp(ch, newcover);
	}
	close(p[0]);
done:
	if(pid < 0)
		sendp(ch, nil);
	chanclose(ch);
	chanfree(ch);
	if(pid >= 0)
		postnote(PNGROUP, pid, "interrupt");
	threadexits(nil);
}

static int
playerret(Player *player)
{
	return recvul(player->ev) == Everror ? -1 : 0;
}

static void
stop(Player *player)
{
	if(player == nil)
		return;

	if(player == playernext)
		playernext = nil;
	sendul(player->ctl, Cstop);
}

static void playerthread(void *player_);

static Player *
newplayer(int pcur, int loadnext)
{
	Player *player;

	if(playernext != nil && loadnext){
		if(pcur == playernext->pcur){
			player = playernext;
			playernext = nil;
			goto done;
		}
		stop(playernext);
		playernext = nil;
	}

	player = mallocz(sizeof(*player), 1);
	player->ctl = chancreate(sizeof(ulong), 0);
	player->ev = chancreate(sizeof(ulong), 0);
	player->pcur = pcur;

	threadcreate(playerthread, player, 4096);
	if(playerret(player) < 0)
		return nil;

done:
	if(pcur < plnum-1 && playernext == nil && loadnext)
		playernext = newplayer(pcur+1, 0);

	return player;
}

static int
start(Player *player)
{
	if(player != nil)
		sendul(player->ctl, Cstart);
	return -1;
}

static void
playerthread(void *player_)
{
	char *buf, cmd[64], seekpos[12], *fmt;
	Player *player;
	Ioproc *io;
	Image *thiscover;
	ulong c;
	int p[2], fd, pid, noinit, trycoverload;
	long n, r;
	vlong boffset, boffsetlast;
	Meta *cur;

	threadsetname("player");
	player = player_;
	noinit = 0;
	boffset = 0;
	buf = nil;
	trycoverload = 1;
	io = nil;
	pid = -1;

restart:
	cur = getmeta(player->pcur);
	fmt = cur->filefmt;
	fd = -1;
	if(*fmt && (fd = open(cur->path, OREAD)) < 0){
		fprint(2, "%r\n");
		sendul(player->ev, Everror);
		goto freeplayer;
	}

	pipe(p);
	if((pid = rfork(RFPROC|RFFDG|RFREND|RFNOTEG)) == 0){
		if(fd < 0)
			fd = open("/dev/null", OREAD);
		dup(fd, 0); close(fd);
		dup(p[0], 1); close(p[0]);
		dup(fd = open("/dev/null", OWRITE), 2); close(fd);
		close(p[1]);
		if(*fmt){
			snprint(cmd, sizeof(cmd), "/bin/audio/%sdec", fmt);
			snprint(seekpos, sizeof(seekpos), "%g", (double)boffset/Bps);
			execl(cmd, cmd, "-s", seekpos, nil);
		}else{
			execl("/bin/play", "play", "-o", "/fd/1", cur->path, nil);
		}
		sysfatal("execl: %r");
	}
	if(pid < 0)
		sysfatal("rfork: %r");
	if(fd >= 0)
		close(fd);
	close(p[0]);

	c = 0;
	if(!noinit){
		sendul(player->ev, Evready);
		buf = malloc(Relbufsz);
		if((io = ioproc()) == nil)
			sysfatal("player: %r");
		if((n = ioreadn(io, p[1], buf, Relbufsz)) < 0)
			fprint(2, "player: %r\n");
		if(recv(player->ctl, &c) < 0 || c != Cstart)
			goto freeplayer;
		if(n < 1)
			goto next;
		boffset = iowrite(io, audio, buf, n);
		noinit = 1;
	}

	byteswritten = boffsetlast = boffset;
	pcurplaying = player->pcur;
	if(c != Cseekrel)
		redraw(1);

	while(1){
		n = ioreadn(io, p[1], buf, Relbufsz);
		if(n <= 0)
			break;

		thiscover = nil;
		if(player->img != nil && nbrecv(player->img, &thiscover) != 0){
			freeimage(cover);
			cover = thiscover;
			redraw(1);
			player->img = nil;
		}
		r = nbrecv(player->ctl, &c);
		if(r < 0){
			goto stop;
		}else if(r != 0){
			if(c == Ctoggle){
				if(recv(player->ctl, &c) < 0 || c == Cstop)
					goto stop;
			}else if(c == Cseekrel){
				boffset = MAX(0, boffset + player->seek*Bps);
				n = 0;
				break;
			}else if(c == Cstop){
				goto stop;
			}
		}

		boffset += n;
		byteswritten = boffset;
		if(iowrite(io, audio, buf, n) != n)
			fprint(2, "player: %r\n");
		if(trycoverload){
			trycoverload = 0;
			player->img = chancreate(sizeof(Image*), 0);
			proccreate(coverload, player, 4096);
		}
		if(labs(boffset/Relbufsz - boffsetlast/Relbufsz) > 0){
			boffsetlast = boffset;
			redraw(0);
		}
	}

	if(n < 1){ /* seeking backwards or end of the song */
		close(p[1]);
		p[1] = -1;
		if(c != Cseekrel || boffset >= getmeta(pcurplaying)->duration/1000*Bps){
next:
			playercurr = nil;
			playercurr = newplayer((player->pcur+1) % plnum, 1);
			start(playercurr);
			goto stop;
		}
		goto restart;
	}

stop:
	if(player->img != nil)
		freeimage(recvp(player->img));
freeplayer:
	chanfree(player->ctl);
	chanfree(player->ev);
	if(pid >= 0)
		postnote(PNGROUP, pid, "interrupt");
	closeioproc(io);
	if(p[1] >= 0)
		close(p[1]);
	if(player == playercurr)
		playercurr = nil;
	if(player == playernext)
		playernext = nil;
	free(buf);
	free(player);
	threadexits(nil);
}

static void
toggle(Player *player)
{
	if(player != nil)
		sendul(player->ctl, Ctoggle);
}

static void
seekrel(Player *player, double off)
{
	if(player != nil){
		player->seek = off;
		sendul(player->ctl, Cseekrel);
	}
}

static void
readplist(void)
{
	Meta *m;
	char *s, *e, *endrec;
	int i, n, sz, alloc, tagsz, intval;

	s = nil;
	for(alloc = sz = 0;;){
		alloc += 65536;
		if((s = realloc(s, alloc)) == nil)
			sysfatal("no memory");
		for(n = 0; sz < alloc; sz += n){
			n = readn(0, s+sz, alloc-sz);
			if(n < 0)
				sysfatal("%r");
			if(n == 0)
				break;
		}
		if(n == 0)
			break;
	}

	plraw = s;
	plrawsize = sz;
	plraw[plrawsize-1] = 0;
	if(sz < 4 || s[0] != '#' || s[1] != ' ' || !isdigit(s[2]) || (s = memchr(plraw, '\n', sz)) == nil)
		sysfatal("invalid playlist");
	s++; /* at the start of the first record */

	plnum = atoi(plraw+2);
	pl = calloc(plnum, sizeof(Meta));
	for(i = 0; i < plnum; i++, s = endrec){
		if(plraw+plrawsize < s+10)
			sysfatal("truncated playlist");
		if(s[0] != '#' || s[1] != ' ' || !isdigit(s[2]))
			sysfatal("invalid record");
		if((n = strtol(s+2, &e, 10)) < 0 || n > plnum)
			sysfatal("invalid track index");
		if(pl[n].path != nil)
			sysfatal("duplicate track index");

		s[-1] = 0;
		sz = strtol(e, &s, 10);
		*s++ = 0; /* skip '\n' */
		if(s+sz > plraw+plrawsize)
			sysfatal("truncated playlist");
		s[sz-1] = 0; /* '\n'→'\0' to mark the end of the record */
		endrec = s+sz;
		m = &pl[n];

		for(;;){
			if(s[0] == Pimage){
				m->imageoffset = strtol(s+2, &e, 10);
				m->imagesize = strtol(e+1, &s, 10);
				m->imagereader = strtol(s+1, &e, 10);
				m->imagefmt = e + 1;
				s = strchr(e+2, '\n') + 1;
			}else if(s[0] == Pchannels || s[0] == Pduration || s[0] == Psamplerate){
				intval = strtol(s+2, &e, 10);
				if(s[0] == Pduration) m->duration = intval;
				s = e + 1;
			}else if(s[0] == Ppath){
				m->path = s+2;
				break; /* always the last one */
			}else if(s[0] == Pfilefmt){
				m->filefmt = s+2;
				s = strchr(s+2, '\n') + 1;
			}else{
				tagsz = strtol(s+1, &e, 10);
				if(e+tagsz >= plraw+plrawsize)
					sysfatal("truncated playlist");
				e++; /* point to tag value */
				e[tagsz] = 0; /* '\n'→'\0' to mark the end of the tag value */
				if(s[0] == Palbum) m->album = e;
				else if(s[0] == Partist && m->numartist < Maxartist) m->artist[m->numartist++] = e;
				else if(s[0] == Pdate) m->date = e;
				else if(s[0] == Ptitle) m->title = e;
				else if(s[0] == Pdate) m->date = e;
				else if(s[0] == Ptrack) m->track = e;
				else sysfatal("unknown tag type %c", s[0]);
				s = e + tagsz + 1;
			}
			s[-1] = 0;
		}
		if(m->filefmt == nil)
			sysfatal("old playlist format?\nplease re-run audio/mkplist");
	}
}

static void
search(char d)
{
	Meta *m;
	static char buf[64];
	static int sz;
	int inc, i, a;

	inc = (d == '/' || d == 'n') ? 1 : -1;
	if(d == '/' || d == '?')
		sz = enter(inc > 0 ? "forward:" : "backward:", buf, sizeof(buf), mctl, kctl, nil);
	if(sz < 1)
		return;

	for(i = pcur+inc; i >= 0 && i < plnum; i += inc){
		m = getmeta(i);
		for(a = 0; a < m->numartist; a++){
			if(cistrstr(m->artist[a], buf) != nil)
				break;
		}
		if(m->album != nil && cistrstr(m->album, buf) != nil)
			break;
		if(m->title != nil && cistrstr(m->title, buf) != nil)
			break;
		if(cistrstr(m->path, buf) != nil)
			break;
	}
	if(i >= 0 && i < plnum){
		pcur = i;
		redraw(1);
	}
}

static void
chvolume(int d)
{
	int f, l, r, ol, or;
	Biobuf b;
	char *s, *a[4];

	if((f = open("/dev/volume", ORDWR)) < 0)
		return;
	Binit(&b, f, OREAD);

	l = r = 0;
	for(; (s = Brdline(&b, '\n')) != nil;) {
		if(strncmp(s, "master", 6) == 0 && tokenize(s, a, 3) == 3){
			l = ol = atoi(a[1]);
			r = or = atoi(a[2]);
			for(;;){
				l += d;
				r += d;
				fprint(f, "master %d %d\n", l, r);
				Bseek(&b, 0, 0);
				for(; (s = Brdline(&b, '\n')) != nil;){
					if(strncmp(s, "master", 6) == 0 && tokenize(s, a, 3) == 3){
						if(atoi(a[1]) == l && atoi(a[2]) == r)
							goto end;
						if(atoi(a[1]) != ol && atoi(a[2]) != or)
							goto end;
						if (l < 0 || r < 0 || l > 100 || r > 100)
							goto end;
						break;
					}
				}
			}
		}
	}

end:
	volume = (l+r)/2;
	if(volume > 100)
		volume = 100;
	else if(volume < 0)
		volume = 0;

	Bterm(&b);
	close(f);
}

static void
toggleshuffle(void)
{
	int i, m, xi, a, c, pcurnew, pcurplayingnew;

	if(shuffle == nil){
		if(plnum < 2)
			return;

		m = plnum;
		if(plnum < 4){
			a = 1;
			c = 3;
			m = 7;
		}else{
			m += 1;
			m |= m >> 1;
			m |= m >> 2;
			m |= m >> 4;
			m |= m >> 8;
			m |= m >> 16;
			a = 1 + nrand(m/4)*4;     /* 1 ≤ a < m   && a mod 4 = 1 */
			c = 3 + nrand((m-2)/2)*2; /* 3 ≤ c < m-1 && c mod 2 = 1 */
		}

		shuffle = malloc(plnum*sizeof(*shuffle));
		xi = pcurplaying < 0 ? pcur : pcurplaying;
		pcurplayingnew = -1;
		pcurnew = 0;
		for(i = 0; i < plnum;){
			if(xi < plnum){
				if(pcur == xi)
					pcurnew = i;
				if(pcurplaying == xi)
					pcurplayingnew = i;
				shuffle[i++] = xi;
			}
			xi = (a*xi + c) & m;
		}
		pcur = pcurnew;
		pcurplaying = pcurplayingnew;
	}else{
		pcur = shuffle[pcur];
		if(pcurplaying >= 0)
			pcurplaying = shuffle[pcurplaying];
		free(shuffle);
		shuffle = nil;
	}

	stop(playernext);
	if(pcur < plnum-1)
		playernext = newplayer(pcur+1, 0);
}

static void
usage(void)
{
	fprint(2, "usage: %s [-s] [-c aAdDtTp]\n", argv0);
	sysfatal("usage");
}

void
threadmain(int argc, char **argv)
{
	Rune key;
	Mouse m;
	Alt a[] =
	{
		{ nil, &m, CHANRCV },
		{ nil, nil, CHANRCV },
		{ nil, &key, CHANRCV },
		{ nil, nil, CHANEND },
	};
	int n, scrolling, oldpcur, oldbuttons, pnew, shuffled, themetid;

	shuffled = 0;
	ARGBEGIN{
	case 's':
		shuffled = 1;
		break;
	case 'c':
		cols = EARGF(usage());
		if(strlen(cols) >= nelem(colwidth))
			sysfatal("max %d columns allowed", nelem(colwidth));
		break;
	default:
		usage();
		break;
	}ARGEND;

	readplist();
	if(plnum < 1){
		fprint(2, "empty playlist\n");
		sysfatal("empty");
	}

	if((audio = open("/dev/audio", OWRITE)) < 0)
		sysfatal("audio: %r");

	if(initdraw(nil, nil, "zuke") < 0)
		sysfatal("initdraw: %r");
	unlockdisplay(display);
	if((mctl = initmouse(nil, screen)) == nil)
		sysfatal("initmouse: %r");
	if((kctl = initkeyboard(nil)) == nil)
		sysfatal("initkeyboard: %r");

	a[0].c = mctl->c;
	a[1].c = mctl->resizec;
	a[2].c = kctl->c;

	f = display->defaultfont;
	srand(time(0));
	pcurplaying = -1;
	chvolume(0);
	fmtinstall('P', positionfmt);
	threadsetname("zuke");

	if(shuffled){
		pcur = nrand(plnum);
		toggleshuffle();
	}

	themeinit();
	redraw(1);
	oldbuttons = 0;
	scrolling = 0;
	themetid = -1;

	for(;;){
ev:
		oldpcur = pcur;
		if(seekmx != newseekmx)
			redraw(0);

		switch(alt(a)){
		case 0:
			if(ptinrect(m.xy, seekbar)){
				seekoff = getmeta(pcurplaying)->duration * (double)(m.xy.x-1-seekbar.min.x) / (double)Dx(seekbar);
				if(seekoff < 0)
					seekoff = 0;
				newseekmx = m.xy.x;
			}else{
				newseekmx = -1;
			}

			if(m.buttons != 2)
				scrolling = 0;
			if(m.buttons == 0)
				break;
			if(m.buttons == 8){
				scroll = MAX(scroll-scrollsz/4-1, 0);
				redraw(1);
				break;
			}else if(m.buttons == 16){
				scroll = MIN(scroll+scrollsz/4+1, plnum-scrollsz-1);
				redraw(1);
				break;
			}


			if(oldbuttons == 0 && !scrolling && ptinrect(m.xy, insetrect(seekbar, -4))){
				if(ptinrect(m.xy, seekbar))
					seekrel(playercurr, seekoff/1000.0 - byteswritten/Bps);
				break;
			}

			n = (m.xy.y - screen->r.min.y)/f->height;

			if(oldbuttons == 0 && m.xy.x <= screen->r.min.x+Scrollwidth){
				if(m.buttons == 1){
					scroll = MAX(0, scroll-n-1);
					redraw(1);
					break;
				}else if(m.buttons == 4){
					scroll = MIN(scroll+n+1, plnum-scrollsz-1);
					redraw(1);
					break;
				}else if(m.buttons == 2){
					scrolling = 1;
				}
			}

			if(m.buttons == 4){
				n = menuhit(3, mctl, &menu3, nil);
				if(n == 0)
					themetid = proccreate(themeproc, &audio, 4096);
				else if(n == 1)
					goto end;
				goto ev;
			}

			if(scrolling){
				if(scrollsz >= plnum)
					break;
				scroll = (m.xy.y - screen->r.min.y - Scrollheight/4)*(plnum-scrollsz) / (Dy(screen->r)-Scrollheight/2);
				scroll = CLAMP(scroll, 0, plnum-scrollsz-1);
				redraw(1);
			}else if(m.buttons == 1 || m.buttons == 2){
				pcur = scroll + n;
				if(m.buttons == 2){
					stop(playercurr);
					playercurr = newplayer(pcur, 1);
					start(playercurr);
				}
			}
			break;
		case 1: /* resize */
			if(getwindow(display, Refnone) < 0)
				sysfatal("getwindow: %r");
			redraw(1);
			break;
		case 2:
			switch(key){
			case Kleft:
				seekrel(playercurr, -(double)Seek);
				break;
			case Kright:
				seekrel(playercurr, Seek);
				break;
			case ',':
				seekrel(playercurr, -(double)Seekfast);
				break;
			case '.':
				seekrel(playercurr, Seekfast);
				break;
			case Kup:
				pcur--;
				break;
			case Kpgup:
				pcur -= scrollsz;
				break;
			case Kdown:
				pcur++;
				break;
			case Kpgdown:
				pcur += scrollsz;
				break;
			case Kend:
				pcur = plnum-1;
				break;
			case Khome:
				pcur = 0;
				break;
			case 10:
				stop(playercurr);
				playercurr = newplayer(pcur, 1);
				start(playercurr);
				break;
			case 'q':
			case Kdel:
				stop(playercurr);
				goto end;
			case 'i':
			case 'o':
				if(pcur == pcurplaying)
					oldpcur = -1;
				pcur = pcurplaying;
				break;
			case 'b':
			case '>':
				if(playercurr == nil)
					break;
				pnew = pcurplaying;
				if(++pnew >= plnum)
					pnew = 0;
				stop(playercurr);
				playercurr = newplayer(pnew, 1);
				start(playercurr);
				redraw(1);
				break;
			case 'z':
			case '<':
				if(playercurr == nil)
					break;
				pnew = pcurplaying;
				if(--pnew < 0)
					pnew = plnum-1;
				stop(playercurr);
				playercurr = newplayer(pnew, 1);
				start(playercurr);
				redraw(1);
				break;
			case '-':
				chvolume(-1);
				redraw(0);
				break;
			case '+':
			case '=':
				chvolume(+1);
				redraw(0);
				break;
			case 'v':
				stop(playercurr);
				playercurr = nil;
				pcurplaying = -1;
				freeimage(cover);
				cover = nil;
				redraw(1);
				break;
			case 's':
				toggleshuffle();
				redraw(1);
				break;
			case 'c':
			case 'p':
				toggle(playercurr);
				break;
			case '/':
			case '?':
			case 'n':
			case 'N':
				search(key);
				break;
			}
		}

		if(pcur != oldpcur){
			pcur = CLAMP(pcur, 0, plnum-1);
			if(pcur < scroll)
				scroll = pcur;
			else if(pcur > scroll + scrollsz)
				scroll = pcur - scrollsz;
			scroll = CLAMP(scroll, 0, plnum-scrollsz);

			if(pcur != oldpcur)
				redraw(1);
		}
	}

end:
	threadint(themetid);
	threadexitsall(nil);
}