shithub: lola

Download patch

ref: bb604c5cdaa9a6c85d06cb9555e3e428d57b4315
author: demyxology <spicycoldnoodles@gmail.com>
date: Thu Jan 2 17:26:40 EST 2025

"just

--- /dev/null
+++ b/data.c
@@ -1,0 +1,248 @@
+#include "inc.h"
+
+Image *background;
+Image *colors[NumColors];
+
+Cursor whitearrow = {
+	{0, 0},
+	{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFC, 
+	 0xFF, 0xF0, 0xFF, 0xF0, 0xFF, 0xF8, 0xFF, 0xFC, 
+	 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFC, 
+	 0xF3, 0xF8, 0xF1, 0xF0, 0xE0, 0xE0, 0xC0, 0x40, },
+	{0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x06, 0xC0, 0x1C, 
+	 0xC0, 0x30, 0xC0, 0x30, 0xC0, 0x38, 0xC0, 0x1C, 
+	 0xC0, 0x0E, 0xC0, 0x07, 0xCE, 0x0E, 0xDF, 0x1C, 
+	 0xD3, 0xB8, 0xF1, 0xF0, 0xE0, 0xE0, 0xC0, 0x40, }
+};
+
+Cursor query = {
+	{-7,-7},
+	{0x0f, 0xf0, 0x1f, 0xf8, 0x3f, 0xfc, 0x7f, 0xfe, 
+	 0x7c, 0x7e, 0x78, 0x7e, 0x00, 0xfc, 0x01, 0xf8, 
+	 0x03, 0xf0, 0x07, 0xe0, 0x07, 0xc0, 0x07, 0xc0, 
+	 0x07, 0xc0, 0x07, 0xc0, 0x07, 0xc0, 0x07, 0xc0, },
+	{0x00, 0x00, 0x0f, 0xf0, 0x1f, 0xf8, 0x3c, 0x3c, 
+	 0x38, 0x1c, 0x00, 0x3c, 0x00, 0x78, 0x00, 0xf0, 
+	 0x01, 0xe0, 0x03, 0xc0, 0x03, 0x80, 0x03, 0x80, 
+	 0x00, 0x00, 0x03, 0x80, 0x03, 0x80, 0x00, 0x00, }
+};
+
+Cursor crosscursor = {
+	{-7, -7},
+	{0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0,
+	 0x03, 0xC0, 0x03, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF,
+	 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0xC0, 0x03, 0xC0,
+	 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, },
+	{0x00, 0x00, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80,
+	 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x7F, 0xFE,
+	 0x7F, 0xFE, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80,
+	 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x00, 0x00, }
+};
+
+Cursor boxcursor = {
+	{-7, -7},
+	{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+	 0xFF, 0xFF, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F,
+	 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xFF, 0xFF,
+	 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, },
+	{0x00, 0x00, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE,
+	 0x70, 0x0E, 0x70, 0x0E, 0x70, 0x0E, 0x70, 0x0E,
+	 0x70, 0x0E, 0x70, 0x0E, 0x70, 0x0E, 0x70, 0x0E,
+	 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x00, 0x00, }
+};
+
+Cursor sightcursor = {
+	{-7, -7},
+	{0x1F, 0xF8, 0x3F, 0xFC, 0x7F, 0xFE, 0xFB, 0xDF,
+	 0xF3, 0xCF, 0xE3, 0xC7, 0xFF, 0xFF, 0xFF, 0xFF,
+	 0xFF, 0xFF, 0xFF, 0xFF, 0xE3, 0xC7, 0xF3, 0xCF,
+	 0x7B, 0xDF, 0x7F, 0xFE, 0x3F, 0xFC, 0x1F, 0xF8, },
+	{0x00, 0x00, 0x0F, 0xF0, 0x31, 0x8C, 0x21, 0x84,
+	 0x41, 0x82, 0x41, 0x82, 0x41, 0x82, 0x7F, 0xFE,
+	 0x7F, 0xFE, 0x41, 0x82, 0x41, 0x82, 0x41, 0x82,
+	 0x21, 0x84, 0x31, 0x8C, 0x0F, 0xF0, 0x00, 0x00, }
+};
+
+Cursor tl = {
+	{-4, -4},
+	{0xfe, 0x00, 0x82, 0x00, 0x8c, 0x00, 0x87, 0xff, 
+	 0xa0, 0x01, 0xb0, 0x01, 0xd0, 0x01, 0x11, 0xff, 
+	 0x11, 0x00, 0x11, 0x00, 0x11, 0x00, 0x11, 0x00, 
+	 0x11, 0x00, 0x11, 0x00, 0x11, 0x00, 0x1f, 0x00, },
+	{0x00, 0x00, 0x7c, 0x00, 0x70, 0x00, 0x78, 0x00, 
+	 0x5f, 0xfe, 0x4f, 0xfe, 0x0f, 0xfe, 0x0e, 0x00, 
+	 0x0e, 0x00, 0x0e, 0x00, 0x0e, 0x00, 0x0e, 0x00, 
+	 0x0e, 0x00, 0x0e, 0x00, 0x0e, 0x00, 0x00, 0x00, }
+};
+
+Cursor t = {
+	{-7, -8},
+	{0x00, 0x00, 0x00, 0x00, 0x03, 0x80, 0x06, 0xc0, 
+	 0x1c, 0x70, 0x10, 0x10, 0x0c, 0x60, 0xfc, 0x7f, 
+	 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0xff, 0xff, 
+	 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, },
+	{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 
+	 0x03, 0x80, 0x0f, 0xe0, 0x03, 0x80, 0x03, 0x80, 
+	 0x7f, 0xfe, 0x7f, 0xfe, 0x7f, 0xfe, 0x00, 0x00, 
+	 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }
+};
+
+Cursor tr = {
+	{-11, -4},
+	{0x00, 0x7f, 0x00, 0x41, 0x00, 0x31, 0xff, 0xe1, 
+	 0x80, 0x05, 0x80, 0x0d, 0x80, 0x0b, 0xff, 0x88, 
+	 0x00, 0x88, 0x0, 0x88, 0x00, 0x88, 0x00, 0x88, 
+	 0x00, 0x88, 0x00, 0x88, 0x00, 0x88, 0x00, 0xf8, },
+	{0x00, 0x00, 0x00, 0x3e, 0x00, 0x0e, 0x00, 0x1e, 
+	 0x7f, 0xfa, 0x7f, 0xf2, 0x7f, 0xf0, 0x00, 0x70, 
+	 0x00, 0x70, 0x00, 0x70, 0x00, 0x70, 0x00, 0x70, 
+	 0x00, 0x70, 0x00, 0x70, 0x00, 0x70, 0x00, 0x00, }
+};
+
+Cursor r = {
+	{-8, -7},
+	{0x07, 0xc0, 0x04, 0x40, 0x04, 0x40, 0x04, 0x58, 
+	 0x04, 0x68, 0x04, 0x6c, 0x04, 0x06, 0x04, 0x02, 
+	 0x04, 0x06, 0x04, 0x6c, 0x04, 0x68, 0x04, 0x58, 
+	 0x04, 0x40, 0x04, 0x40, 0x04, 0x40, 0x07, 0xc0, },
+	{0x00, 0x00, 0x03, 0x80, 0x03, 0x80, 0x03, 0x80, 
+	 0x03, 0x90, 0x03, 0x90, 0x03, 0xf8, 0x03, 0xfc, 
+	 0x03, 0xf8, 0x03, 0x90, 0x03, 0x90, 0x03, 0x80, 
+	 0x03, 0x80, 0x03, 0x80, 0x03, 0x80, 0x00, 0x00, }
+};
+
+Cursor br = {
+	{-11, -11},
+	{0x00, 0xf8, 0x00, 0x88, 0x00, 0x88, 0x00, 0x88, 
+	 0x00, 0x88, 0x00, 0x88, 0x00, 0x88, 0x00, 0x88, 
+	 0xff, 0x88, 0x80, 0x0b, 0x80, 0x0d, 0x80, 0x05, 
+	 0xff, 0xe1, 0x00, 0x31, 0x00, 0x41, 0x00, 0x7f, },
+	{0x00, 0x00, 0x00, 0x70, 0x00, 0x70, 0x00, 0x70, 
+	 0x0, 0x70, 0x00, 0x70, 0x00, 0x70, 0x00, 0x70, 
+	 0x00, 0x70, 0x7f, 0xf0, 0x7f, 0xf2, 0x7f, 0xfa, 
+	 0x00, 0x1e, 0x00, 0x0e, 0x00, 0x3e, 0x00, 0x00, }
+};
+
+Cursor b = {
+	{-7, -7},
+	{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
+	 0xff, 0xff, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 
+	 0xfc, 0x7f, 0x0c, 0x60, 0x10, 0x10, 0x1c, 0x70, 
+	 0x06, 0xc0, 0x03, 0x80, 0x00, 0x00, 0x00, 0x00, },
+	{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
+	 0x00, 0x00, 0x7f, 0xfe, 0x7f, 0xfe, 0x7f, 0xfe, 
+	 0x03, 0x80, 0x03, 0x80, 0x0f, 0xe0, 0x03, 0x80, 
+	 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }
+};
+
+Cursor bl = {
+	{-4, -11},
+	{0x1f, 0x00, 0x11, 0x00, 0x11, 0x00, 0x11, 0x00, 
+	 0x11, 0x00, 0x11, 0x00, 0x11, 0x00, 0x11, 0x00, 
+	 0x11, 0xff, 0xd0, 0x01, 0xb0, 0x01, 0xa0, 0x01, 
+	 0x87, 0xff, 0x8c, 0x00, 0x82, 0x00, 0xfe, 0x00, },
+	{0x00, 0x00, 0x0e, 0x00, 0x0e, 0x00, 0x0e, 0x00, 
+	 0x0e, 0x00, 0x0e, 0x00, 0x0e, 0x00, 0x0e, 0x00, 
+	 0x0e, 0x00, 0x0f, 0xfe, 0x4f, 0xfe, 0x5f, 0xfe, 
+	 0x78, 0x00, 0x70, 0x00, 0x7c, 0x00, 0x00, 0x0, }
+};
+
+Cursor l = {
+	{-7, -7},
+	{0x03, 0xe0, 0x02, 0x20, 0x02, 0x20, 0x1a, 0x20, 
+	 0x16, 0x20, 0x36, 0x20, 0x60, 0x20, 0x40, 0x20, 
+	 0x60, 0x20, 0x36, 0x20, 0x16, 0x20, 0x1a, 0x20, 
+	 0x02, 0x20, 0x02, 0x20, 0x02, 0x20, 0x03, 0xe0, },
+	{0x00, 0x00, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 
+	 0x09, 0xc0, 0x09, 0xc0, 0x1f, 0xc0, 0x3f, 0xc0, 
+	 0x1f, 0xc0, 0x09, 0xc0, 0x09, 0xc0, 0x01, 0xc0, 
+	 0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x00, 0x00, }
+};
+
+Cursor *corners[9] = {
+	&tl,	&t,	&tr,
+	&l,	nil,	&r,
+	&bl,	&b,	&br,
+};
+
+Image*
+mkcolor(ulong col)
+{
+// TODO: what if we can't get RGBA32?
+//	return allocimage(display, Rect(0,0,1,1), screen->chan, 1, col);
+	return allocimage(display, Rect(0,0,1,1), RGBA32, 1, col);
+}
+
+Image*
+getcolor(char *name, ulong col)
+{
+	Image *img;
+
+	if(name == nil)
+		return mkcolor(col);
+
+	name = smprint("th_%s", name);
+
+	img = namedimage(display, name);
+	if(img == nil){
+		img = mkcolor(col);
+		if(nameimage(img, name, 1) == 0)
+			panic("couldn't name image\n");
+	}
+	free(name);
+	return img;
+}
+
+Image *pal[8];
+
+Image*
+mkicon(char *px, int w, int h)
+{
+	int i, j, idx;
+	Image *img;
+
+	img = allocimage(display, Rect(0,0,w,h), RGBA32, 1, 0x00000000);
+	for(i = 0; i < h; i++)
+		for(j = 0; j < w; j++) {
+			idx = px[i*w + j];
+			if(idx)
+				draw(img, Rect(j,i,j+1,i+1), pal[idx], nil, ZP);
+		}
+	return img;
+}
+
+void
+initdata(void)
+{
+	// this is probably dumb.
+	pal[0] = mkcolor(0x00000000);
+	pal[1] = mkcolor(0x000000FF);
+	pal[2] = mkcolor(0xFFFFFFFF);
+	pal[3] = mkcolor(0xC0C0C0FF);
+	pal[4] = mkcolor(0x808080FF);
+	pal[5] = mkcolor(0x0000FFFF);
+	pal[6] = mkcolor(0x87888FFF);
+	pal[7] = mkcolor(0xC0C7C8FF);
+
+	inittheme();
+
+	/* these might have already been set by the theme */
+	if(background == nil)
+		background = getcolor("background", 0x777777FF);
+	if(colors[BACK] == nil)
+		colors[BACK] = getcolor("back", 0xFFFFFFFF);
+	if(colors[HIGH] == nil)
+		colors[HIGH] = getcolor("high", 0xCCCCCCFF);
+	if(colors[BORD] == nil)
+		colors[BORD] = getcolor("bord", 0x999999FF);
+	if(colors[TEXT] == nil)
+		colors[TEXT] = getcolor("text", 0x000000FF);
+	if(colors[HTEXT] == nil)
+		colors[HTEXT] = getcolor("htext", 0x000000FF);
+	if(colors[PALETEXT] == nil)
+		colors[PALETEXT] = getcolor("paletext", 0x666666FF);
+	if(colors[HOLDTEXT] == nil)
+		colors[HOLDTEXT] = getcolor("holdtext", DMedblue);
+	if(colors[PALEHOLDTEXT] == nil)
+		colors[PALEHOLDTEXT] = getcolor("paleholdtext", DGreyblue);
+}
--- /dev/null
+++ b/deskmenu.c
@@ -1,0 +1,169 @@
+#include "inc.h"
+
+static	Image	*menutxt;
+static	Image	*back;
+static	Image	*high;
+static	Image	*bord;
+static	Image	*text;
+static	Image	*htext;
+
+enum
+{
+	Border = 2,
+	ItemBorder = 1,
+	Itemwidth = 40,
+	Itemheight = 30
+};
+
+static
+void
+menucolors(void)
+{
+	/* Main tone is greenish, with negative selection */
+	back = getcolor("menuback", 0xEAFFEAFF);
+	high = getcolor("menuhigh", 0x448844FF);	/* dark green */
+	bord = getcolor("menubord", 0x88CC88FF);	/* not as dark green */
+	text = getcolor("menutext", 0x000000FF);
+	htext = getcolor("menuhtext", 0xEAFFEAFF);
+	if(back==nil || high==nil || bord==nil || text==nil || htext==nil)
+		goto Error;
+	return;
+
+    Error:
+	freeimage(back);
+	freeimage(high);
+	freeimage(bord);
+	freeimage(text);
+	freeimage(htext);
+	back = display->white;
+	high = display->black;
+	bord = display->black;
+	text = display->black;
+	htext = display->white;
+}
+
+static Rectangle
+menurect(Rectangle r, int i, int j)
+{
+	if(i < 0 || j < 0)
+		return Rect(0, 0, 0, 0);
+	return rectaddpt(Rect(0, 0, Itemwidth, Itemheight),
+		Pt(r.min.x+i*Itemwidth, r.min.y+j*Itemheight));
+}
+
+static void
+paintitem(Image *m, Rectangle contr, int i, int j, int highlight)
+{
+	Rectangle r;
+
+	if(i < 0 || j < 0)
+		return;
+	r = menurect(contr, i, j);
+	draw(m, r, highlight? high : back, nil, ZP);
+	border(m, r, ItemBorder, bord, ZP);
+}
+
+static Point
+menusel(Rectangle r, Point p)
+{
+	if(!ptinrect(p, r))
+		return Pt(-1,-1);
+	return Pt((p.x-r.min.x)/Itemwidth, (p.y-r.min.y)/Itemheight);
+}
+
+
+static Point
+menuscan(Image *m, int but, Mousectl *mc, Rectangle contr)
+{
+	Point ij, lastij;
+
+	lastij = menusel(contr, mc->xy);
+	paintitem(m, contr, lastij.x, lastij.y, 1);
+	readmouse(mc);
+	while(mc->buttons & (1<<(but-1))){
+		ij = menusel(contr, mc->xy);
+		if(!eqpt(ij, lastij))
+			paintitem(m, contr, lastij.x, lastij.y, 0);
+		if(ij.x == -1 || ij.y == -1)
+			return Pt(-1,-1);
+		lastij = ij;
+		paintitem(m, contr, lastij.x, lastij.y, 1);
+
+		readmouse(mc);
+	}
+	return lastij;
+}
+
+static void
+menupaint(Image *m, Rectangle contr, int nx, int ny)
+{
+	int i, j;
+
+	draw(m, contr, back, nil, ZP);
+	for(i = 0; i < nx; i++)
+	for(j = 0; j < ny; j++)
+		paintitem(m, contr, i, j, 0);
+}
+
+static Point
+clampscreen(Rectangle r)
+{
+	Point pt;
+
+	pt = ZP;
+	if(r.max.x>screen->r.max.x)
+		pt.x = screen->r.max.x-r.max.x;
+	if(r.max.y>screen->r.max.y)
+		pt.y = screen->r.max.y-r.max.y;
+	if(r.min.x<screen->r.min.x)
+		pt.x = screen->r.min.x-r.min.x;
+	if(r.min.y<screen->r.min.y)
+		pt.y = screen->r.min.y-r.min.y;
+	return pt;
+}
+
+Point
+dmenuhit(int but, Mousectl *mc, int nx, int ny, Point last)
+{
+	Rectangle r, menur, contr;
+	Point delta;
+	Point sel;
+
+	if(back == nil)
+		menucolors();
+
+	if(last.x < 0) last.x = 0;
+	if(last.x >= nx) last.x = nx-1;
+	if(last.y < 0) last.y = 0;
+	if(last.y >= ny) last.y = ny-1;
+
+	r = insetrect(Rect(0, 0, nx*Itemwidth, ny*Itemheight), -Border);
+	r = rectsubpt(r, Pt(last.x*Itemwidth+Itemwidth/2, last.y*Itemheight+Itemheight/2));
+	menur = rectaddpt(r, mc->xy);
+	delta = clampscreen(menur);
+	menur = rectaddpt(menur, delta);
+	contr = insetrect(menur, Border);
+
+	Image *b, *backup;
+	{
+		b = screen;
+		backup = allocimage(display, menur, screen->chan, 0, -1);
+		draw(backup, menur, screen, nil, menur.min);
+	}
+	draw(b, menur, back, nil, ZP);
+	border(b, menur, Border, bord, ZP);
+	menupaint(b, contr, nx, ny);
+
+	sel = Pt(-1, -1);
+	while(mc->buttons & (1<<(but-1))){
+		sel = menuscan(b, but, mc, contr);
+	}
+
+	if(backup){
+		draw(screen, menur, backup, nil, menur.min);
+		freeimage(backup);
+	}
+	flushimage(display, 1);
+
+	return sel;
+}
--- /dev/null
+++ b/flat.c
@@ -1,0 +1,111 @@
+#include "inc.h"
+
+int bordersz = 4;
+int titlesz = 18;
+int tabsz = 18;
+
+enum {
+	TITLE,
+	LTITLE,
+	TITLEHOLD,
+	LTITLEHOLD,
+	TITLETEXT,
+	LTITLETEXT,
+	TITLEHOLDTEXT,
+	LTITLEHOLDTEXT,
+
+	NumWinColors
+};
+
+Image *wincolors[NumWinColors];
+Image *shadecol;
+
+void
+wdecor(Window *w)
+{
+	if(w->frame == nil)
+		return;
+	int c = wcolsel(w);
+	int tc = TITLE + wcolsel(w);
+
+	Rectangle r;
+	int margin;
+
+	if(!w->noborder){
+		r = w->rect;
+		border(w->frame, r, bordersz, wincolors[tc], ZP);
+	}
+
+	if(!w->notitle){
+		r = w->titlerect;
+		r.max.y = r.min.y + titlesz;
+		draw(w->frame, r, wincolors[tc], nil, ZP);
+
+		margin = w->noborder ? titlesz : titlesz + bordersz;
+		margin = (margin - font->height)/2;
+		Point pt = Pt(r.min.x, w->rect.min.y + margin + 1);
+		if(w->cur)
+			string(w->frame, pt, wincolors[TITLETEXT+c], pt, font, w->cur->label);
+	}
+
+	r = rectsubpt(w->tabrect, Pt(0,1));
+	draw(w->frame, r, wincolors[c], nil, ZP);
+
+	int n = w->ref;
+	if(n > 1){
+		int wd = Dx(r)/n;
+		int xxx = r.max.x;
+		r.max.x = r.min.x + wd;
+		for(WinTab *t = w->tab; t; t = t->next){
+			if(t->next == nil)
+				r.max.x = xxx;
+			if(t != w->cur)
+				draw(w->frame, r, shadecol, nil, ZP);
+			margin = (tabsz - font->height)/2;
+			Point pt = Pt(r.min.x+bordersz/2, r.min.y + margin);
+			string(w->frame, pt, wincolors[TITLETEXT+c], pt, font, t->label);
+			r = rectaddpt(r, Pt(wd,0));
+		}
+	}
+}
+
+void
+wtitlectl(Window *w)
+{
+	if(mctl->buttons & 7){
+		wraise(w);
+		wfocus(w);
+		if(mctl->buttons & 1) {
+			if(!w->maximized)
+				grab(w, 1);
+		}
+		if(mctl->buttons & 4){
+			Window *ww = pick();
+			if(ww){
+				tmigrate(w->cur, ww);
+				wraise(ww);
+				wfocus(ww);
+			}
+		}
+	}
+}
+
+void
+inittheme(void)
+{
+	wincolors[TITLE] = getcolor("title", DGreygreen);
+	wincolors[LTITLE] = getcolor("ltitle", DPalegreygreen);
+//	wincolors[TITLE] = getcolor("title", 0x2F78EDFF);
+//	wincolors[LTITLE] = getcolor("ltitle", 0x7C9DE3FF);
+
+	wincolors[TITLEHOLD] = getcolor("titlehold", DMedblue);
+	wincolors[LTITLEHOLD] = getcolor("ltitlehold", DPalegreyblue);
+
+
+	wincolors[TITLETEXT] = getcolor("titletext", 0xFFFFFFFF);
+	wincolors[LTITLETEXT] = getcolor("ltitletext", 0x808080FF);
+	wincolors[TITLEHOLDTEXT] = getcolor("titleholdtext", 0xFFFFFFFF);
+	wincolors[LTITLEHOLDTEXT] = getcolor("ltitleholdtext", 0xC0C0C0FF);
+
+	shadecol = getcolor(nil, 0x00000020);
+}
--- /dev/null
+++ b/fs.c
@@ -1,0 +1,1067 @@
+#include "inc.h"
+
+enum {
+	Qroot,
+	Qwsys,
+	Qscreen,
+	Qsnarf,
+	Qwctl,
+	Qtap,
+	Qpick,
+	Qglobal = Qpick,	/* last global one */
+
+	/* these need a window */
+	Qcons,
+	Qconsctl,
+	Qcursor,
+	Qwinid,
+	Qwinname,
+	Qlabel,
+	Qkbd,
+	Qmouse,
+	Qtext,
+	Qwdir,
+	Qwindow,
+
+	NQids,
+};
+
+typedef struct Dirent Dirent;
+struct Dirent
+{
+	int path;
+	int type;
+	char *name;
+	uint mode;
+};
+
+Dirent dirents[] = {
+	{ Qroot,	QTDIR,	".",	0500|DMDIR },
+	{ Qwsys,	QTDIR,	"wsys",	0500|DMDIR },
+	{ Qwinid,	QTFILE,	"winid",	0400 },
+	{ Qwinname,	QTFILE,	"winname",	0400 },
+	{ Qwdir,	QTFILE,	"wdir",	0600 },
+	{ Qlabel,	QTFILE,	"label",	0600 },
+	{ Qsnarf,	QTFILE,	"snarf",	0600 },
+	{ Qtext,	QTFILE,	"text",	0600 },
+	{ Qcons,	QTFILE, "cons",	0600 },
+	{ Qconsctl,	QTFILE, "consctl",	0200 },
+	{ Qkbd,	QTFILE, "kbd",	0600 },
+	{ Qmouse,	QTFILE, "mouse",	0600 },
+	{ Qcursor,	QTFILE, "cursor",	0600 },
+	{ Qscreen,	QTFILE, "screen",	0400 },
+	{ Qwindow,	QTFILE, "window",	0400 },
+	{ Qwctl,	QTFILE, "wctl",	0600 },
+	{ Qpick,	QTFILE, "pick",	0400 },
+	{ Qtap,	QTFILE, "kbdtap",	0660 }
+};
+
+char Eperm[] = "permission denied";
+char Eexist[] = "file does not exist";		// XXX
+char Enotdir[] = "not a directory";		// XXX
+char Ebadfcall[] = "bad fcall type";		// XXX
+char Eoffset[] = "illegal offset";
+char Enomem[] = "out of memory";
+
+char Eflush[] =		"interrupted";
+char Einuse[] =		"file in use";
+char Edeleted[] =	"window deleted";
+char Etooshort[] =	"buffer too small";
+char Eshort[] =		"short i/o request";
+char Elong[] = 		"snarf buffer too long";
+char Eunkid[] = 	"unknown id in attach";
+char Ebadrect[] = 	"bad rectangle in attach";		// XXX
+char Ewindow[] = 	"cannot make window";
+char Enowindow[] = 	"window has no image";			// XXX
+char Ebadmouse[] = 	"bad format on /dev/mouse";
+
+int fsysfd;
+char srvpipe[64];
+char *user;
+
+/* Extension of a Req, req->aux. also has a thread. */
+typedef struct Xreq Xreq;
+struct Xreq
+{
+	Req *req;
+	Channel *xc;
+	Channel *flush;		/* cancel read/write */
+	Xreq *next;
+};
+#define XR(req) ((Xreq*)(req)->aux)
+static Xreq *xreqfree;
+
+/* Extension of a Fid, fid->aux */
+typedef struct Xfid Xfid;
+struct Xfid
+{
+	WinTab *w;
+	RuneConvBuf cnv;
+};
+#define XF(fid) ((Xfid*)(fid)->aux)
+
+typedef struct XreqMsg XreqMsg;
+struct XreqMsg
+{
+	Req *r;
+	void (*f)(Req*);
+};
+
+static void
+xreqthread(void *a)
+{
+	Xreq *xr = a;
+	XreqMsg xm;
+
+	threadsetname("xreg.%p", xr);
+	for(;;){
+		recv(xr->xc, &xm);
+		xr->req = xm.r;
+		xm.r->aux = xr;
+		(*xm.f)(xm.r);
+		/* return to pool */
+		xr->req = nil;
+		xr->next = xreqfree;
+		xreqfree = xr;
+	}
+}
+
+static Xreq*
+getxreq(void)
+{
+	Xreq *xr;
+	if(xreqfree){
+		xr = xreqfree;
+		xreqfree = xr->next;
+	}else{
+		xr = emalloc(sizeof(Xreq));
+		xr->xc = chancreate(sizeof(XreqMsg), 0);
+		xr->flush = chancreate(sizeof(int), 0);
+		threadcreate(xreqthread, xr, mainstacksize);
+	}
+	xr->next = nil;
+	return xr;
+}
+
+static void
+toxreq(Req *r, void (*f)(Req*))
+{
+	Xreq *xr;
+	XreqMsg xm;
+
+	xr = getxreq();
+	xm.r = r;
+	xm.f = f;
+	send(xr->xc, &xm);
+}
+
+static Xfid*
+getxfid(WinTab *w)
+{
+	Xfid *xf;
+	xf = emalloc(sizeof(Xfid));
+	memset(&xf->cnv, 0, sizeof(xf->cnv));
+	xf->w = w;
+	if(w)
+		incref(w);
+	return xf;
+}
+
+#define QID(w, q) ((w)<<8|(q))
+#define QWIN(q) ((q)>>8)
+#define QFILE(q) ((q)&0xFF)
+#define ID(w) ((w) ? (w)->id : 0)
+
+static void
+fsattach(Req *r)
+{
+	WinTab *w;
+	char *end;
+	int id;
+	Wctlcmd cmd;
+
+	if(strcmp(r->ifcall.uname, user) != 0){
+		respond(r, Eperm);
+		return;
+	}
+
+	if(strncmp(r->ifcall.aname, "new", 3) == 0){
+		cmd = parsewctl(r->ifcall.aname, ZR);
+		if(cmd.error){
+			respond(r, cmd.error);
+			return;
+		}
+		if(cmd.id > 0){
+			w = wfind(cmd.id);
+			if(w == nil){
+				respond(r, Eunkid);
+				return;
+			}
+			w = tcreate(w->w, cmd.scrolling);
+		}else
+			w = wtcreate(cmd.r, cmd.hidden, cmd.scrolling);
+		if(w == nil){
+			respond(r, Ewindow);
+			return;
+		}
+		wincmd(w, cmd.pid, cmd.dir, nil);
+		flushimage(display, 1);
+		decref(w);	/* don't delete, xfid will take it */
+	}else if(strncmp(r->ifcall.aname, "none", 4) == 0){
+		w = nil;
+	}else if(id = strtol(r->ifcall.aname, &end, 10), *end == '\0'){
+		w = wfind(id);
+		if(w == nil){
+			respond(r, Eunkid);
+			return;
+		}
+	}else{
+		respond(r, Eunkid);
+		return;
+	}
+
+	r->fid->aux = getxfid(w);
+	r->fid->qid = (Qid){QID(ID(w),Qroot),0,QTDIR};
+	r->ofcall.qid = r->fid->qid;
+	respond(r, nil);
+}
+
+static char*
+fsclone(Fid *fid, Fid *newfid)
+{
+	if(XF(fid))
+		newfid->aux = getxfid(XF(fid)->w);
+	return nil;
+}
+
+int
+skipfile(char *name)
+{
+	return gotscreen && strcmp(name, "screen") == 0 ||
+	   snarffd >= 0 && strcmp(name, "snarf") == 0 ||
+	   !servekbd && strcmp(name, "kbd") == 0;
+}
+
+static char*
+fswalk1(Fid *fid, char *name, Qid *qid)
+{
+	int i;
+	Dirent *d;
+	Xfid *xf;
+	WinTab *w;
+	int dir;
+
+	xf = fid->aux;
+	w = xf->w;
+	dir = QFILE(fid->qid.path);
+	if(dir == Qroot){
+		if(strcmp(name, "..") == 0){
+			/* This sucks because we don't know which window we came from
+			 * error out for now */
+			return "vorwärts immer, rückwärts nimmer";
+		}
+		for(i = 0; i < nelem(dirents); i++){
+			d = &dirents[i];
+			if((w || d->path <= Qglobal) &&
+			   !skipfile(d->name) && strcmp(name, d->name) == 0){
+				fid->qid = (Qid){QID(ID(w),d->path), 0, d->type};
+				*qid = fid->qid;
+				return nil;
+			}
+		}
+	}else if(dir == Qwsys){
+		char *end;
+		int id;
+		if(strcmp(name, "..") == 0){
+			fid->qid = (Qid){QID(ID(w),Qroot), 0, QTDIR};
+			*qid = fid->qid;
+			return nil;
+		}
+		if(id = strtol(name, &end, 10), *end == '\0'){
+			w = wfind(id);
+			if(w || id == 0){
+				if(w)
+					incref(w);
+				wrelease(xf->w);
+				xf->w = w;
+				fid->qid = (Qid){QID(ID(w),Qroot), 0, QTDIR};
+				*qid = fid->qid;
+				return nil;
+			}
+		}
+	}
+	return "no such file";
+}
+
+static int
+genrootdir(int n, Dir *d, void *a)
+{
+	WinTab *w = a;
+	int i;
+
+	n++;	/* -1 is root dir */
+	i = 0;
+	while(n--){
+		i++;
+		if(i >= nelem(dirents))
+			return -1;
+		/* we know the last file is never skipped */
+		while(w == nil && dirents[i].path > Qglobal ||
+		      skipfile(dirents[i].name))
+			i++;
+	}
+
+	d->atime = time(nil);
+	d->mtime = d->atime;
+	d->uid = estrdup9p(user);
+	d->gid = estrdup9p(d->uid);
+	d->muid = estrdup9p(d->uid);
+	d->qid = (Qid){QID(ID(w),dirents[i].path), 0, dirents[i].type};
+	if(dirents[i].path == Qsnarf)
+		d->qid.vers = snarfversion;
+	d->mode = dirents[i].mode;
+	d->name = estrdup9p(dirents[i].name);
+	d->length = 0;
+	return 0;
+}
+
+static int
+genwsysdir(int n, Dir *d, void*)
+{
+	WinTab *w;
+
+	if(n == -1){
+		genrootdir(0, d, nil);
+		free(d->name);
+		d->name = estrdup9p("wsys");
+		return 0;
+	}
+	if(n < nwintabs){
+		w = wintabs[n];
+		genrootdir(-1, d, w);
+		free(d->name);
+		d->name = smprint("%d", w->id);
+		return 0;
+	}
+	return -1;
+}
+
+static int ntsnarf;
+static char *tsnarf;
+
+static void
+fsopen(Req *r)
+{
+	Xfid *xf;
+	WinTab *w;
+	int rd, wr;
+
+	xf = XF(r->fid);
+	w = xf->w;
+
+	/* TODO: check and sanitize mode */
+
+	if(w && w->deleted){
+		respond(r, Edeleted);
+		return;
+	}
+
+	/* only text can be truncated (not implemented yet) */
+	if(QFILE(r->fid->qid.path) != Qtext)
+		r->ifcall.mode &= (OREAD|OWRITE|ORDWR);		
+
+	rd = r->ifcall.mode==ORDWR || r->ifcall.mode==OREAD;
+	wr = r->ifcall.mode==ORDWR || r->ifcall.mode==OWRITE;
+	switch(QFILE(r->fid->qid.path)){
+	case Qtext:
+		if(r->ifcall.mode & OTRUNC)
+			xdelete(&w->text, 0, w->text.nr);
+		break;
+
+	case Qsnarf:
+		if(wr)
+			ntsnarf = 0;
+		break;
+
+	case Qconsctl:
+		if(w->consctlopen){
+			respond(r, Einuse);
+			return;
+		}
+		w->consctlopen = TRUE;
+		break;
+
+	case Qkbd:
+		if(w->kbdopen){
+			respond(r, Einuse);
+			return;
+		}
+		w->kbdopen = TRUE;
+		break;
+
+	case Qmouse:
+		if(w->mouseopen){
+			respond(r, Einuse);
+			return;
+		}
+		w->resized = FALSE;
+		w->mouseopen = TRUE;
+		break;
+
+	case Qwctl:
+		if(w && rd){
+			/* can only have one reader of wctl */
+			if(w->wctlopen){
+				respond(r, Einuse);
+				return;
+			}
+			w->wctlopen = TRUE;
+			w->wctlready = TRUE;
+			wsendmsg(w, Wakeup);
+		}
+		break;
+
+	case Qpick:
+		if(xf->w){
+			wrelease(xf->w);
+			xf->w = nil;
+		}
+		/* pick window from main thread.
+		 * TODO: this may not be optimal because
+		 *       it might block this thread. */
+		Channel *wc = chancreate(sizeof(WinTab*), 0);
+		sendp(pickchan, wc);
+		w = recvp(wc);
+		/* actually want the current tab */
+		if(w) w = ((Window*)w)->cur;
+		xf->w = w;
+		if(w)
+			incref(w);
+		chanfree(wc);
+		break;
+
+	case Qtap:
+		if(rd && totap || wr && fromtap){
+			respond(r, Einuse);
+			return;
+		}
+		if(rd){
+			totap = chancreate(sizeof(Channel**), 0);
+			sendp(opentap, totap);
+		}
+		if(wr){
+			fromtap = chancreate(sizeof(char*), 32);
+			sendp(opentap, fromtap);
+		}
+		break;
+	}
+
+	respond(r, nil);
+}
+
+static void
+fsclose(Fid *fid)
+{
+	Xfid *xf;
+	WinTab *w;
+	Text *x;
+	int rd, wr;
+
+	xf = XF(fid);
+	if(xf == nil)
+		return;
+	w = xf->w;
+	x = &w->text;
+
+	rd = fid->omode==ORDWR || fid->omode==OREAD;
+	wr = fid->omode==ORDWR || fid->omode==OWRITE;
+	if(fid->omode != -1)
+	switch(QFILE(fid->qid.path)){
+	/* replace snarf buffer when /dev/snarf is closed */
+	case Qsnarf:
+		if(wr){
+			setsnarf(tsnarf, ntsnarf);
+			ntsnarf = 0;
+		}
+		break;
+
+	case Qconsctl:
+		if(x->rawmode){
+			x->rawmode = 0;
+			wsendmsg(w, Rawoff);
+		}
+		if(w->holdmode > 0){
+			w->holdmode = 1;
+			wsendmsg(w, Holdoff);
+		}
+		w->consctlopen = FALSE;
+		break;
+
+	case Qkbd:
+		w->kbdopen = FALSE;
+		break;
+
+	case Qmouse:
+		w->mouseopen = FALSE;
+		w->resized = FALSE;
+		wsendmsg(w, Refresh);
+		break;
+
+	case Qcursor:
+		w->cursorp = nil;
+		wsetcursor(w);
+		break;
+
+	case Qwctl:
+		if(w && rd)
+			w->wctlopen = FALSE;
+		break;
+
+	case Qtap:
+		if(wr && fromtap)
+			sendp(closetap, fromtap);
+		if(rd && totap)
+			sendp(closetap, totap);
+		break;
+	}
+
+	if(xf->w)
+		wrelease(xf->w);
+	free(xf->cnv.buf);
+	free(xf);
+	fid->aux = nil;
+}
+
+static int
+readimgdata(Image *i, char *t, Rectangle r, int offset, int n)
+{
+	int ww, oo, y, m;
+	uchar *tt;
+
+	ww = bytesperline(r, i->depth);
+	r.min.y += offset/ww;
+	if(r.min.y >= r.max.y)
+		return 0;
+	y = r.min.y + (n + ww-1)/ww;
+	if(y < r.max.y)
+		r.max.y = y;
+	m = ww * Dy(r);
+	oo = offset % ww;
+	if(oo == 0 && n >= m)
+		return unloadimage(i, r, (uchar*)t, n);
+	if((tt = malloc(m)) == nil)
+		return -1;
+	m = unloadimage(i, r, tt, m) - oo;
+	if(m > 0){
+		if(n < m) m = n;
+		memmove(t, tt + oo, m);
+	}
+	free(tt);
+	return m;
+}
+
+/* Fill request from image,
+ * returns only either header or data */
+char*
+readimg(Req *r, Image *img)
+{
+	char *head;
+	char cbuf[30];
+	Rectangle rect;
+	int n;
+
+	rect = img->r;
+	if(r->ifcall.offset < 5*12){
+		head = smprint("%11s %11d %11d %11d %11d ",
+			chantostr(cbuf, img->chan),
+			rect.min.x, rect.min.y, rect.max.x, rect.max.y);
+		readstr(r, head);
+		free(head);
+	}else{
+		/* count is unsigned, so check with n */
+		n = readimgdata(img, r->ofcall.data, rect, r->ifcall.offset-5*12, r->ifcall.count);
+		if(n < 0)
+			return Enomem;
+		r->ofcall.count = n;
+	}
+	return nil;
+}
+
+static char*
+waitblocking(Req *r, Channel *waitchan, Channel **replychan)
+{
+	WinTab *w;
+	enum { Adata, Agone, Aflush, NALT };
+	Alt alts[NALT+1];
+
+	w = XF(r->fid)->w;
+
+	*replychan = nil;
+	alts[Adata] = ALT(waitchan, replychan, CHANRCV);
+	alts[Agone] = w ? ALT(w->gone, nil, CHANRCV)
+			: ALT(nil, nil, CHANNOP);
+	alts[Aflush] = ALT(XR(r)->flush, nil, CHANRCV);
+	alts[NALT].op = CHANEND;
+	switch(alt(alts)){
+	case Adata: return nil;
+	case Agone: return Edeleted;
+	case Aflush: return Eflush;
+	}
+	assert(0);	/* can't happen */
+	return nil;
+}
+
+static char*
+readblocking(Req *r, Channel *readchan)
+{
+	Channel *chan;
+	Stringpair pair;
+	char *err;
+
+	if(err = waitblocking(r, readchan, &chan))
+		return err;
+	pair.s = r->ofcall.data;
+	pair.ns = r->ifcall.count;
+	send(chan, &pair);
+	recv(chan, &pair);
+	r->ofcall.count = min(r->ifcall.count, pair.ns);
+	return nil;
+}
+
+static void
+xread(Req *r)
+{
+	WinTab *w;
+	char *data;
+
+	w = XF(r->fid)->w;
+
+	if(w && w->deleted){
+		respond(r, Edeleted);
+		return;
+	}
+
+	switch(QFILE(r->fid->qid.path)){
+	case Qwinid:
+		data = smprint("%11d ", w->id);
+		readstr(r, data);
+		free(data);
+		break;
+	case Qwinname:
+		readstr(r, w->name);
+		break;
+	case Qlabel:
+		readstr(r, w->label);
+		break;
+	case Qsnarf:
+		data = smprint("%.*S", nsnarf, snarf);
+		readstr(r, data);
+		free(data);
+		break;
+	case Qtext:
+		data = smprint("%.*S", w->text.nr, w->text.r);
+		readstr(r, data);
+		free(data);
+		break;
+	case Qcons:
+		respond(r, readblocking(r, w->consread));
+		return;
+	case Qkbd:
+		respond(r, readblocking(r, w->kbdread));
+		return;
+	case Qmouse:
+		respond(r, readblocking(r, w->mouseread));
+		return;
+	case Qcursor:
+		respond(r, "cursor read not implemented");
+		return;
+	case Qscreen:
+		respond(r, readimg(r, screen));
+		return;
+	case Qwindow:
+		respond(r, readimg(r, w->w->frame));
+		return;
+	case Qwctl:
+/* TODO: what's with the Etooshort conditions?? */
+		if(w == nil){
+			if(r->ifcall.count < 4*12){
+				respond(r, Etooshort);
+				return;
+			}
+//			data = smprint("%11d %11d %11d %11d %11s %11s ",
+			data = smprint("%11d %11d %11d %11d %11d %11d ",
+				screen->r.min.x, screen->r.min.y, screen->r.max.x, screen->r.max.y,
+//				"nowindow", "nowindow");
+				screenoff.x, screenoff.y);
+			readstr(r, data);
+			free(data);
+		}else{
+			if(r->ifcall.count < 4*12){
+				respond(r, Etooshort);
+				return;
+			}
+			respond(r, readblocking(r, w->wctlread));
+			return;
+		}
+		break;
+	case Qpick:
+		data = smprint("%11d ", w ? w->id : -1);
+		readstr(r, data);
+		free(data);
+		break;
+	case Qtap:
+		respond(r, readblocking(r, totap));
+		return;
+	default:
+		respond(r, "cannot read");
+		return;
+	}
+	respond(r, nil);
+}
+
+static void
+xwrite(Req *r)
+{
+	Xfid *xf;
+	WinTab *w;
+	Text *x;
+	vlong offset;
+	u32int count;
+	char *data, *p, *e, *err;
+	Point pt;
+	Channel *chan;
+	Stringpair pair;
+
+	xf = XF(r->fid);
+	w = xf->w;
+	x = &w->text;
+	offset = r->ifcall.offset;
+	count = r->ifcall.count;
+	data = r->ifcall.data;
+	r->ofcall.count = count;
+
+	/* custom emalloc9p allows us this */
+	data[count] = '\0';
+
+	if(w && w->deleted){
+		respond(r, Edeleted);
+		return;
+	}
+	int f = QFILE(r->fid->qid.path);
+	switch(f){
+	case Qtext:
+	case Qcons:
+		if(err = waitblocking(r, w->conswrite, &chan)){
+			respond(r, err);
+			return;
+		}
+		cnvsize(&xf->cnv, count);
+		memmove(xf->cnv.buf+xf->cnv.n, data, count);
+		xf->cnv.n += count;
+		pair = b2r(&xf->cnv);
+		send(chan, &pair);
+		break;
+
+	case Qconsctl:
+		if(strncmp(data, "holdon", 6) == 0){
+			wsendmsg(w, Holdon);
+			break;
+		}
+		if(strncmp(data, "holdoff", 7) == 0){
+			wsendmsg(w, Holdoff);
+			break;
+		}
+		if(strncmp(data, "rawon", 5) == 0){
+			if(w->holdmode){
+				w->holdmode = 1;
+				wsendmsg(w, Holdoff);
+			}
+			if(x->rawmode++ == 0)
+				wsendmsg(w, Rawon);
+			break;
+		}
+		if(strncmp(data, "rawoff", 6) == 0){
+			if(--x->rawmode == 0)
+				wsendmsg(w, Rawoff);
+			break;
+		}
+		respond(r, "unknown control message");
+		return;
+
+	case Qmouse:
+		if(data[0] != 'm' && data[0] != 'M'){
+			respond(r, Ebadmouse);
+			return;
+		}
+		p = nil;
+		pt.x = strtoul(data+1, &p, 0);
+		if(p == nil){
+			respond(r, Eshort);
+			return;
+		}
+		pt.y = strtoul(p, nil, 0);
+		wmovemouse(w->w, pt, data[0] == 'M');
+		break;
+
+	case Qcursor:
+		if(count < 2*4+2*2*16)
+			w->cursorp = nil;
+		else{
+			w->cursor.offset.x = BGLONG(data+0*4);
+			w->cursor.offset.y = BGLONG(data+1*4);
+			memmove(w->cursor.clr, data+2*4, 2*2*16);
+			w->cursorp = &w->cursor;
+		}
+		cursor = (void*)(uintptr)~0;	/* invalide cache */
+		wsetcursor(w);
+		break;
+
+	case Qlabel:
+		if(offset != 0){
+			respond(r, "non-zero offset writing label");
+			return;
+		}
+		wsetlabel(w, data);
+		break;
+
+	case Qsnarf:
+		if(count == 0)
+			break;
+		/* always append only */
+		if(ntsnarf > MAXSNARF){	/* avoid thrashing when people cut huge text */
+			respond(r, Elong);
+			return;
+		}
+		p = realloc(tsnarf, ntsnarf+count);
+		if(p == nil){
+			respond(r, Enomem);
+			return;
+		}
+		tsnarf = p;
+		memmove(tsnarf+ntsnarf, data, count);
+		ntsnarf += count;
+		break;
+
+	case Qwdir:
+		if(count > 0 && data[count-1] == '\n')
+			data[--count] = '\0';
+		if(count == 0)
+			break;
+		/* assume data comes in a single write */
+		if(data[0] == '/')
+			p = smprint("%.*s", count, data);
+		else
+			p = smprint("%s/%.*s", w->dir, count, data);
+		if(p == nil){
+			respond(r, Enomem);
+			return;
+		}
+		free(w->dir);
+		w->dir = cleanname(p);
+		break;
+
+	case Qwctl:
+		respond(r, writewctl(w, data));
+		return;
+
+	case Qtap:
+		if(count < 2){
+			respond(r, "malformed key");
+			return;
+		}
+		e = data + count;
+		for(p = data; p < e; p += strlen(p)+1){
+			switch(*p){
+			case '\0':
+				r->ofcall.count = p - data;
+				respond(r, "null message type");
+				return;
+			case 'z':
+				/* ignore context change */
+				break;
+			default:
+				chanprint(fromtap, "%s", p);
+				break;	
+			}
+		}
+		break;
+
+	default:
+		respond(r, "cannot write");
+		return;
+	}
+	respond(r, nil);
+}
+
+static void
+fsread(Req *r)
+{
+	if((r->fid->qid.type & QTDIR) == 0){
+		toxreq(r, xread);
+		return;
+	}
+
+	switch(QFILE(r->fid->qid.path)){
+	case Qroot:
+		dirread9p(r, genrootdir, XF(r->fid)->w);
+		break;
+	case Qwsys:
+		dirread9p(r, genwsysdir, nil);
+		break;
+	}
+	respond(r, nil);
+}
+
+static void
+fswrite(Req *r)
+{
+	toxreq(r, xwrite);
+}
+
+static void
+fsflush(Req *r)
+{
+	Xreq *xr;
+	int dummy = 0;
+
+	xr = XR(r->oldreq);
+	assert(xr);
+
+	/* TODO: not entirely sure this is right.
+	 * is it possible no-one is listening? */
+	send(xr->flush, &dummy);
+	respond(r, nil);
+}
+
+static void
+fsstat(Req *r)
+{
+	int f;
+
+	f = QFILE(r->fid->qid.path);
+	genrootdir(f-1, &r->d, XF(r->fid)->w);
+	respond(r, nil);
+}
+
+Srv fsys = {
+	.attach		fsattach,
+	.open		fsopen,
+	.read		fsread,
+	.write		fswrite,
+	.stat		fsstat,
+	.flush		fsflush,
+	.walk1		fswalk1,
+	.clone		fsclone,
+	.destroyfid	fsclose,
+	nil
+};
+
+static Ioproc *io9p;
+
+/* copy & paste from /sys/src/libc/9sys/read9pmsg.c
+ * changed to use ioreadn instead of readn */
+int
+read9pmsg(int fd, void *abuf, uint n)
+{
+	int m, len;
+	uchar *buf;
+
+	buf = abuf;
+
+	/* read count */
+	m = ioreadn(io9p, fd, buf, BIT32SZ);
+	if(m != BIT32SZ){
+		if(m < 0)
+			return -1;
+		return 0;
+	}
+
+	len = GBIT32(buf);
+	if(len <= BIT32SZ || len > n){
+		werrstr("bad length in 9P2000 message header");
+		return -1;
+	}
+	len -= BIT32SZ;
+	m = ioreadn(io9p, fd, buf+BIT32SZ, len);
+	if(m < len)
+		return 0;
+	return BIT32SZ+m;
+}
+
+/* +1 so we can always zero-terminate a write buffer */
+void *emalloc9p(ulong sz) { return emalloc(sz+1); }
+void *erealloc9p(void *v, ulong sz) { return erealloc(v, sz+1); }
+char *estrdup9p(char *s) { return estrdup(s); }
+
+void
+post(char *name, int srvfd)
+{
+	char buf[80];
+	int fd;
+
+	snprint(buf, sizeof buf, "/srv/%s", name);
+	fd = create(buf, OWRITE|ORCLOSE|OCEXEC, 0600);
+	if(fd < 0)
+		panic(buf);
+	if(fprint(fd, "%d", srvfd) < 0)
+		panic("post");
+	putenv("wsys", buf);
+	/* leave fd open */
+}
+
+/*
+ * Build pipe with OCEXEC set on second fd.
+ * Can't put it on both because we want to post one in /srv.
+ */
+int
+cexecpipe(int *p0, int *p1)
+{
+	/* pipe the hard way to get close on exec */
+	if(bind("#|", "/mnt/temp", MREPL) == -1)
+		return -1;
+	*p0 = open("/mnt/temp/data", ORDWR);
+	*p1 = open("/mnt/temp/data1", ORDWR|OCEXEC);
+	unmount(nil, "/mnt/temp");
+	if(*p0<0 || *p1<0)
+		return -1;
+	return 0;
+}
+
+static void
+srvthread(void*)
+{
+	threadsetname("fs");
+	srv(&fsys);
+}
+
+void
+startfs(void)
+{
+	io9p = ioproc();
+
+	if(cexecpipe(&fsysfd, &fsys.infd) < 0)
+		panic("pipe");
+	fsys.outfd = fsys.infd;
+	user = getuser();
+	snprint(srvpipe, sizeof(srvpipe), "lola.%s.%lud", user, (ulong)getpid());
+	post(srvpipe, fsysfd);
+//	chatty9p++;
+	threadcreate(srvthread, nil, mainstacksize);
+}
+
+int
+fsmount(int id)
+{	char buf[32];
+
+	close(fsys.infd);	/* close server end so mount won't hang if exiting */
+	snprint(buf, sizeof buf, "%d", id);
+	if(mount(fsysfd, -1, "/mnt/wsys", MREPL, buf) == -1){
+		fprint(2, "mount failed: %r\n");
+		return -1;
+	}
+	if(bind("/mnt/wsys", "/dev", MBEFORE) == -1){
+		fprint(2, "bind failed: %r\n");
+		return -1;
+	}
+	return 0;
+}
--- /dev/null
+++ b/inc.h
@@ -1,0 +1,397 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <keyboard.h>
+#include <mouse.h>
+#include <cursor.h>
+#include <frame.h>
+#include <fcall.h>
+#include <9p.h>
+#include <complete.h>
+#include <plumb.h>
+
+#include <ctype.h>
+
+typedef uchar bool;
+enum {
+	FALSE = 0,
+	TRUE = 1,
+
+	BIG = 3,
+	MAXWINDOWS = 1000
+};
+
+#define ALT(c, v, t) (Alt){ c, v, t, nil, nil, 0 }
+
+#define CTRL(c) ((c)&0x1F)
+
+
+typedef struct RKeyboardctl RKeyboardctl;
+struct RKeyboardctl
+{
+	Keyboardctl;
+	int kbdfd;
+};
+RKeyboardctl *initkbd(char *file, char *kbdfile);
+
+
+extern Rune *snarf;
+extern int nsnarf;
+extern int snarfversion;
+extern int snarffd;
+enum { MAXSNARF = 100*1024 };
+void putsnarf(void);
+void getsnarf(void);
+void setsnarf(char *s, int ns);
+
+typedef struct Text Text;
+struct Text
+{
+	Frame;
+	Rectangle scrollr, lastsr;
+	Image *i;
+	Rune *r;
+	uint nr;
+	uint maxr;
+	uint org;	/* start of Frame's text */
+	uint q0, q1;	/* selection */
+	uint qh;	/* host point, output here */
+
+	/* not entirely happy with this in here */
+	bool rawmode;
+	Rune *raw;
+	int nraw;
+
+	int posx;
+};
+
+void xinit(Text *x, Rectangle textr, Rectangle scrollr, int tabwidth, Font *ft, Image *b, Image **cols);
+void xsetrects(Text *x, Rectangle textr, Rectangle scrollr);
+void xclear(Text *x);
+void xredraw(Text *x);
+void xcoldraw(Text *x);
+void xfullredraw(Text *x);
+uint xinsert(Text *x, Rune *r, int n, uint q0);
+void xfill(Text *x);
+void xdelete(Text *x, uint q0, uint q1);
+void xsetselect(Text *x, uint q0, uint q1);
+void xselect(Text *x, Mousectl *mc);
+void xscrdraw(Text *x);
+void xscroll(Text *x, Mousectl *mc, int but);
+void xscrolln(Text *x, int n);
+void xshow(Text *x, uint q0);
+void xplacetick(Text *x, uint q);
+void xtype(Text *x, Rune r);
+int xninput(Text *x);
+void xaddraw(Text *x, Rune *r, int nr);
+void xlook(Text *x);
+void xsnarf(Text *x);
+void xcut(Text *x);
+void xpaste(Text *x);
+void xsend(Text *x);
+int xplumb(Text *w, char *src, char *dir, int maxsize);
+
+enum
+{
+	// NCOL is defined by libframe, add more after it
+	PALETEXT = NCOL,
+	HOLDTEXT,
+	PALEHOLDTEXT,
+
+	NumColors
+};
+
+extern Image *background;
+extern Image *colors[NumColors];
+extern Cursor whitearrow;
+extern Cursor query;
+extern Cursor crosscursor;
+extern Cursor boxcursor;
+extern Cursor sightcursor;
+extern Cursor *corners[9];
+void initdata(void);
+
+extern int tabwidth;
+extern bool scrolling;
+extern bool notitle;
+extern int ndeskx;
+extern int ndesky;
+
+extern Screen *wscreen;
+extern Image *fakebg;
+extern Mousectl *mctl;
+extern char *startdir;
+extern bool shiftdown, ctldown;
+extern bool gotscreen;
+extern bool servekbd;
+
+
+typedef struct RuneConvBuf RuneConvBuf;
+struct RuneConvBuf
+{
+	char *buf;
+	int maxbuf;	// allocated size
+	int nb;		// size
+	int n;		// filled
+};
+
+typedef struct Stringpair Stringpair;
+struct Stringpair	/* rune and nrune or byte and nbyte */
+{
+	void		*s;
+	int		ns;
+};
+
+typedef struct Mousestate Mousestate;
+struct Mousestate
+{
+	Mouse;
+	ulong	counter;	/* serial no. of mouse event */
+};
+
+typedef struct Mousequeue Mousequeue;
+struct Mousequeue
+{
+	Mousestate	q[16];
+	int	ri;	/* read index into queue */
+	int	wi;	/* write index */
+	ulong	counter;	/* serial no. of last mouse event we received */
+	ulong	lastcounter;	/* serial no. of last mouse event sent to client */
+	int	lastb;	/* last button state we received */
+	bool	full;	/* filled the queue; no more recording until client comes back */	
+};
+
+typedef struct Queue Queue;
+struct Queue
+{
+	char *q[32];
+	int ri;
+	int wi;
+	bool full;
+};
+int qadd(Queue *q, char *data);
+char *qget(Queue *q);
+int qempty(Queue *q);
+
+enum
+{
+	Resized,
+	Deleted,
+	Refresh,
+	Holdon,
+	Holdoff,
+	Rawon,
+	Rawoff,
+	Wakeup
+};
+
+extern int bordersz;
+extern int titlesz;
+extern int tabsz;
+
+typedef struct Window Window;
+typedef struct WinTab WinTab;
+
+struct Window
+{
+	Ref;
+	Window *lower;
+	Window *higher;
+	bool hidden;
+	Image *frame;
+	Screen *screen;
+	int noborder;
+	bool notitle;
+	bool maximized;
+	bool sticky;
+	Rectangle rect;
+	Rectangle titlerect;
+	Rectangle tabrect;
+	Rectangle contrect;
+	Rectangle scrollr;
+	Rectangle textr;
+	Rectangle origrect;
+
+	// tmp
+	WinTab *tab;
+	WinTab *cur;
+};
+
+struct WinTab
+{
+	Ref;
+	bool deleted;
+	Window *w;
+	WinTab *next;
+	Image *content;
+	int id;
+	char name[32];
+	int namecount;
+	char *label;
+	int notefd;
+	char *dir;
+
+	Text text;
+	int holdmode;
+	bool scrolling;
+	bool wctlready;
+	bool wctlopen;
+
+	Mousectl mc;
+	Mousequeue mq;
+	int mouseopen;
+	int resized;
+
+	Cursor *cursorp;
+	Cursor cursor;
+
+	Channel *kbd;
+	Queue kq;
+	bool consctlopen;
+	bool kbdopen;
+
+	Channel *gone;		/* window gone */
+	Channel *ctl;		/* Wctlmesg */
+	/* channels to xreqs */
+	Channel *conswrite;
+	Channel *consread;
+	Channel *kbdread;
+	Channel *mouseread;
+	Channel *wctlread;
+	Channel *complete;
+
+	char *threadname;	/* for debugging */
+};
+
+extern Window *bottomwin, *topwin;
+extern Window *windows[MAXWINDOWS];
+extern int nwindows;
+extern WinTab *wintabs[MAXWINDOWS];
+extern int nwintabs;
+extern Window *focused, *cursorwin;
+extern Point screenoff;
+
+Window *wcreate(Rectangle r, bool hidden);
+int wcolsel(Window *w);
+void wtitlectl(Window *w);
+void wdecor(Window *w);
+void wmaximize(Window *w);
+void wrestore(Window *w);
+void wresize(Window *w, Rectangle r);
+void wrecreate(Window *w);
+Window *wpointto(Point pt);
+void wdelete(Window *w);
+void wmove(Window *w, Point pos);
+void wraise(Window *w);
+void wlower(Window *w);
+void wfocus(Window *w);
+void wunfocus(Window *w);
+int whide(Window *w);
+int wunhide(Window *w);
+void wmovemouse(Window *w, Point pt, bool force);
+
+void wrelease(WinTab *w);
+void wsendmsg(WinTab *w, int type);
+WinTab *wfind(int id);
+void wsetcursor(WinTab *w);
+void wsetlabel(WinTab *w, char *label);
+void wsetname(WinTab *w);
+void wsetpid(WinTab *w, int pid, int dolabel);
+void wsethold(WinTab *w, int hold);
+void wtype(WinTab *w, Rune r);
+int wincmd(WinTab *w, int pid, char *dir, char **argv);
+
+WinTab *tcreate(Window *w, bool scrolling);
+void tfocus(WinTab *t);
+void tdelete(WinTab *t);
+void tmigrate(WinTab *t, Window *w);
+void tmoveleft(WinTab *t);
+void tmoveright(WinTab *t);
+
+WinTab *wtcreate(Rectangle r, bool hidden, bool scrolling);
+
+void screenoffset(int offx, int offy);
+
+typedef struct Wctlcmd Wctlcmd;
+struct Wctlcmd
+{
+	int cmd;
+	Rectangle r;
+	char *args;
+	int pid;
+	int id;
+	bool hidden;
+	bool scrolling;
+	char *dir;
+	char *error;
+};
+
+Wctlcmd parsewctl(char *s, Rectangle r);
+char *writewctl(WinTab *w, char *data);
+
+
+extern Cursor *cursor;
+void setcursoroverride(Cursor *c, int ov);
+void setcursornormal(Cursor *c);
+
+Rectangle newrect(void);
+int goodrect(Rectangle r);
+Rectangle centerrect(Rectangle r, Rectangle s);
+void borderTL(Image *img, Rectangle r, Image *c);
+void borderBR(Image *img, Rectangle r, Image *c);
+void winborder(Image *img, Rectangle r, Image *c1, Image *c2);
+
+void refresh(void);
+Point dmenuhit(int but, Mousectl *mc, int nx, int ny, Point last);
+void drainmouse(Mousectl *mc, WinTab *w);
+Window *pick(void);
+void grab(Window *w, int btn);
+void btn3menu(void);
+
+void inittheme(void);
+Image *getcolor(char *name, ulong defcol);
+Image *mkicon(char *px, int w, int h);
+
+
+extern Channel *opentap;	/* open fromtap or totap */
+extern Channel *closetap;	/* close fromtap or totap */
+extern Channel *fromtap;	/* input from kbd tap program to window */
+extern Channel *totap;		/* our keyboard input to tap program */
+
+extern Channel *pickchan;
+
+
+extern Srv fsys;
+void startfs(void);
+int fsmount(int id);
+
+#define	runemalloc(n)		malloc((n)*sizeof(Rune))
+#define	runerealloc(a, n)	realloc(a, (n)*sizeof(Rune))
+#define	runemove(a, b, n)	memmove(a, b, (n)*sizeof(Rune))
+#define min(a, b)	((a) < (b) ? (a) : (b))
+#define max(a, b)	((a) > (b) ? (a) : (b))
+
+void panic(char *s);
+void *emalloc(ulong size);
+void *erealloc(void *p, ulong size);
+char *estrdup(char *s);
+int handlebs(Stringpair *pair);
+void cnvsize(RuneConvBuf *cnv, int nb);
+int r2bfill(RuneConvBuf *cnv, Rune *rp, int nr);
+void r2bfinish(RuneConvBuf *cnv, Stringpair *pair);
+Stringpair b2r(RuneConvBuf *cnv);
+
+
+typedef struct Timer Timer;
+struct Timer
+{
+	int		dt;
+	int		cancel;
+	Channel	*c;	/* chan(int) */
+	Timer	*next;
+};
+void timerinit(void);
+Timer *timerstart(int dt);
+void timerstop(Timer *t);
+void timercancel(Timer *t);
--- /dev/null
+++ b/kbd.c
@@ -1,0 +1,107 @@
+#include "inc.h"
+
+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;
+}
--- /dev/null
+++ b/main.c
@@ -1,0 +1,1100 @@
+#include "inc.h"
+
+int tabwidth;
+bool scrolling;
+bool notitle;
+int ndeskx = 3;
+int ndesky = 3;
+
+RKeyboardctl *kbctl;
+Mousectl *mctl;
+char *startdir;
+bool shiftdown, ctldown;
+bool gotscreen;
+bool servekbd;
+
+Screen *wscreen;
+Image *fakebg;
+
+Channel *pickchan;
+
+void
+killprocs(void)
+{
+	int i;
+
+	for(i = 0; i < nwintabs; i++)
+		if(wintabs[i]->notefd >= 0)
+			write(wintabs[i]->notefd, "hangup", 6);
+}
+
+static char *oknotes[] ={
+	"delete",
+	"hangup",
+	"kill",
+	"exit",
+	nil,	// for debugging
+	nil
+};
+
+int
+notehandler(void*, char *msg)
+{
+	int i;
+
+	killprocs();
+	for(i = 0; oknotes[i]; i++)
+		if(strncmp(oknotes[i], msg, strlen(oknotes[i])) == 0)
+			threadexitsall(msg);
+	fprint(2, "lola %d: abort: %s\n", getpid(), msg);
+	abort();
+}
+
+/*
+ * /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 };
+
+WinTab*
+new(Rectangle r)
+{
+	WinTab *w;
+
+	w = wtcreate(r, FALSE, scrolling);
+	assert(w);
+	if(wincmd(w, 0, nil, rcargv) == 0)
+		return nil;
+	return w;
+}
+
+WinTab*
+newtab(Window *ww)
+{
+	WinTab *w;
+
+	w = tcreate(ww, scrolling);
+	assert(w);
+	if(wincmd(w, 0, nil, rcargv) == 0)
+		return nil;
+	return w;
+}
+
+
+void
+drainmouse(Mousectl *mc, WinTab *w)
+{
+	if(w) send(w->mc.c, &mc->Mouse);
+	while(mc->buttons){
+		readmouse(mc);
+		/* stop sending once focus changes.
+		 * buttons released in wfocus() */
+		if(w && w->w != focused) w = nil;
+		if(w) send(w->mc.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, int btn)
+{
+	if(w == nil)
+		w = clickwindow(btn, mctl);
+	if(w == nil)
+		setcursoroverride(nil, FALSE);
+	else{
+		Rectangle r = dragrect(btn, w->frame->r, mctl);
+		if((Dx(r) > 0 || Dy(r) > 0) && !eqrect(r, w->frame->r)){
+			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);
+	}
+
+//TODO(tab): temp hack
+	else{
+		Window *ww = wpointto(r.min);
+		if(w == nil && ww)
+			newtab(ww);
+	}
+}
+
+void
+bandresize(Window *w)
+{
+	Rectangle r;
+	r = bandrect(w->frame->r, mctl->buttons, mctl);
+	if(!eqrect(r, w->frame->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->frame->r) == 0 || Dy(t->frame->r) == 0 || rectXrect(r, t->frame->r) == 0)
+			continue;
+		if(r.min.y < t->frame->r.min.y)
+			if(!obscured(w, Rect(r.min.x, r.min.y, r.max.x, t->frame->r.min.y), t))
+				return 0;
+		if(r.min.x < t->frame->r.min.x)
+			if(!obscured(w, Rect(r.min.x, r.min.y, t->frame->r.min.x, r.max.y), t))
+				return 0;
+		if(r.max.y > t->frame->r.max.y)
+			if(!obscured(w, Rect(r.min.x, t->frame->r.max.y, r.max.x, r.max.y), t))
+				return 0;
+		if(r.max.x > t->frame->r.max.x)
+			if(!obscured(w, Rect(t->frame->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*(bordersz+1)+font->height)
+		return 0;
+//TODO(vdesk) this changes
+	/* 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, bordersz)))
+		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*bordersz);
+	dy = min(400, Dy(screen->r) - 2*bordersz);
+	minx = 32 + 16*i;
+	miny = 32 + 16*i;
+	i++;
+	i %= 10;
+
+	return Rect(minx, miny, minx+dx, miny+dy);
+}
+
+
+void
+btn2menu(WinTab *w)
+{
+	enum {
+		Cut,
+		Paste,
+		Snarf,
+		Plumb,
+		Look,
+		Send,
+		Scroll
+	};
+	static char *str[] = {
+		"cut",
+		"paste",
+		"snarf",
+		"plumb",
+		"look",
+		"send",
+		"scroll",
+		nil
+	};
+	static Menu menu = { str };
+
+	int sel;
+	Text *x;
+	Cursor *c;
+
+	x = &w->text;
+	str[Scroll] = w->scrolling ? "noscroll" : "scroll";
+	sel = menuhit(2, mctl, &menu, wscreen);
+	switch(sel){
+	case Cut:
+		xsnarf(x);
+		xcut(x);
+		break;
+	case Paste:
+		xpaste(x);
+		break;
+	case Snarf:
+		xsnarf(x);
+		break;
+	case Plumb:
+		if(xplumb(x, "lola", 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)
+{
+	enum {
+		New,
+		Reshape,
+		Move,
+		Delete,
+		Hide,
+		Hidden
+	};
+	static char *str[Hidden+1 + MAXWINDOWS] = {
+		"New",
+		"Resize",
+		"Move",
+		"Delete",
+		"Hide",
+		nil
+	};
+	static Menu menu = { str };
+
+	static Window *hidden[MAXWINDOWS];
+	int nhidden;
+	Window *w, *t;
+	int i, sel;
+
+	nhidden = 0;
+	for(i = 0; i < nwindows; i++){
+		t = windows[i];
+		if(!rectXrect(screen->r, t->frame->r))
+			continue;
+		if(t->hidden || obscured(t, t->frame->r, t->higher)){
+			hidden[nhidden] = windows[i];
+			str[nhidden+Hidden] = windows[i]->cur->label;
+			nhidden++;	
+		}
+	}
+	str[nhidden+Hidden] = nil;
+
+	sel = menuhit(3, mctl, &menu, wscreen);
+	switch(sel){
+	case New:
+		sweep(nil);
+		break;
+	case Reshape:
+		w = pick();
+		if(w) sweep(w);
+		break;
+	case Move:
+		grab(nil, 3);
+		break;
+	case Delete:
+		w = pick();
+		if(w) wdelete(w);
+		break;
+	case Hide:
+		w = pick();
+		if(w) whide(w);
+		break;
+	default:
+		if(sel >= Hidden){
+			w = hidden[sel-Hidden];
+			if(w->hidden)
+				wunhide(w);
+			else{
+				wraise(w);
+				wfocus(w);
+			}
+		}
+		break;
+	}
+}
+
+void
+btn13menu(void)
+{
+	enum {
+		RefreshScreen,
+		Scroll,
+		Title,
+		Exit
+	};
+	static char *str[] = {
+		"Refresh",
+		"Scroll",
+		"Title",
+		"Exit",
+		nil
+	};
+	static Menu menu = { str };
+
+	str[Scroll] = scrolling ? "!Scroll" : "Scroll";
+	str[Title] = notitle ? "Title" : "!Title";
+	switch(menuhit(3, mctl, &menu, wscreen)){
+	case RefreshScreen:
+		refresh();
+		break;
+	case Scroll:
+		scrolling = !scrolling;
+		break;
+	case Title:
+		notitle = !notitle;
+		break;
+	case Exit:
+		killprocs();
+		threadexitsall(nil);
+	}
+}
+
+void
+btn12menu(void)
+{
+	int dx, dy, i, j;
+
+	dx = Dx(screen->r);
+	dy = Dy(screen->r);
+	i = screenoff.x/dx;
+	j = screenoff.y/dy;
+	Point ssel = dmenuhit(2, mctl, ndeskx, ndesky, Pt(i,j));
+	if(ssel.x >= 0 && ssel.y >= 0 && 
+	   (ssel.x*dx != screenoff.x || ssel.y*dy != screenoff.y))
+		screenoffset(ssel.x*dx, ssel.y*dy);
+}
+
+static void
+wtabctl(Window *w)
+{
+	Rectangle r;
+
+	if(mctl->buttons & 7){
+		wraise(w);
+		wfocus(w);
+		r = w->tabrect;
+		int n = w->ref;
+		if(n > 1){
+			int wd = Dx(r)/n;
+			r.max.x = r.min.x + wd;
+			for(WinTab *t = w->tab; t; t = t->next){
+				if(ptinrect(mctl->xy, r)){
+					if(mctl->buttons & 1){
+						tfocus(t);
+						/* chording */
+						while(mctl->buttons){
+							int b = mctl->buttons;
+							if(b & 6){
+								if(b & 2)
+									tmoveleft(t);
+								else
+									tmoveright(t);
+							}
+							while(mctl->buttons == b)
+								readmouse(mctl);
+						}
+					}else if(mctl->buttons & 2){
+						tdelete(t);
+						while(mctl->buttons)
+							readmouse(mctl);
+					}else if(mctl->buttons & 4){
+						Point pt = mctl->xy;
+						Window *ww = pick();
+						if(ww){
+							/* move tab into clicked window */
+							tmigrate(t, ww);
+							wraise(ww);
+							wfocus(ww);
+						}else{
+							/* HACK: pick doesn't say whether we cancelled
+							 * or clicked background */
+							ww = wpointto(mctl->xy);
+							if(ww == nil){
+								r = rectaddpt(w->rect, subpt(mctl->xy, pt));
+								ww = wcreate(r, 0);
+								tmigrate(t, ww);
+							}
+						}
+						return;
+					}
+					break;
+				}
+				r = rectaddpt(r, Pt(wd,0));
+			}
+		}
+	}
+}
+
+void
+mthread(void*)
+{
+	Window *w;
+	Channel *wc;
+
+	threadsetname("mousethread");
+	enum { Amouse, Apick, NALT };
+	Alt alts[NALT+1] = {
+		[Amouse]	{.c = mctl->c, .v = &mctl->Mouse, .op = CHANRCV},
+		[Apick]		{.c = pickchan, .v = &wc, .op = CHANRCV},
+		[NALT]		{.op = CHANEND},
+	};
+	for(;;){
+		// normally done in readmouse
+		Display *d = mctl->image->display;
+		if(d->bufp > d->buf)
+			flushimage(d, 1);
+		switch(alt(alts)){
+		case Apick:
+			sendp(wc, pick());
+			break;
+		case Amouse:
+			w = wpointto(mctl->xy);
+			cursorwin = w;
+again:
+			if(w == nil){
+				/* background */
+				setcursornormal(nil);
+				while(mctl->buttons & 1){
+					if(mctl->buttons & 2)
+						btn12menu();
+					else if(mctl->buttons & 4)
+						btn13menu();
+					readmouse(mctl);
+				}
+				if(mctl->buttons & 4)
+					btn3menu();
+			}else if(!ptinrect(mctl->xy, w->contrect)){
+				/* decoration */
+				if(!w->noborder &&
+				   !ptinrect(mctl->xy, insetrect(w->frame->r, bordersz))){
+					/* border */
+					setcursornormal(corners[whichcorner(w->frame->r, mctl->xy)]);
+					if(mctl->buttons & 7){
+						wraise(w);
+						wfocus(w);
+						if(mctl->buttons & 4)
+							grab(w, 3);
+						if(mctl->buttons & 3)
+							bandresize(w);
+					}
+				}else{
+					/* title bar */
+					setcursornormal(nil);
+					if(ptinrect(mctl->xy, w->titlerect))
+						wtitlectl(w);
+					else
+						wtabctl(w);
+				}
+			}else if(w->cur == nil){
+				/* no tab in window */
+			}else if(w != focused){
+				/* inactive window */
+				wsetcursor(w->cur);
+				if(mctl->buttons & 7 ||
+				   mctl->buttons & (8|16) && focused && focused->cur->mouseopen){
+					wraise(w);
+					wfocus(w);
+					if(mctl->buttons & 1)
+						drainmouse(mctl, nil);
+					else
+						goto again;
+				}
+			}else if(!w->cur->mouseopen){
+				/* active text window */
+				wsetcursor(w->cur);
+				if(mctl->buttons && topwin != w)
+					wraise(w);
+				if(mctl->buttons & (1|8|16) || ptinrect(mctl->xy, w->cur->text.scrollr))
+					drainmouse(mctl, w->cur);
+				if(mctl->buttons & 2){
+					incref(w->cur);
+					btn2menu(w->cur);
+					wrelease(w->cur);
+				}
+				if(mctl->buttons & 4)
+					btn3menu();
+			}else{
+				/* active graphics window */
+				wsetcursor(w->cur);
+				drainmouse(mctl, w->cur);
+			}
+		}
+	}
+}
+
+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;
+
+		freeimage(fakebg);
+		freescreen(wscreen);
+		wscreen = allocscreen(screen, background, 0);
+		fakebg = allocwindow(wscreen, screen->r, Refbackup, DNofill);
+		draw(fakebg, fakebg->r, background, nil, ZP);
+
+		delta = subpt(nr.min, or.min);
+		for(w = bottomwin; w; w = w->higher){
+			if(w->maximized){
+				wrestore(w);
+				wresize(w, rectaddpt(w->frame->r, delta));
+				wmaximize(w);
+			}else
+				wresize(w, rectaddpt(w->frame->r, delta));
+		}
+
+		flushimage(display, 1);
+	}
+}
+
+void
+refresh(void)
+{
+	Window *w;
+
+	draw(fakebg, fakebg->r, background, nil, ZP);
+	for(w = bottomwin; w; w = w->higher){
+		if(w->maximized){
+			wrestore(w);
+			wresize(w, w->frame->r);
+			wmaximize(w);
+		}else
+			wresize(w, w->frame->r);
+	}
+}
+
+/*
+ *    kbd    -----+-------> to tap
+ *                 \
+ *                  \
+ * from tap  --------+----> window
+ */
+
+Channel *opentap;	/* open fromtap or totap */
+Channel *closetap;	/* close fromtap or totap */
+Channel *fromtap;	/* input from kbd tap program to window */
+Channel *totap;		/* our keyboard input to tap program */
+
+void
+keyboardtap(void*)
+{
+	char *s, *z;
+	Channel *fschan, *chan;
+	int n;
+	Stringpair pair;
+	WinTab *cur, *prev;
+	Queue tapq;
+
+	threadsetname("keyboardtap");
+
+	fschan = chancreate(sizeof(Stringpair), 0);
+	enum { Akbd, Afromtap, Atotap, Aopen, Aclose,  NALT };
+	Alt alts[NALT+1] = {
+		[Akbd]		{.c = kbctl->c, .v = &s, .op = CHANRCV},
+		[Afromtap]	{.c = nil, .v = &s, .op = CHANNOP},
+		[Atotap]	{.c = nil, .v = &fschan, .op = CHANNOP},
+		[Aopen]		{.c = opentap, .v = &chan, .op = CHANRCV},
+		[Aclose]	{.c = closetap, .v = &chan, .op = CHANRCV},
+		[NALT]		{.op = CHANEND},
+	};
+
+	memset(&tapq, 0, sizeof(tapq));
+	cur = nil;
+	for(;;){
+		if(alts[Atotap].c && !qempty(&tapq))
+			alts[Atotap].op = CHANSND;
+		else
+			alts[Atotap].op = CHANNOP;
+		switch(alt(alts)){
+		case Akbd:
+			/* from keyboard to tap or to window */
+			if(*s == 'k' || *s == 'K'){
+				shiftdown = utfrune(s+1, Kshift) != nil;
+				ctldown = utfrune(s+1, Kctl) != nil;
+			}
+			prev = cur;
+			cur = focused ? focused->cur : nil;
+			if(totap){
+				if(cur != prev && cur){
+					/* notify tap of focus change */
+					z = smprint("z%d", cur->id);
+					if(!qadd(&tapq, z))
+						free(z);
+				}
+				/* send to tap */
+				if(qadd(&tapq, s))
+					break;
+				/* tap is wedged, send directly instead */
+			}
+			if(cur)
+				sendp(cur->kbd, s);
+			else
+				free(s);
+			break;
+
+		case Afromtap:
+			/* from tap to window */
+			if(cur && focused && cur == focused->cur)
+				sendp(cur->kbd, s);
+			else
+				free(s);
+			break;
+
+		case Atotap:
+			/* send queued up messages */
+			recv(fschan, &pair);
+			s = qget(&tapq);
+			n = strlen(s)+1;
+			pair.ns = min(n, pair.ns);
+			memmove(pair.s, s, pair.ns);
+			free(s);
+			send(fschan, &pair);
+			break;
+
+		case Aopen:
+			if(chan == fromtap){
+				alts[Afromtap].c = fromtap;
+				alts[Afromtap].op = CHANRCV;
+			}
+			if(chan == totap)
+				alts[Atotap].c = totap;
+			break;
+
+		case Aclose:
+			if(chan == fromtap){
+				fromtap = nil;
+				alts[Afromtap].c = nil;
+				alts[Afromtap].op = CHANNOP;
+				// TODO: empty chan
+			}
+			if(chan == totap){
+				totap = nil;
+				alts[Atotap].c = nil;
+				alts[Atotap].op = CHANNOP;
+				while(!qempty(&tapq))
+					free(qget(&tapq));
+			}
+			chanfree(chan);
+			break;
+		}
+	}
+}
+
+void
+initcmd(void *arg)
+{
+	char *cmd;
+	char *wsys;
+	int fd;
+
+	cmd = arg;
+	rfork(RFENVG|RFFDG|RFNOTEG|RFNAMEG);
+	wsys = getenv("wsys");
+	fd = open(wsys, ORDWR);
+	if(fd < 0)
+		fprint(2, "lola: failed to open wsys: %r\n");
+	if(mount(fd, -1, "/mnt/wsys", MREPL, "none") < 0)
+		fprint(2, "lola: failed to mount wsys: %r\n");
+	if(bind("/mnt/wsys", "/dev/", MBEFORE) < 0)
+		fprint(2, "lola: failed to bind wsys: %r\n");
+	free(wsys);
+	close(fd);
+	procexecl(nil, "/bin/rc", "rc", "-c", cmd, nil);
+	fprint(2, "lola: exec failed: %r\n");
+	exits("exec");
+}
+
+void
+usage(void)
+{
+	fprint(2, "usage: lola [-i initcmd] [-s] [-t]\n");
+	exits("usage");
+}
+
+void
+threadmain(int argc, char *argv[])
+{
+	char *initstr, *s;
+	char buf[256];
+if(strcmp(argv[0]+1, ".out") == 0){
+rfork(RFENVG);
+newwindow("-dx 1280 -dy 800");
+scrolling = TRUE;
+notitle = FALSE;
+oknotes[nelem(oknotes)-2] = "interrupt";
+}
+
+	initstr = nil;
+	ARGBEGIN{
+	case 'i':
+		initstr = EARGF(usage());
+		break;
+	case 's':
+		scrolling = TRUE;
+		break;
+	case 't':
+		notitle = TRUE;
+		break;
+	default:
+		usage();
+	}ARGEND
+
+	if(getwd(buf, sizeof(buf)) == nil)
+		startdir = estrdup(".");
+	else
+		startdir = estrdup(buf);
+	s = getenv("tabstop");
+	if(s)
+		tabwidth = strtol(s, nil, 0);
+	if(tabwidth == 0)
+		tabwidth = 4;
+	free(s);
+
+	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");
+	opentap = chancreate(sizeof(Channel*), 0);
+	closetap = chancreate(sizeof(Channel*), 0);
+
+	pickchan = chancreate(sizeof(Channel*), 0);
+
+	servekbd = kbctl->kbdfd >= 0;
+	snarffd = open("/dev/snarf", OREAD|OCEXEC);
+	gotscreen = access("/dev/screen", AEXIST)==0;
+
+	initdata();
+	/* hack to get menu colors referenced,
+	 * so setting them with initstr will work */
+	btn12menu();
+
+	wscreen = allocscreen(screen, background, 0);
+	fakebg = allocwindow(wscreen, screen->r, Refbackup, DNofill);
+	draw(fakebg, fakebg->r, background, nil, ZP);
+
+	timerinit();
+
+
+	threadcreate(mthread, nil, mainstacksize);
+	threadcreate(resthread, nil, mainstacksize);
+	threadcreate(keyboardtap, nil, mainstacksize);
+
+	flushimage(display, 1);
+
+	startfs();
+
+	if(initstr)
+		proccreate(initcmd, initstr, mainstacksize);
+
+	threadnotify(notehandler, 1);
+}
--- /dev/null
+++ b/menuhit.c
@@ -1,0 +1,280 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <mouse.h>
+
+enum
+{
+	Margin = 4,		/* outside to text */
+	Border = 2,		/* outside to selection boxes */
+	Blackborder = 2,	/* width of outlining border */
+	Vspacing = 2,		/* extra spacing between lines of text */
+	Maxunscroll = 25,	/* maximum #entries before scrolling turns on */
+	Nscroll = 20,		/* number entries in scrolling part */
+	Scrollwid = 14,		/* width of scroll bar */
+	Gap = 4,			/* between text and scroll bar */
+};
+
+static	Image	*menutxt;
+static	Image	*back;
+static	Image	*high;
+static	Image	*bord;
+static	Image	*text;
+static	Image	*htext;
+
+Image *getcolor(char *name, ulong col);
+
+static
+void
+menucolors(void)
+{
+	/* Main tone is greenish, with negative selection */
+	back = getcolor("menuback", 0xEAFFEAFF);
+	high = getcolor("menuhigh", 0x448844FF);	/* dark green */
+	bord = getcolor("menubord", 0x88CC88FF);	/* not as dark green */
+	text = getcolor("menutext", 0x000000FF);
+	htext = getcolor("menuhtext", 0xEAFFEAFF);
+	if(back==nil || high==nil || bord==nil || text==nil || htext==nil)
+		goto Error;
+	return;
+
+    Error:
+	freeimage(back);
+	freeimage(high);
+	freeimage(bord);
+	freeimage(text);
+	freeimage(htext);
+	back = display->white;
+	high = display->black;
+	bord = display->black;
+	text = display->black;
+	htext = display->white;
+}
+
+/*
+ * r is a rectangle holding the text elements.
+ * return the rectangle, including its black edge, holding element i.
+ */
+static Rectangle
+menurect(Rectangle r, int i)
+{
+	if(i < 0)
+		return Rect(0, 0, 0, 0);
+	r.min.y += (font->height+Vspacing)*i;
+	r.max.y = r.min.y+font->height+Vspacing;
+	return insetrect(r, Border-Margin);
+}
+
+/*
+ * r is a rectangle holding the text elements.
+ * return the element number containing p.
+ */
+static int
+menusel(Rectangle r, Point p)
+{
+	if(!ptinrect(p, r))
+		return -1;
+	return (p.y-r.min.y)/(font->height+Vspacing);
+}
+
+static void
+paintitem(Image *m, Menu *menu, Rectangle textr, int off, int i, int highlight, Image *save, Image *restore)
+{
+	char *item;
+	Rectangle r;
+	Point pt;
+
+	if(i < 0)
+		return;
+	r = menurect(textr, i);
+	if(restore){
+		draw(m, r, restore, nil, restore->r.min);
+		return;
+	}
+	if(save)
+		draw(save, save->r, m, nil, r.min);
+	item = menu->item? menu->item[i+off] : (*menu->gen)(i+off);
+	pt.x = (textr.min.x+textr.max.x-stringwidth(font, item))/2;
+	pt.y = textr.min.y+i*(font->height+Vspacing);
+	draw(m, r, highlight? high : back, nil, pt);
+	string(m, pt, highlight? htext : text, pt, font, item);
+}
+
+/*
+ * menur is a rectangle holding all the highlightable text elements.
+ * track mouse while inside the box, return what's selected when button
+ * is raised, -1 as soon as it leaves box.
+ * invariant: nothing is highlighted on entry or exit.
+ */
+static int
+menuscan(Image *m, Menu *menu, int but, Mousectl *mc, Rectangle textr, int off, int lasti, Image *save)
+{
+	int i;
+
+	paintitem(m, menu, textr, off, lasti, 1, save, nil);
+	for(readmouse(mc); mc->buttons & (1<<(but-1)); readmouse(mc)){
+		i = menusel(textr, mc->xy);
+		if(i != -1 && i == lasti)
+			continue;
+		paintitem(m, menu, textr, off, lasti, 0, nil, save);
+		if(i == -1)
+			return i;
+		lasti = i;
+		paintitem(m, menu, textr, off, lasti, 1, save, nil);
+	}
+	return lasti;
+}
+
+static void
+menupaint(Image *m, Menu *menu, Rectangle textr, int off, int nitemdrawn)
+{
+	int i;
+
+	draw(m, insetrect(textr, Border-Margin), back, nil, ZP);
+	for(i = 0; i<nitemdrawn; i++)
+		paintitem(m, menu, textr, off, i, 0, nil, nil);
+}
+
+static void
+menuscrollpaint(Image *m, Rectangle scrollr, int off, int nitem, int nitemdrawn)
+{
+	Rectangle r;
+
+	draw(m, scrollr, back, nil, ZP);
+	r.min.x = scrollr.min.x;
+	r.max.x = scrollr.max.x;
+	r.min.y = scrollr.min.y + (Dy(scrollr)*off)/nitem;
+	r.max.y = scrollr.min.y + (Dy(scrollr)*(off+nitemdrawn))/nitem;
+	if(r.max.y < r.min.y+2)
+		r.max.y = r.min.y+2;
+	border(m, r, 1, bord, ZP);
+	if(menutxt == 0)
+		menutxt = allocimage(display, Rect(0, 0, 1, 1), screen->chan, 1, DDarkgreen);	/* border color; BUG? */
+	if(menutxt)
+		draw(m, insetrect(r, 1), menutxt, nil, ZP);
+}
+
+int
+menuhit(int but, Mousectl *mc, Menu *menu, Screen *scr)
+{
+	int i, nitem, nitemdrawn, maxwid, lasti, off, noff, wid, screenitem;
+	int scrolling;
+	Rectangle r, menur, sc, textr, scrollr;
+	Image *b, *save, *backup;
+	Point pt;
+	char *item;
+
+	if(back == nil)
+		menucolors();
+	sc = screen->clipr;
+	replclipr(screen, 0, screen->r);
+	maxwid = 0;
+	for(nitem = 0;
+	    item = menu->item? menu->item[nitem] : (*menu->gen)(nitem);
+	    nitem++){
+		i = stringwidth(font, item);
+		if(i > maxwid)
+			maxwid = i;
+	}
+	if(menu->lasthit<0 || menu->lasthit>=nitem)
+		menu->lasthit = 0;
+	screenitem = (Dy(screen->r)-10)/(font->height+Vspacing);
+	if(nitem>Maxunscroll || nitem>screenitem){
+		scrolling = 1;
+		nitemdrawn = Nscroll;
+		if(nitemdrawn > screenitem)
+			nitemdrawn = screenitem;
+		wid = maxwid + Gap + Scrollwid;
+		off = menu->lasthit - nitemdrawn/2;
+		if(off < 0)
+			off = 0;
+		if(off > nitem-nitemdrawn)
+			off = nitem-nitemdrawn;
+		lasti = menu->lasthit-off;
+	}else{
+		scrolling = 0;
+		nitemdrawn = nitem;
+		wid = maxwid;
+		off = 0;
+		lasti = menu->lasthit;
+	}
+	r = insetrect(Rect(0, 0, wid, nitemdrawn*(font->height+Vspacing)), -Margin);
+	r = rectsubpt(r, Pt(wid/2, lasti*(font->height+Vspacing)+font->height/2));
+	r = rectaddpt(r, mc->xy);
+	pt = ZP;
+	if(r.max.x>screen->r.max.x)
+		pt.x = screen->r.max.x-r.max.x;
+	if(r.max.y>screen->r.max.y)
+		pt.y = screen->r.max.y-r.max.y;
+	if(r.min.x<screen->r.min.x)
+		pt.x = screen->r.min.x-r.min.x;
+	if(r.min.y<screen->r.min.y)
+		pt.y = screen->r.min.y-r.min.y;
+	menur = rectaddpt(r, pt);
+	textr.max.x = menur.max.x-Margin;
+	textr.min.x = textr.max.x-maxwid;
+	textr.min.y = menur.min.y+Margin;
+	textr.max.y = textr.min.y + nitemdrawn*(font->height+Vspacing);
+	if(scrolling){
+		scrollr = insetrect(menur, Border);
+		scrollr.max.x = scrollr.min.x+Scrollwid;
+	}else
+		scrollr = Rect(0, 0, 0, 0);
+
+	if(scr){
+		b = allocwindow(scr, menur, Refbackup, DWhite);
+		if(b == nil)
+			b = screen;
+		backup = nil;
+	}else{
+		b = screen;
+		backup = allocimage(display, menur, screen->chan, 0, -1);
+		if(backup)
+			draw(backup, menur, screen, nil, menur.min);
+	}
+	draw(b, menur, back, nil, ZP);
+	border(b, menur, Blackborder, bord, ZP);
+	save = allocimage(display, menurect(textr, 0), screen->chan, 0, -1);
+	r = menurect(textr, lasti);
+	if(pt.x || pt.y)
+		moveto(mc, divpt(addpt(r.min, r.max), 2));
+	menupaint(b, menu, textr, off, nitemdrawn);
+	if(scrolling)
+		menuscrollpaint(b, scrollr, off, nitem, nitemdrawn);
+	while(mc->buttons & (1<<(but-1))){
+		lasti = menuscan(b, menu, but, mc, textr, off, lasti, save);
+		if(lasti >= 0)
+			break;
+		while(!ptinrect(mc->xy, textr) && (mc->buttons & (1<<(but-1)))){
+			if(scrolling && ptinrect(mc->xy, scrollr)){
+				noff = ((mc->xy.y-scrollr.min.y)*nitem)/Dy(scrollr);
+				noff -= nitemdrawn/2;
+				if(noff < 0)
+					noff = 0;
+				if(noff > nitem-nitemdrawn)
+					noff = nitem-nitemdrawn;
+				if(noff != off){
+					off = noff;
+					menupaint(b, menu, textr, off, nitemdrawn);
+					menuscrollpaint(b, scrollr, off, nitem, nitemdrawn);
+				}
+			}
+			readmouse(mc);
+		}
+	}
+	if(b != screen)
+		freeimage(b);
+	if(backup){
+		draw(screen, menur, backup, nil, menur.min);
+		freeimage(backup);
+	}
+	freeimage(save);
+	replclipr(screen, 0, sc);
+	flushimage(display, 1);
+	if(lasti >= 0){
+		menu->lasthit = lasti+off;
+		return menu->lasthit;
+	}
+	return -1;
+}
--- /dev/null
+++ b/mkfile
@@ -1,0 +1,22 @@
+< /$objtype/mkfile
+
+TARG=lola
+OFILES=\
+	main.$O \
+	text.$O \
+	wind.$O \
+	wctl.$O \
+	fs.$O \
+	util.$O \
+	kbd.$O \
+	time.$O \
+	data.$O \
+	menuhit.$O \
+	deskmenu.$O \
+	win95.$O
+
+HFILES=inc.h
+
+BIN=$home/bin/$objtype
+
+< /sys/src/cmd/mkone
--- /dev/null
+++ b/simple.c
@@ -1,0 +1,257 @@
+#include "inc.h"
+
+int bordersz = 4;
+int titlesz = 17;//19;
+int tabsz = 18;
+
+enum {
+	TITLE,
+	LTITLE,
+	TITLEHOLD,
+	LTITLEHOLD,
+	TITLETEXT,
+	LTITLETEXT,
+	TITLEHOLDTEXT,
+	LTITLEHOLDTEXT,
+	FRAME,
+	LFRAME,
+
+	NumWinColors
+};
+
+Image *wincolors[NumWinColors];
+Image *icons[4];
+
+Image *shadecol;
+
+void
+btn(Image *img, Rectangle r, Image *col, Image *icon, int down)
+{
+	USED(down);
+
+	r = centerrect(r, icon->r);
+	draw(img, r, col, icon, ZP);
+}
+
+int
+btnctl(Image *img, Rectangle r, Image *col, Image *icon)
+{
+	int over, prevover;
+
+	prevover = 1;
+	btn(img, r, col, icon, 1);
+	while(mctl->buttons){
+		readmouse(mctl);
+		over = ptinrect(mctl->xy, r);
+		if(over != prevover)
+			btn(img, r, col, icon, over);
+		prevover = over;
+	}
+	if(prevover)
+		btn(img, r, col, icon, 0);
+	return ptinrect(mctl->xy, r);
+}
+
+void
+wdecor(Window *w)
+{
+	if(w->frame == nil)
+		return;
+	int sel = wcolsel(w);
+	int c = TITLE + sel;
+	int c1 = TITLETEXT + sel;
+	int c2 = FRAME + (sel&1);
+
+	Rectangle r, b1, b2, b3;
+	int margin;
+
+	if(!w->noborder){
+		r = w->rect;
+		border(w->frame, r, bordersz, wincolors[c], ZP);
+		border(w->frame, r, 1, wincolors[c2], ZP);
+	}
+
+	if(!w->notitle){
+		r = w->titlerect;
+		draw(w->frame, r, wincolors[c], nil, ZP);
+
+		b1 = r;
+		b1.max.x -= bordersz/2;
+		b1.min.x = b1.max.x - titlesz + bordersz;
+		b1.max.y = b1.min.y + Dx(b1);
+		b2 = rectsubpt(b1, Pt(titlesz, 0));
+		b3 = rectsubpt(b2, Pt(titlesz, 0));
+		btn(w->frame, b1, wincolors[c1], icons[3], 0);
+		btn(w->frame, b2, wincolors[c1], icons[1+w->maximized], 0);
+		btn(w->frame, b3, wincolors[c1], icons[0], 0);
+
+		margin = w->noborder ? titlesz : titlesz + bordersz;
+		margin = (margin - font->height)/2;
+		Point pt = Pt(r.min.x, w->rect.min.y + margin + 1);
+		if(w->cur)
+			string(w->frame, pt, wincolors[c1], pt, font, w->cur->label);
+	}
+	border(w->frame, insetrect(w->contrect,-1), 1, wincolors[c2], ZP);
+
+	r = rectsubpt(w->tabrect, Pt(0,1));
+	draw(w->frame, r, wincolors[c], nil, ZP);
+
+	int n = w->ref;
+	if(n > 1){
+		int wd = Dx(r)/n;
+		int xxx = r.max.x;
+		r.max.x = r.min.x + wd;
+		for(WinTab *t = w->tab; t; t = t->next){
+			if(t->next == nil)
+				r.max.x = xxx;
+			if(t != w->cur)
+				draw(w->frame, r, shadecol, nil, ZP);
+			margin = (tabsz - font->height)/2;
+			Point pt = Pt(r.min.x+bordersz/2, r.min.y + margin);
+			string(w->frame, pt, wincolors[c1], pt, font, t->label);
+			r = rectaddpt(r, Pt(wd,0));
+		}
+	}
+}
+
+void
+wtitlectl(Window *w)
+{
+	if(mctl->buttons & 7){
+		wraise(w);
+		wfocus(w);
+		if(mctl->buttons & 1) {
+			int c = TITLETEXT + wcolsel(w);
+
+			Rectangle r = w->titlerect;
+			r.max.x -= bordersz/2;
+			Rectangle br = Rect(0,0,titlesz-bordersz,titlesz-bordersz);
+			Rectangle br1 = rectaddpt(br, Pt(r.max.x-titlesz+bordersz, r.min.y));
+			Rectangle br2 = rectsubpt(br1, Pt(titlesz, 0));
+			Rectangle br3 = rectsubpt(br2, Pt(titlesz, 0));
+
+			if(ptinrect(mctl->xy, br1)){
+				if(btnctl(w->frame, br1, wincolors[c], icons[3]))
+					wdelete(w);
+			}else if(ptinrect(mctl->xy, br2)){
+				if(btnctl(w->frame, br2, wincolors[c], icons[1+w->maximized])){
+					if(w->maximized)
+						wrestore(w);
+					else
+						wmaximize(w);
+				}
+			}else if(ptinrect(mctl->xy, br3)){
+				if(btnctl(w->frame, br3, wincolors[c], icons[0]))
+					whide(w);
+			}else if(!w->maximized)
+				grab(w, 1);
+		}
+		if(mctl->buttons & 4){
+			Window *ww = pick();
+			if(ww){
+				tmigrate(w->cur, ww);
+				wraise(ww);
+				wfocus(ww);
+			}
+		}
+	}
+}
+
+static char minbtn[] = {
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+};
+
+static char maxbtn[] = {
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0,
+	0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0,
+	0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0,
+	0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0,
+	0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0,
+	0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0,
+	0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0,
+	0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0,
+	0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+};
+
+static char rstbtn[] = {
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0,
+	0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0,
+	0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0,
+	0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0,
+	0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+};
+
+static char closebtn[] = {
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0,
+	0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0,
+	0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0,
+	0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0,
+	0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+};
+
+void
+inittheme(void)
+{
+	freeimage(colors[HOLDTEXT]);
+	freeimage(colors[PALEHOLDTEXT]);
+	colors[HOLDTEXT] = getcolor("holdtext", 0x990000FF);
+	colors[PALEHOLDTEXT] = getcolor("paleholdtext", 0xBB5D00FF);
+
+//	wincolors[TITLE] = getcolor("title", 0x607DA1FF);
+//	wincolors[LTITLE] = getcolor("ltitle", 0xA1A1A1FF);
+
+//	wincolors[TITLE] = getcolor("title", 0x2F78EDFF);
+//	wincolors[LTITLE] = getcolor("ltitle", 0x7C9DE3FF);
+
+	wincolors[TITLE] = getcolor("title", 0x5297F9FF);
+	wincolors[LTITLE] = getcolor("ltitle", 0x2C60B2FF);
+	wincolors[TITLEHOLD] = getcolor("titlehold", 0xED2F2FFF);
+	wincolors[LTITLEHOLD] = getcolor("ltitlehold", 0xE36A6AFF);
+
+	wincolors[FRAME] = getcolor("frame", 0x000000FF);
+	wincolors[LFRAME] = getcolor("lframe", 0x000000FF);
+
+	wincolors[TITLETEXT] = getcolor("titletext", 0xFFFFFFFF);
+	wincolors[LTITLETEXT] = getcolor("ltitletext", 0xFFFFFFFF);
+	wincolors[TITLEHOLDTEXT] = getcolor("titleholdtext", 0xFFFFFFFF);
+	wincolors[LTITLEHOLDTEXT] = getcolor("ltitleholdtext", 0xFFFFFFFF);
+
+	icons[0] = mkicon(minbtn, 16, 13);
+	icons[1] = mkicon(maxbtn, 16, 13);
+	icons[2] = mkicon(rstbtn, 16, 13);
+	icons[3] = mkicon(closebtn, 16, 13);
+
+	shadecol = getcolor(nil, 0x00000020);
+}
--- /dev/null
+++ b/text.c
@@ -1,0 +1,987 @@
+#include "inc.h"
+
+enum
+{
+	HiWater	= 640000,	/* max size of history */
+	LoWater	= 400000,	/* min size of history after max'ed */
+	MinWater	= 20000,	/* room to leave available when reallocating */
+};
+
+void
+xinit(Text *x, Rectangle textr, Rectangle scrollr, int tabwidth, Font *ft, Image *b, Image **cols)
+{
+	frclear(x, FALSE);
+	frinit(x, textr, ft, b, cols);
+	x->maxtab = x->maxtab/8*tabwidth;
+	x->i = b;
+	x->scrollr = scrollr;
+	x->lastsr = ZR;
+	defaulttext = cols[TEXT];
+	defaultbg = cols[BACK];
+	xfill(x);
+	xsetselect(x, x->q0, x->q1);
+	xscrdraw(x);
+}
+
+void
+xsetrects(Text *x, Rectangle textr, Rectangle scrollr)
+{
+	frsetrects(x, textr, x->b);
+	x->scrollr = scrollr;
+}
+
+void
+xclear(Text *x)
+{
+	free(x->r);
+	x->r = nil;
+	x->nr = 0;
+	free(x->raw);
+	x->r = nil;
+	x->nraw = 0;
+	frclear(x, TRUE);
+};
+
+void
+xredraw(Text *x)
+{
+	frredraw(x);
+	xscrdraw(x);
+}
+
+static uint term_cols[] = {
+    /* text */
+    [30] = 0x000000FF, // black
+    [31] = 0xFF0000FF, // red
+    [32] = 0x00FF00FF, // green
+    [33] = 0xFFFF00FF, // yellow
+    [34] = 0x0000FFFF, // blue
+    [35] = 0xFF00FFFF, // magenta
+    [36] = 0x00FFFFFF, // cyan
+    [37] = 0xFFFFFFFF, // white
+    
+    /* background */
+    [40] = 0x000000FF,
+    [41] = 0xFF0000FF,
+    [42] = 0x00FF00FF,
+    [43] = 0xFFFF00FF,
+    [44] = 0x0000FFFF,
+    [45] = 0xFF00FFFF,
+    [46] = 0x00FFFFFF,
+    [47] = 0xFFFFFFFF,
+};
+
+int rtoi(Rune *r, ulong size){
+    char num[16];
+    int i;
+    for(i = 0; i < size && i < sizeof num; i++){
+        if(r[i] < 128)
+            num[i] = (char) r[i];
+    }
+    num[i] = 0;
+    return atoi(num);
+}
+
+
+uint parsetag(Text *x, uint i, uint *t, uint *b){
+    uint start, code, begin = i;
+    for(; i < x->nchars && x->r[i] != 'm'; i++){
+        if(isdigit(x->r[i])){
+            start = i;
+            while(i < x->nchars && isdigit(x->r[i])) i++;
+            code = rtoi(&x->r[start], i - start);
+            if(code >= 30 && code <= 37) *t = term_cols[code];
+            else if(code >= 40 && code <= 47) *b = term_cols[code];
+        }
+    }
+    return i - begin; 
+}
+
+Rune esc[] = { 27 }; 
+
+void
+xcoldraw(Text *x)
+{
+    uint taglen, start, end, t, b;
+    start = x->org;
+    end   = x->org;
+    t = 0; b = 0;
+    Image *textc, *backc;
+    print("org: %d\n", x->org);
+    for(uint i = 0; i < x->nchars; i++){
+        print("i: %d r: %C\n", i, x->r[i]);
+        if(x->r[i] == 27){
+            start = i;
+            if(i + 1 >= x->nchars) break;
+            i++;
+            while(i < x->nchars && x->r[i++] != 27);
+            frdrawsel0(x, frptofchar(x, start), start, i, x->cols[TEXT], x->cols[BACK]);
+        }
+    }
+    print("org: %d nchars: %d\n--------\n", x->org, x->nchars);
+    return;
+}
+
+void
+xfullredraw(Text *x)
+{
+	xfill(x);
+	x->ticked = 0;
+	if(x->p0 > 0)
+		frdrawsel(x, frptofchar(x, 0), 0, x->p0, 0);
+	if(x->p1 < x->nchars)
+		frdrawsel(x, frptofchar(x, x->p1), x->p1, x->nchars, 0);
+	frdrawsel(x, frptofchar(x, x->p0), x->p0, x->p1, 1);
+	x->lastsr = ZR;
+	xscrdraw(x);
+}
+
+uint
+xinsert(Text *w, Rune *r, int n, uint q0)
+{
+	uint m;
+
+	if(n == 0)
+		return q0;
+	if(w->nr+n>HiWater && q0>=w->org && q0>=w->qh){
+		m = min(HiWater-LoWater, min(w->org, w->qh));
+		w->org -= m;
+		w->qh -= m;
+		if(w->q0 > m)
+			w->q0 -= m;
+		else
+			w->q0 = 0;
+		if(w->q1 > m)
+			w->q1 -= m;
+		else
+			w->q1 = 0;
+		w->nr -= m;
+		runemove(w->r, w->r+m, w->nr);
+		q0 -= m;
+	}
+	if(w->nr+n > w->maxr){
+		/*
+		 * Minimize realloc breakage:
+		 *	Allocate at least MinWater
+		 * 	Double allocation size each time
+		 *	But don't go much above HiWater
+		 */
+		m = max(min(2*(w->nr+n), HiWater), w->nr+n)+MinWater;
+		if(m > HiWater)
+			m = max(HiWater+MinWater, w->nr+n);
+		if(m > w->maxr){
+			w->r = runerealloc(w->r, m);
+			w->maxr = m;
+		}
+	}
+	runemove(w->r+q0+n, w->r+q0, w->nr-q0);
+	runemove(w->r+q0, r, n);
+	w->nr += n;
+	/* if output touches, advance selection, not qh; works best for keyboard and output */
+	if(q0 <= w->q1)
+		w->q1 += n;
+	if(q0 <= w->q0)
+		w->q0 += n;
+	if(q0 < w->qh)
+		w->qh += n;
+	if(q0 < w->org)
+		w->org += n;
+	else if(q0 <= w->org+w->nchars)
+		frinsert(w, r, r+n, q0-w->org);
+	xscrdraw(w);
+	return q0;
+}
+
+void
+xfill(Text *w)
+{
+	Rune *rp;
+	int i, n, m, nl;
+
+	while(w->lastlinefull == FALSE){
+		n = w->nr-(w->org+w->nchars);
+		if(n == 0)
+			break;
+		if(n > 2000)	/* educated guess at reasonable amount */
+			n = 2000;
+		rp = w->r+(w->org+w->nchars);
+
+		/*
+		 * it's expensive to frinsert more than we need, so
+		 * count newlines.
+		 */
+		nl = w->maxlines-w->nlines;
+		m = 0;
+		for(i=0; i<n; ){
+			if(rp[i++] == '\n'){
+				m++;
+				if(m >= nl)
+					break;
+			}
+		}
+		frinsert(w, rp, rp+i, w->nchars);
+	}
+}
+
+void
+xdelete(Text *w, uint q0, uint q1)
+{
+	uint n, p0, p1;
+
+	n = q1-q0;
+	if(n == 0)
+		return;
+	runemove(w->r+q0, w->r+q1, w->nr-q1);
+	w->nr -= n;
+	if(q0 < w->q0)
+		w->q0 -= min(n, w->q0-q0);
+	if(q0 < w->q1)
+		w->q1 -= min(n, w->q1-q0);
+	if(q1 < w->qh)
+		w->qh -= n;
+	else if(q0 < w->qh)
+		w->qh = q0;
+	if(q1 <= w->org)
+		w->org -= n;
+	else if(q0 < w->org+w->nchars){
+		p1 = q1 - w->org;
+		if(p1 > w->nchars)
+			p1 = w->nchars;
+		if(q0 < w->org){
+			w->org = q0;
+			p0 = 0;
+		}else
+			p0 = q0 - w->org;
+		frdelete(w, p0, p1);
+		xfill(w);
+	}
+}
+
+void
+xsetselect(Text *w, uint q0, uint q1)
+{
+	int p0, p1;
+
+	w->posx = -1;
+	/* w->p0 and w->p1 are always right; w->q0 and w->q1 may be off */
+	w->q0 = q0;
+	w->q1 = q1;
+	/* compute desired p0,p1 from q0,q1 */
+	p0 = q0-w->org;
+	p1 = q1-w->org;
+	if(p0 < 0)
+		p0 = 0;
+	if(p1 < 0)
+		p1 = 0;
+	if(p0 > w->nchars)
+		p0 = w->nchars;
+	if(p1 > w->nchars)
+		p1 = w->nchars;
+	if(p0==w->p0 && p1==w->p1)
+		return;
+	/* screen disagrees with desired selection */
+	if(w->p1<=p0 || p1<=w->p0 || p0==p1 || w->p1==w->p0){
+		/* no overlap or too easy to bother trying */
+		frdrawsel(w, frptofchar(w, w->p0), w->p0, w->p1, 0);
+		frdrawsel(w, frptofchar(w, p0), p0, p1, 1);
+		goto Return;
+	}
+	/* overlap; avoid unnecessary painting */
+	if(p0 < w->p0){
+		/* extend selection backwards */
+		frdrawsel(w, frptofchar(w, p0), p0, w->p0, 1);
+	}else if(p0 > w->p0){
+		/* trim first part of selection */
+		frdrawsel(w, frptofchar(w, w->p0), w->p0, p0, 0);
+	}
+	if(p1 > w->p1){
+		/* extend selection forwards */
+		frdrawsel(w, frptofchar(w, w->p1), w->p1, p1, 1);
+	}else if(p1 < w->p1){
+		/* trim last part of selection */
+		frdrawsel(w, frptofchar(w, p1), p1, w->p1, 0);
+	}
+
+    Return:
+	w->p0 = p0;
+	w->p1 = p1;
+}
+
+static void
+xsetorigin(Text *w, uint org, int exact)
+{
+	int i, a, fixup;
+	Rune *r;
+	uint n;
+
+	if(org>0 && !exact){
+		/* org is an estimate of the char posn; find a newline */
+		/* don't try harder than 256 chars */
+		for(i=0; i<256 && org<w->nr; i++){
+			if(w->r[org] == '\n'){
+				org++;
+				break;
+			}
+			org++;
+		}
+	}
+	a = org-w->org;
+	fixup = 0;
+	if(a>=0 && a<w->nchars){
+		frdelete(w, 0, a);
+		fixup = 1;	/* frdelete can leave end of last line in wrong selection mode; it doesn't know what follows */
+	}else if(a<0 && -a<w->nchars){
+		n = w->org - org;
+		r = w->r+org;
+		frinsert(w, r, r+n, 0);
+	}else
+		frdelete(w, 0, w->nchars);
+	w->org = org;
+	xfill(w);
+	xscrdraw(w);
+	xsetselect(w, w->q0, w->q1);
+	if(fixup && w->p1 > w->p0)
+		frdrawsel(w, frptofchar(w, w->p1-1), w->p1-1, w->p1, 1);
+}
+
+
+/*
+ * Scrolling
+ */
+
+static Rectangle
+scrpos(Rectangle r, uint p0, uint p1, uint tot)
+{
+	Rectangle q;
+	int h;
+
+	q = r;
+	h = q.max.y-q.min.y;
+	if(tot == 0)
+		return q;
+	if(tot > 1024*1024){
+		tot>>=10;
+		p0>>=10;
+		p1>>=10;
+	}
+	if(p0 > 0)
+		q.min.y += h*p0/tot;
+	if(p1 < tot)
+		q.max.y -= h*(tot-p1)/tot;
+	if(q.max.y < q.min.y+2){
+		if(q.min.y+2 <= r.max.y)
+			q.max.y = q.min.y+2;
+		else
+			q.min.y = q.max.y-2;
+	}
+	return q;
+}
+
+void
+xscrdraw(Text *w)
+{
+	Rectangle r1, r2;
+
+	if(w->i == nil)
+		return;
+
+	r1 = w->scrollr;
+	r2 = scrpos(r1, w->org, w->org+w->nchars, w->nr);
+	if(!eqrect(r2, w->lastsr)){
+		w->lastsr = r2;
+		draw(w->i, r1, w->cols[BORD], nil, ZP);
+		draw(w->i, insetrect(r2,1), w->cols[BACK], nil, ZP);
+	}
+	
+	xcoldraw(w);
+}
+
+static uint
+xbacknl(Text *w, uint p, uint n)
+{
+	int i, j;
+
+	/* look for start of this line if n==0 */
+	if(n==0 && p>0 && w->r[p-1]!='\n')
+		n = 1;
+	i = n;
+	while(i-->0 && p>0){
+		--p;	/* it's at a newline now; back over it */
+		if(p == 0)
+			break;
+		/* at 128 chars, call it a line anyway */
+		for(j=128; --j>0 && p>0; p--)
+			if(w->r[p-1]=='\n')
+				break;
+	}
+	return p;
+}
+
+static void
+xscrsleep(Mousectl *mc, uint dt)
+{
+	Timer	*timer;
+	int y, b;
+	static Alt alts[3];
+
+	if(display->bufp > display->buf)
+		flushimage(display, 1);
+	timer = timerstart(dt);
+	y = mc->xy.y;
+	b = mc->buttons;
+	alts[0] = ALT(timer->c, nil, CHANRCV);
+	alts[1] = ALT(mc->c, &mc->Mouse, CHANRCV);
+	alts[2].op = CHANEND;
+	for(;;)
+		switch(alt(alts)){
+		case 0:
+			timerstop(timer);
+			return;
+		case 1:
+			if(abs(mc->xy.y-y)>2 || mc->buttons!=b){
+				timercancel(timer);
+				return;
+			}
+			break;
+		}
+}
+
+void
+xscroll(Text *w, Mousectl *mc, int but)
+{
+	uint p0, oldp0;
+	Rectangle s;
+	int y, my, h, first;
+
+	s = insetrect(w->scrollr, 1);
+	h = s.max.y-s.min.y;
+	oldp0 = ~0;
+	first = TRUE;
+	do{
+		my = mc->xy.y;
+		if(my < s.min.y)
+			my = s.min.y;
+		if(my >= s.max.y)
+			my = s.max.y;
+		if(but == 2){
+			y = my;
+			if(y > s.max.y-2)
+				y = s.max.y-2;
+			if(w->nr > 1024*1024)
+				p0 = ((w->nr>>10)*(y-s.min.y)/h)<<10;
+			else
+				p0 = w->nr*(y-s.min.y)/h;
+			if(oldp0 != p0)
+				xsetorigin(w, p0, FALSE);
+			oldp0 = p0;
+			readmouse(mc);
+			continue;
+		}
+		if(but == 1 || but == 4){
+			y = max(1, (my-s.min.y)/w->font->height);
+			p0 = xbacknl(w, w->org, y);
+		}else{
+			y = max(my, s.min.y+w->font->height);
+			p0 = w->org+frcharofpt(w, Pt(s.max.x, y));
+		}
+		if(oldp0 != p0)
+			xsetorigin(w, p0, TRUE);
+		oldp0 = p0;
+		/* debounce */
+		if(first){
+			if(display->bufp > display->buf)
+				flushimage(display, 1);
+			if(but > 3)
+				return;
+			sleep(200);
+			nbrecv(mc->c, &mc->Mouse);
+			first = FALSE;
+		}
+		xscrsleep(mc, 100);
+	}while(mc->buttons & (1<<(but-1)));
+	while(mc->buttons)
+		readmouse(mc);
+}
+
+void
+xscrolln(Text *x, int n)
+{
+	uint q0;
+
+	if(n < 0)
+		q0 = xbacknl(x, x->org, -n);
+	else
+		q0 = x->org+frcharofpt(x, Pt(x->Frame.r.min.x, x->Frame.r.min.y+n*x->font->height));
+	xsetorigin(x, q0, TRUE);
+}
+
+static Text	*selecttext;
+static Mousectl *selectmc;
+static uint	selectq;
+
+static void
+xframescroll(Text *x, int dl)
+{
+	uint endq;
+
+	if(dl == 0){
+		xscrsleep(selectmc, 100);
+		return;
+	}
+	if(dl < 0){
+		endq = x->org+x->p0;
+	}else{
+		if(x->org+x->nchars == x->nr)
+			return;
+		endq = x->org+x->p1;
+	}
+	xscrolln(x, dl);
+	xsetselect(x, min(selectq, endq), max(selectq, endq));
+}
+
+static void
+framescroll(Frame *f, int dl)
+{
+	if(f != &selecttext->Frame)
+		panic("frameselect not right frame");
+	xframescroll(selecttext, dl);
+}
+
+/*
+ * Selection and deletion helpers
+ */
+
+int
+iswordrune(Rune r)
+{
+	return r == '_' || isalpharune(r) || isdigitrune(r);
+}
+
+static int
+xbswidth(Text *w, Rune c)
+{
+	uint q, stop;
+	Rune r;
+	int wd, inword;
+
+	/* there is known to be at least one character to erase */
+	if(c == Kbs)	/* ^H: erase character */
+		return 1;
+	q = w->q0;
+	stop = 0;
+	if(q > w->qh)
+		stop = w->qh;
+	inword = FALSE;
+	while(q > stop){
+		r = w->r[q-1];
+		if(r == '\n'){		/* eat at most one more character */
+			if(q == w->q0)	/* eat the newline */
+				--q;
+			break; 
+		}
+		/* ^W: erase word.
+		 * delete a bunch of non-word characters
+		 * followed by word characters */
+		if(c == CTRL('W')){
+			wd = iswordrune(r);
+			if(wd && !inword)
+				inword = TRUE;
+			else if(!wd && inword)
+				break;
+		}
+		--q;
+	}
+	return w->q0-q;
+}
+
+static Rune left1[] =  { L'{', L'[', L'(', L'<', L'«', 0 };
+static Rune right1[] = { L'}', L']', L')', L'>', L'»', 0 };
+static Rune left2[] =  { L'\n', 0 };
+static Rune left3[] =  { L'\'', L'"', L'`', 0 };
+
+static Rune *left[] = {
+	left1,
+	left2,
+	left3,
+	nil
+};
+static Rune *right[] = {
+	right1,
+	left2,
+	left3,
+	nil
+};
+
+static int
+xclickmatch(Text *x, int cl, int cr, int dir, uint *q)
+{
+	Rune c;
+	int nest;
+
+	nest = 1;
+	for(;;){
+		if(dir > 0){
+			if(*q == x->nr)
+				break;
+			c = x->r[*q];
+			(*q)++;
+		}else{
+			if(*q == 0)
+				break;
+			(*q)--;
+			c = x->r[*q];
+		}
+		if(c == cr){
+			if(--nest==0)
+				return 1;
+		}else if(c == cl)
+			nest++;
+	}
+	return cl=='\n' && nest==1;
+}
+
+static int
+inmode(Rune r, int mode)
+{
+	return (mode == 1) ? iswordrune(r) : r && !isspacerune(r);
+}
+
+static void
+xstretchsel(Text *x, uint pt, uint *q0, uint *q1, int mode)
+{
+	int c, i;
+	Rune *r, *l, *p;
+	uint q;
+
+	*q0 = pt;
+	*q1 = pt;
+	for(i=0; left[i]!=nil; i++){
+		q = *q0;
+		l = left[i];
+		r = right[i];
+		/* try matching character to left, looking right */
+		if(q == 0)
+			c = '\n';
+		else
+			c = x->r[q-1];
+		p = runestrchr(l, c);
+		if(p != nil){
+			if(xclickmatch(x, c, r[p-l], 1, &q))
+				*q1 = q-(c!='\n');
+			return;
+		}
+		/* try matching character to right, looking left */
+		if(q == x->nr)
+			c = '\n';
+		else
+			c = x->r[q];
+		p = runestrchr(r, c);
+		if(p != nil){
+			if(xclickmatch(x, c, l[p-r], -1, &q)){
+				*q1 = *q0+(*q0<x->nr && c=='\n');
+				*q0 = q;
+				if(c!='\n' || q!=0 || x->r[0]=='\n')
+					(*q0)++;
+			}
+			return;
+		}
+	}
+	/* try filling out word to right */
+	while(*q1<x->nr && inmode(x->r[*q1], mode))
+		(*q1)++;
+	/* try filling out word to left */
+	while(*q0>0 && inmode(x->r[*q0-1], mode))
+		(*q0)--;
+}
+
+static Mouse	lastclick;
+static Text	*clickfrm;
+static uint	clickcount;
+
+/* should be called with button 1 down */
+void
+xselect(Text *x, Mousectl *mc)
+{
+	uint q0, q1;
+	int dx, dy, dt, b;
+
+	/* reset click state if mouse is too different from last time */
+	dx = abs(mc->xy.x - lastclick.xy.x);
+	dy = abs(mc->xy.y - lastclick.xy.y);
+	dt = mc->msec - lastclick.msec;
+	if(x != clickfrm || dx > 3 || dy > 3 || dt >= 500)
+		clickcount = 0;
+
+	/* first button down can be a dragging selection or a click.
+	 * subsequent buttons downs can only be clicks.
+	 * both cases can be ended by chording. */
+	selectq = x->org+frcharofpt(x, mc->xy);
+	if(clickcount == 0){
+		/* what a kludge - can this be improved? */
+		selecttext = x;
+		selectmc = mc;
+		x->scroll = framescroll;
+		frselect(x, mc);
+		/* this is correct if the whole selection is visible */
+		q0 = x->org + x->p0;
+		q1 = x->org + x->p1;
+		/* otherwise replace one end with selectq */
+		if(selectq < x->org)
+			q0 = selectq;
+		if(selectq > x->org+x->nchars)
+			q1 = selectq;
+		xsetselect(x, q0, q1);
+
+		/* figure out whether it was a click */
+		if(q0 == q1 && mc->buttons == 0){
+			clickcount = 1;
+			clickfrm = x;
+		}
+	}else{
+		clickcount++;
+		xstretchsel(x, selectq, &q0, &q1, min(clickcount-1, 2));
+		xsetselect(x, q0, q1);
+		if(clickcount >= 3)
+			clickcount = 0;
+		b = mc->buttons;
+		while(mc->buttons == b)
+			readmouse(mc);
+	}
+	lastclick = mc->Mouse;		/* a bit unsure if this is correct */
+
+	/* chording */
+	while(mc->buttons){
+		clickcount = 0;
+		b = mc->buttons;
+		if(b & 6){
+			if(b & 2){
+				xsnarf(x);
+				xcut(x);
+			}else{
+				xpaste(x);
+			}
+		}
+		while(mc->buttons == b)
+			readmouse(mc);
+	}
+}
+
+void
+xshow(Text *w, uint q0)
+{
+	int qe;
+	int nl;
+	uint q;
+
+	qe = w->org+w->nchars;
+	if(w->org<=q0 && (q0<qe || (q0==qe && qe==w->nr)))
+		xscrdraw(w);
+	else{
+		nl = 4*w->maxlines/5;
+		q = xbacknl(w, q0, nl);
+		/* avoid going backwards if trying to go forwards - long lines! */
+		if(!(q0>w->org && q<w->org))
+			xsetorigin(w, q, TRUE);
+		while(q0 > w->org+w->nchars)
+			xsetorigin(w, w->org+1, FALSE);
+	}
+}
+
+void
+xplacetick(Text *x, uint q)
+{
+	xsetselect(x, q, q);
+	xshow(x, q);
+}
+
+void
+xtype(Text *x, Rune r)
+{
+	uint q0, q1;
+	int nb;
+
+	xsnarf(x);
+	xcut(x);
+	switch(r){
+	case CTRL('H'):	/* erase character */
+	case CTRL('W'):	/* erase word */
+	case CTRL('U'):	/* erase line */
+		if(x->q0==0 || x->q0==x->qh)
+			return;
+		nb = xbswidth(x, r);
+		q1 = x->q0;
+		q0 = q1-nb;
+		if(q0 < x->org){
+			q0 = x->org;
+			nb = q1-q0;
+		}
+		if(nb > 0){
+			xdelete(x, q0, q0+nb);
+			xsetselect(x, q0, q0);
+		}
+		break;
+	default:
+		xinsert(x, &r, 1, x->q0);
+		xshow(x, x->q0);
+		break;
+	}
+}
+
+int
+xninput(Text *x)
+{
+	uint q;
+	Rune r;
+
+	for(q = x->qh; q < x->nr; q++){
+		r = x->r[q];
+		if(r == '\n')
+			return q - x->qh + 1;
+		if(r == CTRL('D'))
+			return q - x->qh;
+	}
+	return -1;
+}
+
+void
+xaddraw(Text *x, Rune *r, int nr)
+{
+	x->raw = runerealloc(x->raw, x->nraw+nr);
+	runemove(x->raw+x->nraw, r, nr);
+	x->nraw += nr;
+}
+
+/* TODO: maybe pass what we're looking for in a string */
+void
+xlook(Text *x)
+{
+	int i, n, e;
+
+	i = x->q1;
+	n = i - x->q0;
+	e = x->nr - n;
+	if(n <= 0 || e < n)
+		return;
+
+	if(i > e)
+		i = 0;
+
+	while(runestrncmp(x->r+x->q0, x->r+i, n) != 0){
+		if(i < e)
+			i++;
+		else
+			i = 0;
+	}
+
+	xsetselect(x, i, i+n);
+	xshow(x, i);
+}
+
+Rune *snarf;
+int nsnarf;
+int snarfversion;
+int snarffd;
+
+void
+xsnarf(Text *x)
+{
+	if(x->q1 == x->q0)
+		return;
+	nsnarf = x->q1-x->q0;
+	snarf = runerealloc(snarf, nsnarf);
+	snarfversion++;
+	runemove(snarf, x->r+x->q0, nsnarf);
+	putsnarf();
+}
+
+void
+xcut(Text *x)
+{
+	if(x->q1 == x->q0)
+		return;
+	xdelete(x, x->q0, x->q1);
+	xsetselect(x, x->q0, x->q0);
+	xscrdraw(x);
+}
+
+void
+xpaste(Text *x)
+{
+	uint q0;
+
+	getsnarf();
+	if(nsnarf == 0)
+		return;
+	xcut(x);
+	q0 = x->q0;
+	if(x->rawmode && q0==x->nr){
+		xaddraw(x, snarf, nsnarf);
+		xsetselect(x, q0, q0);
+	}else{
+		q0 = xinsert(x, snarf, nsnarf, x->q0);
+		xsetselect(x, q0, q0+nsnarf);
+	}
+	xscrdraw(x);
+}
+
+void
+xsend(Text *x)
+{
+	getsnarf();
+	xsnarf(x);
+	if(nsnarf == 0)
+		return;
+	if(x->rawmode){
+		xaddraw(x, snarf, nsnarf);
+		if(snarf[nsnarf-1]!='\n' && snarf[nsnarf-1]!=CTRL('D'))
+			xaddraw(x, L"\n", 1);
+	}else{
+		xinsert(x, snarf, nsnarf, x->nr);
+		if(snarf[nsnarf-1]!='\n' && snarf[nsnarf-1]!=CTRL('D'))
+			xinsert(x, L"\n", 1, x->nr);
+	}
+	xplacetick(x, x->nr);
+}
+
+int
+xplumb(Text *w, char *src, char *dir, int maxsize)
+{
+	Plumbmsg *m;
+	static int fd = -2;
+	char buf[32];
+	uint p0, p1;
+
+	if(fd == -2)
+		fd = plumbopen("send", OWRITE|OCEXEC);
+	if(fd < 0)
+		return 0;
+	m = emalloc(sizeof(Plumbmsg));
+	m->src = estrdup(src);
+	m->dst = nil;
+	m->wdir = estrdup(dir);
+	m->type = estrdup("text");
+	p0 = w->q0;
+	p1 = w->q1;
+	if(w->q1 > w->q0)
+		m->attr = nil;
+	else{
+		while(p0>0 && w->r[p0-1]!=' ' && w->r[p0-1]!='\t' && w->r[p0-1]!='\n')
+			p0--;
+		while(p1<w->nr && w->r[p1]!=' ' && w->r[p1]!='\t' && w->r[p1]!='\n')
+			p1++;
+		snprint(buf, sizeof(buf), "click=%d", w->q0-p0);
+		m->attr = plumbunpackattr(buf);
+	}
+	if(p1-p0 > maxsize){
+		plumbfree(m);
+		return 0;	/* too large for 9P */
+	}
+	m->data = smprint("%.*S", p1-p0, w->r+p0);
+	m->ndata = strlen(m->data);
+	if(plumbsend(fd, m) < 0){
+		plumbfree(m);
+		return 1;
+	}
+	plumbfree(m);
+	return 0;
+}
--- /dev/null
+++ b/time.c
@@ -1,0 +1,114 @@
+#include "inc.h"
+
+/* taken from rio */
+
+static Channel*	ctimer;	/* chan(Timer*)[100] */
+static Timer *timer;
+
+static uint
+msec(void)
+{
+	return nsec()/1000000;
+}
+
+void
+timerstop(Timer *t)
+{
+	t->next = timer;
+	timer = t;
+}
+
+void
+timercancel(Timer *t)
+{
+	t->cancel = TRUE;
+}
+
+static void
+timerproc(void*)
+{
+	int i, nt, na, dt, del;
+	Timer **t, *x;
+	uint old, new;
+
+	rfork(RFFDG);
+	threadsetname("TIMERPROC");
+	t = nil;
+	na = 0;
+	nt = 0;
+	old = msec();
+	for(;;){
+		sleep(1);	/* will sleep minimum incr */
+		new = msec();
+		dt = new-old;
+		old = new;
+		if(dt < 0)	/* timer wrapped; go around, losing a tick */
+			continue;
+		for(i=0; i<nt; i++){
+			x = t[i];
+			x->dt -= dt;
+			del = 0;
+			if(x->cancel){
+				timerstop(x);
+				del = 1;
+			}else if(x->dt <= 0){
+				/*
+				 * avoid possible deadlock if client is
+				 * now sending on ctimer
+				 */
+				if(nbsendul(x->c, 0) > 0)
+					del = 1;
+			}
+			if(del){
+				memmove(&t[i], &t[i+1], (nt-i-1)*sizeof t[0]);
+				--nt;
+				--i;
+			}
+		}
+		if(nt == 0){
+			x = recvp(ctimer);
+	gotit:
+			if(nt == na){
+				na += 10;
+				t = realloc(t, na*sizeof(Timer*));
+				if(t == nil)
+					abort();
+			}
+			t[nt++] = x;
+			old = msec();
+		}
+		if(nbrecv(ctimer, &x) > 0)
+			goto gotit;
+	}
+}
+
+void
+timerinit(void)
+{
+	ctimer = chancreate(sizeof(Timer*), 100);
+	proccreate(timerproc, nil, mainstacksize);
+}
+
+/*
+ * timeralloc() and timerfree() don't lock, so can only be
+ * called from the main proc.
+ */
+
+Timer*
+timerstart(int dt)
+{
+	Timer *t;
+
+	t = timer;
+	if(t)
+		timer = timer->next;
+	else{
+		t = emalloc(sizeof(Timer));
+		t->c = chancreate(sizeof(int), 0);
+	}
+	t->next = nil;
+	t->dt = dt;
+	t->cancel = FALSE;
+	sendp(ctimer, t);
+	return t;
+}
--- /dev/null
+++ b/util.c
@@ -1,0 +1,194 @@
+#include "inc.h"
+
+/* Center rect s in rect r */
+Rectangle
+centerrect(Rectangle r, Rectangle s)
+{
+	int dx = (Dx(r) - Dx(s))/2;
+	int dy = (Dy(r) - Dy(s))/2;
+	return rectaddpt(Rect(0, 0, Dx(s), Dy(s)), Pt(r.min.x+dx, r.min.y+dy));
+}
+
+void
+borderTL(Image *img, Rectangle r, Image *c)
+{
+	// left
+	draw(img, Rect(r.min.x, r.min.y, r.min.x+1, r.max.y),
+		c, nil, ZP);
+	// top
+	draw(img, Rect(r.min.x, r.min.y, r.max.x, r.min.y+1),
+		c, nil, ZP);
+}
+
+void
+borderBR(Image *img, Rectangle r, Image *c)
+{
+	// bottom
+	draw(img, Rect(r.min.x, r.max.y-1, r.max.x, r.max.y),
+		c, nil, ZP);
+	// right
+	draw(img, Rect(r.max.x-1, r.min.y, r.max.x, r.max.y),
+		c, nil, ZP);
+}
+
+void
+winborder(Image *img, Rectangle r, Image *c1, Image *c2)
+{
+	borderTL(img, r, c1);
+	borderBR(img, r, c2);
+}
+
+
+void
+panic(char *s)
+{
+	fprint(2, "error: %s: %r\n", s);
+	threadexitsall("error");
+}
+
+void*
+emalloc(ulong size)
+{
+	void *p;
+
+	p = malloc(size);
+	if(p == nil)
+		panic("malloc failed");
+	memset(p, 0, size);
+	return p;
+}
+
+void*
+erealloc(void *p, ulong size)
+{
+	p = realloc(p, size);
+	if(p == nil)
+		panic("realloc failed");
+	return p;
+}
+
+char*
+estrdup(char *s)
+{
+	char *p;
+
+	p = malloc(strlen(s)+1);
+	if(p == nil)
+		panic("strdup failed");
+	strcpy(p, s);
+	return p;
+}
+
+/* Handle backspaces in a rune string.
+ * Set number of final runes,
+ * return number of runes to be deleted initially */
+int
+handlebs(Stringpair *pair)
+{
+	int initial;
+	Rune *start, *rp, *wp;
+	int i;
+
+	initial = 0;
+	start = rp = wp = pair->s;
+	for(i = 0; i < pair->ns; i++){
+		if(*rp == '\b'){
+			if(wp == start)
+				initial++;
+			else
+				wp--;
+		}else
+			*wp++ = *rp;
+		rp++;
+	}
+	pair->ns = wp - start;
+	return initial;
+}
+
+
+void
+cnvsize(RuneConvBuf *cnv, int nb)
+{
+	cnv->nb = nb;
+	if(cnv->maxbuf < nb+UTFmax){
+		cnv->maxbuf = nb+UTFmax;
+		cnv->buf = erealloc(cnv->buf, cnv->maxbuf);
+	}
+}
+
+int
+r2bfill(RuneConvBuf *cnv, Rune *rp, int nr)
+{
+	int i;
+	for(i = 0; cnv->n < cnv->nb && i < nr; i++)
+		cnv->n += runetochar(&cnv->buf[cnv->n], &rp[i]);
+	return i;
+}
+void
+r2bfinish(RuneConvBuf *cnv, Stringpair *pair)
+{
+	int nb;
+
+	nb = pair->ns;
+	pair->ns = min(nb, cnv->n);
+	memmove(pair->s, cnv->buf, pair->ns);
+	cnv->n = max(0, cnv->n-nb);
+	memmove(cnv->buf, cnv->buf+nb, cnv->n);
+}
+
+// TODO: not sure about the signature of this...
+// maybe pass in allocated pair?
+// don't include null runes
+Stringpair
+b2r(RuneConvBuf *cnv)
+{
+	Stringpair pair;
+	Rune *rp;
+	int i;
+
+	rp = runemalloc(cnv->n);
+	pair.s = rp;
+	pair.ns = 0;
+	i = 0;
+	// TODO: optimize this
+	// we know there are full runes until the end
+	while(fullrune(cnv->buf+i, cnv->n-i)){
+		i += chartorune(rp, cnv->buf+i);
+		if(*rp){
+			rp++;
+			pair.ns++;
+		}
+	}
+	memmove(cnv->buf, cnv->buf+i, cnv->n-i);
+	cnv->n -= i;
+
+	return pair;
+}
+
+int
+qadd(Queue *q, char *data)
+{
+	if(q->full)
+		return 0;
+	q->q[q->wi++] = data;
+	q->wi %= nelem(q->q);
+	q->full = q->wi == q->ri;
+	return 1;
+}
+
+char*
+qget(Queue *q)
+{
+	char *data;
+
+	data = q->q[q->ri++];
+	q->ri %= nelem(q->q);
+	q->full = FALSE;
+	return data;
+}
+
+int
+qempty(Queue *q)
+{
+	return q->ri == q->wi && !q->full;
+}
--- /dev/null
+++ b/wctl.c
@@ -1,0 +1,452 @@
+#include "inc.h"
+
+/*
+ * TODO: i feel like this could use some cleanup
+ */
+
+char	Ebadwr[]		= "bad rectangle in wctl request";
+char	Ewalloc[]		= "window allocation failed in wctl request";
+
+/* >= Top are disallowed if mouse button is pressed.
+ * > New need a window. */
+enum
+{
+	Screenoffset,
+	Screenrefresh,
+	New,
+	Newtab,
+	Resize,
+	Move,
+	Scroll,
+	Noscroll,
+	Border,
+	Noborder,
+	Title,
+	Notitle,
+	Sticky,
+	Nosticky,
+	Set,
+	Top,
+	Bottom,
+	Current,
+	Hide,
+	Unhide,
+	Delete,
+};
+
+static char *cmds[] = {
+	[Screenoffset] = "screenoffset",
+	[Screenrefresh] = "refresh",
+	[New]	= "new",
+	[Newtab]	= "newtab",
+	[Resize]	= "resize",
+	[Move]	= "move",
+	[Scroll]	= "scroll",
+	[Noscroll]	= "noscroll",
+	[Border]	= "border",
+	[Noborder]	= "noborder",
+	[Title]	= "title",
+	[Notitle]	= "notitle",
+	[Sticky]	= "sticky",
+	[Nosticky]	= "nosticky",
+	[Set]		= "set",
+	[Top]	= "top",
+	[Bottom]	= "bottom",
+	[Current]	= "current",
+	[Hide]	= "hide",
+	[Unhide]	= "unhide",
+	[Delete]	= "delete",
+	nil
+};
+
+enum
+{
+	Cd,
+	Deltax,
+	Deltay,
+	Hidden,
+	Id,
+	Maxx,
+	Maxy,
+	Minx,
+	Miny,
+	PID,
+	R,
+	Scrolling,
+	Noscrolling,
+};
+
+static char *params[] = {
+	[Cd]	 			= "-cd",
+	[Deltax]			= "-dx",
+	[Deltay]			= "-dy",
+	[Hidden]			= "-hide",
+	[Id]				= "-id",
+	[Maxx]			= "-maxx",
+	[Maxy]			= "-maxy",
+	[Minx]			= "-minx",
+	[Miny]			= "-miny",
+	[PID]				= "-pid",
+	[R]				= "-r",
+	[Scrolling]			= "-scroll",
+	[Noscrolling]		= "-noscroll",
+	nil
+};
+
+static int
+word(char **sp, char *tab[])
+{
+	char *s, *t;
+	int i;
+
+	s = *sp;
+	while(isspacerune(*s))
+		s++;
+	t = s;
+	while(*s!='\0' && !isspacerune(*s))
+		s++;
+	for(i=0; tab[i]!=nil; i++)
+		if(s-t > 0 && strncmp(tab[i], t, s-t) == 0){
+			*sp = s;
+			return i;
+		}
+	return -1;
+}
+
+int
+set(int sign, int neg, int abs, int pos)
+{
+	if(sign < 0)
+		return neg;
+	if(sign > 0)
+		return pos;
+	return abs;
+}
+
+void
+shift(int *minp, int *maxp, int min, int max)
+{
+	if(*maxp > max){
+		*minp += max-*maxp;
+		*maxp = max;
+	}
+	if(*minp < min){
+		*maxp += min-*minp;
+		if(*maxp > max)
+			*maxp = max;
+		*minp = min;
+	}
+}
+
+Rectangle
+rectonscreen(Rectangle r)
+{
+//TODO(vdesk) this changes
+return r;
+	shift(&r.min.x, &r.max.x, screen->r.min.x, screen->r.max.x);
+	shift(&r.min.y, &r.max.y, screen->r.min.y, screen->r.max.y);
+	return r;
+}
+
+/* permit square brackets, in the manner of %R */
+int
+riostrtol(char *s, char **t)
+{
+	int n;
+
+	while(*s!='\0' && (*s==' ' || *s=='\t' || *s=='['))
+		s++;
+	if(*s == '[')
+		s++;
+	n = strtol(s, t, 10);
+	if(*t != s)
+		while((*t)[0] == ']')
+			(*t)++;
+	return n;
+}
+
+Wctlcmd
+parsewctl(char *s, Rectangle r)
+{
+	Wctlcmd cmd;
+
+	int n, nt, param, xy, sign;
+	char *f[2], *t;
+
+	cmd.id = 0;
+	cmd.pid = 0;
+	cmd.hidden = FALSE;
+	cmd.scrolling = scrolling;
+	cmd.dir = nil;
+	cmd.error = nil;
+	cmd.cmd = word(&s, cmds);
+	if(cmd.cmd < 0)
+		goto Lose;
+	switch(cmd.cmd){
+	case Screenoffset:
+		r = ZR;
+		break;
+	case New:
+		r = newrect();
+		break;
+	}
+
+	while((param = word(&s, params)) >= 0){
+		switch(param){	/* special cases */
+		case Hidden:
+			cmd.hidden = TRUE;
+			continue;
+		case Scrolling:
+			cmd.scrolling = TRUE;
+			continue;
+		case Noscrolling:
+			cmd.scrolling = FALSE;
+			continue;
+		case R:
+			r.min.x = riostrtol(s, &t);
+			if(t == s)
+				goto Lose;
+			s = t;
+			r.min.y = riostrtol(s, &t);
+			if(t == s)
+				goto Lose;
+			s = t;
+			r.max.x = riostrtol(s, &t);
+			if(t == s)
+				goto Lose;
+			s = t;
+			r.max.y = riostrtol(s, &t);
+			if(t == s)
+				goto Lose;
+			s = t;
+			continue;
+		}
+		while(isspacerune(*s))
+			s++;
+		if(param == Cd){
+			cmd.dir = s;
+			if((nt = gettokens(cmd.dir, f, nelem(f), " \t\r\n\v\f")) < 1)
+				goto Lose;
+			n = strlen(cmd.dir);
+			if(cmd.dir[0] == '\'' && cmd.dir[n-1] == '\'')
+				(cmd.dir++)[n-1] = '\0'; /* drop quotes */
+			s += n+(nt-1);
+			continue;
+		}
+		sign = 0;
+		if(*s == '-'){
+			sign = -1;
+			s++;
+		}else if(*s == '+'){
+			sign = +1;
+			s++;
+		}
+		if(!isdigitrune(*s))
+			goto Lose;
+		xy = riostrtol(s, &s);
+		switch(param){
+		case -1:
+			cmd.error = "unrecognized wctl parameter";
+			return cmd;
+		case Minx:
+			r.min.x = set(sign, r.min.x-xy, xy, r.min.x+xy);
+			break;
+		case Miny:
+			r.min.y = set(sign, r.min.y-xy, xy, r.min.y+xy);
+			break;
+		case Maxx:
+			r.max.x = set(sign, r.max.x-xy, xy, r.max.x+xy);
+			break;
+		case Maxy:
+			r.max.y = set(sign, r.max.y-xy, xy, r.max.y+xy);
+			break;
+		case Deltax:
+			r.max.x = set(sign, r.max.x-xy, r.min.x+xy, r.max.x+xy);
+			break;
+		case Deltay:
+			r.max.y = set(sign, r.max.y-xy, r.min.y+xy, r.max.y+xy);
+			break;
+		case Id:
+			cmd.id = xy;
+			break;
+		case PID:
+			cmd.pid = xy;
+			break;
+		}
+	}
+	if(cmd.cmd == Screenoffset)
+		cmd.r = r;
+	else
+		cmd.r = rectonscreen(rectaddpt(r, screen->r.min));
+	while(isspacerune(*s))
+		s++;
+	if(cmd.cmd != New && cmd.cmd != Newtab && *s != '\0'){
+		cmd.error = "extraneous text in wctl message";
+		return cmd;
+	}
+	cmd.args = s;
+	return cmd;
+Lose:
+	cmd.error = "unrecognized wctl command";
+	return cmd;
+}
+
+char*
+wctlcmd(WinTab *w, Rectangle r, int cmd)
+{
+	Window *ww = w->w;
+	switch(cmd){
+	case Move:
+		r = rectaddpt(Rect(0,0,Dx(ww->frame->r),Dy(ww->frame->r)), r.min);
+		if(!goodrect(r))
+			return Ebadwr;
+		if(!eqpt(r.min, ww->frame->r.min))
+			wmove(ww, r.min);
+		break;
+	case Resize:
+		if(!goodrect(r))
+			return Ebadwr;
+		if(!eqrect(r, ww->frame->r))
+			wresize(ww, r);
+		break;
+// TODO: these three work somewhat differently in rio
+	case Top:
+		wraise(ww);
+		break;
+	case Bottom:
+		wlower(ww);
+		break;
+	case Current:
+		if(ww->hidden)
+			return "window is hidden";
+		wfocus(ww);
+		wraise(ww);
+		break;
+	case Hide:
+		switch(whide(ww)){
+		case -1: return "window already hidden";
+		case 0: return "hide failed";
+		}
+		break;
+	case Unhide:
+		switch(wunhide(ww)){
+		case -1: return "window not hidden";
+		case 0: return "hide failed";
+		}
+		break;
+	case Delete:
+		wdelete(ww);
+		break;
+	case Scroll:
+		w->scrolling = TRUE;
+		xshow(&w->text, w->text.nr);
+		wsendmsg(w, Wakeup);
+		break;
+	case Noscroll:
+		w->scrolling = FALSE;
+		wsendmsg(w, Wakeup);
+		break;
+	case Border:
+		ww->noborder &= ~1;
+		wrecreate(ww);
+		break;
+	case Noborder:
+		ww->noborder |= 1;
+		wrecreate(ww);
+		break;
+	case Title:
+		ww->notitle = FALSE;
+		wrecreate(ww);
+		break;
+	case Notitle:
+		ww->notitle = TRUE;
+		wrecreate(ww);
+		break;
+	case Sticky:
+		ww->sticky = TRUE;
+		break;
+	case Nosticky:
+		ww->sticky = FALSE;
+		break;
+	default:
+		return "invalid wctl message";
+	}
+	return nil;
+}
+
+char*
+wctlnew(WinTab *w, Wctlcmd cmd)
+{
+	char *argv[4], **args;
+
+	args = nil;
+	if(cmd.pid == 0){
+		argv[0] = "rc";
+		argv[1] = "-c";
+		while(isspacerune(*cmd.args))
+			cmd.args++;
+		if(*cmd.args == '\0'){
+			argv[1] = "-i";
+			argv[2] = nil;
+		}else{
+			argv[2] = cmd.args;
+			argv[3] = nil;
+		}
+		args = argv;
+	}
+	if(wincmd(w, cmd.pid, cmd.dir, args) == 0)
+		return "window creation failed";		
+	return nil;
+}
+
+char*
+writewctl(WinTab *w, char *data)
+{
+	Rectangle r;
+	Wctlcmd cmd;
+
+	if(w == nil)
+		r = ZR;
+	else
+		r = rectsubpt(w->w->frame->r, screen->r.min);
+	cmd = parsewctl(data, r);
+	if(cmd.error)
+		return cmd.error;
+
+	if(cmd.id != 0){
+		w = wfind(cmd.id);
+		if(w == nil)
+			return "no such window id";
+	}
+
+	if(w == nil && cmd.cmd > New)
+		return "command needs to be run within a window";
+
+	switch(cmd.cmd){
+	case Screenoffset:
+		screenoffset(cmd.r.min.x, cmd.r.min.y);
+		return nil;
+	case Screenrefresh:
+		refresh();
+		return nil;
+	case New:
+		w = wtcreate(cmd.r, cmd.hidden, cmd.scrolling);
+		if(w == nil)
+			return "window creation failed";
+		return wctlnew(w, cmd);
+	case Newtab:
+		w = tcreate(w->w, cmd.scrolling);
+		if(w == nil)
+			return "window creation failed";
+		return wctlnew(w, cmd);
+	case Set:
+		if(cmd.pid > 0)
+			wsetpid(w, cmd.pid, 0);
+		return nil;
+	default:
+		incref(w);
+		cmd.error = wctlcmd(w, cmd.r, cmd.cmd);
+		wrelease(w);
+		return cmd.error;
+	}
+}
--- /dev/null
+++ b/win3.c
@@ -1,0 +1,338 @@
+#include "inc.h"
+
+int bordersz = 4;
+int titlesz = 19;
+int tabsz = 23;
+
+enum {
+	ColDefault,
+	ColHilight,
+	ColShadow,
+	ColTitle,
+	ColTitleInact,
+	ColTitleText,
+	ColTitleTextInact,
+
+	ColFrame,
+	ColBorder,
+	ColBorderInact,
+
+	NumWinColors
+};
+
+Image *wincolors[NumWinColors];
+Image *icons[5];
+
+void
+winbtn(Image *img, Rectangle r, Image *icon, int down)
+{
+	draw(img, r, wincolors[ColDefault], nil, ZP);
+	if(down){
+		borderTL(img, r, wincolors[ColShadow]);
+		r = insetrect(r, 1);
+	}else{
+		winborder(img, r, wincolors[ColHilight], wincolors[ColShadow]);
+		r = insetrect(r, 1);
+		borderBR(img, r, wincolors[ColShadow]);
+	}
+
+	r = centerrect(r, icon->r);
+	if(down)
+		r = rectaddpt(r, Pt(1,1));
+	draw(img, r, icon, icon, ZP);
+}
+
+void
+winbtnflat(Image *img, Rectangle r, Image *icon, Image *icondown, int down)
+{
+	if(down){
+		draw(img, r, wincolors[ColShadow], nil, ZP);
+	}else{
+		draw(img, r, wincolors[ColDefault], nil, ZP);
+	}
+
+	r = centerrect(r, icon->r);
+	if(down)
+		icon = icondown;
+	draw(img, r, icon, icon, ZP);
+}
+
+int
+winbtnctl(Image *img, Rectangle r, Image *icon)
+{
+	int over, prevover;
+
+	prevover = 1;
+	winbtn(img, r, icon, 1);
+	while(mctl->buttons){
+		readmouse(mctl);
+		over = ptinrect(mctl->xy, r);
+		if(over != prevover)
+			winbtn(img, r, icon, over);
+		prevover = over;
+	}
+	if(prevover)
+		winbtn(img, r, icon, 0);
+	return ptinrect(mctl->xy, r);
+}
+
+int
+winbtnctlflat(Image *img, Rectangle r, Image *icon, Image *icondown)
+{
+	int over, prevover;
+
+	prevover = 1;
+	winbtnflat(img, r, icon, icondown, 1);
+	while(mctl->buttons){
+		readmouse(mctl);
+		over = ptinrect(mctl->xy, r);
+		if(over != prevover)
+			winbtnflat(img, r, icon, icondown, over);
+		prevover = over;
+	}
+	if(prevover)
+		winbtnflat(img, r, icon, icondown, 0);
+	return ptinrect(mctl->xy, r);
+}
+
+
+
+void
+wdecor(Window *w)
+{
+	if(w->frame == nil)
+		return;
+
+	int inact = w != focused;
+	Rectangle r;
+
+	if(!w->noborder){
+		r = w->rect;
+		border(w->frame, r, bordersz, wincolors[ColBorder + inact], ZP);
+		border(w->frame, r, 1, wincolors[ColFrame], ZP);
+		border(w->frame, insetrect(r,3), 1, wincolors[ColFrame], ZP);
+
+		Rectangle br = rectaddpt(Rect(0,0,1,bordersz), r.min);
+		int dx = Dx(r);
+		int dy = Dy(r);
+		int off = bordersz+titlesz-1;
+		draw(w->frame, rectaddpt(br, Pt(off,0)), wincolors[ColFrame], nil, ZP);
+		draw(w->frame, rectaddpt(br, Pt(off,dy-bordersz)), wincolors[ColFrame], nil, ZP);
+		draw(w->frame, rectaddpt(br, Pt(dx-1-off,0)), wincolors[ColFrame], nil, ZP);
+		draw(w->frame, rectaddpt(br, Pt(dx-1-off,dy-bordersz)), wincolors[ColFrame], nil, ZP);
+
+		br = rectaddpt(Rect(0,0,bordersz,1), r.min);
+		draw(w->frame, rectaddpt(br, Pt(0,off)), wincolors[ColFrame], nil, ZP);
+		draw(w->frame, rectaddpt(br, Pt(dx-bordersz,off)), wincolors[ColFrame], nil, ZP);
+		draw(w->frame, rectaddpt(br, Pt(0,dy-1-off)), wincolors[ColFrame], nil, ZP);
+		draw(w->frame, rectaddpt(br, Pt(dx-bordersz,dy-1-off)), wincolors[ColFrame], nil, ZP);
+
+		r = insetrect(r, bordersz);
+	}
+
+	if(!w->notitle){
+		r = w->titlerect;
+		r.max.y -= 1;
+		draw(w->frame, r, wincolors[ColTitle + inact], nil, ZP);
+		draw(w->frame, Rect(r.min.x,r.max.y,r.max.x,r.max.y+1), wincolors[ColFrame], nil, ZP);
+
+		// menu
+		Rectangle br = Rect(r.min.x,r.min.y,r.min.x+titlesz-1,r.min.y+titlesz-1);
+		winbtnflat(w->frame, br, icons[3], icons[4], 0);
+		border(w->frame, insetrect(br,-1), 1, display->black, ZP);
+
+		// max/restore
+		br.max.x = r.max.x;
+		br.min.x = br.max.x-titlesz+1;
+		winbtn(w->frame, br, icons[1+w->maximized], 0);
+		border(w->frame, insetrect(br,-1), 1, display->black, ZP);
+
+		// min
+		br = rectaddpt(br, Pt(-titlesz,0));
+		winbtn(w->frame, br, icons[0], 0);
+		border(w->frame, insetrect(br,-1), 1, display->black, ZP);
+
+		if(w->cur){
+			int sp = (Dx(r)-stringwidth(font, w->cur->label))/2;
+			Point pt = Pt(r.min.x+sp, r.min.y);
+			string(w->frame, pt, wincolors[ColTitleText + inact], pt, font, w->cur->label);
+		}
+	}
+
+	r = w->tabrect;
+	draw(w->frame, r, wincolors[ColDefault], nil, ZP);
+	draw(w->frame, Rect(r.min.x,r.max.y-1,r.max.x,r.max.y), display->black, nil, ZP);
+
+	int n = w->ref;
+	if(n > 1){
+		int wd = Dx(r)/n;
+		r.max.y -= 1;
+		int xxx = r.max.x;
+		r.max.x = r.min.x + wd;
+		for(WinTab *t = w->tab; t; t = t->next){
+			if(t->next == nil)
+				r.max.x = xxx;
+			if(t == w->cur)
+				border(w->frame, insetrect(r,1), 1, display->black, ZP);
+			int margin = (tabsz - font->height)/2;
+			Point pt = Pt(r.min.x+bordersz/2, r.min.y + margin);
+			string(w->frame, pt, display->black, pt, font, t->label);
+			r = rectaddpt(r, Pt(wd,0));
+		}
+	}
+}
+
+void
+wtitlectl(Window *w)
+{
+	if(mctl->buttons & 7){
+		wraise(w);
+		wfocus(w);
+		if(mctl->buttons & 1) {
+			Rectangle r = w->rect;
+			if(!w->noborder)
+				r = insetrect(r, bordersz);
+			Rectangle br = Rect(0,0,titlesz-1,titlesz-1);
+			Rectangle br1 = rectaddpt(br, r.min);
+			Rectangle br2 = rectaddpt(br1, Pt(Dx(r)-titlesz+1, 0));
+			Rectangle br3 = rectaddpt(br2, Pt(-titlesz, 0));
+
+			if(ptinrect(mctl->xy, br1)){
+				if(winbtnctlflat(w->frame, br1, icons[3], icons[4]))
+					wdelete(w);
+			}else if(ptinrect(mctl->xy, br2)){
+				if(winbtnctl(w->frame, br2, icons[1+w->maximized])){
+					if(w->maximized)
+						wrestore(w);
+					else
+						wmaximize(w);
+				}
+			}else if(ptinrect(mctl->xy, br3)){
+				if(winbtnctl(w->frame, br3, icons[0]))
+					whide(w);
+			}else if(!w->maximized)
+				grab(w, 1);
+		}
+		if(mctl->buttons & 4)
+			btn3menu();
+	}
+}
+
+
+static char minbtn[] = {
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+};
+
+static char maxbtn[] = {
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+};
+
+static char rstbtn[] = {
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+};
+
+static char menubtn[] = {
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
+	0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 6, 0,
+	0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 6, 0,
+	0, 0, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+};
+static char menubtninv[] = {
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0,
+	0, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 7, 0,
+	0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 7, 0,
+	0, 0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+};
+
+
+void
+inittheme(void)
+{
+	background = getcolor("background", 0xC0C7C8FF);
+
+	wincolors[ColDefault] = getcolor("button_face", 0xC0C7C8FF);
+	wincolors[ColHilight] = getcolor("button_hilight", 0xFFFFFFFF);
+	wincolors[ColShadow] = getcolor("button_shadow", 0x87888FFF);
+	wincolors[ColTitle] = getcolor("titlebar_active", 0x5787a8FF);
+	wincolors[ColTitleInact] = getcolor("titlebar_inactive", 0xFFFFFFFF);
+	wincolors[ColTitleText] = getcolor("titlebar_text_active", 0xFFFFFFFF);
+	wincolors[ColTitleTextInact] = getcolor("titlebar_text_inactive", 0x000000FF);
+	wincolors[ColFrame] = getcolor("window_frame", 0x000000FF);
+	wincolors[ColBorder] = getcolor("border_active", 0xC0C7C8FF);
+	wincolors[ColBorderInact] = getcolor("border_inactive", 0xFFFFFFFF);
+
+	icons[0] = mkicon(minbtn, 16, 16);
+	icons[1] = mkicon(maxbtn, 16, 16);
+	icons[2] = mkicon(rstbtn, 16, 16);
+	icons[3] = mkicon(menubtn, 16, 16);
+	icons[4] = mkicon(menubtninv, 16, 16);
+}
--- /dev/null
+++ b/win95.c
@@ -1,0 +1,279 @@
+#include "inc.h"
+
+int bordersz = 4;
+int titlesz = 19;
+int tabsz = 20;
+
+enum {
+	ColDefault,
+	ColLight1,
+	ColLight2,
+	ColDark1,
+	ColDark2,
+	ColTitle,
+	ColTitleInact,
+	ColTitleText,
+	ColTitleTextInact,
+
+	NumWinColors
+};
+
+Image *wincolors[NumWinColors];
+Image *icons[5];
+
+void
+winbtn(Image *img, Rectangle r, Image *icon, int down)
+{
+	if(down){
+		winborder(img, r, wincolors[ColDark2], wincolors[ColLight2]);
+		r = insetrect(r, 1);
+		winborder(img, r, wincolors[ColDark1], wincolors[ColLight1]);
+	}else{
+		winborder(img, r, wincolors[ColLight2], wincolors[ColDark2]);
+		r = insetrect(r, 1);
+		winborder(img, r, wincolors[ColLight1], wincolors[ColDark1]);
+	}
+	r = insetrect(r, 1);
+	draw(img, r, wincolors[ColDefault], nil, ZP);
+
+	r = insetrect(r,-2);
+	if(down)
+		r = rectaddpt(r, Pt(1,1));
+	draw(img, r, icon, icon, ZP);
+}
+
+void
+winframe(Image *img, Rectangle r)
+{
+	winborder(img, r, wincolors[ColLight1], wincolors[ColDark2]);
+	r = insetrect(r, 1);
+	winborder(img, r, wincolors[ColLight2], wincolors[ColDark1]);
+}
+
+int
+winbtnctl(Image *img, Rectangle r, Image *icon)
+{
+	int over, prevover;
+
+	prevover = 1;
+	winbtn(img, r, icon, 1);
+	while(mctl->buttons){
+		readmouse(mctl);
+		over = ptinrect(mctl->xy, r);
+		if(over != prevover)
+			winbtn(img, r, icon, over);
+		prevover = over;
+	}
+	if(prevover)
+		winbtn(img, r, icon, 0);
+	return ptinrect(mctl->xy, r);
+}
+
+void
+wdecor(Window *w)
+{
+	if(w->frame == nil)
+		return;
+
+	int inact = w != focused;
+	Rectangle r;
+
+	if(!w->noborder){
+		r = w->rect;
+		border(w->frame, r, bordersz, wincolors[ColDefault], ZP);
+		winframe(w->frame, r);
+	}
+
+	if(!w->notitle){
+		r = w->titlerect;
+		r.max.y -= 1;
+		draw(w->frame, r, wincolors[ColTitle + inact], nil, ZP);
+		draw(w->frame, Rect(r.min.x,r.max.y,r.max.x,r.max.y+1), wincolors[ColDefault], nil, ZP);
+
+		// draw buttons
+		Rectangle br = insetrect(r, 2);
+		br.min.x = br.max.x - Dy(br) - 2;
+		winbtn(w->frame, br, icons[3], 0);
+		br = rectaddpt(br, Pt(-Dx(br)-2, 0));
+		winbtn(w->frame, br, icons[1+w->maximized], 0);
+		br = rectaddpt(br, Pt(-Dx(br), 0));
+		winbtn(w->frame, br, icons[0], 0);
+
+		br = rectaddpt(icons[4]->r, insetrect(r,1).min);
+		draw(w->frame, br, icons[4], icons[4], ZP);
+
+		Point pt = Pt(r.min.x + 2 + titlesz-1, r.min.y);
+		if(w->cur)
+			string(w->frame, pt, wincolors[ColTitleText + inact], pt, font, w->cur->label);
+	}
+
+
+	r = w->tabrect;
+	draw(w->frame, r, wincolors[ColDefault], nil, ZP);
+	draw(w->frame, Rect(r.min.x,r.max.y-1,r.max.x,r.max.y), display->black, nil, ZP);
+
+	int n = w->ref;
+	if(n > 1){
+		int wd = Dx(r)/n;
+		r.max.y -= 1;
+		int xxx = r.max.x;
+		r.max.x = r.min.x + wd;
+		for(WinTab *t = w->tab; t; t = t->next){
+			if(t->next == nil)
+				r.max.x = xxx;
+			if(t == w->cur)
+				border(w->frame, insetrect(r,1), 1, display->black, ZP);
+			int margin = (tabsz - font->height)/2;
+			Point pt = Pt(r.min.x+bordersz/2, r.min.y + margin);
+			string(w->frame, pt, display->black, pt, font, t->label);
+			r = rectaddpt(r, Pt(wd,0));
+		}
+	}
+}
+
+void
+wtitlectl(Window *w)
+{
+	if(mctl->buttons & 7){
+		wraise(w);
+		wfocus(w);
+		if(mctl->buttons & 1) {
+			Rectangle r = w->titlerect;
+			r.max.y -= 1;
+			Rectangle br1 = insetrect(r, 2);
+			br1.min.x = br1.max.x - Dy(br1) - 2;
+			Rectangle br2 = rectaddpt(br1, Pt(-Dx(br1)-2, 0));
+			Rectangle br3 = rectaddpt(br1, Pt(-2*Dx(br1)-2, 0));
+
+			if(ptinrect(mctl->xy, br1)){
+				if(winbtnctl(w->frame, br1, icons[3]))
+					wdelete(w);
+			}else if(ptinrect(mctl->xy, br2)){
+				if(winbtnctl(w->frame, br2, icons[1+w->maximized])){
+					if(w->maximized)
+						wrestore(w);
+					else
+						wmaximize(w);
+				}
+			}else if(ptinrect(mctl->xy, br3)){
+				if(winbtnctl(w->frame, br3, icons[0]))
+					whide(w);
+			}else if(!w->maximized)
+				grab(w, 1);
+		}
+		if(mctl->buttons & 4)
+			btn3menu();
+	}
+}
+
+
+
+static char minbtn[] = {
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+};
+
+static char maxbtn[] = {
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0,
+	0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0,
+	0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0,
+	0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0,
+	0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0,
+	0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0,
+	0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0,
+	0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0,
+	0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+};
+
+static char rstbtn[] = {
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0,
+	0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0,
+	0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0,
+	0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0,
+	0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+};
+
+static char closebtn[] = {
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+};
+
+static char appbtn[] = {
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1,
+	0, 0, 2, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 3, 1,
+	0, 0, 2, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 3, 1,
+	0, 0, 2, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 1,
+	0, 0, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1,
+	0, 0, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1,
+	0, 0, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1,
+	0, 0, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1,
+	0, 0, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1,
+	0, 0, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1,
+	0, 0, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1,
+	0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+};
+
+void
+inittheme(void)
+{
+	background = getcolor("background", 0x008080FF);
+
+	wincolors[ColDefault] = getcolor("3d_face", 0xC0C0C0FF);
+//	wincolors[ColLight1] = getcolor("3d_hilight1", 0xDFDFDFFF);
+	wincolors[ColLight1] = getcolor("3d_hilight1", 0xC0C0C0FF);
+	wincolors[ColLight2] = getcolor("3d_hilight2", 0xFFFFFFFF);
+	wincolors[ColDark1] = getcolor("3d_shadow1", 0x808080FF);
+	wincolors[ColDark2] = getcolor("3d_shadow2", 0x000000FF);
+	wincolors[ColTitle] = getcolor("titlebar_active", 0x000080FF);
+	wincolors[ColTitleInact] = getcolor("titlebar_inactive", 0x808080FF);
+	wincolors[ColTitleText] = getcolor("titlebar_text_active", 0xFFFFFFFF);
+	wincolors[ColTitleTextInact] = getcolor("titlebar_text_inactive", 0xC0C0C0FF);
+
+	icons[0] = mkicon(minbtn, 16, 14);
+	icons[1] = mkicon(maxbtn, 16, 14);
+	icons[2] = mkicon(rstbtn, 16, 14);
+	icons[3] = mkicon(closebtn, 16, 14);
+	icons[4] = mkicon(appbtn, 16, 16);
+}
--- /dev/null
+++ b/wind.c
@@ -1,0 +1,1390 @@
+#include "inc.h"
+
+Window *bottomwin, *topwin;
+Window *windows[MAXWINDOWS];
+int nwindows;
+WinTab *wintabs[MAXWINDOWS];
+int nwintabs;
+Window *focused, *cursorwin;
+
+Point screenoff;
+
+static void wrepaint(Window *w);
+static void winthread(void *arg);
+
+static void
+wlistpushback(Window *w)
+{
+	w->higher = bottomwin;
+	if(bottomwin) bottomwin->lower = w;
+	w->lower = nil;
+	if(topwin == nil) topwin = w;
+	bottomwin = w;
+}
+
+static void
+wlistpushfront(Window *w)
+{
+	w->lower = topwin;
+	if(topwin) topwin->higher = w;
+	w->higher = nil;
+	if(bottomwin == nil) bottomwin = w;
+	topwin = w;
+}
+
+static void
+wlistremove(Window *w)
+{
+	if(w->lower)
+		w->lower->higher = w->higher;
+	else
+		bottomwin = w->higher;
+	if(w->higher)
+		w->higher->lower = w->lower;
+	else
+		topwin = w->lower;
+	w->higher = nil;
+	w->lower = nil;
+}
+
+void
+wmaximize(Window *w)
+{
+	w->maximized = 1;
+	w->noborder |= 2;
+	w->origrect = w->frame->r;
+	wresize(w, screen->r);
+}
+
+void
+wrestore(Window *w)
+{
+	w->maximized = 0;
+	w->noborder &= ~2;
+	wresize(w, w->origrect);
+}
+
+static void
+wcalcrects(Window *w, Rectangle r)
+{
+	w->rect = r;
+	w->contrect = r;
+	if(!w->noborder)
+		w->contrect = insetrect(w->contrect, bordersz);
+
+	w->titlerect = ZR;
+	if(!w->notitle){
+		w->titlerect = w->contrect;
+		w->titlerect.max.y = w->titlerect.min.y + titlesz;
+		w->contrect.min.y += titlesz;
+	}
+
+	w->tabrect = ZR;
+	if(w->ref > 1){
+		w->tabrect = w->contrect;
+		w->tabrect.max.y = w->tabrect.min.y + tabsz;
+		w->contrect.min.y += tabsz;
+	}
+
+	r = insetrect(w->contrect, 1);
+	w->scrollr = r;
+	w->scrollr.max.x = w->scrollr.min.x + 12;
+	w->textr = r;
+	w->textr.min.x = w->scrollr.max.x + 4;
+}
+
+int
+wcolsel(Window *w)
+{
+	return (w != focused) + (w->cur && w->cur->holdmode)*2;
+}
+
+static void
+wsetcolors(WinTab *w)
+{
+// TODO: this should use wcolsel
+	int c = w->holdmode ?
+		w->w == focused ? HOLDTEXT : PALEHOLDTEXT :
+		w->w == focused ? TEXT : PALETEXT;
+	w->text.cols[TEXT] = colors[c];
+}
+
+static void
+tinit(WinTab *t)
+{
+	Window *w;
+
+	if(t->deleted)
+		return;
+	w = t->w;
+	t->mc.image = w->frame;
+	t->content = allocwindow(w->screen, w->contrect, Refbackup, DNofill);
+	assert(t->content);
+	draw(t->content, t->content->r, colors[BACK], nil, ZP);
+	xinit(&t->text, w->textr, w->scrollr, tabwidth, font, t->content, colors);
+}
+
+static void
+tdeinit(WinTab *t)
+{
+	/* have to move image out of the way
+	 * because client program can hold a ref
+	 * and mess up our drawing. */
+	if(t->content)
+		originwindow(t->content, t->content->r.min, screen->r.max);
+	freeimage(t->content);
+	t->content = nil;
+	t->mc.image = nil;
+}
+
+/* get rid of window visually */
+static void
+wremove(Window *w)
+{
+	if(w->frame)
+		originwindow(w->frame, w->frame->r.min, screen->r.max);
+}
+
+static void
+wfreeimages(Window *w)
+{
+	freescreen(w->screen);
+	w->screen = nil;
+
+	freeimage(w->frame);
+	w->frame = nil;
+}
+
+/* create images, destroy first if they exist.
+ * either only tab images e.g. when the tab bar appears/disappears
+ * or full window when window is resized/moved. */
+static void
+wreinit(Window *w, bool all)
+{
+	Rectangle r, hr;
+
+	for(WinTab *t = w->tab; t; t = t->next)
+		tdeinit(t);
+
+	if(all){
+		r = w->rect;
+		/* reference can be held by client program
+		 * indefinitely which would keep this on screen. */
+		wremove(w);
+		wfreeimages(w);
+		if(w->hidden){
+			hr = rectaddpt(r, subpt(screen->r.max, r.min));
+			w->frame = allocwindow(wscreen, hr, Refbackup, DNofill);
+			originwindow(w->frame, r.min, hr.min);
+		}else
+			w->frame = allocwindow(wscreen, r, Refbackup, DNofill);
+		w->screen = allocscreen(w->frame, colors[BACK], 0);
+		assert(w->screen);
+	}
+
+	for(WinTab *t = w->tab; t; t = t->next)
+		tinit(t);
+
+	tfocus(w->cur);
+}
+
+// TODO: find better name
+void
+wrecreate(Window *w)
+{
+	wcalcrects(w, w->rect);
+	wreinit(w, 0);
+	for(WinTab *t = w->tab; t; t = t->next)
+		wsendmsg(t, Resized);
+}
+
+
+static int id = 1;
+
+Window*
+wcreate(Rectangle r, bool hidden)
+{
+	Window *w;
+
+	w = emalloc(sizeof(Window));
+	w->hidden = hidden;
+	w->notitle = notitle;	// TODO: argument?
+	wcalcrects(w, r);
+	wreinit(w, 1);
+	wlistpushfront(w);
+	// TODO: could be more graceful here
+	assert(nwindows < MAXWINDOWS);
+	windows[nwindows++] = w;
+
+	wfocus(w);
+
+	return w;
+}
+
+void
+tfocus(WinTab *t)
+{
+	if(t == nil || t->deleted)
+		return;
+	t->w->cur = t;
+	topwindow(t->content);
+	wrepaint(t->w);
+}
+
+WinTab*
+tcreate(Window *w, bool scrolling)
+{
+	WinTab *t, **tp;
+
+	/* recreate window when tab bar appears
+	 * before we attach the new tab. */
+	incref(w);
+	if(w->ref == 2)
+		wrecreate(w);
+
+	t = emalloc(sizeof(WinTab));
+	incref(t);
+	t->w = w;
+	for(tp = &w->tab; *tp; tp = &(*tp)->next);
+	*tp = t;
+	tinit(t);
+	t->id = id++;
+	t->notefd = -1;
+	wsetlabel(t, "<unnamed>");
+	t->dir = estrdup(startdir);
+	t->scrolling = scrolling;
+
+	t->mc.c = chancreate(sizeof(Mouse), 16);
+	t->gone = chancreate(sizeof(int), 0);
+	t->kbd = chancreate(sizeof(char*), 16);
+	t->ctl = chancreate(sizeof(int), 0);
+	t->conswrite = chancreate(sizeof(Channel**), 0);
+	t->consread = chancreate(sizeof(Channel**), 0);
+	t->kbdread = chancreate(sizeof(Channel**), 0);
+	t->mouseread = chancreate(sizeof(Channel**), 0);
+	t->wctlread = chancreate(sizeof(Channel**), 0);
+	t->complete = chancreate(sizeof(Completion*), 0);
+	threadcreate(winthread, t, mainstacksize);
+
+	wsetname(t);
+	// TODO: could be more graceful here
+	assert(nwintabs < MAXWINDOWS);
+	wintabs[nwintabs++] = t;
+
+	tfocus(t);
+
+	return t;
+}
+
+WinTab*
+wtcreate(Rectangle r, bool hidden, bool scrolling)
+{
+	return tcreate(wcreate(r, hidden), scrolling);
+}
+
+/* called from winthread when it exits */
+static void
+wfree(WinTab *w)
+{
+	if(w->notefd >= 0)
+		close(w->notefd);
+	xclear(&w->text);
+	chanclose(w->mc.c);
+	chanclose(w->gone);
+	chanclose(w->kbd);
+	chanclose(w->ctl);
+	chanclose(w->conswrite);
+	chanclose(w->consread);
+	chanclose(w->kbdread);
+	chanclose(w->mouseread);
+	chanclose(w->wctlread);
+	chanclose(w->complete);
+	free(w->label);
+	free(w);
+}
+
+static void
+wdestroy(Window *w)
+{
+	int i;
+
+	assert(w != focused);	/* this must be done elsewhere */
+	assert(w->tab == nil);
+	wlistremove(w);
+	for(i = 0; i < nwindows; i++)
+		if(windows[i] == w){
+			nwindows--;
+			memmove(&windows[i], &windows[i+1], (nwindows-i)*sizeof(Window*));
+			break;
+		}
+	wfreeimages(w);
+	free(w);
+	flushimage(display, 1);
+}
+
+int
+inwinthread(WinTab *w)
+{
+	return w->threadname == threadgetname();
+}
+
+/* decrement reference, close window once all tabs gone. */
+static int
+wrelease_(Window *w)
+{
+	if(decref(w) == 0){
+		assert(w->tab == nil);
+		assert(w->cur == nil);
+		wremove(w);
+		wunfocus(w);
+		assert(w != focused);
+		wdestroy(w);
+		return 0;
+	}else{
+		assert(w->ref > 0);
+		return w->ref;
+	}
+}
+
+/* logically and visually close the tab.
+ * struct, thread and link into window will stick
+ * around until all references are gone.
+ * safe to call multiple times. */
+static void
+tclose(WinTab *w)
+{
+	int i;
+
+	if(w->deleted)
+		return;
+	w->deleted = TRUE;
+	for(i = 0; i < nwintabs; i++)
+		if(wintabs[i] == w){
+			nwintabs--;
+			memmove(&wintabs[i], &wintabs[i+1], (nwintabs-i)*sizeof(WinTab*));
+			break;
+		}
+	tdeinit(w);
+}
+
+/* detach tab from window */
+void
+tdetach(WinTab *t)
+{
+	WinTab **tp;
+	Window *w = t->w;
+
+	if(w == nil)
+		return;
+
+	/* remove tab from window */
+	for(tp = &w->tab; *tp; tp = &(*tp)->next){
+		if(*tp == t){
+			(*tp) = t->next;
+			t->next = nil;
+			t->w = nil;
+			break;
+		}
+	}
+	assert(t->w == nil);
+	tdeinit(t);
+
+	/* find new focused tab */
+	if(w->cur == t){
+		w->cur = *tp;
+		if(w->cur == nil)
+			for(w->cur = w->tab;
+			    w->cur && w->cur->next;
+			    w->cur = w->cur->next);
+	}
+	if(wrelease_(w) > 0){
+		/* complete redraw if tab bar disappears */
+		if(w->ref == 1)
+			wrecreate(w);
+		else
+			tfocus(w->cur);
+	}
+}
+
+void
+tmigrate(WinTab *t, Window *w)
+{
+	WinTab **tp;
+
+	if(t->w == w)
+		return;
+	tdetach(t);
+
+	/* recreate window when tab bar appears
+	 * before we attach the new tab. */
+	incref(w);
+	if(w->ref == 2)
+		wrecreate(w);
+
+	t->w = w;
+	for(tp = &w->tab; *tp; tp = &(*tp)->next);
+	*tp = t;
+	tinit(t);
+
+	tfocus(t);
+	wsendmsg(t, Resized);
+}
+
+/* this SUCKS, want doubly linked lists */
+static WinTab**
+getprevptr(WinTab *t)
+{
+	WinTab **tp;
+	for(tp = &t->w->tab; *tp; tp = &(*tp)->next)
+		if(*tp == t)
+			return tp;
+	return nil;
+}
+static WinTab*
+getprev(WinTab *t)
+{
+	WinTab *tt;
+	for(tt = t->w->tab; tt; tt = tt->next)
+		if(tt->next == t)
+			return tt;
+	return nil;
+}
+
+static void
+tswapadjacent(WinTab *l, WinTab *r)
+{
+	WinTab **tp;
+
+	tp = getprevptr(l);
+	assert(tp);
+	l->next = r->next;
+	r->next = l;
+	*tp = r;
+	wdecor(l->w);
+}
+
+void
+tmoveleft(WinTab *r)
+{
+	WinTab *l;
+	l = getprev(r);
+	if(l == nil) return;
+	tswapadjacent(l, r);
+}
+
+void
+tmoveright(WinTab *l)
+{
+	WinTab *r;
+	r = l->next;
+	if(r == nil) return;
+	tswapadjacent(l, r);
+}
+
+/* decrement reference, close tab once all references gone. */
+void
+wrelease(WinTab *t)
+{
+	if(decref(t) == 0){
+		/* increment ref count temporarily
+		 * so win thread doesn't exit too early */
+		incref(t);
+		tdetach(t);
+		tclose(t);
+		decref(t);
+		if(!inwinthread(t))
+			wsendmsg(t, Wakeup);
+	}else
+		assert(t->ref > 0);
+}
+
+void
+tdelete(WinTab *t)
+{
+	assert(!t->deleted);
+	tdetach(t);
+	tclose(t);
+
+	wsendmsg(t, Deleted);
+}
+
+
+void
+wsendmsg(WinTab *w, int type)
+{
+	assert(!inwinthread(w));
+	sendul(w->ctl, type);
+}
+
+WinTab*
+wfind(int id)
+{
+	int i;
+
+	for(i = 0; i < nwintabs; i++)
+		if(wintabs[i]->id == id)
+			return wintabs[i];
+	return nil;
+}
+
+Window*
+wpointto(Point pt)
+{
+	Window *w;
+
+	for(w = topwin; w; w = w->lower)
+		if(!w->hidden && ptinrect(pt, w->frame->r))
+			return w;
+	return nil;
+}
+
+void
+wsetcursor(WinTab *w)
+{
+	if(w->w == cursorwin)
+		setcursornormal(w->holdmode ? &whitearrow : w->cursorp);
+}
+
+void
+wsetlabel(WinTab *w, char *label)
+{
+	free(w->label);
+	w->label = estrdup(label);
+	wdecor(w->w);
+}
+
+void
+wsetname(WinTab *w)
+{
+	int i, n;
+	char err[ERRMAX];
+	
+	n = snprint(w->name, sizeof(w->name)-2, "%s.%d.%d", "noborder", w->id, w->namecount++);
+	for(i='A'; i<='Z'; i++){
+		if(nameimage(w->content, w->name, 1) > 0)
+			return;
+		errstr(err, sizeof err);
+		if(strcmp(err, "image name in use") != 0)
+			break;
+		w->name[n] = i;
+		w->name[n+1] = 0;
+	}
+	w->name[0] = 0;
+	fprint(2, "lola: setname failed: %s\n", err);
+}
+
+void
+wsetpid(WinTab *w, int pid, int dolabel)
+{
+	char buf[32];
+	int ofd;
+
+	ofd = w->notefd;
+	if(pid <= 0)
+		w->notefd = -1;
+	else{
+		if(dolabel){
+			snprint(buf, sizeof(buf), "rc %lud", (ulong)pid);
+			wsetlabel(w, buf);
+		}
+		snprint(buf, sizeof(buf), "/proc/%lud/notepg", (ulong)pid);
+		w->notefd = open(buf, OWRITE|OCEXEC);
+	}
+	if(ofd >= 0)
+		close(ofd);
+}
+
+void
+wdelete(Window *w)
+{
+	wremove(w);
+	wunfocus(w);
+	while(w->tab)
+		tdelete(w->tab);
+}
+
+static void
+wrepaint(Window *w)
+{
+	wsetcolors(w->cur);
+	wdecor(w);
+	if(!w->cur->mouseopen)
+		xredraw(&w->cur->text);
+}
+
+/* restore window order after reshaping has disturbed it */
+void
+worder(void)
+{
+	Window *w;
+	for(w = bottomwin; w; w = w->higher)
+		if(!w->hidden)
+			topwindow(w->frame);
+}
+
+void
+wresize(Window *w, Rectangle r)
+{
+	wcalcrects(w, r);
+	wreinit(w, 1);
+	if(w != topwin && !w->hidden)
+		worder();
+	for(WinTab *t = w->tab; t; t = t->next)
+		wsendmsg(t, Resized);
+}
+
+void
+wmove(Window *w, Point pos)
+{
+	wresize(w, rectaddpt(w->frame->r, subpt(pos, w->frame->r.min)));
+}
+
+void
+wraise(Window *w)
+{
+	wlistremove(w);
+	wlistpushfront(w);
+	topwindow(w->frame);
+	flushimage(display, 1);
+}
+
+void
+wlower(Window *w)
+{
+	wlistremove(w);
+	wlistpushback(w);
+	bottomwindow(w->frame);
+	bottomwindow(fakebg);
+	flushimage(display, 1);
+}
+
+static void
+wfocuschanged(Window *w)
+{
+// TODO(tab):
+	if(w == nil || w->cur == nil)
+		return;
+	w->cur->wctlready = TRUE;
+	wrepaint(w);
+	if(!inwinthread(w->cur))
+		wsendmsg(w->cur, Wakeup);
+}
+
+void
+wfocus(Window *w)
+{
+	Window *prev;
+
+	if(w == focused)
+		return;
+	prev = focused;
+	focused = w;
+	if(prev && prev->cur){
+// TODO(tab): check this
+		WinTab *t = prev->cur;
+		/* release keys (if possible) */
+		char *s = estrdup("K");
+		if(nbsendp(t->kbd, s) != 1)
+			free(s);
+		/* release mouse buttons */
+		if(t->mc.buttons){
+			t->mc.buttons = 0;
+			t->mq.counter++;
+		}
+	}
+	wfocuschanged(prev);
+	wfocuschanged(focused);
+}
+
+void
+wunfocus(Window *w)
+{
+	if(w == focused)
+		wfocus(nil);
+}
+
+// TODO(tab): wctl ready everyone?
+int
+whide(Window *w)
+{
+	if(w->hidden)
+		return -1;
+	incref(w->tab);
+	wremove(w);
+	wunfocus(w);
+	w->hidden = TRUE;
+	w->tab->wctlready = TRUE;
+	wsendmsg(w->tab, Wakeup);
+	wrelease(w->tab);
+	return 1;
+}
+
+// TODO(tab): wctl ready everyone?
+int
+wunhide(Window *w)
+{
+	if(!w->hidden)
+		return -1;
+	incref(w->tab);
+	w->hidden = FALSE;
+	w->tab->wctlready = TRUE;
+	originwindow(w->frame, w->frame->r.min, w->frame->r.min);
+	wraise(w);
+	wfocus(w);
+	wrelease(w->tab);
+	return 1;
+}
+
+void
+wsethold(WinTab *w, int hold)
+{
+	int switched;
+
+	if(hold)
+		switched = w->holdmode++ == 0;
+	else
+		switched = --w->holdmode == 0;
+	if(switched){
+		wsetcursor(w);
+		wrepaint(w->w);
+	}
+}
+
+/* Normally the mouse will only be moved inside the window.
+ * The force argument can move the mouse anywhere. */
+void
+wmovemouse(Window *w, Point pt, bool force)
+{
+	// TODO? rio also checks menuing and such
+	if(force ||
+	   w == focused && wpointto(mctl->xy) == w && ptinrect(pt, w->rect))
+		moveto(mctl, pt);
+}
+
+/*
+ * Need to do this in a separate proc because if process we're interrupting
+ * is dying and trying to print tombstone, kernel is blocked holding p->debug lock.
+ */
+static void
+interruptproc(void *v)
+{
+	int *notefd;
+
+	notefd = v;
+	write(*notefd, "interrupt", 9);
+	close(*notefd);
+	free(notefd);
+}
+
+/*
+ * Filename completion
+ */
+
+typedef struct Completejob Completejob;
+struct Completejob
+{
+	char	*dir;
+	char	*str;
+	WinTab	*win;
+};
+
+static void
+completeproc(void *arg)
+{
+	Completejob *job;
+	Completion *c;
+
+	job = arg;
+	threadsetname("namecomplete %s", job->dir);
+
+	c = complete(job->dir, job->str);
+	if(c != nil && sendp(job->win->complete, c) <= 0)
+		freecompletion(c);
+
+	wrelease(job->win);
+
+	free(job->dir);
+	free(job->str);
+	free(job);
+}
+
+static int
+windfilewidth(WinTab *w, uint q0, int oneelement)
+{
+	uint q;
+	Rune r;
+
+	q = q0;
+	while(q > 0){
+		r = w->text.r[q-1];
+		if(r<=' ' || r=='=' || r=='^' || r=='(' || r=='{')
+			break;
+		if(oneelement && r=='/')
+			break;
+		--q;
+	}
+	return q0-q;
+}
+
+static void
+namecomplete(WinTab *w)
+{
+	Text *x;
+	int nstr, npath;
+	Rune *path, *str;
+	char *dir, *root;
+	Completejob *job;
+
+	x = &w->text;
+	/* control-f: filename completion; works back to white space or / */
+	if(x->q0<x->nr && x->r[x->q0]>' ')	/* must be at end of word */
+		return;
+	nstr = windfilewidth(w, x->q0, TRUE);
+	str = x->r+(x->q0-nstr);
+	npath = windfilewidth(w, x->q0-nstr, FALSE);
+	path = x->r+(x->q0-nstr-npath);
+
+	/* is path rooted? if not, we need to make it relative to window path */
+	if(npath>0 && path[0]=='/')
+		dir = smprint("%.*S", npath, path);
+	else {
+		if(strcmp(w->dir, "") == 0)
+			root = ".";
+		else
+			root = w->dir;
+		dir = smprint("%s/%.*S", root, npath, path);
+	}
+	if(dir == nil)
+		return;
+
+	/* run in background, winctl will collect the result on w->complete chan */
+	job = emalloc(sizeof *job);
+	job->str = smprint("%.*S", nstr, str);
+	job->dir = cleanname(dir);
+	job->win = w;
+	incref(w);
+	proccreate(completeproc, job, mainstacksize);
+}
+
+static void
+showcandidates(WinTab *w, Completion *c)
+{
+	Text *x;
+	int i;
+	Fmt f;
+	Rune *rp;
+	uint nr, qline;
+	char *s;
+
+	x = &w->text;
+	runefmtstrinit(&f);
+	if (c->nmatch == 0)
+		s = "[no matches in ";
+	else
+		s = "[";
+	if(c->nfile > 32)
+		fmtprint(&f, "%s%d files]\n", s, c->nfile);
+	else{
+		fmtprint(&f, "%s", s);
+		for(i=0; i<c->nfile; i++){
+			if(i > 0)
+				fmtprint(&f, " ");
+			fmtprint(&f, "%s", c->filename[i]);
+		}
+		fmtprint(&f, "]\n");
+	}
+	rp = runefmtstrflush(&f);
+	nr = runestrlen(rp);
+
+	/* place text at beginning of line before cursor and host point */
+	qline = min(x->qh, x->q0);
+	while(qline>0 && x->r[qline-1] != '\n')
+		qline--;
+
+	if(qline == x->qh){
+		/* advance host point to avoid readback */
+		x->qh = xinsert(x, rp, nr, qline)+nr;
+	}else{
+		xinsert(x, rp, nr, qline);
+	}
+	free(rp);
+}
+
+void
+wkeyctl(WinTab *w, Rune r)
+{
+	Text *x;
+	int nlines, n;
+	int *notefd;
+
+	x = &w->text;
+	nlines = x->maxlines;	/* need signed */
+	if(!w->mouseopen){
+		switch(r){
+
+		/* Scrolling */
+		case Kscrollonedown:
+			n = mousescrollsize(x->maxlines);
+			xscrolln(x, max(n, 1));
+			return;
+		case Kdown:
+			xscrolln(x, shiftdown ? 1 : nlines/3);
+			return;
+		case Kpgdown:
+			xscrolln(x, nlines*2/3);
+			return;
+		case Kscrolloneup:
+			n = mousescrollsize(x->maxlines);
+			xscrolln(x, -max(n, 1));
+			return;
+		case Kup:
+			xscrolln(x, -(shiftdown ? 1 : nlines/3));
+			return;
+		case Kpgup:
+			xscrolln(x, -nlines*2/3);
+			return;
+
+		case Khome:
+			xshow(x, 0);
+			return;
+		case Kend:
+			xshow(x, x->nr);
+			return;
+
+		/* Cursor movement */
+		case Kleft:
+			if(x->q0 > 0)
+				xplacetick(x, x->q0-1);
+			return;
+		case Kright:
+			if(x->q1 < x->nr)
+				xplacetick(x, x->q1+1);
+			return;
+		case CTRL('A'):
+			while(x->q0 > 0 && x->r[x->q0-1] != '\n' &&
+			      x->q0 != x->qh)
+				x->q0--;
+			xplacetick(x, x->q0);
+			return;
+		case CTRL('E'):
+			while(x->q0 < x->nr && x->r[x->q0] != '\n')
+				x->q0++;
+			xplacetick(x, x->q0);
+			return;
+		case CTRL('B'):
+			xplacetick(x, x->qh);
+			return;
+
+		/* Hold mode */
+		case Kesc:
+			wsethold(w, !w->holdmode);
+			return;
+		case Kdel:
+			if(w->holdmode)
+				wsethold(w, FALSE);
+			break;
+		}
+	}
+
+	if(x->rawmode && (x->q0 == x->nr || w->mouseopen))
+		xaddraw(x, &r, 1);
+	else if(r == Kdel){
+		x->qh = x->nr;
+		xshow(x, x->qh);
+		if(w->notefd < 0)
+			return;
+		notefd = emalloc(sizeof(int));
+		*notefd = dup(w->notefd, -1);
+		proccreate(interruptproc, notefd, 4096);
+	}else if(r == CTRL('F') || r == Kins)
+		namecomplete(w);
+	else
+		xtype(x, r);
+}
+
+void
+wmousectl(WinTab *w)
+{
+	int but;
+
+	for(but = 1; but < 6; but++)
+		if(w->mc.buttons == 1<<(but-1))
+			goto found;
+	return;
+found:
+
+	incref(w);
+	if(shiftdown && but > 3)
+		wkeyctl(w, but == 4 ? Kscrolloneup : Kscrollonedown);
+	else if(ptinrect(w->mc.xy, w->text.scrollr) || but > 3)
+		xscroll(&w->text, &w->mc, but);
+	else if(but == 1)
+		xselect(&w->text, &w->mc);
+	wrelease(w);
+}
+
+void
+winctl(WinTab *w, int type)
+{
+	Text *x;
+	int i;
+
+	if(type == Deleted)
+		if(w->notefd >= 0)
+			write(w->notefd, "hangup", 6);
+	if(w->deleted)
+		return;
+
+	x = &w->text;
+	switch(type){
+	case Resized:
+		wsetname(w);
+		w->resized = TRUE;
+		w->wctlready = TRUE;
+		w->mc.buttons = 0;	/* avoid re-triggering clicks on resize */
+		w->mq.counter++;	/* cause mouse to be re-read */
+		break;
+
+	case Refresh:
+		/* take over window again */
+		draw(w->content, w->content->r, x->cols[BACK], nil, ZP);
+		xfullredraw(&w->text);
+		break;
+
+	case Holdon:
+		wsethold(w, TRUE);
+		break;
+	case Holdoff:
+		wsethold(w, FALSE);
+		break;
+
+	case Rawon:
+		break;
+	case Rawoff:
+// TODO: better to remove one by one? not sure if wkeyctl is safe
+		for(i = 0; i < x->nraw; i++)
+			wkeyctl(w, x->raw[i]);
+		x->nraw = 0;
+		break;
+	}
+}
+
+static void
+winthread(void *arg)
+{
+	WinTab *w;
+	Text *x;
+	Rune r, *rp;
+	char *s;
+	int cm;
+	enum { AKbd, AMouse, ACtl, AConsWrite, AConsRead,
+		AKbdRead, AMouseRead, AWctlRead, AComplete, Agone, NALT };
+	Alt alts[NALT+1];
+	Channel *fsc;
+	Stringpair pair;
+	int i, nb, nr, initial;
+	uint q0;
+	RuneConvBuf cnv;
+	Mousestate m;
+	Completion *comp;
+
+	w = arg;
+	threadsetname("winthread-%d", w->id);
+	w->threadname = threadgetname();
+	x = &w->text;
+	nr = 0;
+	memset(&cnv, 0, sizeof(cnv));
+	fsc = chancreate(sizeof(Stringpair), 0);
+
+	alts[AKbd] = ALT(w->kbd, &s, CHANRCV);
+	alts[AMouse] = ALT(w->mc.c, &w->mc.Mouse, CHANRCV);
+	alts[ACtl] = ALT(w->ctl, &cm, CHANRCV);
+	alts[AConsWrite] = ALT(w->conswrite, &fsc, CHANSND);
+	alts[AConsRead] = ALT(w->consread, &fsc, CHANSND);
+	alts[AKbdRead] = ALT(w->kbdread, &fsc, CHANSND);
+	alts[AMouseRead] = ALT(w->mouseread, &fsc, CHANSND);
+	alts[AWctlRead] = ALT(w->wctlread, &fsc, CHANSND);
+	alts[AComplete] = ALT(w->complete, &comp, CHANRCV);
+	alts[Agone] = ALT(w->gone, nil, CHANNOP);
+	alts[NALT].op = CHANEND;
+
+	for(;;){
+		if(w->deleted){
+			alts[Agone].op = CHANSND;
+			alts[AConsWrite].op = CHANNOP;
+			alts[AConsRead].op = CHANNOP;
+			alts[AKbdRead].op = CHANNOP;
+			alts[AMouseRead].op = CHANNOP;
+			alts[AWctlRead].op = CHANNOP;
+		}else{
+			nr = xninput(x);
+			if(!w->holdmode && (nr >= 0 || cnv.n > 0 || x->rawmode && x->nraw > 0))
+				alts[AConsRead].op = CHANSND;
+			else
+				alts[AConsRead].op = CHANNOP;
+			if(w->scrolling || w->mouseopen || x->qh <= x->org+x->nchars)
+				alts[AConsWrite].op = CHANSND;
+			else
+				alts[AConsWrite].op = CHANNOP;
+			if(w->kbdopen && !qempty(&w->kq))
+				alts[AKbdRead].op = CHANSND;
+			else
+				alts[AKbdRead].op = CHANNOP;
+			if(w->mouseopen && w->mq.counter != w->mq.lastcounter)
+				alts[AMouseRead].op = CHANSND;
+			else
+				alts[AMouseRead].op = CHANNOP;
+			alts[AWctlRead].op = w->wctlready ? CHANSND : CHANNOP;
+		}
+
+		switch(alt(alts)){
+		case AKbd:
+			if(!qadd(&w->kq, s))
+				free(s);
+			if(!w->kbdopen)
+			while(!qempty(&w->kq)){
+				s = qget(&w->kq);
+				if(*s == 'c'){
+					chartorune(&r, s+1);
+					if(r)
+						wkeyctl(w, r);
+				}
+				free(s);
+			}
+			break;
+
+		case AKbdRead:
+			recv(fsc, &pair);
+			nb = 0;
+			while(!qempty(&w->kq)){
+				s = w->kq.q[w->kq.ri];
+				i = strlen(s)+1;
+				if(nb+i > pair.ns)
+					break;
+				qget(&w->kq);
+				memmove((char*)pair.s + nb, s, i);
+				free(s);
+				nb += i;
+			}
+			pair.ns = nb;
+			send(fsc, &pair);
+			break;
+
+		case AMouse:
+			if(w->mouseopen){
+				Mousestate *mp;
+				w->mq.counter++;
+				/* queue click events in ring buffer.
+				 * pure movement only in else branch of the case below */
+				if(!w->mq.full && w->mq.lastb != w->mc.buttons){
+					mp = &w->mq.q[w->mq.wi++];
+					w->mq.wi %= nelem(w->mq.q);
+					w->mq.full = w->mq.wi == w->mq.ri;
+					mp->Mouse = w->mc;
+					mp->counter = w->mq.counter;
+					w->mq.lastb = w->mc.buttons;
+				}
+			}else
+				wmousectl(w);
+			break;
+
+		case AMouseRead:
+			recv(fsc, &pair);
+			w->mq.full = FALSE;
+			/* first return queued clicks, then current state */
+			if(w->mq.wi != w->mq.ri){
+				m = w->mq.q[w->mq.ri++];
+				w->mq.ri %= nelem(w->mq.q);
+			}else
+				m = (Mousestate){w->mc.Mouse, w->mq.counter};
+			w->mq.lastcounter = m.counter;
+
+			pair.ns = snprint(pair.s, pair.ns, "%c%11d %11d %11d %11ld ",
+				"mr"[w->resized], m.xy.x, m.xy.y, m.buttons, m.msec);
+			w->resized = FALSE;
+			send(fsc, &pair);
+			break;
+
+		case AConsWrite:
+			recv(fsc, &pair);
+			initial = handlebs(&pair);
+			if(initial){
+				initial = min(initial, x->qh);
+				xdelete(x, x->qh-initial, x->qh);
+			}
+			x->qh = xinsert(x, pair.s, pair.ns, x->qh) + pair.ns;
+			free(pair.s);
+			if(w->scrolling || w->mouseopen)
+				xshow(x, x->qh);
+			xscrdraw(x);
+			break;
+
+		case AConsRead:
+			recv(fsc, &pair);
+			cnvsize(&cnv, pair.ns);
+			nr = r2bfill(&cnv, x->r+x->qh, nr);
+			x->qh += nr;
+			/* if flushed by ^D, skip the ^D */
+			if(!(nr > 0 && x->r[x->qh-1] == '\n') &&
+			   x->qh < x->nr && x->r[x->qh] == CTRL('D'))
+				x->qh++;
+			if(x->rawmode){
+				nr = r2bfill(&cnv, x->raw, x->nraw);
+				x->nraw -= nr;
+				runemove(x->raw, x->raw+nr, x->nraw);
+			}
+			r2bfinish(&cnv, &pair);
+			send(fsc, &pair);
+			break;
+
+		case AWctlRead:
+			w->wctlready = FALSE;
+			recv(fsc, &pair);
+Window *ww = w->w;
+			pair.ns = snprint(pair.s, pair.ns, "%11d %11d %11d %11d %11s %11s ",
+				ww->frame->r.min.x, ww->frame->r.min.y,
+				ww->frame->r.max.x, ww->frame->r.max.y,
+				ww->cur == w ? ww == focused ? "current" : "notcurrent"
+					: "tab",
+				ww->hidden ? "hidden" : "visible");
+			send(fsc, &pair);
+			break;
+
+		case ACtl:
+			winctl(w, cm);
+			break;
+
+		case AComplete:
+			if(w->content){
+				if(!comp->advance)
+					showcandidates(w, comp);
+				if(comp->advance){
+					rp = runesmprint("%s", comp->string);
+					if(rp){
+						nr = runestrlen(rp);
+						q0 = x->q0;
+						q0 = xinsert(x, rp, nr, q0);
+						xshow(x, q0+nr);
+						free(rp);
+					}
+				}
+			}
+			freecompletion(comp);
+			break;
+		}
+		flushimage(display, 1);
+
+		/* window is gone, clean up and exit thread */
+		if(w->ref == 0){
+			wfree(w);
+			chanfree(fsc);
+			free(cnv.buf);
+			return;
+		}
+	}
+}
+
+static void
+shellproc(void *args)
+{
+	WinTab *w;
+	Channel *pidc;
+	void **arg;
+	char *cmd, *dir;
+	char **argv;
+
+	arg = args;
+	w = arg[0];
+	pidc = arg[1];
+	cmd = arg[2];
+	argv = arg[3];
+	dir = arg[4];
+	rfork(RFNAMEG|RFFDG|RFENVG);
+	if(fsmount(w->id) < 0){
+		fprint(2, "mount failed: %r\n");
+		sendul(pidc, 0);
+		threadexits("mount failed");
+	}
+	close(0);
+	if(open("/dev/cons", OREAD) < 0){
+		fprint(2, "can't open /dev/cons: %r\n");
+		sendul(pidc, 0);
+		threadexits("/dev/cons");
+	}
+	close(1);
+	if(open("/dev/cons", OWRITE) < 0){
+		fprint(2, "can't open /dev/cons: %r\n");
+		sendul(pidc, 0);
+		threadexits("open");	/* BUG? was terminate() */
+	}
+	/* remove extra ref hanging from creation.
+	 * not in main proc here, so be careful with wrelease! */
+	assert(w->ref > 1);
+	wrelease(w);
+
+	notify(nil);
+	dup(1, 2);
+	if(dir)
+		chdir(dir);
+	procexec(pidc, cmd, argv);
+	_exits("exec failed");
+}
+
+int
+wincmd(WinTab *w, int pid, char *dir, char **argv)
+{
+	Channel *cpid;
+	void *args[5];
+
+	if(argv){
+		cpid = chancreate(sizeof(int), 0);
+		assert(cpid);
+		args[0] = w;
+		args[1] = cpid;
+		args[2] = "/bin/rc";
+		args[3] = argv;
+		args[4] = dir;
+		proccreate(shellproc, args, mainstacksize);
+		pid = recvul(cpid);
+		chanfree(cpid);
+		if(pid == 0){
+			wdelete(w->w);
+			return 0;
+		}
+	}
+
+	wsetpid(w, pid, 1);
+	if(dir){
+		free(w->dir);
+		w->dir = estrdup(dir);
+	}
+
+	return pid;
+}
+
+void
+screenoffset(int offx, int offy)
+{
+	Window *w;
+	Point off, delta;
+
+	off = Pt(offx, offy);
+	delta = subpt(off, screenoff);
+	screenoff = off;
+	for(w = bottomwin; w; w = w->higher){
+		if(w->sticky){
+			/* Don't move but cause resize event because
+			 * program may want some kind of notification */
+			wmove(w, w->frame->r.min);
+			continue;
+		}
+		if(w->maximized){
+			wrestore(w);
+			wmove(w, subpt(w->frame->r.min, delta));
+			wmaximize(w);
+		}else
+			wmove(w, subpt(w->frame->r.min, delta));
+	}
+
+	flushimage(display, 1);
+}