shithub: lola

ref: 8b40d63248fa6b3895203964adaf95d600956113
dir: /main.c/

View raw version
#include "inc.h"

typedef struct RKeyboardctl RKeyboardctl;
struct RKeyboardctl
{
	Keyboardctl;
	int kbdfd;
};

RKeyboardctl *kbctl;
Mousectl *mctl;
int scrolling = 1;
char *startdir;
int shiftdown;
int gotscreen;
int servekbd;

Screen *wscreen;

void
killprocs(void)
{
	int i;

	for(i = 0; i < nwindows; i++)
		if(windows[i]->notefd >= 0)
			write(windows[i]->notefd, "hangup", 6);
}

/*
 * /dev/snarf updates when the file is closed, so we must open our own
 * fd here rather than use snarffd
 */
void
putsnarf(void)
{
	int fd, i, n;

	if(snarffd<0 || nsnarf==0)
		return;
	fd = open("/dev/snarf", OWRITE|OCEXEC);
	if(fd < 0)
		return;
	/* snarf buffer could be huge, so fprint will truncate; do it in blocks */
	for(i=0; i<nsnarf; i+=n){
		n = nsnarf-i;
		if(n >= 256)
			n = 256;
		if(fprint(fd, "%.*S", n, snarf+i) < 0)
			break;
	}
	close(fd);
}

void
setsnarf(char *s, int ns)
{
	free(snarf);
	snarf = runesmprint("%.*s", ns, s);
	nsnarf = runestrlen(snarf);
	snarfversion++;
}

void
getsnarf(void)
{
	int i, n;
	char *s, *sn;

	if(snarffd < 0)
		return;
	sn = nil;
	i = 0;
	seek(snarffd, 0, 0);
	for(;;){
		if(i > MAXSNARF)
			break;
		if((s = realloc(sn, i+1024+1)) == nil)
			break;
		sn = s;
		if((n = read(snarffd, sn+i, 1024)) <= 0)
			break;
		i += n;
	}
	if(i == 0)
		return;
	sn[i] = 0;
	setsnarf(sn, i);
	free(sn);
}

static int overridecursor;
static Cursor *ovcursor;
static Cursor *normalcursor;
Cursor *cursor;

void
setmousecursor(Cursor *c)
{
	if(cursor == c)
		return;
	cursor = c;
	setcursor(mctl, c);
}

void
setcursoroverride(Cursor *c, int ov)
{
	overridecursor = ov;
	ovcursor = c;
	setmousecursor(overridecursor ? ovcursor : normalcursor);
}

void
setcursornormal(Cursor *c)
{
	normalcursor = c;
	setmousecursor(overridecursor ? ovcursor : normalcursor);
}

char *rcargv[] = { "rc", "-i", nil };

Window*
new(Rectangle r)
{
	Window *w;

	w = wcreate(r, FALSE, scrolling);
	assert(w);
	if(wincmd(w, 0, nil, rcargv) == 0)
		return nil;
	return w;
}

void
drainmouse(Mousectl *mc, Channel *c)
{
	if(c) send(c, &mc->Mouse);
	while(mc->buttons){
		readmouse(mc);
		if(c) send(c, &mc->Mouse);
	}
}

Window*
clickwindow(int but, Mousectl *mc)
{
	Window *w;

	but = 1<<(but-1);
	setcursoroverride(&sightcursor, TRUE);
	drainmouse(mc, nil);
	while(!(mc->buttons & but)){
		readmouse(mc);
		if(mc->buttons & (7^but)){
			setcursoroverride(nil, FALSE);
			drainmouse(mc, nil);
			return nil;
		}
	}
	w = wpointto(mc->xy);
	return w;
}

Rectangle
dragrect(int but, Rectangle r, Mousectl *mc)
{
	Rectangle rc;
	Point start, end;

	but = 1<<(but-1);
	setcursoroverride(&boxcursor, TRUE);
	start = mc->xy;
	end = mc->xy;
	do{
		rc = rectaddpt(r, subpt(end, start));
		drawgetrect(rc, 1);
		readmouse(mc);
		drawgetrect(rc, 0);
		end = mc->xy;
	}while(mc->buttons == but);

	setcursoroverride(nil, FALSE);
	if(mc->buttons & (7^but)){
		rc.min.x = rc.max.x = 0;
		rc.min.y = rc.max.y = 0;
		drainmouse(mc, nil);
	}
	return rc;
}

Rectangle
sweeprect(int but, Mousectl *mc)
{
	Rectangle r, rc;

	but = 1<<(but-1);
	setcursoroverride(&crosscursor, TRUE);
	drainmouse(mc, nil);
	while(!(mc->buttons & but)){
		readmouse(mc);
		if(mc->buttons & (7^but))
			goto Return;
	}
	r.min = mc->xy;
	r.max = mc->xy;
	do{
		rc = canonrect(r);
		drawgetrect(rc, 1);
		readmouse(mc);
		drawgetrect(rc, 0);
		r.max = mc->xy;
	}while(mc->buttons == but);

    Return:
	setcursoroverride(nil, FALSE);
	if(mc->buttons & (7^but)){
		rc.min.x = rc.max.x = 0;
		rc.min.y = rc.max.y = 0;
		drainmouse(mc, nil);
	}
	return rc;
}

int
whichside(int x, int lo, int hi)
{
	return	x < lo+20 ? 0 :
		x > hi-20 ? 2 :
		1;
}

/* 0 1 2
 * 3   5
 * 6 7 8 */
int
whichcorner(Rectangle r, Point p)
{
	int i, j;
	
	i = whichside(p.x, r.min.x, r.max.x);
	j = whichside(p.y, r.min.y, r.max.y);
	return 3*j+i;
}

/* replace corner or edge of rect with point */
Rectangle
changerect(Rectangle r, int corner, Point p)
{
	switch(corner){
	case 0: return Rect(p.x, p.y, r.max.x, r.max.y);
	case 1: return Rect(r.min.x, p.y, r.max.x, r.max.y);
	case 2: return Rect(r.min.x, p.y, p.x+1, r.max.y);
	case 3: return Rect(p.x, r.min.y, r.max.x, r.max.y);
	case 5: return Rect(r.min.x, r.min.y, p.x+1, r.max.y);
	case 6: return Rect(p.x, r.min.y, r.max.x, p.y+1);
	case 7: return Rect(r.min.x, r.min.y, r.max.x, p.y+1);
	case 8: return Rect(r.min.x, r.min.y, p.x+1, p.y+1);
	}
	return r;
}

Rectangle
bandrect(Rectangle r, int but, Mousectl *mc)
{
	Rectangle or, nr;
	int corner, ncorner;

	or = r;
	corner = whichcorner(r, mc->xy);
	setcursornormal(corners[corner]);

	do{
		drawgetrect(r, 1);
		readmouse(mc);
		drawgetrect(r, 0);
		nr = canonrect(changerect(r, corner, mc->xy));
		if(goodrect(nr))
			r = nr;
		ncorner = whichcorner(r, mc->xy);
		/* can switch from edge to corner, but not vice versa */
		if(ncorner != corner && ncorner != 4 && (corner|~ncorner) & 1){
			corner = ncorner;
			setcursornormal(corners[corner]);
		}
	}while(mc->buttons == but);

	if(mc->buttons){
		drainmouse(mctl, nil);
		return or;
	}

	setcursornormal(nil);
	return r;
}

Window*
pick(void)
{
	Window *w1, *w2;

	w1 = clickwindow(3, mctl);
	drainmouse(mctl, nil);
	setcursoroverride(nil, FALSE);
	w2 = wpointto(mctl->xy);
	if(w1 != w2)
		return nil;
	return w1;
}

void
grab(Window *w)
{
	if(w == nil)
		w = clickwindow(3, mctl);
	if(w == nil)
		setcursoroverride(nil, FALSE);
	else{
		Rectangle r = dragrect(3, w->img->r, mctl);
		if(Dx(r) > 0 || Dy(r) > 0){
			wmove(w, r.min);
			wfocus(w);
			flushimage(display, 1);
		}
	}
}

void
sweep(Window *w)
{
	Rectangle r = sweeprect(3, mctl);
	if(goodrect(r)){
		if(w){
			wresize(w, r);
			wraise(w);
			wfocus(w);
		}else{
			new(r);
		}
		flushimage(display, 1);
	}
}

void
bandresize(Window *w)
{
	Rectangle r;
	r = bandrect(w->img->r, mctl->buttons, mctl);
	if(!eqrect(r, w->img->r)){
		wresize(w, r);
		flushimage(display, 1);
	}
}

int
obscured(Window *w, Rectangle r, Window *t)
{
	if(Dx(r) < font->height || Dy(r) < font->height)
		return 1;
	if(!rectclip(&r, screen->r))
		return 1;
	for(; t; t = t->higher){
		if(t->hidden || Dx(t->img->r) == 0 || Dy(t->img->r) == 0 || rectXrect(r, t->img->r) == 0)
			continue;
		if(r.min.y < t->img->r.min.y)
			if(!obscured(w, Rect(r.min.x, r.min.y, r.max.x, t->img->r.min.y), t))
				return 0;
		if(r.min.x < t->img->r.min.x)
			if(!obscured(w, Rect(r.min.x, r.min.y, t->img->r.min.x, r.max.y), t))
				return 0;
		if(r.max.y > t->img->r.max.y)
			if(!obscured(w, Rect(r.min.x, t->img->r.max.y, r.max.x, r.max.y), t))
				return 0;
		if(r.max.x > t->img->r.max.x)
			if(!obscured(w, Rect(t->img->r.max.x, r.min.y, r.max.x, r.max.y), t))
				return 0;
		return 1;
	}
	return 0;
}

/* Check that newly created window will be of manageable size */
int
goodrect(Rectangle r)
{
	if(badrect(r) || !eqrect(canonrect(r), r))
		return 0;
	/* reasonable sizes only please */
	if(Dx(r) > BIG*Dx(screen->r))
		return 0;
	if(Dy(r) > BIG*Dy(screen->r))
		return 0;
	/*
	 * the height has to be big enough to fit one line of text.
	 * that includes the border on each side with an extra pixel
	 * so that the text is still drawn
	 */
	if(Dx(r) < 100 || Dy(r) < 2*(Borderwidth+1)+font->height)
		return 0;
	/* window must be on screen */
	if(!rectXrect(screen->r, r))
		return 0;
	/* must have some screen and border visible so we can move it out of the way */
	if(rectinrect(screen->r, insetrect(r, Borderwidth)))
		return 0;
	return 1;
}

/* Rectangle for new window */
Rectangle
newrect(void)
{
	static int i = 0;
	int minx, miny, dx, dy;

	dx = min(600, Dx(screen->r) - 2*Borderwidth);
	dy = min(400, Dy(screen->r) - 2*Borderwidth);
	minx = 32 + 16*i;
	miny = 32 + 16*i;
	i++;
	i %= 10;

	return Rect(minx, miny, minx+dx, miny+dy);
}


enum {
	Cut,
	Paste,
	Snarf,
	Plumb,
	Look,
	Send,
	Scroll
};
char *menu2str[] = {
	"cut",
	"paste",
	"snarf",
	"plumb",
	"look",
	"send",
	"scroll",
	nil
};
Menu menu2 = { menu2str };

enum {
	New,
	Reshape,
	Move,
	Delete,
	Hide,
	Exit
};
int Hidden = Exit+1;
char *menu3str[7 + nelem(hidden)] = {
	"New",
	"Resize",
	"Move",
	"Delete",
	"Hide",
	"Exit",
	nil
};
Menu menu3 = { menu3str };

void
btn2menu(Window *w)
{
	int sel;
	Text *x;
	Cursor *c;

	x = &w->text;
	menu2str[Scroll] = w->scrolling ? "noscroll" : "scroll";
	sel = menuhit(2, mctl, &menu2, wscreen);
	switch(sel){
	case Cut:
		xsnarf(x);
		xcut(x);
		xscrdraw(x);			// TODO let cut handle this?
		break;
	case Paste:
		xpaste(x);
		break;
	case Snarf:
		xsnarf(x);
		xscrdraw(x);			// TODO let snarf handle this?
		break;
	case Plumb:
		if(xplumb(x, w->dir, fsys.msize-1024)){
			c = cursor;
			setcursoroverride(&query, TRUE);
			sleep(300);
			setcursoroverride(c, FALSE);
		}
		break;
	case Look:
		xlook(x);
		break;
	case Send:
		xsend(x);
		break;
	case Scroll:
		w->scrolling = !w->scrolling;
		if(w->scrolling)
			xshow(x, x->nr);
		break;
	}
	wsendmsg(w, Wakeup);
}

void
btn3menu(void)
{
	Window *w, *t;
	int i, sel;

	nhidden = 0;
	for(i = 0; i < nwindows; i++){
		t = windows[i];
		if(t->hidden || obscured(t, t->img->r, t->higher)){
			hidden[nhidden] = windows[i];
			menu3str[nhidden+Hidden] = windows[i]->label;
			nhidden++;	
		}
	}
	menu3str[nhidden+Hidden] = nil;

	sel = menuhit(3, mctl, &menu3, wscreen);
	switch(sel){
	case New:
		sweep(nil);
		break;
	case Reshape:
		w = pick();
		if(w) sweep(w);
		break;
	case Move:
		grab(nil);
		break;
	case Delete:
		w = pick();
		if(w) wdelete(w);
		break;
	case Hide:
		w = pick();
		if(w) whide(w);
		break;
	case Exit:
		killprocs();
		threadexitsall(nil);
	default:
		if(sel >= Hidden){
			w = hidden[sel-Hidden];
			if(w->hidden)
				wunhide(w);
			else{
				wraise(w);
				wfocus(w);
			}
		}
		break;
	}
}

void
mthread(void*)
{
	Window *w;

	threadsetname("mousethread");
	while(readmouse(mctl) != -1){
		w = wpointto(mctl->xy);
		cursorwin = w;
again:
		if(w == nil){
			setcursornormal(nil);
			if(mctl->buttons & 4)
				btn3menu();
		}else if(!ptinrect(mctl->xy, w->contrect)){
			/* border */
			setcursornormal(corners[whichcorner(w->img->r, mctl->xy)]);
			if(mctl->buttons & 7){
				wraise(w);
				wfocus(w);
				if(mctl->buttons & 4)
					grab(w);
				if(mctl->buttons & 3)
					bandresize(w);
			}
		}else if(w != focused){
			wsetcursor(w);
			if(mctl->buttons & 7 ||
			   mctl->buttons & (8|16) && focused->mouseopen){
				wraise(w);
				wfocus(w);
				if(mctl->buttons & 1)
					drainmouse(mctl, nil);
				else
					goto again;
			}
		}else if(!w->mouseopen){
			wsetcursor(w);
			if(mctl->buttons && topwin != w)
				wraise(w);
			if(mctl->buttons & (1|8|16) || ptinrect(mctl->xy, w->text.scrollr))
				drainmouse(mctl, w->mc.c);
			if(mctl->buttons & 2){
				incref(w);
				btn2menu(w);
				wrelease(w);
			}
			if(mctl->buttons & 4)
				btn3menu();
		}else{
			wsetcursor(w);
			drainmouse(mctl, w->mc.c);
		}
	}
}

void
resthread(void*)
{
	Window *w;
	Rectangle or, nr;
	Point delta;

	threadsetname("resizethread");
	for(;;){
		recvul(mctl->resizec);
		or = screen->clipr;
		if(getwindow(display, Refnone) < 0)
			sysfatal("resize failed: %r");
		nr = screen->clipr;

		freescrtemps();
		freescreen(wscreen);
		wscreen = allocscreen(screen, background, 0);
		draw(screen, screen->r, background, nil, ZP);

		delta = subpt(nr.min, or.min);
		for(w = bottomwin; w; w = w->higher){
			Rectangle r = w->img->r;
			freeimage(w->img);
			w->img = nil;
			wresize(w, rectaddpt(r, delta));
			if(w->hidden)
				originwindow(w->img, w->img->r.min, screen->r.max);
		}

		flushimage(display, 1);
	}
}

static void
_ioproc(void *arg)
{
	int m, n, nerr;
	char buf[1024], *e, *p;
	Rune r;
	RKeyboardctl *kc;

	kc = arg;
	threadsetname("kbdproc");
	n = 0;
	nerr = 0;
	if(kc->kbdfd >= 0){
		while(kc->kbdfd >= 0){
			m = read(kc->kbdfd, buf, sizeof(buf)-1);
			if(m <= 0){
				yield();	/* if error is due to exiting, we'll exit here */
				if(kc->kbdfd < 0)
					break;
				fprint(2, "keyboard: short read: %r\n");
				if(m<0 || ++nerr>10)
					threadexits("read error");
				continue;
			}
			/* one read can return multiple messages, delimited by NUL
			 * split them up for sending on the channel */
			e = buf+m;
			e[-1] = 0;
			e[0] = 0;
			for(p = buf; p < e; p += strlen(p)+1)
				chanprint(kc->c, "%s", p);
		}
	}else{
		while(kc->consfd >= 0){
			m = read(kc->consfd, buf+n, sizeof buf-n);
			if(m <= 0){
				yield();	/* if error is due to exiting, we'll exit here */
				if(kc->consfd < 0)
					break;
				fprint(2, "keyboard: short read: %r\n");
				if(m<0 || ++nerr>10)
					threadexits("read error");
				continue;
			}
			nerr = 0;
			n += m;
			while(n>0 && fullrune(buf, n)){
				m = chartorune(&r, buf);
				n -= m;
				memmove(buf, buf+m, n);
				if(chanprint(kc->c, "c%C", r) < 0)
					break;
			}
		}
	}
	chanfree(kc->c);
	free(kc->file);
	free(kc);
}

RKeyboardctl*
initkbd(char *file, char *kbdfile)
{
	RKeyboardctl *kc;
	char *t;

	if(file == nil)
		file = "/dev/cons";
	if(kbdfile == nil)
		kbdfile = "/dev/kbd";

	kc = mallocz(sizeof(RKeyboardctl), 1);
	if(kc == nil)
		return nil;
	kc->file = strdup(file);
// TODO: handle file == nil
	kc->consfd = open(file, ORDWR|OCEXEC);
	t = malloc(strlen(file)+16);
	if(kc->consfd<0 || t==nil)
		goto Error1;
	sprint(t, "%sctl", file);
	kc->ctlfd = open(t, OWRITE|OCEXEC);
	if(kc->ctlfd < 0){
		fprint(2, "initkeyboard: can't open %s: %r\n", t);
		goto Error2;
	}
	if(ctlkeyboard(kc, "rawon") < 0){
		fprint(2, "initkeyboard: can't turn on raw mode on %s: %r\n", t);
		close(kc->ctlfd);
		goto Error2;
	}
	free(t);
	kc->kbdfd = open(kbdfile, OREAD|OCEXEC);
	kc->c = chancreate(sizeof(char*), 20);
	kc->pid = proccreate(_ioproc, kc, 4096);
	return kc;

Error2:
	close(kc->consfd);
Error1:
	free(t);
	free(kc->file);
	free(kc);
	return nil;
}

/*
 *    kbd    -----+-------> to tap
 *                 \
 *                  \
 * from tap  --------+----> window
 */

Channel *ctltap;	/* open/close */
Channel *resptap;	/* open/close err */
Channel	*fromtap;	/* input from kbd tap program to window */
Channel *totap;		/* our keyboard input to tap program */
Channel *wintap;	/* tell the tapthread which Window to send to */

static int tapseats[] = { [OREAD] Tapoff, [OWRITE] Tapoff };

char*
tapctlmsg(char *msg)
{
	int perm;

	perm = msg[1];
	switch(msg[0]){
	case Tapoff:
		if(perm == ORDWR)
			tapseats[OREAD] = Tapoff, tapseats[OWRITE] = Tapoff;
		else
			tapseats[perm] = Tapoff;
		break;
	case Tapon:
		switch(perm){
		case ORDWR:
			if(tapseats[OREAD] != Tapoff || tapseats[OWRITE] != Tapoff)
				return "seat taken";
			tapseats[OREAD] = Tapon, tapseats[OWRITE] = Tapon;
			break;
		case OREAD: case OWRITE:
			if(tapseats[perm] != Tapoff)
				return "seat taken";
			tapseats[perm] = Tapon;
			break;
		}
		break;
	}
	return nil;
}

void
keyboardtap(void*)
{
	char *s, *ctl;
	char *e;
	char *watched;
	Channel *fschan;
	int n;
	Stringpair pair;
	Window *w, *cur;

	threadsetname("keyboardtap");
	fschan = chancreate(sizeof(Stringpair), 0);
	enum { Awin, Actl, Afrom, Adev, Ato, Ainp, Awatch, NALT };
	Alt alts[NALT+1] = {
		[Awin]	{.c = wintap, .v = &w, .op = CHANRCV},
		[Actl]	{.c = ctltap, .v = &ctl, .op = CHANRCV},
		[Afrom]	{.c = fromtap, .v = &s, .op = CHANRCV},
		[Adev]	{.c = kbctl->c, .v = &s, .op = CHANRCV},
		[Ato]	{.c = totap, .v = &fschan, .op = CHANNOP},
		[Ainp]	{.c = nil, .v = &s, .op = CHANNOP},
		[Awatch]{.c = totap, .v = &fschan, .op = CHANNOP},
		[NALT]	{.op = CHANEND},
	};

	cur = nil;
	watched = nil;
	for(;;)
		switch(alt(alts)){
		case Awin:
			cur = w;
			if(cur != nil){
				alts[Ainp].c = cur->kbd;
				if(tapseats[OREAD] == Tapoff)	
					goto Reset;
				if(alts[Awatch].op == CHANSND)
					free(watched);
				watched = smprint("%c%d", Tapfocus, cur->id);
				alts[Awatch].op = CHANSND;
			}
			if(alts[Ainp].op != CHANNOP || alts[Ato].op != CHANNOP)
				free(s);
			goto Reset;
		case Actl:
			e = tapctlmsg(ctl);
			sendp(resptap, e);
			if(e != nil || *ctl != Tapoff){
				free(ctl);
				break;
			}
			free(ctl);
			goto Reset;
		case Afrom:
			if(cur == nil){
				free(s);
				break;
			}
			alts[Afrom].op = CHANNOP;
			alts[Adev].op = CHANNOP;
			alts[Ato].op = CHANNOP;
			alts[Ainp].op = CHANSND;
			break;
		case Adev:
			if(tapseats[OWRITE] == Tapoff && cur == nil){
				free(s);
				break;
			}
			alts[Afrom].op = CHANNOP;
			alts[Adev].op = CHANNOP;
			if(tapseats[OWRITE] == Tapoff)
				alts[Ainp].op = CHANSND;
			else
				alts[Ato].op = CHANSND;
			break;

		/* These two do the xreq channel dance
		 * ugly... */
		case Ato:
			recv(fschan, &pair);
			n = strlen(s)+1;
			pair.ns = min(n, pair.ns);
			memmove(pair.s, s, pair.ns);
			free(s);
			send(fschan, &pair);
			goto Reset;
		case Awatch:
			recv(fschan, &pair);
			n = strlen(watched)+1;
			pair.ns = min(n, pair.ns);
			memmove(pair.s, watched, pair.ns);
			free(watched);
			send(fschan, &pair);
			alts[Awatch].op = CHANNOP;
			break;

		case Ainp:
			if(*s == 'k' || *s == 'K')
				shiftdown = utfrune(s+1, Kshift) != nil;
		Reset:
			alts[Ainp].op = CHANNOP;
			alts[Ato].op = CHANNOP;
			alts[Afrom].op = CHANRCV;
			alts[Adev].op = CHANRCV;
			break;
		}
}

void
threadmain(int, char *[])
{
	char buf[256];
//rfork(RFENVG);
//newwindow("-dx 1280 -dy 800");

	if(getwd(buf, sizeof(buf)) == nil)
		startdir = estrdup(".");
	else
		startdir = estrdup(buf);
	if(initdraw(nil, nil, "lola") < 0)
		sysfatal("initdraw: %r");
	kbctl = initkbd(nil, nil);
	if(kbctl == nil)
		sysfatal("inikeyboard: %r");
	mctl = initmouse(nil, screen);
	if(mctl == nil)
		sysfatal("initmouse: %r");
	totap = chancreate(sizeof(Channel**), 0);
	fromtap = chancreate(sizeof(char*), 32);
	wintap = chancreate(sizeof(Window*), 0);
	ctltap = chancreate(sizeof(char*), 0);
	resptap = chancreate(sizeof(char*), 0);

	servekbd = kbctl->kbdfd >= 0;
	snarffd = open("/dev/snarf", OREAD|OCEXEC);
	gotscreen = access("/dev/screen", AEXIST)==0;

	initdata();

	wscreen = allocscreen(screen, background, 0);
	draw(screen, screen->r, background, nil, ZP);

	timerinit();


	threadcreate(mthread, nil, mainstacksize);
	threadcreate(resthread, nil, mainstacksize);
	/* proc so mouse keeps working if tap program crashes */
	proccreate(keyboardtap, nil, mainstacksize);

	flushimage(display, 1);

	fs();
	// not reached
}