shithub: orca

ref: cdeb13f59a9fcbc0c9a81bfd99678aa16ea3dabd
dir: /plan9.c/

View raw version
#include "plan9.h"
#include "field.h"
#include "gbuffer.h"
#include "sim.h"
#include <bio.h>
#include <draw.h>
#include <mouse.h>
#include <keyboard.h>
#include <thread.h>

#define MIN(x,y) ((x)<=(y)?(x):(y))
#define MAX(x,y) ((x)>=(y)?(x):(y))
#define is_movement(c) (c == 'W' || c == 'N' || c == 'E' || c == 'S')
#define is_send(c) (c == ':' || c == '%' || c == '!' || c == '?' || c == ';' || c == ';' || c == '$')

enum {
	Txtoff = 16,
	Coloff = 2,

	Cresize = 0,
	Ckey,
	Cmouse,
	Credraw,
	Numchan,

	Sfancy = 0,
	Splain,
	Snone,
	Numstyles,

	Minsert = 0,
	Mappend,
	Mslide,
	Mselect,
	Nummodes,

	Menu3load = 0,
	Menu3save,
	Menu3dotstyle,
	Menu3rulerstyle,
	Menu3exit,
	Nummenu3,

	Dback = 0,
	Dfhigh,
	Dfmed,
	Dflow,
	Dfinv,
	Dbmed,
	Dbinv,
	Numcolors,

	/* this might become a bad idea in the future */
	Mark_flag_group_highlight = 1<<6,
	Mark_flag_selected = 1<<7,

	Midicn = 0<<4, /* cable number. FIXME: write midifs */
};

typedef struct Snap Snap;

struct Snap {
	int bpm, apm;
	vlong tick;
	Point cur, scroll;
	Rectangle sel;
	Field field;
};

static int bpm = 120, apm = 120;
static Point rulers = {8, 8};
static int rulerstyle = Sfancy, dotstyle = Sfancy;

static char *ip, *udpport;
static int udp = -1;

static char *midipath;
static int midi = -1;

static Rune *linebuf;
static Rune cursor = '@';
static vlong tick;
static Point glyphsz;
static Point cur, scroll, txtoff, move = { .x = 1, .y = 1 };
static Rectangle sel;
static Field field;
static Mbuf_reusable mbuf, mscr;
static char filename[256];
static Field fscr, fsel;
static bool altdown, shiftdown, ctldown, pause, forward;
static int mode = Minsert;
static long framedev; /* frame deviation in ms */
static char *shellcmd;

static Snap *snaps;
static int numsnaps, nextsnap;
static int maxsnaps = 100;

static struct {
	struct {
		char *menu;
		Rune r;
	}dot;
	struct {
		char *menu;
		Rune r[9];
	}ruler;
}styles[Numstyles] = {
	[Sfancy] = {
		.dot = {"fancy dots", L'·'},
		.ruler = {"fancy rulers", {L'┌', L'┬', L'┐', L'├', L'┼', L'┤', L'└', L'┴', L'┘'}},
	},
	[Splain] = {
		.dot = {"plain dots", '.'},
		.ruler = {"plain rulers", {'+', '+', '+', '+', '+', '+', '+', '+', '+'}},
	},
	[Snone] = {
		.dot = {"no dots", ' '},
		.ruler = {"no rulers", {0}},
	},
};

static struct {
	u8int u[4];
	Usz at;
}noteoff[16*128]; /* 16 channels, 128 notes each */

static u32int theme[Numcolors] = {
	[Dback] = 0x000000ff,
	[Dfhigh] = 0xffffffff,
	[Dfmed] = 0x777777ff,
	[Dflow] = 0x272727ff,
	[Dfinv] = 0x000000ff,
	[Dbmed] = 0x72dec2ff,
	[Dbinv] = 0xffb545ff,
};

static Image *color[Numcolors];

static char *modes[Nummodes] = {
	[Minsert] = "insert",
	[Mappend] = "append",
	[Mslide] = "slide ",
	[Mselect] = "select",
};

static char *menu3i[Nummenu3+1] = {
	[Menu3load] = "load",
	[Menu3save] = "save",
	[Menu3exit] = "exit",
};

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

static void
snapshot(void)
{
	Snap *s;
	int i;

	if (maxsnaps < 1)
		return;

	if (nextsnap >= maxsnaps) {
		nextsnap--;
		field_deinit(&snaps->field);
		memmove(snaps, snaps+1, sizeof(Snap)*nextsnap);
	}
	s = &snaps[nextsnap++];
	s->bpm = bpm;
	s->apm = apm;
	s->tick = tick;
	s->cur = cur;
	s->scroll = scroll;
	s->sel = sel;
	for (i = nextsnap; i < numsnaps; i++)
		field_deinit(&snaps[i].field);
	field_init(&s->field);
	field_copy(&field, &s->field);
	numsnaps = nextsnap;
}

static void
gotosnapshot(int i)
{
	Snap *s;

	s = &snaps[i];
	bpm = s->bpm;
	apm = s->apm;
	tick = s->tick;
	cur = s->cur;
	scroll = s->scroll;
	sel = s->sel;
	field_copy(&s->field, &field);
}

static void
undo(void)
{
	if (nextsnap < 1)
		return;

	if (nextsnap == numsnaps) {
		snapshot();
		nextsnap--;
	}
	gotosnapshot(--nextsnap);
}

static void
redo(void)
{
	if (nextsnap >= numsnaps-1)
		return;

	gotosnapshot(++nextsnap);
	if (nextsnap == numsnaps)
		nextsnap--;
}

Usz
orca_round_up_power2(Usz x)
{
	x -= 1;
	x |= x >> 1;
	x |= x >> 2;
	x |= x >> 4;
	x |= x >> 8;
	x |= x >> 16;
	return x + 1;
}

bool
orca_is_valid_glyph(Glyph c)
{
	if (c >= '0' && c <= '9')
		return true;
	if (c >= 'A' && c <= 'Z')
		return true;
	if (c >= 'a' && c <= 'z')
		return true;
	switch (c) {
	case '!':
	case '#':
	case '%':
	case '*':
	case '.':
	case ':':
	case ';':
	case '=':
	case '?':
	case '$':
		return true;
	}
	return false;
}

static Glyph
fieldget(int x, int y)
{
	if (x < field.width && y < field.height && x >= 0 && y >= 0)
		return field.buffer[x + field.width*y];

	return 0;
}

static void
fieldset(int x, int y, Glyph c)
{
	if (x < field.width && y < field.height && x >= 0 && y >= 0)
		field.buffer[x + field.width*y] = c;
}

static void
fieldsetn(int x, int y, Glyph c, int n)
{
	if (y >= 0 && y < field.height) {
		for (; n > 0 && x < field.width; n--, x++)
			field.buffer[x + field.width*y] = c;
	}
}

static void
selpasteb(Biobuf *b)
{
	char *s;
	int cols, rows, i, n;

	for (cols = rows = 0; (s = Brdstr(b, '\n', 1)) != nil;) {
		if ((n = Blinelen(b)) > cols)
			cols = MIN(n, field.width-sel.min.x);
		if (sel.min.y+rows < field.height) {
			for (i = 0; i < n; i++)
				if (!orca_is_valid_glyph(s[i]))
					s[i] = '.';
			memmove(&field.buffer[sel.min.x + field.width*(sel.min.y+rows)], s, MIN(n, field.width-sel.min.x));
			rows++;
		}
		free(s);
	}
	sel.max.x = sel.min.x + MAX(0, cols-1);
	sel.max.y = sel.min.y + MAX(0, rows-1);
	if (sel.max.x < cur.x)
		cur.x = sel.max.x;
	if (sel.max.y < cur.y)
		cur.y = sel.max.y;
}

static void
runshell(void *x)
{
	int *p;

	p = x;
	dup(p[0], 0); close(p[0]); close(p[1]);
	dup(p[3], 1); close(p[3]); close(p[2]);
	dup(open("/dev/null", OWRITE), 2);
	procexecl(nil, "/bin/rc", "rc", "-c", shellcmd, nil);
	threadexits("exec: %r");
}

static void
shellpipe(char *s)
{
	Biobuf *in, *out;
	int x, y, p[4];
	Glyph g;

	shellcmd = s+1;
	pipe(p);
	pipe(p+2);
	procrfork(runshell, p, 4096, RFFDG);
	close(p[0]);
	close(p[3]);
	out = Bfdopen(p[1], OWRITE);
	in = Bfdopen(p[2], OREAD);
	for (y = sel.min.y; *s != '<' && y <= sel.max.y; y++) {
		for (x = sel.min.x; x <= sel.max.x; x++) {
			if ((g = fieldget(x, y)) == '.')
				g = ' ';
			Bputc(out, g);
		}
		Bputc(out, '\n');
	}
	Bterm(out);
	if (*s != '>')
		selpasteb(in);
	Bterm(in);
}

static void
midiopen(char *path)
{
	if (midi >= 0) {
		close(midi);
		midi = -1;
	}
	if (path != nil) {
		free(midipath);
		midipath = strdup(path);
	}
	if (midi < 0 && midipath[0] && (midi = open(midipath, OWRITE)) < 0) {
		fprint(2, "midi failed: %r\n");
		/* FIXME display error */
	}
}

static void
netdial(char *newip, char *newudpport)
{
	if (udp >= 0) {
		close(udp);
		udp = -1;
	}
	if (newip != nil) {
		free(ip);
		ip = strdup(newip);
	}
	if (newudpport != nil) {
		free(udpport);
		udpport = strdup(newudpport);
	}
	if (udp < 0 && ip[0] && udpport[0] && (udp = dial(netmkaddr(ip, "udp", udpport), nil, nil, nil)) < 0) {
		fprint(2, "udp failed: %r\n");
		/* FIXME display error */
	}
}

static void
selset(Rune key)
{
	int y;
	bool commented;

	if (key == '#') {
		commented = true;
		for (y = sel.min.y; y <= sel.max.y && commented; y++)
			commented = fieldget(sel.min.x, y) == key && fieldget(sel.max.x, y) == key;
		if (commented)
			key = '.';
	} else {
		commented = false;
	}

	for (y = sel.min.y; y <= sel.max.y; y++) {
		if (key == '#' || commented) {
			fieldset(sel.min.x, y, key);
			fieldset(sel.max.x, y, key);
		} else {
			fieldsetn(sel.min.x, y, key, Dx(sel)+1);
		}
	}
}

static void
selcopy(void)
{
	Biobuf *b;
	int y;

	if ((b = Bopen("/dev/snarf", OWRITE)) != nil) {
		for (y = sel.min.y; y <= sel.max.y; y++) {
			Bwrite(b, &field.buffer[sel.min.x + field.width*y], Dx(sel)+1);
			Bputc(b, '\n');
		}
		Bterm(b);
	}
}

static void
selpaste(void)
{
	Biobuf *b;

	if ((b = Bopen("/dev/snarf", OREAD)) != nil)
		selpasteb(b);
}

static void
nosnapshot(void)
{
}

static void
command(char *s, void (*snapshot)(void))
{
	char *a;
	int x, y;

	if (s[0] == ',') {
		snapshot();
		cur = ZP;
		sel = Rect(0, 0, field.width, field.height);
		s++;
	}

	if (s[0] == '|' || s[0] == '>' || s[0] == '<') {
		if (s[0] != '>')
			snapshot();
		shellpipe(s);
		return;
	}

	if ((a = strchr(s, ':')) != nil || (a = strchr(s, ' ')) != nil)
		*a++ = 0;

	if (s[0] == 'p' && s[1] == 'l') /* play */
		pause = false;
	else if (s[0] == 's' && s[1] == 't') /* stop */
		pause = true;
	else if (s[0] == 'r' && s[1] == 'u') /* run */
		forward = true;
	else if (s[0] == 'c' && s[1] == 'o') /* copy */
		selcopy();
	else if (s[0] == 'p' && s[1] == 'a') { /* paste */
		snapshot();
		selpaste();
	} else if (s[0] == 'e' && s[1] == 'r') { /* erase */
		snapshot();
		selset('.');
	} else if (s[0] == 'p' && s[1] == 'r') { /* print */
		for (y = sel.min.y; y <= sel.max.y; y++) {
			for (x = sel.min.x; x <= sel.max.x; x++)
				putchar(field.buffer[x + y*field.width]);
			putchar('\n');
		}
		fflush(stdout);
	} else if (a != nil) {
		x = atoi(a);

		if (s[0] == 'b' && s[1] == 'p') /* bpm */
			apm = bpm = MAX(1, x);
		else if (s[0] == 'a' && s[1] == 'p') /* apm */
			apm = MAX(1, x);
		else if (s[0] == 'f' && s[1] == 'r') /* frame */
			tick = MAX(0, x);
		else if (s[0] == 's' && s[1] == 'k') /* skip */
			tick = MAX(0, tick+x);
		else if (s[0] == 'r' && s[1] == 'e') /* rewind */
			tick = MAX(0, tick-x);
		else if (s[0] == 'i' && s[1] == 'p') /* ip */
			netdial(a, nil);
		else if (s[0] == 'u' && s[1] == 'd') /* udp */
			netdial(nil, a);
		else if (s[0] == 'm' && s[1] == 'i') /* midi */
			midiopen(a);

		/* FIXME color, find, select, inject, write, time */
	} else if (strcmp(s, "undo") == 0) {
		undo();
	} else if (strcmp(s, "redo") == 0) {
		redo();
	}
}

static void
process(Oevent_list *events)
{
	int i, off, t;
	Oevent *e;
	u8int u[4];
	char tmp[64];

	for (e = events->buffer, i = 0; i < events->count; i++, e++) {
		t = e->any.oevent_type;

		if (midi >= 0) {
			if (t == Oevent_type_midi_note) {
				Oevent_midi_note *n = &e->midi_note;
				u[0] = Midicn | 0x9;
				u[1] = 0x90 | n->channel;
				u[2] = (n->octave + 1)*12 + n->note;
				u[3] = n->velocity;
				write(midi, u, 4);

				off = n->channel*128 + u[2];
				noteoff[off].u[0] = Midicn | 0x8;
				noteoff[off].u[1] = 0x80 | n->channel;
				noteoff[off].u[2] = u[2];
				noteoff[off].u[3] = 0;
				noteoff[off].at = tick + n->duration;
				continue;
			} else if (t == Oevent_type_midi_cc) {
				Oevent_midi_cc *c = &e->midi_cc;
				u[0] = Midicn | 0xb;
				u[1] = 0xb0 | c->channel;
				u[2] = c->control;
				u[3] = c->value;
				write(midi, u, 4);
				continue;
			} else if (t == Oevent_type_midi_pb) {
				Oevent_midi_pb *p = &e->midi_pb;
				u[0] = Midicn | 0xe;
				u[1] = 0xe0 | p->channel;
				u[2] = p->lsb;
				u[3] = p->msb;
				write(midi, u, 4);
				continue;
			}
		}

		if (udp >= 0) {
			if (t == Oevent_type_udp_string) {
				Oevent_udp_string *u = &e->udp_string;
				write(udp, u->chars, u->count); /* FIXME show errors */
				continue;
			}
		}

		if (t == Oevent_type_cmd_string) {
			Oevent_cmd_string *c = &e->cmd_string;
			memmove(tmp, c->chars, c->count);
			tmp[c->count] = 0;
			command(tmp, nosnapshot);
		}
	}

	for (i = 0; midi >= 0 && i < nelem(noteoff); i++) {
		if (noteoff[i].at > 0 && noteoff[i].at < tick) {
			write(midi, noteoff[i].u, 4);
			noteoff[i].at = 0;
		}
	}
}

/*
 * nsec() is wallclock and can be adjusted by timesync
 * so need to use cycles() instead, but fall back to
 * nsec() in case we can't
 *
 * "fasthz" is how many ticks there are in a second
 * can be read from /dev/time
 *
 * perhaps using RDTSCP is even better
 */
static uvlong
nanosec(void)
{
	static uvlong fasthz, xstart;
	uvlong x, div;
	int f, n, i;
	char tmp[128], *e;

	if (fasthz == ~0ULL)
		return nsec() - xstart;

	if (fasthz == 0) {
		fasthz = ~0ULL;
		if ((f = open("/dev/time", OREAD)) >= 0 && (n = read(f, tmp, sizeof(tmp)-1)) > 2) {
			tmp[n] = 0;
			e = tmp;
			for (i = 0; i < 3; i++)
				strtoll(e, &e, 10);
			fasthz = strtoll(e, nil, 10);
			if (fasthz < 1)
				fasthz = ~0ULL;
			close(f);
			cycles(&xstart);
		}
		if (fasthz == ~0ULL) {
			fprint(2, "couldn't get fasthz, falling back to nsec()\n");
			fprint(2, "you might want to disable aux/timesync\n");
			xstart = nsec();
			return 0;
		}
	}
	cycles(&x);
	if (x < xstart) { /* wrap around */
		xstart = 0;
		x += 0 - xstart;
	}
	x -= xstart;

	for (div = 1000000000ULL; x < 0x1999999999999999ULL && div > 1 ; div /= 10ULL, x *= 10ULL);

	return x / (fasthz / div);
}

static void
orcathread(void *drawchan)
{
	vlong start, end, n;
	vlong processold, processnew;
	Oevent_list events;
	int w, h;

	threadsetname("orca/sim");

	oevent_list_init(&events);

	processnew = nanosec();
	for (;;) {
		start = nanosec();
		w = field.width;
		h = field.height;
		mbuffer_clear(mbuf.buffer, h, w);
		oevent_list_clear(&events);
		orca_run(field.buffer, mbuf.buffer, h, w, tick, &events, 0);

		processold = processnew;
		processnew = nanosec();
		process(&events);
		nbsendul(drawchan, 0);

		forward = false;
		do {
			end = 15000000000LL/bpm; /* 1e9*60/4 */
			n = nanosec() - start;
			if (n >= end && !pause)
				break;
			/* unpause is not precise at all */
			if (pause || end - n > 750000000LL)
				sleep(70);
			else if (end - n > 25000000LL)
				sleep(20);
			else if (end - n > 10000000LL)
				sleep(1);
		} while (!forward);

		framedev = (processnew - processold - 15000000000LL/bpm)/1000000LL;
		tick++;

		if (apm < bpm)
			bpm--;
		else if (apm > bpm)
			bpm++;
	}
}

static void
redraw(int complete)
{
	static Point oldscroll;
	Rectangle r;
	Point p, top, bot;
	int x, y, rx, ry, i;
	Point max;
	int oldbg, oldfg, bg, fg, attr, off;
	bool selected, grouphl;
	char s[32];
	Rune c, csel;

	/* bottom text is always in the same place */
	bot.x = screen->r.min.x + Txtoff;
	bot.y = screen->r.max.y - glyphsz.y*2 - Txtoff;

	txtoff.x = Txtoff;
	txtoff.y = Txtoff;
	max.x = (Dx(screen->r) - 2*Txtoff) / glyphsz.x;
	if (max.x > field.width) {
		txtoff.x += (max.x - field.width) * glyphsz.x / 2;
		max.x = field.width;
	}
	max.y = (bot.y - screen->r.min.y - Txtoff - glyphsz.y) / glyphsz.y;
	if (max.y > field.height) {
		txtoff.y += (max.y - field.height) * glyphsz.y / 2;
		max.y = field.height;
	}
	top = addpt(screen->r.min, txtoff);

	if (cur.x >= max.x+scroll.x-1)
		scroll.x = cur.x-max.x+1;
	else if (cur.x < scroll.x)
		scroll.x = cur.x;

	if (cur.y >= max.y+scroll.y-1)
		scroll.y = cur.y-max.y+1;
	else if (cur.y < scroll.y)
		scroll.y = cur.y;

	if (!eqpt(oldscroll, scroll))
		complete = 1;
	oldscroll = scroll;

	if (complete) {
		r = screen->r;
		r.max.y = r.min.y + txtoff.y;
		draw(screen, r, color[Dback], nil, ZP);
		r = screen->r;
		r.max.x = r.min.x + txtoff.x;
		draw(screen, r, color[Dback], nil, ZP);
	}

	off = field.width*cur.y + cur.x;
	csel = field.buffer[off];

	bg = -1;
	fg = -1;
	r = screen->r;
	p.y = top.y;
	for (y = scroll.y; y < MIN(field.height, scroll.y+max.y); y++) {
		p.x = top.x;
		for (x = scroll.x, i = 0; x < MIN(field.width, scroll.x+max.x); x++) {
			oldbg = bg;
			oldfg = fg;
			off = field.width*y + x;

			c = field.buffer[off];
			attr = mbuf.buffer[off];
			selected = x >= sel.min.x && y >= sel.min.y && x <= sel.max.x && y <= sel.max.y;
			if (selected)
				attr |= Mark_flag_selected;
			else
				attr &= ~Mark_flag_selected;

			/* highlight the same char */
			grouphl = c != '.' && csel == c && (x != cur.x || y != cur.y) && (attr & (Mark_flag_input|Mark_flag_lock|Mark_flag_output)) == 0;
			if (grouphl)
				attr |= Mark_flag_group_highlight;
			else
				attr &= ~Mark_flag_group_highlight;

			if (c == '.' && eqpt(Pt(x, y), cur))
				c = cursor;

			if (!grouphl && !complete && c == fscr.buffer[off] && attr == mscr.buffer[off]) {
				if (i > 0) {
					p = runestringnbg(screen, p, color[oldfg], ZP, font, linebuf, i, color[oldbg], ZP);
					i = 0;
				}
				p.x += glyphsz.x;
				continue;
			}

			fscr.buffer[off] = c;
			mscr.buffer[off] = attr;

			bg = selected ? Dbinv : Dback;
			fg = selected ? Dfinv : (grouphl ? Dbinv: Dfmed);

			if (c == '.')
				c = styles[dotstyle].dot.r;

			if (c == styles[dotstyle].dot.r && (attr & ~Mark_flag_selected) == 0) {
				if ((x % rulers.x) == 0 && (y % rulers.y) == 0) {
					rx = !!x + (x + 1) / field.width;
					ry = !!y + (y + 1) / field.height;
					c = rulerstyle == Snone ? styles[dotstyle].dot.r : styles[rulerstyle].ruler.r[ry*3+rx];
				}
				fg = selected ? Dfmed : Dflow;
			} else if (!selected && !grouphl) {
				if (c == '#') {
					fg = Dfmed;
				} else {
					if ((c >= 'A' && c <= 'Z' && !is_movement(c)) || is_send(c)) {
						bg = Dbmed;
						fg = Dfinv;
					}
					if (attr & Mark_flag_input) {
						bg = Dback;
						fg = Dfhigh;
					} else if (attr & Mark_flag_lock) {
						bg = Dback;
						fg = Dfmed;
					}
				}

				if (attr & Mark_flag_output) {
					bg = Dfhigh;
					fg = Dfinv;
				}
				if (attr & Mark_flag_haste_input) {
					bg = Dback;
					fg = Dbmed;
				}
			}

			if (bg != oldbg || fg != oldfg) {
				p = runestringnbg(screen, p, color[oldfg], ZP, font, linebuf, i, color[oldbg], ZP);
				i = 0;
			}
			linebuf[i++] = c;
		}
		runestringnbg(screen, p, color[fg], ZP, font, linebuf, i, color[bg], ZP);
		p.y += glyphsz.y;
	}

	r = screen->r;
	r.min.x += txtoff.x + max.x*glyphsz.x;
	draw(screen, r, color[Dback], nil, ZP);

	r = screen->r;
	r.min.y += txtoff.y + max.y*glyphsz.y;
	draw(screen, r, color[Dback], nil, ZP);

	p = top;
	p.y += glyphsz.y*(max.y-1)/2;
	if (scroll.x > 0) {
		p.x = top.x - txtoff.x;
		string(screen, p, color[Dfmed], ZP, font, "←");
	}
	if (max.x+scroll.x < field.width) {
		p.x = top.x + max.x*glyphsz.x + txtoff.x - glyphsz.x;
		string(screen, p, color[Dfmed], ZP, font, "→");
	}

	p = top;
	p.x += glyphsz.x*(max.x-1)/2;
	if (scroll.y > 0) {
		p.y = screen->r.min.y;
		string(screen, p, color[Dfmed], ZP, font, "↑");
	}
	if (max.y+scroll.y < field.height) {
		p.y = top.y + max.y*glyphsz.y;
		string(screen, p, color[Dfmed], ZP, font, "↓");
	}

	i = 0;
	sprint(s, "%udx%ud", field.width, field.height);
	i += runesprint(linebuf, "%-10s", s);
	sprint(s, "%d/%d", rulers.x, rulers.y);
	i += runesprint(linebuf+i, "%-9s", s);
	sprint(s, "%lldf%c", MAX(0, tick), pause ? '~' : 0);
	i += runesprint(linebuf+i, "%-9s", s);
	off = sprint(s, "%d", bpm);
	if (apm != bpm)
		off += sprint(s+off, "%+d", apm-bpm);
	sprint(s+off, "%c", (tick % 4) == 0 ? '*' : 0);
	i += runesprint(linebuf+i, "%-9s", s);
	sprint(s, "%ldms", labs(framedev));
	i += runesprint(linebuf+i, "%-8s", s);
	runestringn(screen, bot, color[Dfhigh], ZP, font, linebuf, i);
	bot.y += glyphsz.y;

	i = 0;
	sprint(s, "%ud,%ud", cur.x, cur.y);
	i += runesprint(linebuf, "%-10s", s);
	sprint(s, "%d:%d", sel.min.x < cur.x ? -Dx(sel) : Dx(sel), sel.max.x < cur.x ? -Dy(sel) : Dy(sel));
	i += runesprint(linebuf+i, "%-9s", s);
	i += runesprint(linebuf+i, "%-9s", modes[altdown ? Mslide : mode]);
	i += runesprint(linebuf+i, "%s", filename[0] ? filename : "unnamed");
	runestringn(screen, bot, color[Dfhigh], ZP, font, linebuf, i);

	flushimage(display, 1);
}

static int
fieldload(char *path)
{
	Dir *d;
	Field_load_error e;

	if ((d = dirstat(path)) == nil || d->length < 1 || (d->type & DMDIR) != 0) {
		free(d);
		werrstr("invalid orca file");
		return -1;
	}

	if ((e = field_load_file(path, &field)) != Field_load_error_ok) {
		werrstr(field_load_error_string(e));
		return -1;
	}
	cur = ZP;
	sel = ZR;

	return 0;
}

static int
fieldsave(char *path)
{
	FILE *f;

	if ((f = fopen(path, "w")) == nil)
		return -1;
	field_fput(&field, f);
	fclose(f);
	return 0;
}

static Point
ptclamp(Point p)
{
	p.x = MAX(0, MIN((int)field.width-1, p.x));
	p.y = MAX(0, MIN((int)field.height-1, p.y));
	return p;
}

static void
curmove(int x, int y)
{
	Point xy;

	xy = Pt(x, y);
	cur = ptclamp(addpt(cur, xy));
	sel.min = ptclamp(addpt(sel.min, xy));
	sel.max = ptclamp(addpt(sel.max, xy));
}

static void
selmove(int x, int y)
{
	int i;

	if (sel.min.x+x < 0 || sel.min.x >= field.width || sel.min.y+y < 0 || sel.min.y+y >= field.height)
		return;

	field_resize_raw(&fsel, Dy(sel)+1, Dx(sel)+1);
	gbuffer_copy_subrect(
		field.buffer, fsel.buffer,
		field.height, field.width, Dy(sel)+1, Dx(sel)+1,
		sel.min.y, sel.min.x, 0, 0, Dy(sel)+1, Dx(sel)+1
	);

	for (i = sel.min.y; i <= sel.max.y; i++) {
		fieldsetn(sel.min.x, i, '.', Dx(sel)+1);
		memset(&mbuf.buffer[sel.min.x + field.width*i], 0, Dx(sel)+1);
	}

	gbuffer_copy_subrect(
		fsel.buffer, field.buffer,
		Dy(sel)+1, Dx(sel)+1, field.height, field.width,
		0, 0, sel.min.y+y, sel.min.x+x, Dy(sel)+1, Dx(sel)+1
	);
}

static void
selmap(int (*f)(int))
{
	int x, y;

	for (y = sel.min.y; y <= sel.max.y; y++) {
		for (x = sel.min.x; x <= sel.max.x; x++) {
			fieldset(x, y, f(fieldget(x, y)));
		}
	}
}

static int
snaplow(int n, int rulern)
{
	n--;
	n -= (n % rulern) > 0 ? (n % rulern) : rulern;
	return MAX(1, n+1);
}

static int
snaphigh(int n, int rulern)
{
	n += rulern;
	n -= n % rulern - 1;
	return n;
}

static void
screensize(int *w, int *h)
{
	*w = snaplow((Dx(screen->r) - 2*Txtoff) / glyphsz.x, rulers.x);
	*h = snaplow(((Dy(screen->r) - 2*Txtoff) - 3*glyphsz.y) / glyphsz.y, rulers.y);
}

static void
stdinproc(void *)
{
	char buf[256];
	int n, i;

	threadsetname("stdinproc");

	for (;;) {
		if ((n = read(0, buf, sizeof(buf)-1)) <= 0)
			break;

		for (i = 0; i < n; i++) {
			if (buf[i] == '\r' || buf[i] == '\n' || buf[i] == 0) {
				buf[i] = 0;
				while (buf[i+1] == '\n')
					i++;
				command(buf, snapshot);
				n -= i;
				memmove(buf, buf+i+1, n);
				i = 0;
			}
		}
	}

	threadexits(nil);
}

static void
kbdproc(void *cchan)
{
	char buf[128], buf2[128], *s;
	int kfd, n;
	Rune r;

	threadsetname("kbdproc");
	if ((kfd = open("/dev/kbd", OREAD)) < 0)
		sysfatal("/dev/kbd: %r");

	buf2[0] = 0;
	buf2[1] = 0;
	buf[0] = 0;
	for (;;) {
		if (buf[0] != 0) {
			n = strlen(buf)+1;
			memmove(buf, buf+n, sizeof(buf)-n);
		}
		if (buf[0] == 0) {
			n = read(kfd, buf, sizeof(buf)-1);
			if (n <= 0)
				break;
			buf[n-1] = 0;
			buf[n] = 0;
		}

		switch (buf[0]) {
		case 'c':
			if (chartorune(&r, buf+1) > 0 && r != Runeerror)
				nbsend(cchan, &r);
		default:
			continue;

		case 'k':
			s = buf+1;
			while (*s) {
				s += chartorune(&r, s);
				if (utfrune(buf2+1, r) == nil) {
					if (r == Kalt) {
						altdown = true;
					} else if (r == Kshift) {
						shiftdown = true;
					} else if (r == Kctl) {
						ctldown = true;
						move = Pt(rulers.x, rulers.y);
					}
				}
			}
			break;

		case 'K':
			s = buf2+1;
			while (*s) {
				s += chartorune(&r, s);
				if (utfrune(buf+1, r) == nil) {
					if (r == Kalt) {
						altdown = false;
					} else if (r == Kshift) {
						shiftdown = false;
					} else if (r == Kctl) {
						ctldown = false;
						move = Pt(1, 1);
					}
				}
			}
			break;
		}
		strcpy(buf2, buf);
	}

	threadexits(nil);
}

static void
selext(int xdt, int ydt)
{
	if (sel.max.x > cur.x || (sel.min.x == cur.x && xdt > 0))
		sel.max.x += xdt;
	else
		sel.min.x += xdt;

	if (sel.max.y > cur.y || (sel.min.y == cur.y && ydt > 0))
		sel.max.y += ydt;
	else
		sel.min.y += ydt;

	sel.min.x = MAX(0, MIN((int)field.width-1, sel.min.x));
	sel.max.x = MAX(0, MIN((int)field.width-1, sel.max.x));
	sel.min.y = MAX(0, MIN((int)field.height-1, sel.min.y));
	sel.max.y = MAX(0, MIN((int)field.height-1, sel.max.y));
}

static Point
ptmouse(Point p)
{
	p = subpt(subpt(p, screen->r.min), txtoff);
	p.x /= glyphsz.x;
	p.y /= glyphsz.y;
	p.x += scroll.x;
	p.y += scroll.y;

	return ptclamp(p);
}

static void
usage(void)
{
	print("usage: %s [-p] [-b bpm] [-c cursor] [-l undo_limit] [-s WxH] [-r random_seed] [-i ip_address] [-u udp_port] [-m midi_path] [file]\n", argv0);
	threadexitsall("usage");
}

void
threadmain(int argc, char **argv)
{
	Mousectl *mctl;
	Keyboardctl kctl;
	Rune r;
	Mouse m;
	Point p;
	char tmp[256];
	int oldw, oldh, w, h, n, oldbuttons;
	long seed;
	bool complete;
	Alt a[Numchan+1] = {
		[Ckey] = { nil, &r, CHANRCV },
		[Cmouse] = { nil, &m, CHANRCV },
		[Cresize] = { nil, nil, CHANRCV },
		[Credraw] = { nil, nil, CHANRCV },
		{ nil, nil, CHANEND },
	};

	srand(time(0));
	w = h = 0;
	ip = strdup("127.0.0.1");
	udpport = strdup("41961");
	midipath = strdup("");

	ARGBEGIN{
	case 'p':
		pause = true;
		break;
	case 'b':
		bpm = atoi(EARGF(usage()));
		if (bpm < 1) {
			fprint(2, "invalid bpm %d\n", bpm);
			threadexitsall("args");
		}
		break;
	case 's':
		if (sscanf(EARGF(usage()), "%dx%d", &w, &h) != 2)
			usage();
		if (w <= 0 || h <= 0 || w > ORCA_X_MAX || h > ORCA_Y_MAX) {
			fprint(2, "invalid dimensions %dx%d\n", w, h);
			threadexitsall("args");
		}
		break;
	case 'r':
		if ((seed = atol(EARGF(usage()))) < 0) {
			fprint(2, "invalid seed %ld\n", seed);
			threadexitsall("args");
		}
		srand(seed);
		break;
	case 'c':
		if (chartorune(&cursor, EARGF(usage())) < 1 || cursor == Runeerror) {
			fprint(2, "invalid cursor \"%s\"\n", EARGF(usage()));
			threadexitsall("args");
		}
		break;
	case 'i':
		free(ip);
		ip = EARGF(usage());
		break;
	case 'u':
		free(udpport);
		udpport = EARGF(usage());
		break;
	case 'm':
		free(midipath);
		midipath = EARGF(usage());
		break;
	case 'l':
		if ((maxsnaps = atoi(EARGF(usage()))) < 0) {
			fprint(2, "invalid undo limit %s\n", EARGF(usage()));
			threadexitsall("args");
		}
		break;
	default:
		usage();
	}ARGEND

	if (argc > 1)
		usage();

	if (argc == 1) {
		field_init(&field);
		snprint(filename, sizeof(filename), "%s", argv[0]);
		if (fieldload(filename) != 0) {
			fprint(2, "%s: %r\n", filename);
			threadexitsall("file");
		}
		w = field.width;
		h = field.height;
	}

	threadsetname("orca/draw");

	if(initdraw(nil, nil, "orca") < 0)
		sysfatal("initdraw: %r");
	if ((mctl = initmouse(nil, screen)) == nil)
		sysfatal("initmouse: %r");
	display->locking = 1;
	unlockdisplay(display);

	a[Ckey].c = chancreate(sizeof(Rune), 20);
	a[Cmouse].c = mctl->c;
	a[Cresize].c = mctl->resizec;
	a[Credraw].c = chancreate(sizeof(ulong), 0);

	proccreate(kbdproc, a[Ckey].c, mainstacksize);
	kctl.c = a[Ckey].c;

	proccreate(stdinproc, nil, mainstacksize);

	for (n = 0; n < Numcolors; n++)
		color[n] = allocimage(display, Rect(0, 0, 1, 1), RGB24, 1, theme[n]);
	glyphsz.x = stringwidth(font, "@");
	glyphsz.y = font->height;

	if (filename[0] == 0) {
		if (w == 0 || h == 0)
			screensize(&w, &h);
		field_init_fill(&field, h, w, '.');
	}

	field_init_fill(&fscr, h, w, '.');
	field_init(&fsel);

	linebuf = malloc(sizeof(Rune)*MAX(w+1, 64));
	memset(noteoff, 0, sizeof(noteoff));

	mbuf_reusable_init(&mbuf);
	mbuf_reusable_ensure_size(&mbuf, h, w);
	memset(mbuf.buffer, 0, w*h);
	mbuf_reusable_init(&mscr);
	mbuf_reusable_ensure_size(&mscr, h, w);
	memset(mscr.buffer, 0, w*h);

	proccreate(orcathread, a[Credraw].c, mainstacksize);
	shiftdown = false;
	altdown = false;
	complete = true;
	move.x = 1;
	move.y = 1;
	oldbuttons = 0;

	netdial(nil, nil);
	midiopen(nil);

	snaps = calloc(maxsnaps, sizeof(Snap));

	for (;;) {
		redraw(complete);
		complete = false;
		oldw = w = field.width;
		oldh = h = field.height;

noredraw:
		switch (alt(a)) {
		case -1:
			goto end;

		case Cmouse:
			if (m.buttons == 1) {
				if (altdown) {
					p = ptmouse(m.xy);
					if (!eqpt(p, cur)) {
						snapshot();
						selmove(p.x-cur.x, p.y-cur.y);
						curmove(p.x-cur.x, p.y-cur.y);
					}
				} else if (oldbuttons == 0 && !shiftdown) {
					cur = ptmouse(m.xy);
					sel.min = cur;
					sel.max = cur;
				} else if (oldbuttons == 1 || shiftdown) {
					sel.max = ptmouse(m.xy);
					sel.min = sel.max;
					if (sel.max.x < cur.x)
						sel.max.x = cur.x;
					else
						sel.min.x = cur.x;
					if (sel.max.y < cur.y)
						sel.max.y = cur.y;
					else
						sel.min.y = cur.y;
				}
				oldbuttons = m.buttons;
				break;
			}
			oldbuttons = m.buttons;

			if (m.buttons == 3) { /* cut */
				selcopy();
				selset('.');
			} else if (m.buttons == 5) { /* paste */
				selpaste();
			} else if (m.buttons == 4) { /* menu */
				menu3i[Menu3dotstyle] = styles[(dotstyle+1) % Numstyles].dot.menu;
				menu3i[Menu3rulerstyle] = styles[(rulerstyle+1) % Numstyles].ruler.menu;
				n = menuhit(3, mctl, &menu3, nil);
				snprint(tmp, sizeof(tmp), "%s", filename);
				if (n == Menu3load) {
					if (enter("load from:", tmp, sizeof(tmp), mctl, &kctl, nil) > 0 && fieldload(tmp) == 0) {
						w = field.width;
						h = field.height;
						snprint(filename, sizeof(filename), "%s", tmp);
					}
				} else if (n == Menu3save) {
					if ((tmp[0] != 0 || enter("save to:", tmp, sizeof(tmp), mctl, &kctl, nil) > 0) && fieldsave(tmp) == 0)
						snprint(filename, sizeof(filename), "%s", tmp);
				} else if (n == Menu3dotstyle) {
					dotstyle = ++dotstyle % Numstyles;
				} else if (n == Menu3rulerstyle) {
					rulerstyle = ++rulerstyle % Numstyles;
				} else if (n == Menu3exit) {
					goto end;
				}
				complete = true;
			} else {
				goto noredraw;
			}
			break;

		case Cresize:
			getwindow(display, Refnone);
			complete = true;
			scroll = ZP;
			break;

		case Credraw:
			break;

		case Ckey:
			switch (r) {
			case '\n': /* C-j */
				/* FIXME bang it */
				break;
			case Kup:
				if (shiftdown || mode == Mselect)
					selext(0, -move.y);
				else {
					if (altdown || mode == Mslide) {
						snapshot();
						selmove(0, -move.y);
					}
					curmove(0, -move.y);
				}
				break;
			case Kdown:
				if (shiftdown || mode == Mselect)
					selext(0, move.y);
				else {
					if (altdown || mode == Mslide) {
						snapshot();
						selmove(0, move.y);
					}
					curmove(0, move.y);
				}
				break;
			case Kleft:
				if (shiftdown || mode == Mselect) {
					selext(-move.x, 0);
				} else {
					if (altdown || mode == Mslide) {
						snapshot();
						selmove(-move.x, 0);
					}
					curmove(-move.x, 0);
				}
				break;
			case Kright:
				if (shiftdown || mode == Mselect)
					selext(move.x, 0);
				else {
					if (altdown || mode == Mslide) {
						snapshot();
						selmove(move.x, 0);
					}
					curmove(move.x, 0);
				}
				break;
			case Ksoh: /* C-a */
				if (shiftdown || mode == Mselect)
					selext(-ORCA_X_MAX, 0);
				else
					curmove(-ORCA_X_MAX, 0);
				break;
			case Kenq: /* C-e */
				if (shiftdown || mode == Mselect)
					selext(ORCA_X_MAX, 0);
				else
					curmove(ORCA_X_MAX, 0);
				break;
			case Khome:
				if (shiftdown || mode == Mselect)
					selext(0, -ORCA_Y_MAX);
				else
					curmove(0, -ORCA_Y_MAX);
				break;
			case Kend:
				if (shiftdown || mode == Mselect)
					selext(0, ORCA_Y_MAX);
				else
					curmove(0, ORCA_Y_MAX);
				break;
			case 0x12: /* C-r */
				tick = -1;
				forward = true;
				break;
			case 0x13: /* C-s */
				tmp[0] = 0;
				if (filename[0])
					fieldsave(filename);
				else if (enter("file path:", tmp, sizeof(tmp), nil, &kctl, nil) > 0 && fieldsave(tmp) == 0)
					snprint(filename, sizeof(filename), "%s", tmp);
				break;
			case 0x18: /* C-x */
				snapshot();
				selcopy();
				selset('.');
				break;
			case Ketx: /* C-c */
				selcopy();
				break;
			case 0x16: /* C-v */
				snapshot();
				selpaste();
				break;
			case '[':
				rulers.x = MAX(4, rulers.x-1);
				complete = true;
				break;
			case ']':
				rulers.x = MIN(16, rulers.x+1);
				complete = true;
				break;
			case '{':
				rulers.y = MAX(4, rulers.y-1);
				complete = true;
				break;
			case '}':
				rulers.y = MIN(16, rulers.y+1);
				complete = true;
				break;
			case '(':
				snapshot();
				w = snaplow(w, rulers.x);
				break;
			case ')':
				snapshot();
				w = snaphigh(w, rulers.x);
				break;
			case '_':
				snapshot();
				h = snaplow(h, rulers.y);
				break;
			case '+':
				snapshot();
				h = snaphigh(h, rulers.y);
				break;
			case '>':
				apm = ++bpm;
				break;
			case '<':
				apm = bpm = MAX(1, bpm-1);
				break;
			case 0x09: /* C-i */
			case Kins:
				mode = mode != Mappend ? Mappend : Minsert;
				break;
			case 0x0b: /* C-k */
				tmp[0] = 0;
				if (enter("command:", tmp, sizeof(tmp), nil, &kctl, nil) > 0)
					command(tmp, snapshot);
				break;
			case Kesc:
				if (mode == Mslide || mode != Minsert)
					mode = Minsert;
				else {
					sel.min = cur;
					sel.max = cur;
				}
				break;
			case Kack: /* C-f */
				forward = true;
				break;
			case 0x1a: /* C-z */
				undo();
				w = field.width;
				h = field.height;
				break;
			case 0x19:
				redo();
				w = field.width;
				h = field.height;
				break;
			case '`':
			case '~':
			case L'´':
				mode = mode != Mslide ? Mslide : Minsert;
				break;
			case '\'':
				mode = mode != Mselect ? Mselect : Minsert;
				break;
			case Knack: /* C-u */
				snapshot();
				selmap(toupper);
				break;
			case 0x0c: /* C-l */
				snapshot();
				selmap(tolower);
				break;
			case Kbs: /* C-h */
				snapshot();
				if (mode != Mappend) {
					selset('.');
				} else {
					curmove(-1, 0);
					fieldset(cur.x, cur.y, '.');
				}
				break;
			case ' ':
				if (mode != Mappend) {
					pause = !pause;
					break;
				}
			default:
				if (r == Kdel || r == ' ')
					r = '.';
				if (orca_is_valid_glyph(r)) {
					snapshot();
					if (mode != Mappend) {
						selset(r);
					} else {
						fieldset(cur.x, cur.y, r);
						curmove(1, 0);
					}
				} else {
//					fprint(2, "unhandled key %04x\n", r);
					goto noredraw;
				}
				break;
			}
		}

		if (field.width != oldw)
			w = field.width;
		if (field.height != oldh)
			h = field.height;

		if (w != oldw || h != oldh) {
			mbuf_reusable_ensure_size(&mscr, h, w);
			memset(mscr.buffer, 0, w*h);
			for (n = 0; n < oldh; n++)
				memmove(&mscr.buffer[n*w], &mbuf.buffer[n*oldw], MIN(w, oldw));
			mbuf_reusable_ensure_size(&mbuf, h, w);
			memmove(mbuf.buffer, mscr.buffer, w*h);
			linebuf = realloc(linebuf, sizeof(Rune)*MAX(w+1, 64));

			field_copy(&field, &fscr);
			field_resize_raw(&field, h, w);
			memset(field.buffer, '.', w*h);
			gbuffer_copy_subrect(
				fscr.buffer, field.buffer,
				fscr.height, fscr.width, h, w,
				0, 0, 0, 0, MIN(h, fscr.height), MIN(w, fscr.width)
			);
			field_resize_raw(&fscr, h, w);
			curmove(0, 0);

			complete = true;
		}
	}

end:
	chanclose(a[Ckey].c);
	chanclose(a[Credraw].c);
	mbuf_reusable_deinit(&mscr);
	field_deinit(&fscr);
	field_deinit(&fsel);
	free(linebuf);
	close(udp);

	threadexitsall(nil);
}