shithub: orca

ref: 95aa7eb86bc157c28e419968bddc397a3d5117c2
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>

typedef struct Key Key;

#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,

	Cchar = 0,
	Ckey,
	Cmouse,
	Cresize,
	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,
	Dbhigh,
	Dbmed,
	Dblow,
	Dbinv,
	Numcolors,

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

struct Key {
	int down;
	Rune rune;
};

static Rune *linebuf;
static vlong tick;
static int gridw = 8, gridh = 8;
static int rulerstyle = Sfancy, dotstyle = Sfancy;
static int bpm = 120, apm = 120;
static Point cur;
static Rectangle sel;
static int charw, charh;
static Field field;
static Mbuf_reusable mbuf, mscr;
static char filename[256];
static Channel *cchan;
static Field copyfield, selfield;
static bool altdown, pause, forward;
static int mode = Minsert;
static long framedev; /* frame deviation >= 1ms */
static Rune *linebuf;

static char *style[Numstyles] = {
	[Sfancy] = "fancy",
	[Splain] = "plain",
	[Snone] = "no",
};

static Rune dot[Numstyles] = {
	[Sfancy] = L'·',
	[Splain] = '.',
	[Snone] = ' ',
};

static Rune ruler[Numstyles][9] = {
	[Sfancy] = {
		L'┌', L'┬', L'┐',
		L'├', L'┼', L'┤',
		L'└', L'┴', L'┘',
	},
	[Splain] = {
		'+', '+', '+',
		'+', '+', '+',
		'+', '+', '+',
	},
	[Snone] = {
		' ', ' ', ' ',
		' ', ' ', ' ',
		' ', ' ', ' ',
	},
};

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] = 0x444444ff,
	[Dfinv] = 0x000000ff,
	[Dbhigh] = 0xddddddff,
	[Dbmed] = 0x72dec2ff,
	[Dblow] = 0x222222ff,
	[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,
};

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 '?':
		return true;
	}
	return false;
}

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

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

	if (strcmp(s, "play") == 0)
		pause = false;
	else if (strcmp(s, "stop") == 0)
		pause = true;
	else if (strcmp(s, "run") == 0)
		forward = true;
	else if (a != nil) {
		x = atoi(a);

		if (strcmp(s, "bpm") == 0)
			apm = bpm = MAX(1, x);
		else if (strcmp(s, "apm") == 0)
			apm = MAX(1, x);
		else if (strcmp(s, "frame") == 0)
			tick = MAX(0, x);
		else if (strcmp(s, "skip") == 0)
			tick = MAX(0, tick+x);
		else if (strcmp(s, "rewind") == 0)
			tick = MAX(0, tick-x);

		/* FIXME color, find, select, inject, write, time */
	}
}

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

	for (e = events->buffer, i = 0; i < events->count; i++, e++) {
		if (e->any.oevent_type == Oevent_type_midi_note) {
			Oevent_midi_note *n = &e->midi_note;
			u[0] = 1;
			u[1] = 0x90 | n->channel;
			u[2] = (n->octave + 1)*12 + n->note;
			u[3] = n->velocity;
			write(1, u, 4);

			off = n->channel*128 + u[2];
			noteoff[off].u[1] = 0x80 | n->channel;
			noteoff[off].u[2] = u[2];
			noteoff[off].u[3] = 0;
			noteoff[off].at = tick + n->duration;
		} else if (e->any.oevent_type == Oevent_type_cmd_string) {
			Oevent_cmd_string *c = &e->cmd_string;
			memmove(tmp, c->chars, c->count);
			tmp[c->count] = 0;
			command(tmp);
		}
	}

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

/*
 * nsec() is wallclock and can be adjusted by timesync
 * so need to use cycles() instead
 *
 * "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 < 1) {
		if ((f = open("/dev/time", OREAD)) < 0)
			sysfatal("failed to open /dev/time");
		if ((n = read(f, tmp, sizeof(tmp)-1)) < 2)
			sysfatal("failed to read /dev/time");
		tmp[n] = 0;
		e = tmp;
		for (i = 0; i < 3; i++)
			strtoll(e, &e, 10);
		fasthz = strtoll(e, nil, 10);
		if (fasthz < 1)
			sysfatal("failed to read fasthz");
		close(f);
		cycles(&xstart);
	}
	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)
{
	Rectangle r;
	Point p, top, bot;
	int x, y, rx, ry, i;
	int oldbg, oldfg, bg, fg, attr, off;
	bool selected, grouphl;
	char s[32];
	Rune c, csel;

	p = addpt(screen->r.min, Pt(Txtoff, Txtoff));
	top = p;
	bot.x = top.x;
	bot.y = screen->r.max.y - Txtoff - charh*2;

	if (complete) {
		r = screen->r;
		r.max.y = r.min.y + Txtoff;
		draw(screen, r, color[Dback], nil, ZP);
		r = screen->r;
		r.max.x = r.min.x + Txtoff;
		draw(screen, r, color[Dback], nil, ZP);
		r = screen->r;
		r.min.x += MIN(field.width*charw, Dx(r)-Txtoff) + Txtoff;
		draw(screen, r, color[Dback], nil, ZP);
	}

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

	bg = -1;
	fg = -1;
	for (y = 0; y < field.height && p.y < bot.y-charh; y++) {
		p.x = top.x;
		for (x = i = 0; x < field.width && x < screen->r.max.x-Txtoff; 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 = '@';

			if (!grouphl && !complete && c == copyfield.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 += charw;
				continue;
			}

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

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

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

			if (c == dot[dotstyle] && attr == 0) {
				if ((x % gridw) == 0 && (y % gridh) == 0) {
					rx = !!x + (x + 1) / field.width;
					ry = !!y + (y + 1) / field.height;
					c = rulerstyle == Snone ? dot[dotstyle] : ruler[rulerstyle][ry*3+rx];
				}
				fg = 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 = Dfhigh;
					}
				}

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

			if (i > 0 && (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 += charh;
	}

	r = screen->r;
	r.min.y = MIN(top.y + field.height*charh, bot.y-charh);
	draw(screen, r, color[Dback], nil, ZP);

	i = 0;
	sprint(s, "%udx%ud", field.width, field.height);
	i += runesprint(linebuf, "%-10s", s);
	sprint(s, "%d/%d", gridw, gridh);
	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 += charh;

	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)
{
	Field_load_error e;

	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 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 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
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;
	char *s;
	int cols, rows, n;

	if ((b = Bopen("/dev/snarf", OREAD)) != nil) {
		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) {
				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;
		Bterm(b);
	}
}

static Point
ptfield(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 = ptfield(addpt(cur, xy));
	sel.min = ptfield(addpt(sel.min, xy));
	sel.max = ptfield(addpt(sel.max, xy));
}

static void
fieldmodeset(Rune key)
{
	if (mode == Minsert) {
		selset(key);
	} else {
		fieldset(cur.x, cur.x, key);
		curmove(1, 0);
	}
}

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(&selfield, Dy(sel), Dx(sel));
	gbuffer_copy_subrect(
		field.buffer, selfield.buffer,
		field.height, field.width, Dy(sel), Dx(sel),
		sel.min.x, sel.min.y, 0, 0, Dy(sel), Dx(sel)
	);

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

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

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 gridn)
{
	n--;
	n -= (n % gridn) > 0 ? (n % gridn) : gridn;
	return MAX(1, n+1);
}

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

static void
screensize(int *w, int *h)
{
	*w = snaplow((Dx(screen->r) - 2*Txtoff) / charw, gridw);
	*h = snaplow(((Dy(screen->r) - 2*Txtoff) - 3*charh) / charh, gridh);
}

static void
kbdproc(void *k)
{
	char buf[128], buf2[128], *s;
	Channel *kchan;
	int kfd, n;
	Key key;
	Rune r;

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

	kchan = k;
	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);
			/* no break */
		default:
			continue;

		case 'k':
			s = buf+1;
			while (*s){
				s += chartorune(&r, s);
				if (utfrune(buf2+1, r) == nil) {
					key.down = 1;
					key.rune = r;
					nbsend(kchan, &key);
				}
			}
			break;

		case 'K':
			s = buf2+1;
			while (*s) {
				s += chartorune(&r, s);
				if(utfrune(buf+1, r) == nil) {
					key.down = 0;
					key.rune = r;
					nbsend(kchan, &key);
				}
			}
			break;
		}
		strcpy(buf2, buf);
	}
}

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

void
threadmain(int argc, char **argv)
{
	Mousectl *mctl;
	Key key;
	Keyboardctl kctl;
	Mouse m;
	char tmp[256];
	Channel *kchan;
	int oldw, oldh, w, h, n;
	int movex, movey;
	bool shiftdown, complete, ctldown;
	Alt a[Numchan+1] = {
		[Cchar] = { nil, &key.rune, CHANRCV },
		[Ckey] = { nil, &key, CHANRCV },
		[Cmouse] = { nil, &m, CHANRCV },
		[Cresize] = { nil, nil, CHANRCV },
		[Credraw] = { nil, nil, CHANRCV },
		{ nil, nil, CHANEND },
	};

	USED(argc, argv);

	kchan = chancreate(sizeof(Key), 20);
	cchan = chancreate(sizeof(Rune), 20);
	proccreate(kbdproc, kchan, mainstacksize);

	srand(time(0));
	threadsetname("orca/draw");

	if(initdraw(nil, nil, "orca") < 0)
		sysfatal("initdraw: %r");
	if ((mctl = initmouse(nil, screen)) == nil)
		sysfatal("initmouse: %r");
	kctl.c = cchan;
	kctl.file = "/dev/null";
	kctl.consfd = kctl.pid = kctl.ctlfd = -1;

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

	for (n = 0; n < Numcolors; n++)
		color[n] = allocimage(display, Rect(0, 0, 1, 1), RGBA32, 1, setalpha(theme[n] & ~0xff, theme[n] & 0xff));
	charw = stringwidth(font, "X");
	charh = font->height;

	screensize(&w, &h);
	field_init_fill(&field, h, w, '.');
	field_init_fill(&copyfield, h, w, '.');
	field_init(&selfield);

	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;
	movex = 1;
	movey = 1;

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

noredraw:
		switch (alt(a)) {
		case Cmouse:
			if (m.buttons == 4) {
				menu3i[Menu3dotstyle] = tmp;
				menu3i[Menu3rulerstyle] = 1 + menu3i[Menu3dotstyle] + sprintf(tmp, "%s dots", style[(dotstyle+1) % Numstyles]);
				sprintf(menu3i[Menu3rulerstyle], "%s rulers", style[(rulerstyle+1) % Numstyles]);
				n = menuhit(3, mctl, &menu3, nil);
				if (n == Menu3load || n == Menu3save) {
					strncpy(tmp, filename, sizeof(tmp));
					if (enter(n == Menu3load ? "load from:" : "save to:", tmp, sizeof(tmp), mctl, &kctl, nil) > 0) {
						if (n == Menu3load && fieldload(tmp) == 0) {
							w = field.width;
							h = field.height;
							strncpy(filename, tmp, sizeof(filename));
						} else if (n == Menu3save && fieldsave(tmp) == 0) {
							strncpy(filename, tmp, sizeof(filename));
						}
					}
				} 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;
			break;

		case Ckey:
			switch (key.rune) {
			case Kshift:
				shiftdown = key.down;
				break;
			case Kalt:
				altdown = key.down;
				break;
			case Kctl:
				ctldown = key.down;
				movex = ctldown ? gridw : 1;
				movey = ctldown ? gridh : 1;
				break;
			case Kup:
			case Kdown:
			case Kleft:
			case Kright:
				break;
			default:
//				fprint(2, "unknown key down/up 0x%x\n", key.rune);
				break;
			}
			break;

		case Credraw:
			break;

		case Cchar:
			switch (key.rune) {
			case 0x0b: /* C-k */
				movey = 1;
			case Kup:
				if (shiftdown || mode == Mselect)
					selext(0, -movey);
				else {
					if (altdown || mode == Mslide)
						selmove(0, -movey);
					curmove(0, -movey);
				}
				break;
			case '\n': /* C-j */
				movey = 1;
			case Kdown:
				if (shiftdown || mode == Mselect)
					selext(0, movey);
				else {
					if (altdown || mode == Mslide)
						selmove(0, movey);
					curmove(0, movey);
				}
				break;
			case Kbs: /* C-h */
				movex = 1;
			case Kleft:
				if (shiftdown || mode == Mselect) {
					selext(-movex, 0);
				} else {
					if (altdown || mode == Mslide)
						selmove(-movex, 0);
					curmove(-movex, 0);
				}
				break;
			case 0x0c: /* C-l */
				movex = 1;
				if (shiftdown) { /* FIXME this conflicts with vim keys btw */
					selmap(tolower);
					break;
				}
			case Kright:
				if (shiftdown || mode == Mselect)
					selext(movex, 0);
				else {
					if (altdown || mode == Mslide)
						selmove(movex, 0);
					curmove(movex, 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), mctl, &kctl, nil) > 0 && fieldsave(tmp) == 0)
					strncpy(filename, tmp, sizeof(filename));
				break;
			case 0x18: /* C-x */
				selcopy();
				selset('.');
				break;
			case Ketx: /* C-c */
				selcopy();
				break;
			case 0x16: /* C-v */
				selpaste();
				break;
			case '[':
				gridw = MAX(4, gridw-1);
				complete = true;
				break;
			case ']':
				gridw = MIN(16, gridw+1);
				complete = true;
				break;
			case '{':
				gridh = MAX(4, gridh-1);
				complete = true;
				break;
			case '}':
				gridh = MIN(16, gridh+1);
				complete = true;
				break;
			case '(':
				w = snaplow(w, gridw);
				break;
			case ')':
				w = snaphigh(w, gridw);
				break;
			case '_':
				h = snaplow(h, gridh);
				break;
			case '+':
				h = snaphigh(h, gridh);
				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 Kstx:
				tmp[0] = 0;
				if (enter("command:", tmp, sizeof(tmp), mctl, &kctl, nil) > 0)
					command(tmp);
				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 '`':
			case '~':
			case L'´':
				mode = mode != Mslide ? Mslide : Minsert;
				break;
			case '\'':
				mode = mode != Mselect ? Mselect : Minsert;
				break;
			case Knack: /* C-u */
				if (shiftdown)
					selmap(toupper);
				break;
			case ' ':
				if (mode != Mappend) {
					pause = !pause;
					break;
				}
			default:
				if (key.rune == Kdel || key.rune == ' ')
					key.rune = '.';
				if (orca_is_valid_glyph(key.rune) || key.rune == '$') {
					fieldmodeset(key.rune);
				} else {
//					fprint(2, "unhandled key %04x\n", key.rune);
					goto noredraw;
				}
				break;
			}
		}

		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, &copyfield);
			field_resize_raw(&field, h, w);
			memset(field.buffer, '.', w*h);
			gbuffer_copy_subrect(
				copyfield.buffer, field.buffer,
				copyfield.height, copyfield.width, h, w,
				0, 0, 0, 0, MIN(h, copyfield.height), MIN(w, copyfield.width)
			);
			field_resize_raw(&copyfield, h, w);

			complete = true;
		}
	}

end:
	mbuf_reusable_deinit(&mscr);
	field_deinit(&copyfield);
	field_deinit(&selfield);
	free(linebuf);

	threadexitsall(nil);
}