shithub: fork

Download patch

ref: 9339cf07c8fb51a828d2feb3414170f3cea01a75
parent: 494281e28693c5505a1a077b8c6694b503488dc2
author: qwx <qwx@sciops.net>
date: Mon Aug 14 01:43:06 EDT 2023

add a bof rio fork

diff: cannot open b/sys/src/cmd/bof//null: file does not exist: 'b/sys/src/cmd/bof//null'
--- /dev/null
+++ b/sys/src/cmd/bof/dat.h
@@ -1,0 +1,355 @@
+enum
+{
+	Qdir,			/* /dev for this window */
+	Qscreen,
+	Qsnarf,
+	Qwctl,
+	Qtap,
+	Qwsys,		/* directory of window directories */
+	Qwsysdir,		/* window directory, child of wsys */
+
+	Qcons,
+	Qconsctl,
+	Qcursor,
+	Qwdir,
+	Qwinid,
+	Qwinname,
+	Qlabel,
+	Qkbd,
+	Qmouse,
+	Qtext,
+	Qwindow,
+	Qdot,
+
+	QMAX,
+	Qglobal = Qcons,	/* anything >= must have non nil window */
+};
+
+#define	STACK	8192
+#define	MAXSNARF	100*1024
+
+typedef	struct	Consreadmesg Consreadmesg;
+typedef	struct	Conswritemesg Conswritemesg;
+typedef struct	Kbdreadmesg Kbdreadmesg;
+typedef	struct	Stringpair Stringpair;
+typedef	struct	Dirtab Dirtab;
+typedef	struct	Fid Fid;
+typedef	struct	Filsys Filsys;
+typedef	struct	Mouseinfo	Mouseinfo;
+typedef	struct	Mousereadmesg Mousereadmesg;
+typedef	struct	Mousestate	Mousestate;
+typedef	struct	Ref Ref;
+typedef	struct	Timer Timer;
+typedef	struct	Wctlmesg Wctlmesg;
+typedef	struct	Window Window;
+typedef	struct	Xfid Xfid;
+typedef struct	Tapmesg Tapmesg;
+
+enum
+{
+	Selborder		= 4,		/* border of selected window */
+	Unselborder	= 1,		/* border of unselected window */
+	Scrollwid 		= 12,		/* width of scroll bar */
+	Scrollgap 		= 4,		/* gap right of scroll bar */
+	BIG			= 3,		/* factor by which window dimension can exceed screen */
+	TRUE		= 1,
+	FALSE		= 0,
+};
+
+#define	QID(w,q)	((w<<8)|(q))
+#define	WIN(q)	((((ulong)(q).path)>>8) & 0xFFFFFF)
+#define	FILE(q)	(((ulong)(q).path) & 0xFF)
+
+enum	/* control messages */
+{
+	Wakeup,
+	Reshaped,
+	Topped,
+	Repaint,
+	Refresh,
+	Movemouse,
+	Rawon,
+	Rawoff,
+	Holdon,
+	Holdoff,
+	Truncate,
+	Deleted,
+	Exited,
+};
+
+struct Wctlmesg
+{
+	int		type;
+	Rectangle	r;
+	void		*p;
+};
+
+struct Conswritemesg
+{
+	Channel	*cw;		/* chan(Stringpair) */
+};
+
+struct Consreadmesg
+{
+	Channel	*c1;		/* chan(tuple(char*, int) == Stringpair) */
+	Channel	*c2;		/* chan(tuple(char*, int) == Stringpair) */
+};
+
+struct Mousereadmesg
+{
+	Channel	*cm;		/* chan(Mouse) */
+};
+
+struct Stringpair	/* rune and nrune or byte and nbyte */
+{
+	void		*s;
+	int		ns;
+};
+
+struct Mousestate
+{
+	Mouse;
+	ulong	counter;	/* serial no. of mouse event */
+};
+
+struct Mouseinfo
+{
+	Mousestate	queue[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 */
+	uchar	qfull;	/* filled the queue; no more recording until client comes back */	
+};
+
+struct Window
+{
+	Ref;
+	QLock;
+	Frame;
+	Image		*i;		/* window image, nil when deleted */
+	Mousectl	mc;
+	Mouseinfo	mouse;
+	Channel		*ck;		/* chan(char*) */
+	Channel		*cctl;		/* chan(Wctlmesg)[4] */
+	Channel		*conswrite;	/* chan(Conswritemesg) */
+	Channel		*consread;	/* chan(Consreadmesg) */
+	Channel		*mouseread;	/* chan(Mousereadmesg) */
+	Channel		*wctlread;	/* chan(Consreadmesg) */
+	Channel		*kbdread;	/* chan(Consreadmesg) */
+	Channel		*complete;	/* chan(Completion*) */
+	Channel		*gone;		/* chan(char*) */
+	uint			nr;			/* number of runes in window */
+	uint			maxr;		/* number of runes allocated in r */
+	Rune			*r;
+	uint			nraw;
+	Rune			*raw;
+	uint			org;
+	uint			q0;
+	uint			q1;
+	uint			qh;
+	int			id;
+	char			name[32];
+	uint			namecount;
+	Rectangle		scrollr;
+	/*
+	 * Rio once used originwindow, so screenr could be different from i->r.
+	 * Now they're always the same but the code doesn't assume so.
+	*/
+	Rectangle		screenr;	/* screen coordinates of window */
+	int			resized;
+	int			wctlready;
+	Rectangle		lastsr;
+	int			topped;
+	int			notefd;
+	uchar		scrolling;
+	Cursor		cursor;
+	Cursor		*cursorp;
+	uchar		holding;
+	uchar		rawing;
+	uchar		ctlopen;
+	uchar		wctlopen;
+	uchar		deleted;
+	uchar		mouseopen;
+	uchar		kbdopen;
+	uchar		winnameread;
+	char			*label;
+	char			*dir;
+};
+
+void		winctl(void*);
+void		winshell(void*);
+Window*	wlookid(int);
+Window*	wmk(Image*, Mousectl*, Channel*, Channel*, int);
+Window*	wpointto(Point);
+Window*	wtop(Point);
+void		wtopme(Window*);
+void		wbottomme(Window*);
+char*	wcontents(Window*, int*);
+int		wclose(Window*);
+uint		wbacknl(Window*, uint, uint);
+void		wcurrent(Window*);
+void		wuncurrent(Window*);
+void		wcut(Window*);
+void		wpaste(Window*);
+void		wplumb(Window*);
+void		wlook(Window*);
+void		wscrdraw(Window*);
+void		wscroll(Window*, int);
+void		wsend(Window*);
+void		wsendctlmesg(Window*, int, Rectangle, void*);
+void		wsetcursor(Window*, int);
+void		wsetname(Window*);
+void		wsetorigin(Window*, uint, int);
+void		wsetpid(Window*, int, int);
+void		wshow(Window*, uint);
+void		wsnarf(Window*);
+void 		wscrsleep(Window*, uint);
+
+struct Dirtab
+{
+	char		*name;
+	uchar	type;
+	uint		qid;
+	uint		perm;
+};
+
+struct Fid
+{
+	int		fid;
+	int		busy;
+	int		open;
+	int		mode;
+	Qid		qid;
+	Window	*w;
+	Dirtab	*dir;
+	Fid		*next;
+	int		nrpart;
+	uchar	rpart[UTFmax];
+};
+
+struct Xfid
+{
+		Ref;
+		Xfid		*next;
+		Xfid		*free;
+		Fcall;
+		Channel	*c;	/* chan(void(*)(Xfid*)) */
+		Fid		*f;
+		uchar	*buf;
+		Filsys	*fs;
+		int		flushtag;	/* our tag, so flush can find us */
+		Channel	*flushc;	/* channel(int) to notify us we're being flushed */
+};
+
+Channel*	xfidinit(void);
+void		xfidctl(void*);
+void		xfidflush(Xfid*);
+void		xfidattach(Xfid*);
+void		xfidopen(Xfid*);
+void		xfidclose(Xfid*);
+void		xfidread(Xfid*);
+void		xfidwrite(Xfid*);
+
+enum
+{
+	Nhash	= 16,
+};
+
+struct Filsys
+{
+		int		cfd;
+		int		sfd;
+		int		pid;
+		char		*user;
+		Channel	*cxfidalloc;	/* chan(Xfid*) */
+		Channel	*csyncflush;	/* chan(int) */
+		Fid		*fids[Nhash];
+};
+
+Filsys*	filsysinit(Channel*);
+int		filsysmount(Filsys*, int);
+Xfid*		filsysrespond(Filsys*, Xfid*, Fcall*, char*);
+void		filsyscancel(Xfid*);
+
+void		deletetimeoutproc(void*);
+
+struct Timer
+{
+	int		dt;
+	int		cancel;
+	Channel	*c;	/* chan(int) */
+	Timer	*next;
+};
+
+Font		*font;
+Mousectl	*mousectl;
+Mouse	*mouse;
+Display	*display;
+Image	*view;
+Screen	*wscreen;
+Cursor	boxcursor;
+Cursor	crosscursor;
+Cursor	sightcursor;
+Cursor	whitearrow;
+Cursor	query;
+Cursor	*corners[9];
+Cursor	skull;
+
+enum {
+	Cback,
+	Chigh,
+	Cbord,
+	Ctext,
+	Chtext,
+	Ctitle,
+	Cltitle,
+	Chold,
+	Clhold,
+	Cpalehold,
+	Cpaletext,
+	Csize,
+	Crioback,
+	NCOLS,
+};
+Image	*cols[NCOLS];
+
+Window	**window;
+Window	*wkeyboard;	/* window of simulated keyboard */
+int		nwindow;
+int		snarffd;
+int		gotscreen;
+int		servekbd;
+
+enum{
+	Tapon = 'b',
+	Tapoff = 'e',
+	Tapfocus = 'z',
+};
+Channel *ctltap;	/* open/close */
+Channel *resptap;	/* open/close err */
+Channel	*fromtap;	/* input from kbd tap program to window */
+Channel *totap;		/* our keyboard input to tap program */
+Channel *wintap;	/* tell the tapthread which Window to send to */
+
+Window	*input;
+QLock	all;			/* BUG */
+Filsys	*filsys;
+Window	*hidden[100];
+int		nhidden;
+int		nsnarf;
+Rune*	snarf;
+int		scrolling;
+int		maxtab;
+Channel*	winclosechan;
+char		*startdir;
+int		sweeping;
+char		srvpipe[];
+char		srvwctl[];
+int		errorshouldabort;
+int		menuing;		/* menu action is pending; waiting for window to be indicated */
+int		snarfversion;	/* updated each time it is written */
+int		messagesize;		/* negotiated in 9P version setup */
+int		shiftdown;
+int		debug;
--- /dev/null
+++ b/sys/src/cmd/bof/data.c
@@ -1,0 +1,214 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <draw.h>
+#include <thread.h>
+#include <cursor.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <frame.h>
+#include <fcall.h>
+#include "dat.h"
+#include "fns.h"
+
+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 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 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 skull = {
+	{-7,-7},
+	{0x00, 0x00, 0x00, 0x00, 0xc0, 0x03, 0xe7, 0xe7, 
+	 0xff, 0xff, 0xff, 0xff, 0x3f, 0xfc, 0x1f, 0xf8, 
+	 0x0f, 0xf0, 0x3f, 0xfc, 0xff, 0xff, 0xff, 0xff, 
+	 0xef, 0xf7, 0xc7, 0xe3, 0x00, 0x00, 0x00, 0x00,},
+	{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x03,
+	 0xE7, 0xE7, 0x3F, 0xFC, 0x0F, 0xF0, 0x0D, 0xB0,
+	 0x07, 0xE0, 0x06, 0x60, 0x37, 0xEC, 0xE4, 0x27,
+	 0xC3, 0xC3, 0x00, 0x00, 0x00, 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,
+};
+
+void
+iconinit(void)
+{
+	int i;
+
+	Theme th[nelem(cols)] = {
+		[Cback] { "back", 0xFFFFFFFF },
+		[Cbord] { "border", 0x999999FF },
+		[Ctext] { "text", DBlack },
+		[Chtext] { "htext", DBlack },
+		[Chigh] { "high", 0xCCCCCCFF },
+		[Ctitle] { "title", DGreygreen },
+		[Cltitle] { "ltitle", DPalegreygreen },
+		[Chold] { "hold", DMedblue },
+		[Clhold] { "lhold", DGreyblue },
+		[Cpalehold] { "palehold", DPalegreyblue },
+		[Cpaletext] { "paletext", 0x666666FF },
+		[Csize] { "size", DRed },
+		[Crioback] { "rioback", 0x777777FF },
+	};
+	readtheme(th, nelem(th), nil);
+	for(i=0; i<nelem(cols); i++){
+		if((cols[i] = allocimage(display, Rect(0,0,1,1),
+		screen->chan, 1, th[i].c)) == nil)
+			sysfatal("allocimage: %r");
+	}
+}
--- /dev/null
+++ b/sys/src/cmd/bof/fns.h
@@ -1,0 +1,36 @@
+int	whide(Window*);
+int	wunhide(Window*);
+void	freescrtemps(void);
+int	parsewctl(char**, Rectangle, Rectangle*, int*, int*, int*, int*, char**, char*, char*);
+int	writewctl(Xfid*, char*);
+Window *new(Image*, int, int, int, char*, char*, char**);
+void	riosetcursor(Cursor*);
+int	min(int, int);
+int	max(int, int);
+Rune*	strrune(Rune*, Rune);
+int	isalnum(Rune);
+int	isspace(Rune);
+void	timerstop(Timer*);
+void	timercancel(Timer*);
+Timer*	timerstart(int);
+void	error(char*);
+void	killprocs(void);
+int	shutdown(void*, char*);
+void	iconinit(void);
+void	*erealloc(void*, uint);
+void *emalloc(uint);
+char *estrdup(char*);
+void	button3menu(void);
+void	button2menu(Window*);
+void	cvttorunes(char*, int, Rune*, int*, int*, int*);
+/* was (byte*,int)	runetobyte(Rune*, int); */
+char* runetobyte(Rune*, int, int*);
+void	putsnarf(void);
+void	getsnarf(void);
+void	timerinit(void);
+int	goodrect(Rectangle);
+int	inborder(Rectangle, Point);
+
+#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))
--- /dev/null
+++ b/sys/src/cmd/bof/fsys.c
@@ -1,0 +1,702 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <cursor.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <frame.h>
+#include <fcall.h>
+#include "dat.h"
+#include "fns.h"
+
+char Eperm[] = "permission denied";
+char Eexist[] = "file does not exist";
+char Enotdir[] = "not a directory";
+char Ebadfcall[] = "bad fcall type";
+char Eoffset[] = "illegal offset";
+char Enomem[] = "out of memory";
+
+int	messagesize = 8192+IOHDRSZ;	/* good start */
+
+Dirtab dirtab[]=
+{
+	{ ".",			QTDIR,	Qdir,			0500|DMDIR },
+	{ "screen",		QTFILE,	Qscreen,		0400 },
+	{ "snarf",		QTFILE,	Qsnarf,		0600 },
+	{ "wctl",		QTFILE,	Qwctl,		0600 },
+	{ "kbdtap",	QTFILE,	Qtap,	0660 },
+	{ "wsys",		QTDIR,	Qwsys,		0500|DMDIR },
+
+	{ "cons",		QTFILE,	Qcons,		0600 },
+	{ "cursor",		QTFILE,	Qcursor,		0600 },
+	{ "consctl",	QTFILE,	Qconsctl,		0200 },
+	{ "winid",		QTFILE,	Qwinid,		0400 },
+	{ "winname",	QTFILE,	Qwinname,	0400 },
+	{ "label",		QTFILE,	Qlabel,		0600 },
+	{ "kbd",	QTFILE,	Qkbd,		0600 },
+	{ "mouse",	QTFILE,	Qmouse,		0600 },
+	{ "text",		QTFILE,	Qtext,		0600 },
+	{ "wdir",		QTFILE,	Qwdir,		0600 },
+	{ "window",	QTFILE,	Qwindow,		0400 },
+	{ "dot",	QTFILE,	Qdot,			0600 },
+	{ nil, }
+};
+
+static uint		getclock(void);
+static void		filsysproc(void*);
+static Fid*		newfid(Filsys*, int);
+static int		dostat(Filsys*, int, Dirtab*, uchar*, int, uint);
+
+int	clockfd;
+int	firstmessage = 1;
+
+char	srvpipe[64];
+char	srvwctl[64];
+
+static	Xfid*	filsysflush(Filsys*, Xfid*, Fid*);
+static	Xfid*	filsysversion(Filsys*, Xfid*, Fid*);
+static	Xfid*	filsysauth(Filsys*, Xfid*, Fid*);
+static	Xfid*	filsysattach(Filsys*, Xfid*, Fid*);
+static	Xfid*	filsyswalk(Filsys*, Xfid*, Fid*);
+static	Xfid*	filsysopen(Filsys*, Xfid*, Fid*);
+static	Xfid*	filsyscreate(Filsys*, Xfid*, Fid*);
+static	Xfid*	filsysread(Filsys*, Xfid*, Fid*);
+static	Xfid*	filsyswrite(Filsys*, Xfid*, Fid*);
+static	Xfid*	filsysclunk(Filsys*, Xfid*, Fid*);
+static	Xfid*	filsysremove(Filsys*, Xfid*, Fid*);
+static	Xfid*	filsysstat(Filsys*, Xfid*, Fid*);
+static	Xfid*	filsyswstat(Filsys*, Xfid*, Fid*);
+
+Xfid* 	(*fcall[Tmax])(Filsys*, Xfid*, Fid*) =
+{
+	[Tflush]	= filsysflush,
+	[Tversion]	= filsysversion,
+	[Tauth]	= filsysauth,
+	[Tattach]	= filsysattach,
+	[Twalk]	= filsyswalk,
+	[Topen]	= filsysopen,
+	[Tcreate]	= filsyscreate,
+	[Tread]	= filsysread,
+	[Twrite]	= filsyswrite,
+	[Tclunk]	= filsysclunk,
+	[Tremove]= filsysremove,
+	[Tstat]	= filsysstat,
+	[Twstat]	= filsyswstat,
+};
+
+void
+post(char *name, char *envname, int srvfd)
+{
+	int fd;
+	char buf[32];
+
+	fd = create(name, OWRITE|ORCLOSE|OCEXEC, 0600);
+	if(fd < 0)
+		error(name);
+	snprint(buf, sizeof(buf), "%d", srvfd);
+	if(write(fd, buf, strlen(buf)) != strlen(buf))
+		error("srv write");
+	putenv(envname, name);
+}
+
+/*
+ * 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;
+}
+
+Filsys*
+filsysinit(Channel *cxfidalloc)
+{
+	Filsys *fs;
+
+	fs = emalloc(sizeof(Filsys));
+	if(cexecpipe(&fs->cfd, &fs->sfd) < 0)
+		goto Rescue;
+	fmtinstall('F', fcallfmt);
+	clockfd = open("/dev/time", OREAD|OCEXEC);
+	fs->user = getuser();
+	fs->csyncflush = chancreate(sizeof(int), 0);
+	if(fs->csyncflush == nil)
+		error("chancreate syncflush");
+	fs->cxfidalloc = cxfidalloc;
+
+	proccreate(filsysproc, fs, 10000);
+	snprint(srvpipe, sizeof(srvpipe), "/srv/rio.%s.%lud", fs->user, (ulong)getpid());
+	post(srvpipe, "wsys", fs->cfd);
+
+	return fs;
+
+Rescue:
+	free(fs);
+	return nil;
+}
+
+static
+void
+filsysproc(void *arg)
+{
+	int n;
+	Xfid *x;
+	Fid *f;
+	Fcall t;
+	uchar *buf;
+	Filsys *fs;
+
+	threadsetname("FILSYSPROC");
+	fs = arg;
+	fs->pid = getpid();
+	x = nil;
+	for(;;){
+		buf = malloc(messagesize+UTFmax);	/* UTFmax for appending partial rune in xfidwrite */
+		if(buf == nil)
+			error(Enomem);
+		n = read9pmsg(fs->sfd, buf, messagesize);
+		if(n <= 0){
+			yield();	/* if threadexitsall'ing, will not return */
+			fprint(2, "rio: %d: read9pmsg: %d %r\n", getpid(), n);
+			errorshouldabort = 0;
+			error("eof or i/o error on server channel");
+		}
+		if(x == nil){
+			send(fs->cxfidalloc, nil);
+			recv(fs->cxfidalloc, &x);
+			x->fs = fs;
+		}
+		x->buf = buf;
+		if(convM2S(buf, n, x) != n)
+			error("convert error in convM2S");
+		if(debug)
+			fprint(2, "rio:<-%F\n", &x->Fcall);
+		if(fcall[x->type] == nil)
+			x = filsysrespond(fs, x, &t, Ebadfcall);
+		else{
+			if(x->type==Tversion || x->type==Tauth)
+				f = nil;
+			else
+				f = newfid(fs, x->fid);
+			x->f = f;
+			x  = (*fcall[x->type])(fs, x, f);
+		}
+		firstmessage = 0;
+	}
+}
+
+/*
+ * Called only from a different FD group
+ */
+int
+filsysmount(Filsys *fs, int id)
+{
+	char buf[32];
+
+	close(fs->sfd);	/* close server end so mount won't hang if exiting */
+	snprint(buf, sizeof buf, "%d", id);
+	if(mount(fs->cfd, -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;
+}
+
+Xfid*
+filsysrespond(Filsys *fs, Xfid *x, Fcall *t, char *err)
+{
+	int n;
+
+	if(err){
+		t->type = Rerror;
+		t->ename = err;
+	}else
+		t->type = x->type+1;
+	t->fid = x->fid;
+	t->tag = x->tag;
+	if(x->buf == nil)
+		error("no buffer in respond");
+	n = convS2M(t, x->buf, messagesize);
+	if(n <= 0)
+		error("convert error in convS2M");
+	if(write(fs->sfd, x->buf, n) != n)
+		error("write error in respond");
+	if(debug)
+		fprint(2, "rio:->%F\n", t);
+	free(x->buf);
+	x->buf = nil;
+	x->flushtag = -1;
+	return x;
+}
+
+void
+filsyscancel(Xfid *x)
+{
+	if(x->buf){
+		free(x->buf);
+		x->buf = nil;
+	}
+}
+
+static
+Xfid*
+filsysversion(Filsys *fs, Xfid *x, Fid*)
+{
+	Fcall t;
+
+	if(!firstmessage)
+		return filsysrespond(x->fs, x, &t, "version request not first message");
+	if(x->msize < 256)
+		return filsysrespond(x->fs, x, &t, "version: message size too small");
+	messagesize = x->msize;
+	t.msize = messagesize;
+	t.version = "9P2000";
+	if(strncmp(x->version, "9P", 2) != 0)
+		t.version = "unknown";
+	return filsysrespond(fs, x, &t, nil);
+}
+
+static
+Xfid*
+filsysauth(Filsys *fs, Xfid *x, Fid*)
+{
+	Fcall t;
+
+	return filsysrespond(fs, x, &t, "rio: authentication not required");
+}
+
+static
+Xfid*
+filsysflush(Filsys *fs, Xfid *x, Fid*)
+{
+	sendp(x->c, xfidflush);
+
+	/*
+	 * flushes need to be replied in order. xfidflush() will
+	 * awaken us when the flush has been queued.
+	 */
+	recv(fs->csyncflush, nil);
+
+	return nil;
+}
+
+static
+Xfid*
+filsysattach(Filsys *, Xfid *x, Fid *f)
+{
+	Fcall t;
+
+	if(strcmp(x->uname, x->fs->user) != 0)
+		return filsysrespond(x->fs, x, &t, Eperm);
+	f->busy = TRUE;
+	f->open = FALSE;
+	f->qid.path = Qdir;
+	f->qid.type = QTDIR;
+	f->qid.vers = 0;
+	f->dir = dirtab;
+	f->nrpart = 0;
+	sendp(x->c, xfidattach);
+	return nil;
+}
+
+static
+int
+numeric(char *s)
+{
+	for(; *s!='\0'; s++)
+		if(*s<'0' || '9'<*s)
+			return 0;
+	return 1;
+}
+
+static
+int
+skipdir(char *name)
+{
+	/* don't serve these if it's provided in the environment */
+	if(snarffd>=0 && strcmp(name, "snarf")==0)
+		return 1;
+	if(gotscreen && strcmp(name, "screen")==0)
+		return 1;
+	if(!servekbd && strcmp(name, "kbd")==0)
+		return 1;
+	return 0;
+}
+
+static
+Xfid*
+filsyswalk(Filsys *fs, Xfid *x, Fid *f)
+{
+	Fcall t;
+	Fid *nf;
+	int i, id;
+	uchar type;
+	ulong path;
+	Dirtab *d, *dir;
+	Window *w;
+	char *err;
+	Qid qid;
+
+	if(f->open)
+		return filsysrespond(fs, x, &t, "walk of open file");
+	nf = nil;
+	if(x->fid  != x->newfid){
+		/* BUG: check exists */
+		nf = newfid(fs, x->newfid);
+		if(nf->busy)
+			return filsysrespond(fs, x, &t, "clone to busy fid");
+		nf->busy = TRUE;
+		nf->open = FALSE;
+		nf->dir = f->dir;
+		nf->qid = f->qid;
+		nf->w = f->w;
+		if(f->w != nil)
+			incref(f->w);
+		nf->nrpart = 0;	/* not open, so must be zero */
+		f = nf;	/* walk f */
+	}
+
+	t.nwqid = 0;
+	err = nil;
+
+	/* update f->qid, f->dir only if walk completes */
+	qid = f->qid;
+	dir = f->dir;
+
+	if(x->nwname > 0){
+		for(i=0; i<x->nwname; i++){
+			if((qid.type & QTDIR) == 0){
+				err = Enotdir;
+				break;
+			}
+			if(strcmp(x->wname[i], "..") == 0){
+				type = QTDIR;
+				path = Qdir;
+				dir = dirtab;
+				if(FILE(qid) == Qwsysdir)
+					path = Qwsys;
+				id = 0;
+    Accept:
+				if(i == MAXWELEM){
+					err = "name too long";
+					break;
+				}
+				qid.type = type;
+				qid.vers = 0;
+				qid.path = QID(id, path);
+				t.wqid[t.nwqid++] = qid;
+				continue;
+			}
+
+			if(qid.path == Qwsys){
+				/* is it a numeric name? */
+				if(!numeric(x->wname[i]))
+					break;
+				/* yes: it's a directory */
+				id = atoi(x->wname[i]);
+				qlock(&all);
+				w = wlookid(id);
+				if(w == nil){
+					qunlock(&all);
+					break;
+				}
+				path = Qwsysdir;
+				type = QTDIR;
+				qunlock(&all);
+				incref(w);
+				if(f->w)
+					sendp(winclosechan, f->w);
+				f->w = w;
+				dir = dirtab;
+				goto Accept;
+			}
+			if(skipdir(x->wname[i]))
+				break;
+			id = WIN(f->qid);
+			d = dirtab;
+			d++;	/* skip '.' */
+			for(; d->name; d++)
+				if(strcmp(x->wname[i], d->name) == 0){
+					if(f->w == nil && d->qid >= Qglobal)
+						break;
+					path = d->qid;
+					type = d->type;
+					dir = d;
+					goto Accept;
+				}
+
+			break;	/* file not found */
+		}
+
+		if(i==0 && err==nil)
+			err = Eexist;
+	}
+
+	if(err!=nil || t.nwqid<x->nwname){
+		if(nf){
+			if(nf->w)
+				sendp(winclosechan, nf->w);
+			nf->open = FALSE;
+			nf->busy = FALSE;
+		}
+	}else if(t.nwqid == x->nwname){
+		f->dir = dir;
+		f->qid = qid;
+	}
+
+	return filsysrespond(fs, x, &t, err);
+}
+
+static
+Xfid*
+filsysopen(Filsys *fs, Xfid *x, Fid *f)
+{
+	Fcall t;
+	int m;
+
+	/* can't truncate anything but Qtext, so just disregard */
+	if(FILE(f->qid) != Qtext)
+		x->mode &= ~OTRUNC;
+	x->mode &= ~OCEXEC;
+	/* can't execute or remove anything */
+	if(x->mode==OEXEC || (x->mode&ORCLOSE))
+		goto Deny;
+	switch(x->mode & ~OTRUNC){
+	default:
+		goto Deny;
+	case OREAD:
+		m = 0400;
+		break;
+	case OWRITE:
+		m = 0200;
+		break;
+	case ORDWR:
+		m = 0600;
+		break;
+	}
+	if(((f->dir->perm&~(DMDIR|DMAPPEND))&m) != m)
+		goto Deny;
+
+	sendp(x->c, xfidopen);
+	return nil;
+
+    Deny:
+	return filsysrespond(fs, x, &t, Eperm);
+}
+
+static
+Xfid*
+filsyscreate(Filsys *fs, Xfid *x, Fid*)
+{
+	Fcall t;
+
+	return filsysrespond(fs, x, &t, Eperm);
+}
+
+static
+int
+idcmp(void *a, void *b)
+{
+	return *(int*)a - *(int*)b;
+}
+
+static
+Xfid*
+filsysread(Filsys *fs, Xfid *x, Fid *f)
+{
+	Fcall t;
+	uchar *b;
+	int i, n, o, e, len, j, k, *ids;
+	Dirtab *d, dt;
+	uint clock;
+	char buf[32];
+
+	if((f->qid.type & QTDIR) == 0){
+		sendp(x->c, xfidread);
+		return nil;
+	}
+	o = x->offset;
+	e = x->offset+x->count;
+	clock = getclock();
+	b = malloc(messagesize-IOHDRSZ);	/* avoid memset of emalloc */
+	if(b == nil)
+		return filsysrespond(fs, x, &t, Enomem);
+	n = 0;
+	switch(FILE(f->qid)){
+	case Qdir:
+	case Qwsysdir:
+		d = dirtab;
+		d++;	/* first entry is '.' */
+		for(i=0; d->name!=nil && i<e; d++){
+			if(skipdir(d->name))
+				continue;
+			if(f->w == nil && d->qid >= Qglobal)
+				continue;
+			len = dostat(fs, WIN(x->f->qid), d, b+n, x->count-n, clock);
+			if(len <= BIT16SZ)
+				break;
+			if(i >= o)
+				n += len;
+			i += len;
+		}
+		break;
+	case Qwsys:
+		qlock(&all);
+		ids = emalloc(nwindow*sizeof(int));
+		for(j=0; j<nwindow; j++)
+			ids[j] = window[j]->id;
+		qunlock(&all);
+		qsort(ids, nwindow, sizeof ids[0], idcmp);
+		dt.name = buf;
+		for(i=0, j=0; j<nwindow && i<e; i+=len){
+			k = ids[j];
+			sprint(dt.name, "%d", k);
+			dt.qid = QID(k, Qdir);
+			dt.type = QTDIR;
+			dt.perm = DMDIR|0700;
+			len = dostat(fs, k, &dt, b+n, x->count-n, clock);
+			if(len == 0)
+				break;
+			if(i >= o)
+				n += len;
+			j++;
+		}
+		free(ids);
+		break;
+	}
+	t.data = (char*)b;
+	t.count = n;
+	filsysrespond(fs, x, &t, nil);
+	free(b);
+	return x;
+}
+
+static
+Xfid*
+filsyswrite(Filsys*, Xfid *x, Fid*)
+{
+	sendp(x->c, xfidwrite);
+	return nil;
+}
+
+static
+Xfid*
+filsysclunk(Filsys *fs, Xfid *x, Fid *f)
+{
+	Fcall t;
+
+	if(f->open){
+		f->busy = FALSE;
+		f->open = FALSE;
+		sendp(x->c, xfidclose);
+		return nil;
+	}
+	if(f->w)
+		sendp(winclosechan, f->w);
+	f->busy = FALSE;
+	f->open = FALSE;
+	return filsysrespond(fs, x, &t, nil);
+}
+
+static
+Xfid*
+filsysremove(Filsys *fs, Xfid *x, Fid*)
+{
+	Fcall t;
+
+	return filsysrespond(fs, x, &t, Eperm);
+}
+
+static
+Xfid*
+filsysstat(Filsys *fs, Xfid *x, Fid *f)
+{
+	Fcall t;
+
+	t.stat = emalloc(messagesize-IOHDRSZ);
+	t.nstat = dostat(fs, WIN(x->f->qid), f->dir, t.stat, messagesize-IOHDRSZ, getclock());
+	x = filsysrespond(fs, x, &t, nil);
+	free(t.stat);
+	return x;
+}
+
+static
+Xfid*
+filsyswstat(Filsys *fs, Xfid *x, Fid*)
+{
+	Fcall t;
+
+	return filsysrespond(fs, x, &t, Eperm);
+}
+
+static
+Fid*
+newfid(Filsys *fs, int fid)
+{
+	Fid *f, *ff, **fh;
+
+	ff = nil;
+	fh = &fs->fids[fid&(Nhash-1)];
+	for(f=*fh; f; f=f->next)
+		if(f->fid == fid)
+			return f;
+		else if(ff==nil && f->busy==FALSE)
+			ff = f;
+	if(ff){
+		ff->fid = fid;
+		return ff;
+	}
+	f = emalloc(sizeof *f);
+	f->fid = fid;
+	f->next = *fh;
+	*fh = f;
+	return f;
+}
+
+static
+uint
+getclock(void)
+{
+	char buf[32];
+
+	seek(clockfd, 0, 0);
+	read(clockfd, buf, sizeof buf);
+	return atoi(buf);
+}
+
+static
+int
+dostat(Filsys *fs, int id, Dirtab *dir, uchar *buf, int nbuf, uint clock)
+{
+	Dir d;
+
+	d.qid.path = QID(id, dir->qid);
+	if(dir->qid == Qsnarf)
+		d.qid.vers = snarfversion;
+	else
+		d.qid.vers = 0;
+	d.qid.type = dir->type;
+	d.mode = dir->perm;
+	d.length = 0;	/* would be nice to do better */
+	d.name = dir->name;
+	d.uid = fs->user;
+	d.gid = fs->user;
+	d.muid = fs->user;
+	d.atime = clock;
+	d.mtime = clock;
+	return convD2M(&d, buf, nbuf);
+}
--- /dev/null
+++ b/sys/src/cmd/bof/mkfile
@@ -1,0 +1,30 @@
+</$objtype/mkfile
+BIN=/$objtype/bin
+
+TARG=bof
+OFILES=\
+	rio.$O\
+	data.$O\
+	fsys.$O\
+	scrl.$O\
+	time.$O\
+	util.$O\
+	wctl.$O\
+	wind.$O\
+	xfid.$O\
+
+HFILES=dat.h\
+	fns.h\
+
+UPDATE=\
+	mkfile\
+	$HFILES\
+	${OFILES:%.$O=%.c}\
+
+</sys/src/cmd/mkone
+
+$O.out:	/$objtype/lib/libdraw.a /$objtype/lib/libframe.a \
+	/$objtype/lib/libthread.a /$objtype/lib/libplumb.a /$objtype/lib/libc.a
+syms:V:
+	$CC -a $CFLAGS rio.c	> syms
+	$CC -aa $CFLAGS *.c 	>>syms
--- /dev/null
+++ b/sys/src/cmd/bof/rio.c
@@ -1,0 +1,1428 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <cursor.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <frame.h>
+#include <fcall.h>
+#include <plumb.h>
+#include "dat.h"
+#include "fns.h"
+
+/*
+ *  WASHINGTON (AP) - The Food and Drug Administration warned
+ * consumers Wednesday not to use ``Rio'' hair relaxer products
+ * because they may cause severe hair loss or turn hair green....
+ *    The FDA urged consumers who have experienced problems with Rio
+ * to notify their local FDA office, local health department or the
+ * company at 1‑800‑543‑3002.
+ */
+
+void		resize(void);
+void		move(void);
+void		delete(void);
+void		hide(void);
+void		unhide(int);
+void		newtile(int);
+void		confirmexit(void);
+Image*	sweep(void);
+Image*	bandsize(Window*);
+Image*	drag(Window*);
+void		resized(void);
+Channel	*exitchan;	/* chan(int) */
+Channel	*winclosechan; /* chan(Window*); */
+Channel	*kbdchan;	/* chan(char*); */
+Rectangle	viewr;
+int		threadrforkflag = 0;	/* should be RFENVG but that hides rio from plumber */
+
+void	mousethread(void*);
+void	keyboardtap(void*);
+void	winclosethread(void*);
+void	initcmd(void*);
+Channel* initkbd(void);
+
+char		*fontname;
+
+enum
+{
+	New,
+	Reshape,
+	Move,
+	Delete,
+	Hide,
+	Exit,
+};
+
+enum
+{
+	Cut,
+	Paste,
+	Snarf,
+	Plumb,
+	Look,
+	Send,
+	Scroll,
+};
+
+char		*menu2str[] = {
+ [Cut]		"cut",
+ [Paste]		"paste",
+ [Snarf]		"snarf",
+ [Plumb]		"plumb",
+ [Look]		"look",
+ [Send]		"send",
+ [Scroll]		"scroll",
+			nil
+};
+
+Menu menu2 =
+{
+	menu2str
+};
+
+int	Hidden = Exit+1;
+
+char		*menu3str[100] = {
+ [New]		"New",
+ [Reshape]	"Resize",
+ [Move]		"Move",
+ [Delete]		"Delete",
+ [Hide]		"Hide",
+ [Exit]		"Exit",
+			nil
+};
+
+Menu menu3 =
+{
+	menu3str
+};
+
+char *rcargv[] = { "rc", "-i", nil };
+char *kbdargv[] = { "rc", "-c", nil, nil };
+
+int errorshouldabort = 0;
+
+void
+derror(Display*, char *errorstr)
+{
+	error(errorstr);
+}
+
+void
+usage(void)
+{
+	fprint(2, "usage: rio [-b] [-f font] [-i initcmd] [-k kbdcmd] [-s]\n");
+	exits("usage");
+}
+
+void
+threadmain(int argc, char *argv[])
+{
+	char *initstr, *kbdin, *s;
+	char buf[256];
+	Image *i;
+	Rectangle r;
+
+	initstr = nil;
+	kbdin = nil;
+	maxtab = 0;
+	ARGBEGIN{
+	case 'f':
+		fontname = EARGF(usage());
+		break;
+	case 'i':
+		initstr = EARGF(usage());
+		break;
+	case 'k':
+		if(kbdin != nil)
+			usage();
+		kbdin = EARGF(usage());
+		break;
+	case 's':
+		scrolling = TRUE;
+		break;
+	case 'D':
+		debug++;
+		break;
+	default:
+		usage();
+	}ARGEND
+
+	if(getwd(buf, sizeof buf) == nil)
+		startdir = estrdup(".");
+	else
+		startdir = estrdup(buf);
+	if(fontname == nil)
+		fontname = getenv("font");
+	s = getenv("tabstop");
+	if(s != nil)
+		maxtab = strtol(s, nil, 0);
+	if(maxtab == 0)
+		maxtab = 4;
+	free(s);
+
+	if(fontname){
+		/* check font before barging ahead */
+		if(access(fontname, 0) < 0){
+			fprint(2, "rio: can't access %s: %r\n", fontname);
+			exits("font open");
+		}
+		putenv("font", fontname);
+	}
+
+	snarffd = open("/dev/snarf", OREAD|OCEXEC);
+	gotscreen = access("/dev/screen", AEXIST)==0;
+
+	if(geninitdraw(nil, derror, nil, "rio", nil, Refnone) < 0){
+		fprint(2, "rio: can't open display: %r\n");
+		exits("display open");
+	}
+	iconinit();
+
+	exitchan = chancreate(sizeof(int), 0);
+	winclosechan = chancreate(sizeof(Window*), 0);
+
+	view = screen;
+	viewr = view->r;
+	mousectl = initmouse(nil, screen);
+	if(mousectl == nil)
+		error("can't find mouse");
+	mouse = mousectl;
+	kbdchan = initkbd();
+	if(kbdchan == nil)
+		error("can't find keyboard");
+	totap = chancreate(sizeof(char*), 32);
+	fromtap = chancreate(sizeof(char*), 32);
+	wintap = chancreate(sizeof(Window*), 0);
+	ctltap = chancreate(sizeof(char*), 0);
+	resptap = chancreate(sizeof(char*), 0);
+	proccreate(keyboardtap, nil, STACK);
+
+	wscreen = allocscreen(screen, cols[Crioback], 0);
+	if(wscreen == nil)
+		error("can't allocate screen");
+	draw(view, viewr, cols[Crioback], nil, ZP);
+	flushimage(display, 1);
+
+	timerinit();
+	threadcreate(mousethread, nil, STACK);
+	threadcreate(winclosethread, nil, STACK);
+	filsys = filsysinit(xfidinit());
+
+	if(filsys == nil)
+		fprint(2, "rio: can't create file system server: %r\n");
+	else{
+		errorshouldabort = 1;	/* suicide if there's trouble after this */
+		if(initstr)
+			proccreate(initcmd, initstr, STACK);
+		if(kbdin){
+			kbdargv[2] = kbdin;
+			r = screen->r;
+			r.min.y = r.max.y-Dy(r)/3;
+			i = allocwindow(wscreen, r, Refbackup, DNofill);
+			wkeyboard = new(i, FALSE, scrolling, 0, nil, "/bin/rc", kbdargv);
+			if(wkeyboard == nil)
+				error("can't create keyboard window");
+		}
+		threadnotify(shutdown, 1);
+		recv(exitchan, nil);
+	}
+	killprocs();
+	closedisplay(display);
+	threadexitsall(nil);
+}
+
+/*
+ * /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
+getsnarf(void)
+{
+	int i, n, nb, nulls;
+	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;
+	if((snarf = runerealloc(snarf, i+1)) != nil)
+		cvttorunes(sn, i, snarf, &nb, &nsnarf, &nulls);
+	free(sn);
+}
+
+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, "rio: failed to open wsys: %r\n");
+	if(mount(fd, -1, "/mnt/wsys", MREPL, "none") < 0)
+		fprint(2, "rio: failed to mount wsys: %r\n");
+	if(bind("/mnt/wsys", "/dev/", MBEFORE) < 0)
+		fprint(2, "rio: failed to bind wsys: %r\n");
+	free(wsys);
+	close(fd);
+	procexecl(nil, "/bin/rc", "rc", "-c", cmd, nil);
+	fprint(2, "rio: exec failed: %r\n");
+	exits("exec");
+}
+
+char *oknotes[] =
+{
+	"delete",
+	"hangup",
+	"kill",
+	"exit",
+	nil
+};
+
+int
+shutdown(void *, char *msg)
+{
+	int i;
+	static Lock shutdownlk;
+	
+	killprocs();
+	for(i=0; oknotes[i]; i++)
+		if(strncmp(oknotes[i], msg, strlen(oknotes[i])) == 0){
+			lock(&shutdownlk);	/* only one can threadexitsall */
+			threadexitsall(msg);
+		}
+	fprint(2, "rio %d: abort: %s\n", getpid(), msg);
+	abort();
+	exits(msg);
+	return 0;
+}
+
+void
+killprocs(void)
+{
+	int i;
+
+	for(i=0; i<nwindow; i++)
+		if(window[i]->notefd >= 0)
+			write(window[i]->notefd, "hangup", 6); 
+}
+
+static int tapseats[] = { [OREAD] Tapoff, [OWRITE] Tapoff };
+
+char*
+tapctlmsg(char *msg)
+{
+	int perm;
+
+	perm = msg[1];
+	switch(msg[0]){
+	case Tapoff:
+		if(perm == ORDWR)
+			tapseats[OREAD] = Tapoff, tapseats[OWRITE] = Tapoff;
+		else
+			tapseats[perm] = Tapoff;
+		break;
+	case Tapon:
+		switch(perm){
+		case ORDWR:
+			if(tapseats[OREAD] != Tapoff || tapseats[OWRITE] != Tapoff)
+				return "seat taken";
+			tapseats[OREAD] = Tapon, tapseats[OWRITE] = Tapon;
+			break;
+		case OREAD: case OWRITE:
+			if(tapseats[perm] != Tapoff)
+				return "seat taken";
+			tapseats[perm] = Tapon;
+			break;
+		}
+		break;
+	}
+	return nil;
+}
+
+void
+keyboardtap(void*)
+{
+	char *s, *ctl;
+	char *e;
+	char *watched;
+	Window *w, *cur;
+	static char keys[64];
+
+	threadsetname("keyboardtap");
+	enum { Awin, Actl, Afrom, Adev, Ato, Ainp, Awatch, NALT };
+	static Alt alts[NALT+1];
+	/* ctl */
+	alts[Awin].c = wintap;
+	alts[Awin].v = &w;
+	alts[Awin].op = CHANRCV;
+	alts[Actl].c = ctltap;
+	alts[Actl].v = &ctl;
+	alts[Actl].op = CHANRCV;
+	/* kbd input */
+	alts[Afrom].c = fromtap;
+	alts[Afrom].v = &s;
+	alts[Afrom].op = CHANRCV;
+	alts[Adev].c = kbdchan;
+	alts[Adev].v = &s;
+	alts[Adev].op = CHANRCV;
+	/* kbd output */
+	alts[Ato].c = totap;
+	alts[Ato].v = &s;
+	alts[Ato].op = CHANNOP;
+	alts[Ainp].c = nil;
+	alts[Ainp].v = &s;
+	alts[Ainp].op = CHANNOP;
+	alts[Awatch].c = totap;
+	alts[Awatch].v = &watched;
+	alts[Awatch].op = CHANNOP;
+	alts[NALT].op = CHANEND;
+
+	cur = nil;
+	watched = nil;
+	keys[0] = 0;
+	for(;;)
+		switch(alt(alts)){
+		case Awin:
+			cur = w;
+			if(cur != nil){
+				alts[Ainp].c = cur->ck;
+				if(tapseats[OREAD] != Tapoff){	
+					if(alts[Awatch].op == CHANSND)
+						free(watched);
+					watched = smprint("%c%d", Tapfocus, cur->id);
+					alts[Awatch].op = CHANSND;
+				}
+			}
+			if(alts[Ainp].op != CHANNOP || alts[Ato].op != CHANNOP)
+				free(s);
+			if(cur == nil)
+				goto Reset;
+			s = smprint("K%s", keys);
+			alts[Ainp].op = CHANSND;
+			alts[Ato].op = CHANNOP;
+			break;
+		case Actl:
+			e = tapctlmsg(ctl);
+			sendp(resptap, e);
+			if(e != nil || *ctl != Tapoff){
+				free(ctl);
+				break;
+			}
+			free(ctl);
+			goto Reset;
+		case Afrom:
+			if(cur == nil){
+				free(s);
+				break;
+			}
+			alts[Afrom].op = CHANNOP;
+			alts[Adev].op = CHANNOP;
+			alts[Ato].op = CHANNOP;
+			alts[Ainp].op = CHANSND;
+			break;
+		case Adev:
+			if(s[0] == 'k' || s[0] == 'K')
+				strcpy(keys, s+1);
+			if(tapseats[OWRITE] == Tapoff && cur == nil){
+				free(s);
+				break;
+			}
+			alts[Afrom].op = CHANNOP;
+			alts[Adev].op = CHANNOP;
+			if(tapseats[OWRITE] == Tapoff)
+				alts[Ainp].op = CHANSND;
+			else
+				alts[Ato].op = CHANSND;
+			break;
+		case Awatch:
+			alts[Awatch].op = CHANNOP;
+			break;
+		case Ainp:
+			if(*s == 'k' || *s == 'K')
+				shiftdown = utfrune(s+1, Kshift) != nil;
+		case Ato:
+		Reset:
+			alts[Ainp].op = CHANNOP;
+			alts[Ato].op = CHANNOP;
+			alts[Afrom].op = CHANRCV;
+			alts[Adev].op = CHANRCV;
+			break;
+		}
+}
+
+int
+inborder(Rectangle r, Point xy)
+{
+	return ptinrect(xy, r) && !ptinrect(xy, insetrect(r, Selborder));
+}
+
+Rectangle
+whichrect(Rectangle r, Point p, int which)
+{
+	switch(which){
+	case 0:	/* top left */
+		r = Rect(p.x, p.y, r.max.x, r.max.y);
+		break;
+	case 2:	/* top right */
+		r = Rect(r.min.x, p.y, p.x+1, r.max.y);
+		break;
+	case 6:	/* bottom left */
+		r = Rect(p.x, r.min.y, r.max.x, p.y+1);
+		break;
+	case 8:	/* bottom right */
+		r = Rect(r.min.x, r.min.y, p.x+1, p.y+1);
+		break;
+	case 1:	/* top edge */
+		r = Rect(r.min.x, p.y, r.max.x, r.max.y);
+		break;
+	case 5:	/* right edge */
+		r = Rect(r.min.x, r.min.y, p.x+1, r.max.y);
+		break;
+	case 7:	/* bottom edge */
+		r = Rect(r.min.x, r.min.y, r.max.x, p.y+1);
+		break;
+	case 3:	/* left edge */
+		r = Rect(p.x, r.min.y, r.max.x, r.max.y);
+		break;
+	}
+	return canonrect(r);
+}
+
+int
+portion(int x, int lo, int hi)
+{
+	x -= lo;
+	hi -= lo;
+	if(x < hi/2){
+		if(x < 20)
+			return 0;
+	} else {
+		if(x > hi-20)
+			return 2;
+	}
+	return 1;
+}
+
+int
+whichcorner(Rectangle r, Point p)
+{
+	int i, j;
+	
+	i = portion(p.x, r.min.x, r.max.x);
+	j = portion(p.y, r.min.y, r.max.y);
+	return 3*j+i;
+}
+
+/* thread to allow fsysproc to synchronize window closing with main proc */
+void
+winclosethread(void*)
+{
+	Window *w;
+
+	threadsetname("winclosethread");
+	for(;;){
+		w = recvp(winclosechan);
+		wclose(w);
+	}
+}
+
+/*
+ * Button 6 - keyboard toggle - has been pressed.
+ * Send event to keyboard, wait for button up, send that.
+ * Note: there is no coordinate translation done here; this
+ * is just about getting button 6 to the keyboard simulator.
+ */
+void
+keyboardhide(void)
+{
+	send(wkeyboard->mc.c, mouse);
+	do
+		readmouse(mousectl);
+	while(mouse->buttons & (1<<5));
+	send(wkeyboard->mc.c, mouse);
+}
+
+void
+mousethread(void*)
+{
+	int sending, inside, scrolling, moving;
+	Window *w, *winput;
+	Image *i;
+	Point xy;
+	Mouse tmp;
+	enum {
+		MReshape,
+		MMouse,
+		NALT
+	};
+	static Alt alts[NALT+1];
+
+	threadsetname("mousethread");
+	sending = FALSE;
+	scrolling = FALSE;
+
+	alts[MReshape].c = mousectl->resizec;
+	alts[MReshape].v = nil;
+	alts[MReshape].op = CHANRCV;
+	alts[MMouse].c = mousectl->c;
+	alts[MMouse].v = &mousectl->Mouse;
+	alts[MMouse].op = CHANRCV;
+	alts[NALT].op = CHANEND;
+
+	for(;;)
+	    switch(alt(alts)){
+		case MReshape:
+			resized();
+			break;
+		case MMouse:
+			if(wkeyboard!=nil && (mouse->buttons & (1<<5))){
+				keyboardhide();
+				break;
+			}
+		Again:
+			moving = FALSE;
+			winput = input;
+			/* override everything for the keyboard window */
+			if(wkeyboard!=nil && ptinrect(mouse->xy, wkeyboard->screenr)){
+				/* make sure it's on top; this call is free if it is */
+				wtopme(wkeyboard);
+				winput = wkeyboard;
+			}
+			if(winput!=nil && !winput->deleted && winput->i!=nil){
+				/* convert to logical coordinates */
+				xy.x = mouse->xy.x + (winput->i->r.min.x-winput->screenr.min.x);
+				xy.y = mouse->xy.y + (winput->i->r.min.y-winput->screenr.min.y);
+
+				/* the up and down scroll buttons are not subject to the usual rules */
+				if((mouse->buttons&(8|16)) && !winput->mouseopen)
+					goto Sending;
+
+				inside = ptinrect(mouse->xy, insetrect(winput->screenr, Selborder));
+				if(winput->mouseopen)
+					scrolling = FALSE;
+				else if(scrolling)
+					scrolling = mouse->buttons;
+				else
+					scrolling = mouse->buttons && ptinrect(xy, winput->scrollr);
+				/* topped will be zero or less if window has been bottomed */
+				if(sending == FALSE && !scrolling && inborder(winput->screenr, mouse->xy) && winput->topped>0)
+					moving = TRUE;
+				else if(inside && (scrolling || winput->mouseopen || (mouse->buttons&1)))
+					sending = TRUE;
+			}else
+				sending = FALSE;
+			if(sending){
+			Sending:
+				wsetcursor(winput, FALSE);
+				if(mouse->buttons == 0)
+					sending = FALSE;
+				tmp = mousectl->Mouse;
+				tmp.xy = xy;
+				send(winput->mc.c, &tmp);
+				continue;
+			}
+			if(moving && (mouse->buttons&7)){
+				incref(winput);
+				sweeping = TRUE;
+				if(mouse->buttons & 3)
+					i = bandsize(winput);
+				else
+					i = drag(winput);
+				sweeping = FALSE;
+				if(i != nil){
+					wcurrent(winput);
+					wsendctlmesg(winput, Reshaped, i->r, i);
+				}
+				wclose(winput);
+				continue;
+			}
+			w = wpointto(mouse->xy);
+			if(w!=nil && inborder(w->screenr, mouse->xy))
+				riosetcursor(corners[whichcorner(w->screenr, mouse->xy)]);
+			else
+				wsetcursor(w, FALSE);
+			/* we're not sending the event, but if button is down maybe we should */
+			if(mouse->buttons){
+				/* w->topped will be zero or less if window has been bottomed */
+				if(w==nil || (w==winput && w->topped>0)){
+					if(mouse->buttons & 1){
+						;
+					}else if(mouse->buttons & 2){
+						if(winput && !winput->deleted && !winput->mouseopen){
+							incref(winput);
+							button2menu(winput);
+							wclose(winput);
+						}
+					}else if(mouse->buttons & 4)
+						button3menu();
+				}else{
+					/* if button 1 event in the window, top the window and wait for button up. */
+					/* otherwise, top the window and pass the event on */
+					if(wtop(mouse->xy) && (mouse->buttons!=1 || inborder(w->screenr, mouse->xy)))
+						goto Again;
+					goto Drain;
+				}
+			}
+			break;
+
+		Drain:
+			do
+				readmouse(mousectl);
+			while(mousectl->buttons);
+			goto Again;	/* recalculate mouse position, cursor */
+		}
+}
+
+int
+wtopcmp(void *a, void *b)
+{
+	return (*(Window**)a)->topped - (*(Window**)b)->topped;
+}
+
+void
+resized(void)
+{
+	Image *im;
+	int i, j;
+	Rectangle r;
+	Point o, n;
+	Window *w;
+
+	if(getwindow(display, Refnone) < 0)
+		error("failed to re-attach window");
+	freescrtemps();
+	view = screen;
+	freescreen(wscreen);
+	wscreen = allocscreen(screen, cols[Crioback], 0);
+	if(wscreen == nil)
+		error("can't re-allocate screen");
+	draw(view, view->r, cols[Crioback], nil, ZP);
+	o = subpt(viewr.max, viewr.min);
+	n = subpt(view->clipr.max, view->clipr.min);
+	qsort(window, nwindow, sizeof(window[0]), wtopcmp);
+	for(i=0; i<nwindow; i++){
+		w = window[i];
+		r = rectsubpt(w->i->r, viewr.min);
+		r.min.x = (r.min.x*n.x)/o.x;
+		r.min.y = (r.min.y*n.y)/o.y;
+		r.max.x = (r.max.x*n.x)/o.x;
+		r.max.y = (r.max.y*n.y)/o.y;
+		r = rectaddpt(r, view->clipr.min);
+		if(!goodrect(r))
+			r = rectsubpt(w->i->r, subpt(w->i->r.min, r.min));
+		for(j=0; j<nhidden; j++)
+			if(w == hidden[j])
+				break;
+		incref(w);
+		if(j < nhidden){
+			im = allocimage(display, r, screen->chan, 0, DNofill);
+			r = ZR;
+		} else {
+			im = allocwindow(wscreen, r, Refbackup, DNofill);
+		}
+		if(im!=nil)
+			wsendctlmesg(w, Reshaped, r, im);
+		wclose(w);
+	}
+	viewr = view->r;
+	flushimage(display, 1);
+}
+
+int
+obscured(Window *w, Rectangle r, int i)
+{
+	Window *t;
+
+	if(Dx(r) < font->height || Dy(r) < font->height)
+		return 1;
+	if(!rectclip(&r, screen->r))
+		return 1;
+	for(; i<nwindow; i++){
+		t = window[i];
+		if(t == w || t->topped <= w->topped)
+			continue;
+		if(Dx(t->screenr) == 0 || Dy(t->screenr) == 0 || rectXrect(r, t->screenr) == 0)
+			continue;
+		if(r.min.y < t->screenr.min.y)
+			if(!obscured(w, Rect(r.min.x, r.min.y, r.max.x, t->screenr.min.y), i))
+				return 0;
+		if(r.min.x < t->screenr.min.x)
+			if(!obscured(w, Rect(r.min.x, r.min.y, t->screenr.min.x, r.max.y), i))
+				return 0;
+		if(r.max.y > t->screenr.max.y)
+			if(!obscured(w, Rect(r.min.x, t->screenr.max.y, r.max.x, r.max.y), i))
+				return 0;
+		if(r.max.x > t->screenr.max.x)
+			if(!obscured(w, Rect(t->screenr.max.x, r.min.y, r.max.x, r.max.y), i))
+				return 0;
+		return 1;
+	}
+	return 0;
+}
+
+static char*
+shortlabel(char *s)
+{
+	enum { NBUF=60 };
+	static char buf[NBUF*UTFmax];
+	int i, k, l;
+	Rune r;
+
+	l = utflen(s);
+	if(l < NBUF-2)
+		return estrdup(s);
+	k = i = 0;
+	while(i < NBUF/2){
+		k += chartorune(&r, s+k);
+		i++;
+	}
+	strncpy(buf, s, k);
+	strcpy(buf+k, "...");
+	while((l-i) >= NBUF/2-4){
+		k += chartorune(&r, s+k);
+		i++;
+	}
+	strcat(buf, s+k);
+	return estrdup(buf);
+}
+
+void
+button3menu(void)
+{
+	int i, j, n;
+
+	n = nhidden;
+	for(i=0; i<nwindow; i++){
+		for(j=0; j<n; j++)
+			if(window[i] == hidden[j])
+				break;
+		if(j == n)
+			if(obscured(window[i], window[i]->screenr, 0)){
+				hidden[n++] = window[i];
+				if(n >= nelem(hidden))
+					break;
+			}
+	}
+	if(n >= nelem(menu3str)-Hidden)
+		n = nelem(menu3str)-Hidden-1;
+	for(i=0; i<n; i++){
+		free(menu3str[i+Hidden]);
+		menu3str[i+Hidden] = shortlabel(hidden[i]->label);
+	}
+	for(i+=Hidden; menu3str[i]; i++){
+		free(menu3str[i]);
+		menu3str[i] = nil;
+	}
+	sweeping = TRUE;
+	menu3.lasthit = 0;
+	switch(i = menuhit(3, mousectl, &menu3, wscreen)){
+	case -1:
+		break;
+	case New:
+		new(sweep(), FALSE, scrolling, 0, nil, "/bin/rc", nil);
+		break;
+	case Reshape:
+		resize();
+		break;
+	case Move:
+		move();
+		break;
+	case Delete:
+		delete();
+		break;
+	case Hide:
+		hide();
+		break;
+	case Exit:
+		confirmexit();
+		break;
+	default:
+		unhide(i);
+		break;
+	}
+	sweeping = FALSE;
+}
+
+void
+button2menu(Window *w)
+{
+	if(w->scrolling)
+		menu2str[Scroll] = "noscroll";
+	else
+		menu2str[Scroll] = "scroll";
+	switch(menuhit(2, mousectl, &menu2, wscreen)){
+	case Cut:
+		wsnarf(w);
+		wcut(w);
+		wscrdraw(w);
+		break;
+
+	case Snarf:
+		wsnarf(w);
+		break;
+
+	case Paste:
+		getsnarf();
+		wpaste(w);
+		wscrdraw(w);
+		break;
+
+	case Plumb:
+		wplumb(w);
+		break;
+
+	case Look:
+		wlook(w);
+		break;
+
+	case Send:
+		wsend(w);
+		break;
+
+	case Scroll:
+		if(w->scrolling ^= 1)
+			wshow(w, w->nr);
+		break;
+	}
+	flushimage(display, 1);
+	wsendctlmesg(w, Wakeup, ZR, nil);
+}
+
+Point
+onscreen(Point p)
+{
+	p.x = max(screen->clipr.min.x, p.x);
+	p.x = min(screen->clipr.max.x-1, p.x);
+	p.y = max(screen->clipr.min.y, p.y);
+	p.y = min(screen->clipr.max.y-1, p.y);
+	return p;
+}
+
+Image*
+sweep(void)
+{
+	Image *i, *oi;
+	Rectangle r;
+	Point p0, p;
+
+	i = nil;
+	menuing = TRUE;
+	riosetcursor(&crosscursor);
+	while(mouse->buttons == 0)
+		readmouse(mousectl);
+	p0 = onscreen(mouse->xy);
+	p = p0;
+	r.min = p;
+	r.max = p;
+	oi = nil;
+	while(mouse->buttons == 4){
+		if(!eqpt(mouse->xy, p)){
+			p = onscreen(mouse->xy);
+			r = canonrect(Rpt(p0, p));
+			r = whichrect(r, p, whichcorner(r, p));
+			if(Dx(r)>5 && Dy(r)>5){
+				i = allocwindow(wscreen, r, Refnone, DNofill);
+				freeimage(oi);
+				if(i == nil)
+					goto Rescue;
+				oi = i;
+				border(i, r, Selborder, cols[Csize], ZP);
+				draw(i, insetrect(r, Selborder), cols[BACK], nil, ZP);
+			}
+		}
+		readmouse(mousectl);
+	}
+	if(mouse->buttons != 0)
+		goto Rescue;
+	if(i==nil || !goodrect(r))
+		goto Rescue;
+	oi = i;
+	i = allocwindow(wscreen, oi->r, Refbackup, DNofill);
+	freeimage(oi);
+	if(i == nil)
+		goto Rescue;
+	riosetcursor(corners[whichcorner(i->r, mouse->xy)]);
+	goto Return;
+
+ Rescue:
+	riosetcursor(nil);
+	freeimage(i);
+	i = nil;
+	flushimage(display, 1);
+	while(mouse->buttons)
+		readmouse(mousectl);
+
+ Return:
+	menuing = FALSE;
+	return i;
+}
+
+void
+drawedge(Image **bp, Image *col, Rectangle r)
+{
+	Image *b = *bp;
+	if(b != nil && Dx(b->r) == Dx(r) && Dy(b->r) == Dy(r))
+		originwindow(b, r.min, r.min);
+	else{
+		freeimage(b);
+		b = allocwindow(wscreen, r, Refbackup, DNofill);
+		if(b != nil) draw(b, r, col, nil, ZP);
+		*bp = b;
+	}
+}
+
+void
+drawborder(Rectangle r, Image *col)
+{
+	static Image *b[4], *lastcol;
+
+	if(col != lastcol){
+		freeimage(b[0]), b[0] = nil;
+		freeimage(b[1]), b[1] = nil;
+		freeimage(b[2]), b[2] = nil;
+		freeimage(b[3]), b[3] = nil;
+	}
+	if(col != nil){
+		r = canonrect(r);
+		drawedge(&b[0], col, Rect(r.min.x, r.min.y, r.min.x+Borderwidth, r.max.y));
+		drawedge(&b[1], col, Rect(r.min.x+Borderwidth, r.min.y, r.max.x-Borderwidth, r.min.y+Borderwidth));
+		drawedge(&b[2], col, Rect(r.max.x-Borderwidth, r.min.y, r.max.x, r.max.y));
+		drawedge(&b[3], col, Rect(r.min.x+Borderwidth, r.max.y-Borderwidth, r.max.x-Borderwidth, r.max.y));
+	}
+	lastcol = col;
+}
+
+Image*
+drag(Window *w)
+{
+	Point p, op, d, dm, om;
+	Rectangle r;
+
+	menuing = TRUE;
+	riosetcursor(&boxcursor);
+	om = mouse->xy;
+	dm = subpt(om, w->screenr.min);
+	d = subpt(w->screenr.max, w->screenr.min);
+	op = subpt(om, dm);
+	drawborder(Rect(op.x, op.y, op.x+d.x, op.y+d.y), cols[Csize]);
+	while(mouse->buttons==4){
+		p = subpt(mouse->xy, dm);
+		if(!eqpt(p, op)){
+			drawborder(Rect(p.x, p.y, p.x+d.x, p.y+d.y), cols[Csize]);
+			op = p;
+		}
+		readmouse(mousectl);
+	}
+	r = Rect(op.x, op.y, op.x+d.x, op.y+d.y);
+	drawborder(r, nil);
+	p = mouse->xy;
+	riosetcursor(inborder(r, p) ? corners[whichcorner(r, p)] : nil);
+	menuing = FALSE;
+	if(mouse->buttons!=0 || !goodrect(r) || eqrect(r, w->screenr)){
+		flushimage(display, 1);
+		while(mouse->buttons)
+			readmouse(mousectl);
+		return nil;
+	}
+	return allocwindow(wscreen, r, Refbackup, DNofill);
+}
+
+Image*
+bandsize(Window *w)
+{
+	Rectangle r, or;
+	Point p, startp;
+	int which, owhich, but;
+
+	owhich = -1;
+	or = w->screenr;
+	but = mouse->buttons;
+	startp = onscreen(mouse->xy);
+	drawborder(or, cols[Csize]);
+	while(mouse->buttons == but) {
+		p = onscreen(mouse->xy);
+		which = whichcorner(or, p);
+		if(which != owhich && which != 4 && (owhich|~which) & 1){
+			owhich = which;
+			riosetcursor(corners[which]);
+		}
+		r = whichrect(or, p, owhich);
+		if(!eqrect(r, or) && goodrect(r)){
+			drawborder(r, cols[Csize]);
+			or = r;
+		}
+		readmouse(mousectl);
+	}
+	drawborder(or, nil);
+	if(!goodrect(or))
+		riosetcursor(nil);
+	if(mouse->buttons!=0 || !goodrect(or) || eqrect(or, w->screenr)
+	|| abs(p.x-startp.x)+abs(p.y-startp.y) <= 1){
+		flushimage(display, 1);
+		while(mouse->buttons)
+			readmouse(mousectl);
+		return nil;
+	}
+	return allocwindow(wscreen, or, Refbackup, DNofill);
+}
+
+Window*
+pointto(int wait)
+{
+	Window *w;
+
+	menuing = TRUE;
+	riosetcursor(&sightcursor);
+	while(mouse->buttons == 0)
+		readmouse(mousectl);
+	if(mouse->buttons == 4)
+		w = wpointto(mouse->xy);
+	else
+		w = nil;
+	if(wait){
+		while(mouse->buttons){
+			if(mouse->buttons!=4 && w !=nil){	/* cancel */
+				riosetcursor(nil);
+				w = nil;
+			}
+			readmouse(mousectl);
+		}
+		if(w != nil && wpointto(mouse->xy) != w)
+			w = nil;
+	}
+	riosetcursor(nil);
+	menuing = FALSE;
+	return w;
+}
+
+void
+delete(void)
+{
+	Window *w;
+
+	w = pointto(TRUE);
+	if(w!=nil)
+		wsendctlmesg(w, Deleted, ZR, nil);
+}
+
+void
+confirmexit(void)
+{
+	menuing = TRUE;
+	riosetcursor(&skull);
+	while(mouse->buttons == 0)
+		readmouse(mousectl);
+	if(mouse->buttons != 4)
+		goto Nope;
+	while(mouse->buttons){
+		if(mouse->buttons != 4)
+			goto Nope;
+		readmouse(mousectl);
+	}
+	send(exitchan, nil);
+Nope:
+	riosetcursor(nil);
+	menuing = FALSE;
+}
+
+void
+resize(void)
+{
+	Window *w;
+	Image *i;
+
+	w = pointto(TRUE);
+	if(w == nil)
+		return;
+	incref(w);
+	i = sweep();
+	if(i!=nil){
+		wcurrent(w);
+		wsendctlmesg(w, Reshaped, i->r, i);
+	}
+	wclose(w);
+}
+
+void
+move(void)
+{
+	Window *w;
+	Image *i;
+
+	w = pointto(FALSE);
+	if(w == nil)
+		return;
+	incref(w);
+	i = drag(w);
+	if(i!=nil){
+		wcurrent(w);
+		wsendctlmesg(w, Reshaped, i->r, i);
+	}
+	wclose(w);
+}
+
+int
+whide(Window *w)
+{
+	Image *i;
+	int j;
+
+	for(j=0; j<nhidden; j++)
+		if(hidden[j] == w)	/* already hidden */
+			return -1;
+	if(nhidden >= nelem(hidden))
+		return 0;
+	incref(w);
+	wuncurrent(w);
+	i = allocimage(display, w->screenr, w->i->chan, 0, DNofill);
+	if(i!=nil){
+		hidden[nhidden++] = w;
+		wsendctlmesg(w, Reshaped, ZR, i);
+	}
+	wclose(w);
+	return i!=0;
+}
+
+int
+wunhide(Window *w)
+{
+	int j;
+	Image *i;
+
+	for(j=0; j<nhidden; j++)
+		if(hidden[j] == w)
+			break;
+	if(j == nhidden)
+		return -1;	/* not hidden */
+	incref(w);
+	wcurrent(w);
+	i = allocwindow(wscreen, w->i->r, Refbackup, DNofill);
+	if(i!=nil){
+		--nhidden;
+		memmove(hidden+j, hidden+j+1, (nhidden-j)*sizeof(Window*));
+		wsendctlmesg(w, Reshaped, w->i->r, i);
+	}
+	wclose(w);
+	return i!=0;
+}
+
+void
+hide(void)
+{
+	Window *w;
+
+	w = pointto(TRUE);
+	if(w)
+		whide(w);
+}
+
+void
+unhide(int j)
+{
+	Window *w;
+
+	if(j < Hidden)
+		return;
+	j -= Hidden;
+	w = hidden[j];
+	if(w == nil)
+		return;
+	if(j < nhidden){
+		wunhide(w);
+		return;
+	}
+	/* uncover obscured window */
+	for(j=0; j<nwindow; j++)
+		if(window[j] == w){
+			incref(w);
+			wcurrent(w);
+			wtopme(w);
+			wsendctlmesg(w, Topped, ZR, nil);
+			wclose(w);
+			return;
+		}
+}
+
+Window*
+new(Image *i, int hideit, int scrollit, int pid, char *dir, char *cmd, char **argv)
+{
+	Window *w;
+	Mousectl *mc;
+	Channel *cm, *ck, *cctl, *cpid;
+	void **arg;
+
+	if(i == nil)
+		return nil;
+	if(hideit && nhidden >= nelem(hidden)){
+		freeimage(i);
+		return nil;
+	}
+	cm = chancreate(sizeof(Mouse), 0);
+	ck = chancreate(sizeof(char*), 0);
+	cctl = chancreate(sizeof(Wctlmesg), 4);
+	cpid = chancreate(sizeof(int), 0);
+	if(cm==nil || ck==nil || cctl==nil)
+		error("new: channel alloc failed");
+	mc = emalloc(sizeof(Mousectl));
+	*mc = *mousectl;
+	mc->image = i;
+	mc->c = cm;
+	w = wmk(i, mc, ck, cctl, scrollit);
+	free(mc);	/* wmk copies *mc */
+	window = erealloc(window, ++nwindow*sizeof(Window*));
+	window[nwindow-1] = w;
+	if(hideit){
+		hidden[nhidden++] = w;
+		w->screenr = ZR;
+	}
+	threadcreate(winctl, w, STACK);
+	if(!hideit)
+		wcurrent(w);
+	if(pid == 0){
+		arg = emalloc(5*sizeof(void*));
+		arg[0] = w;
+		arg[1] = cpid;
+		arg[2] = cmd;
+		if(argv == nil)
+			arg[3] = rcargv;
+		else
+			arg[3] = argv;
+		arg[4] = dir;
+		proccreate(winshell, arg, STACK);
+		pid = recvul(cpid);
+		free(arg);
+	}
+	if(pid == 0){
+		/* window creation failed */
+		wsendctlmesg(w, Deleted, ZR, nil);
+		chanfree(cpid);
+		return nil;
+	}
+	wsetpid(w, pid, 1);
+	wsetname(w);
+	if(dir){
+		free(w->dir);
+		w->dir = estrdup(dir);
+	}
+	chanfree(cpid);
+	return w;
+}
+
+static void
+kbdproc(void *arg)
+{
+	Channel *c = arg;
+	char buf[1024], *p, *e;
+	int fd, cfd, kfd, n;
+
+	threadsetname("kbdproc");
+
+	if((fd = open("/dev/cons", OREAD)) < 0){
+		chanprint(c, "%r");
+		return;
+	}
+	if((cfd = open("/dev/consctl", OWRITE)) < 0){
+		chanprint(c, "%r");
+		return;
+	}
+	fprint(cfd, "rawon");
+
+	if(sendp(c, nil) <= 0)
+		return;
+
+	if((kfd = open("/dev/kbd", OREAD)) >= 0){
+		close(fd);
+
+		/* only serve a kbd file per window when we got one */
+		servekbd = 1;
+
+		/* read kbd state */
+		while((n = read(kfd, buf, sizeof(buf)-1)) > 0){
+			e = buf+n;
+			e[-1] = 0;
+			e[0] = 0;
+			for(p = buf; p < e; p += strlen(p)+1)
+				chanprint(c, "%s", p);
+		}
+	} else {
+		/* read single characters */
+		p = buf;
+		for(;;){
+			Rune r;
+
+			e = buf + sizeof(buf);
+			if((n = read(fd, p, e-p)) <= 0)
+				break;
+			e = p + n;
+			while(p < e && fullrune(p, e - p)){
+				p += chartorune(&r, p);
+				if(r)
+					chanprint(c, "c%C", r);
+			}
+			n = e - p;
+			memmove(buf, p, n);
+			p = buf + n;
+		}
+	}
+	send(exitchan, nil);
+}
+
+Channel*
+initkbd(void)
+{
+	Channel *c;
+	char *e;
+
+	c = chancreate(sizeof(char*), 16);
+	procrfork(kbdproc, c, STACK, RFCFDG);
+	if(e = recvp(c)){
+		chanfree(c);
+		c = nil;
+		werrstr("%s", e);
+		free(e);
+	}
+	return c;
+}
--- /dev/null
+++ b/sys/src/cmd/bof/scrl.c
@@ -1,0 +1,181 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <cursor.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <frame.h>
+#include <fcall.h>
+#include "dat.h"
+#include "fns.h"
+
+static Image *scrtmp;
+
+static
+Image*
+scrtemps(void)
+{
+	int h;
+
+	if(scrtmp == nil){
+		h = BIG*Dy(screen->r);
+		scrtmp = allocimage(display, Rect(0, 0, 32, h), screen->chan, 0, DNofill);
+	}
+	return scrtmp;
+}
+
+void
+freescrtemps(void)
+{
+	if(scrtmp){
+		freeimage(scrtmp);
+		scrtmp = nil;
+	}
+}
+
+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
+wscrdraw(Window *w)
+{
+	Rectangle r, r1, r2;
+	Image *b;
+
+	b = scrtemps();
+	if(b == nil || w->i == nil)
+		return;
+	r = w->scrollr;
+	r1 = r;
+	r1.min.x = 0;
+	r1.max.x = Dx(r);
+	r2 = scrpos(r1, w->org, w->org+w->nchars, w->nr);
+	if(!eqrect(r2, w->lastsr)){
+		w->lastsr = r2;
+		/* move r1, r2 to (0,0) to avoid clipping */
+		r2 = rectsubpt(r2, r1.min);
+		r1 = rectsubpt(r1, r1.min);
+		draw(b, r1, w->cols[BORD], nil, ZP);
+		draw(b, r2, w->cols[BACK], nil, ZP);
+		r2.min.x = r2.max.x-1;
+		draw(b, r2, w->cols[BORD], nil, ZP);
+		draw(w->i, r, b, nil, Pt(0, r1.min.y));
+	}
+}
+
+void
+wscrsleep(Window *w, uint dt)
+{
+	Timer	*timer;
+	int y, b;
+	static Alt alts[3];
+
+	if(display->bufp > display->buf)
+		flushimage(display, 1);
+	timer = timerstart(dt);
+	y = w->mc.xy.y;
+	b = w->mc.buttons;
+	alts[0].c = timer->c;
+	alts[0].v = nil;
+	alts[0].op = CHANRCV;
+	alts[1].c = w->mc.c;
+	alts[1].v = &w->mc.Mouse;
+	alts[1].op = CHANRCV;
+	alts[2].op = CHANEND;
+	for(;;)
+		switch(alt(alts)){
+		case 0:
+			timerstop(timer);
+			return;
+		case 1:
+			if(abs(w->mc.xy.y-y)>2 || w->mc.buttons!=b){
+				timercancel(timer);
+				return;
+			}
+			break;
+		}
+}
+
+void
+wscroll(Window *w, 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 = w->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)
+				wsetorigin(w, p0, FALSE);
+			oldp0 = p0;
+			readmouse(&w->mc);
+			continue;
+		}
+		if(but == 1 || but == 4){
+			y = max(1, (my-s.min.y)/w->font->height);
+			p0 = wbacknl(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)
+			wsetorigin(w, p0, TRUE);
+		oldp0 = p0;
+		/* debounce */
+		if(first){
+			if(display->bufp > display->buf)
+				flushimage(display, 1);
+			if(but > 3)
+				return;
+			sleep(200);
+			nbrecv(w->mc.c, &w->mc.Mouse);
+			first = FALSE;
+		}
+		wscrsleep(w, 100);
+	}while(w->mc.buttons & (1<<(but-1)));
+	while(w->mc.buttons)
+		readmouse(&w->mc);
+}
--- /dev/null
+++ b/sys/src/cmd/bof/time.c
@@ -1,0 +1,124 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <cursor.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <frame.h>
+#include <fcall.h>
+#include "dat.h"
+#include "fns.h"
+
+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, STACK);
+}
+
+/*
+ * 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/sys/src/cmd/bof/util.c
@@ -1,0 +1,159 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <cursor.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <frame.h>
+#include <fcall.h>
+#include "dat.h"
+#include "fns.h"
+
+void
+cvttorunes(char *p, int n, Rune *r, int *nb, int *nr, int *nulls)
+{
+	uchar *q;
+	Rune *s;
+	int j, w;
+
+	/*
+	 * Always guaranteed that n bytes may be interpreted
+	 * without worrying about partial runes.  This may mean
+	 * reading up to UTFmax-1 more bytes than n; the caller
+	 * knows this.  If n is a firm limit, the caller should
+	 * set p[n] = 0.
+	 */
+	q = (uchar*)p;
+	s = r;
+	for(j=0; j<n; j+=w){
+		if(*q < Runeself){
+			w = 1;
+			*s = *q++;
+		}else{
+			w = chartorune(s, (char*)q);
+			q += w;
+		}
+		if(*s)
+			s++;
+		else if(nulls)
+				*nulls = TRUE;
+	}
+	*nb = (char*)q-p;
+	*nr = s-r;
+}
+
+void
+error(char *s)
+{
+	fprint(2, "rio: %s: %r\n", s);
+	if(errorshouldabort)
+		abort();
+	threadexitsall("error");
+}
+
+void*
+erealloc(void *p, uint n)
+{
+	p = realloc(p, n);
+	if(p == nil)
+		error("realloc failed");
+	setrealloctag(p, getcallerpc(&p));
+	return p;
+}
+
+void*
+emalloc(uint n)
+{
+	void *p;
+
+	p = malloc(n);
+	if(p == nil)
+		error("malloc failed");
+	setmalloctag(p, getcallerpc(&n));
+	memset(p, 0, n);
+	return p;
+}
+
+char*
+estrdup(char *s)
+{
+	char *p;
+
+	p = malloc(strlen(s)+1);
+	if(p == nil)
+		error("strdup failed");
+	setmalloctag(p, getcallerpc(&s));
+	strcpy(p, s);
+	return p;
+}
+
+int
+isalnum(Rune c)
+{
+	/*
+	 * Hard to get absolutely right.  Use what we know about ASCII
+	 * and assume anything above the Latin control characters is
+	 * potentially an alphanumeric.
+	 */
+	if(c <= ' ')
+		return FALSE;
+	if(0x7F<=c && c<=0xA0)
+		return FALSE;
+	if(utfrune("!\"#$%&'()*+,-./:;<=>?@[\\]^`{|}~", c))
+		return FALSE;
+	return TRUE;
+}
+
+int
+isspace(Rune c)
+{
+	return c == 0 || c == ' ' || c == '\t' ||
+		c == '\n' || c == '\r' || c == '\v';
+}
+
+Rune*
+strrune(Rune *s, Rune c)
+{
+	Rune c1;
+
+	if(c == 0) {
+		while(*s++)
+			;
+		return s-1;
+	}
+
+	while(c1 = *s++)
+		if(c1 == c)
+			return s-1;
+	return nil;
+}
+
+int
+min(int a, int b)
+{
+	if(a < b)
+		return a;
+	return b;
+}
+
+int
+max(int a, int b)
+{
+	if(a > b)
+		return a;
+	return b;
+}
+
+char*
+runetobyte(Rune *r, int n, int *ip)
+{
+	char *s;
+	int m;
+
+	s = emalloc(n*UTFmax+1);
+	m = snprint(s, n*UTFmax+1, "%.*S", n, r);
+	*ip = m;
+	return s;
+}
+
--- /dev/null
+++ b/sys/src/cmd/bof/wctl.c
@@ -1,0 +1,499 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <cursor.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <frame.h>
+#include <fcall.h>
+#include <plumb.h>
+#include "dat.h"
+#include "fns.h"
+#include <ctype.h>
+
+char	Ebadwr[]		= "bad rectangle in wctl request";
+char	Ewalloc[]		= "window allocation failed in wctl request";
+
+/* >= Top are disallowed if mouse button is pressed */
+enum
+{
+	New,
+	Resize,
+	Move,
+	Scroll,
+	Noscroll,
+	Set,
+	Top,
+	Bottom,
+	Current,
+	Hide,
+	Unhide,
+	Delete,
+};
+
+static char *cmds[] = {
+	[New]	= "new",
+	[Resize]	= "resize",
+	[Move]	= "move",
+	[Scroll]	= "scroll",
+	[Noscroll]	= "noscroll",
+	[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
+};
+
+/*
+ * Check that newly created window will be of manageable size
+ */
+int
+goodrect(Rectangle r)
+{
+	if(badrect(r) || !eqrect(canonrect(r), r))
+		return 0;
+	/* reasonable sizes only please */
+	if(Dx(r) > BIG*Dx(screen->r))
+		return 0;
+	if(Dy(r) > BIG*Dy(screen->r))
+		return 0;
+	/*
+	 * the height has to be big enough to fit one line of text.
+	 * that includes the border on each side with an extra pixel
+	 * so that the text is still drawn
+	 */
+	if(Dx(r) < 100 || Dy(r) < 2*(Borderwidth+1)+font->height)
+		return 0;
+	/* window must be on screen */
+	if(!rectXrect(screen->r, r))
+		return 0;
+	/* must have some screen and border visible so we can move it out of the way */
+	if(rectinrect(screen->r, insetrect(r, Borderwidth)))
+		return 0;
+	return 1;
+}
+
+static
+int
+word(char **sp, char *tab[])
+{
+	char *s, *t;
+	int i;
+
+	s = *sp;
+	while(isspace(*s))
+		s++;
+	t = s;
+	while(*s!='\0' && !isspace(*s))
+		s++;
+	for(i=0; tab[i]!=nil; i++)
+		if(strncmp(tab[i], t, strlen(tab[i])) == 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;
+}
+
+Rectangle
+newrect(void)
+{
+	static int i = 0;
+	int minx, miny, dx, dy;
+
+	dx = max(Dx(screen->r) / 4, 400);
+	dy = min(Dy(screen->r) / 3, Dy(screen->r) - 1.5*Borderwidth);
+	minx = 16*i;
+	miny = 16*i;
+	i++;
+	i %= 10;
+	return Rect(minx, miny, minx+dx, miny+dy);
+}
+
+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)
+{
+	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;
+}
+
+
+int
+parsewctl(char **argp, Rectangle r, Rectangle *rp, int *pidp, int *idp, int *hiddenp, int *scrollingp, char **cdp, char *s, char *err)
+{
+	int cmd, n, nt, param, xy, sign;
+	char *f[2], *t;
+
+	*pidp = 0;
+	*hiddenp = 0;
+	*scrollingp = scrolling;
+	*cdp = nil;
+	cmd = word(&s, cmds);
+	if(cmd < 0){
+		strcpy(err, "unrecognized wctl command");
+		return -1;
+	}
+	if(cmd == New)
+		r = newrect();
+
+	strcpy(err, "missing or bad wctl parameter");
+	while((param = word(&s, params)) >= 0){
+		switch(param){	/* special cases */
+		case Hidden:
+			*hiddenp = 1;
+			continue;
+		case Scrolling:
+			*scrollingp = 1;
+			continue;
+		case Noscrolling:
+			*scrollingp = 0;
+			continue;
+		case R:
+			r.min.x = riostrtol(s, &t);
+			if(t == s)
+				return -1;
+			s = t;
+			r.min.y = riostrtol(s, &t);
+			if(t == s)
+				return -1;
+			s = t;
+			r.max.x = riostrtol(s, &t);
+			if(t == s)
+				return -1;
+			s = t;
+			r.max.y = riostrtol(s, &t);
+			if(t == s)
+				return -1;
+			s = t;
+			continue;
+		}
+		while(isspace(*s))
+			s++;
+		if(param == Cd){
+			*cdp = s;
+			if((nt = gettokens(*cdp, f, nelem(f), " \t\r\n\v\f")) < 1)
+				return -1;
+			n = strlen(*cdp);
+			if((*cdp)[0] == '\'' && (*cdp)[n-1] == '\'')
+				((*cdp)++)[n-1] = '\0'; /* drop quotes */
+			s += n+(nt-1);
+			continue;
+		}
+		sign = 0;
+		if(*s == '-'){
+			sign = -1;
+			s++;
+		}else if(*s == '+'){
+			sign = +1;
+			s++;
+		}
+		if(!isdigit(*s))
+			return -1;
+		xy = riostrtol(s, &s);
+		switch(param){
+		case -1:
+			strcpy(err, "unrecognized wctl parameter");
+			return -1;
+		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:
+			if(idp != nil)
+				*idp = xy;
+			break;
+		case PID:
+			if(pidp != nil)
+				*pidp = xy;
+			break;
+		}
+	}
+
+	*rp = rectonscreen(rectaddpt(r, screen->r.min));
+
+	while(isspace(*s))
+		s++;
+	if(cmd!=New && *s!='\0'){
+		strcpy(err, "extraneous text in wctl message");
+		return -1;
+	}
+
+	if(argp)
+		*argp = s;
+
+	return cmd;
+}
+
+int
+wctlnew(Rectangle rect, char *arg, int pid, int hideit, int scrollit, char *dir, char *err)
+{
+	char **argv;
+	Image *i;
+
+	if(!goodrect(rect)){
+		strcpy(err, Ebadwr);
+		return -1;
+	}
+	argv = emalloc(4*sizeof(char*));
+	argv[0] = "rc";
+	argv[1] = "-c";
+	while(isspace(*arg))
+		arg++;
+	if(*arg == '\0'){
+		argv[1] = "-i";
+		argv[2] = nil;
+	}else{
+		argv[2] = arg;
+		argv[3] = nil;
+	}
+	if(hideit)
+		i = allocimage(display, rect, screen->chan, 0, DNofill);
+	else
+		i = allocwindow(wscreen, rect, Refbackup, DNofill);
+	if(i == nil){
+		strcpy(err, Ewalloc);
+		return -1;
+	}
+
+	new(i, hideit, scrollit, pid, dir, "/bin/rc", argv);
+
+	free(argv);	/* when new() returns, argv and args have been copied */
+	return 1;
+}
+
+int
+wctlcmd(Window *w, Rectangle r, int cmd, char *err)
+{
+	Image *i;
+
+	switch(cmd){
+	case Move:
+		r = Rect(r.min.x, r.min.y, r.min.x+Dx(w->screenr), r.min.y+Dy(w->screenr));
+		r = rectonscreen(r);
+		/* fall through */
+	case Resize:
+		if(!goodrect(r)){
+			strcpy(err, Ebadwr);
+			return -1;
+		}
+		if(Dx(w->screenr) > 0){
+			if(eqrect(r, w->screenr))
+				return 1;
+			if(w != input){
+				strcpy(err, "window not current");
+				return -1;
+			}
+			i = allocwindow(wscreen, r, Refbackup, DNofill);
+		} else { /* hidden */
+			if(eqrect(r, w->i->r))
+				return 1;
+			wuncurrent(w);
+			i = allocimage(display, r, w->i->chan, 0, DNofill);
+			r = ZR;
+		}
+		if(i == nil){
+			strcpy(err, Ewalloc);
+			return -1;
+		}
+		wsendctlmesg(w, Reshaped, r, i);
+		return 1;
+	case Scroll:
+		w->scrolling = 1;
+		wshow(w, w->nr);
+		wsendctlmesg(w, Wakeup, ZR, nil);
+		return 1;
+	case Noscroll:
+		w->scrolling = 0;
+		wsendctlmesg(w, Wakeup, ZR, nil);
+		return 1;
+	case Top:
+		wtopme(w);
+		return 1;
+	case Bottom:
+		wbottomme(w);
+		return 1;
+	case Current:
+		if(Dx(w->screenr)<=0){
+			strcpy(err, "window is hidden");
+			return -1;
+		}
+		wcurrent(w);
+		wtopme(w);
+		wsendctlmesg(w, Topped, ZR, nil);
+		return 1;
+	case Hide:
+		switch(whide(w)){
+		case -1:
+			strcpy(err, "window already hidden");
+			return -1;
+		case 0:
+			strcpy(err, "hide failed");
+			return -1;
+		default:
+			break;
+		}
+		return 1;
+	case Unhide:
+		switch(wunhide(w)){
+		case -1:
+			strcpy(err, "window not hidden");
+			return -1;
+		case 0:
+			strcpy(err, "hide failed");
+			return -1;
+		default:
+			break;
+		}
+		return 1;
+	case Delete:
+		wsendctlmesg(w, Deleted, ZR, nil);
+		return 1;
+	}
+
+	strcpy(err, "invalid wctl message");
+	return -1;
+}
+
+int
+writewctl(Xfid *x, char *err)
+{
+	int cnt, cmd, id, hideit, scrollit, pid;
+	char *arg, *dir;
+	Rectangle r;
+	Window *w;
+
+	w = x->f->w;
+	cnt = x->count;
+	x->data[cnt] = '\0';
+	id = 0;
+
+	if(w == nil)
+		r = ZR;
+	else
+		r = rectsubpt(w->screenr, screen->r.min);
+	cmd = parsewctl(&arg, r, &r, &pid, &id, &hideit, &scrollit, &dir, x->data, err);
+	if(cmd < 0)
+		return -1;
+
+	if(id != 0){
+		w = wlookid(id);
+		if(w == 0){
+			strcpy(err, "no such window id");
+			return -1;
+		}
+	}
+
+	if(w == nil && cmd != New){
+		strcpy(err, "command needs to be run within a window");
+		return -1;
+	}
+
+	switch(cmd){
+	case New:
+		return wctlnew(r, arg, pid, hideit, scrollit, dir, err);
+	case Set:
+		if(pid > 0)
+			wsetpid(w, pid, 0);
+		return 1;
+	}
+
+	incref(w);
+	id = wctlcmd(w, r, cmd, err);
+	wclose(w);
+
+	return id;
+}
--- /dev/null
+++ b/sys/src/cmd/bof/wind.c
@@ -1,0 +1,1830 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <cursor.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <frame.h>
+#include <fcall.h>
+#include <plumb.h>
+#include <complete.h>
+#include "dat.h"
+#include "fns.h"
+
+Window*
+wlookid(int id)
+{
+	int i;
+
+	for(i=0; i<nwindow; i++)
+		if(window[i]->id == id)
+			return window[i];
+	return nil;
+}
+
+Window*
+wpointto(Point pt)
+{
+	int i;
+	Window *v, *w;
+
+	w = nil;
+	for(i=0; i<nwindow; i++){
+		v = window[i];
+		if(ptinrect(pt, v->screenr))
+		if(w==nil || v->topped>w->topped)
+			w = v;
+	}
+	return w;
+}
+
+static	int	topped;
+
+void
+wtopme(Window *w)
+{
+	if(w!=nil && w->i!=nil && w->topped!=topped){
+		w->topped = ++topped;
+		topwindow(w->i);
+		flushimage(display, 1);
+	}
+}
+
+void
+wbottomme(Window *w)
+{
+	if(w!=nil && w->i!=nil){
+		w->topped = - ++topped;
+		bottomwindow(w->i);
+		flushimage(display, 1);
+	}
+}
+
+Window*
+wtop(Point pt)
+{
+	Window *w;
+
+	w = wpointto(pt);
+	if(w!=nil){
+		incref(w);
+		wcurrent(w);
+		wtopme(w);
+		wsendctlmesg(w, Topped, ZR, nil);
+		wclose(w);
+	}
+	return w;
+}
+
+void
+wcurrent(Window *w)
+{
+	Channel *c;
+
+	if(input == nil){
+		sendp(wintap, w);
+		input = w;
+		return;
+	}
+	if(w == input)
+		return;
+	incref(input);
+	sendp(wintap, w);
+	c = chancreate(sizeof(Window*), 0);
+	wsendctlmesg(input, Repaint, ZR, c);
+	sendp(c, w);		/* send the new input */
+	wclose(recvp(c));	/* release old input */
+	chanfree(c);
+}
+
+void
+wuncurrent(Window *w)
+{
+	Channel *c;
+
+	if(input == nil || w != input)
+		return;
+	c = chancreate(sizeof(Window*), 0);
+	wsendctlmesg(w, Repaint, ZR, c);
+	sendp(c, nil);
+	recvp(c);
+	chanfree(c);
+}
+
+static	Cursor	*lastcursor;
+
+void
+riosetcursor(Cursor *p)
+{
+	if(p==lastcursor)
+		return;
+	setcursor(mousectl, p);
+	lastcursor = p;
+}
+
+void
+wsetcursor(Window *w, int force)
+{
+	Cursor *p;
+
+	if(menuing || sweeping || (w!=input && wpointto(mouse->xy)!=w))
+		return;
+	if(w==nil)
+		p = nil;
+	else {
+		p = w->cursorp;
+		if(p==nil && w->holding)
+			p = &whitearrow;
+	}
+	if(p && force)	/* force cursor reload */
+		lastcursor = nil;
+	riosetcursor(p);
+}
+
+static void
+waddraw(Window *w, Rune *r, int nr)
+{
+	w->raw = runerealloc(w->raw, w->nraw+nr);
+	runemove(w->raw+w->nraw, r, nr);
+	w->nraw += nr;
+}
+
+enum
+{
+	HiWater	= 640000,	/* max size of history */
+	LoWater	= 400000,	/* min size of history after max'ed */
+	MinWater	= 20000,	/* room to leave available when reallocating */
+};
+
+static uint
+winsert(Window *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);
+	return q0;
+}
+
+static void
+wfill(Window *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);
+	}
+}
+
+static void
+wsetselect(Window *w, uint q0, uint q1)
+{
+	int p0, p1;
+
+	/* 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
+wborder(Window *w, int type)
+{
+	Image *col;
+
+	if(w->i == nil)
+		return;
+	if(w->holding){
+		if(type == Selborder)
+			col = cols[Chold];
+		else
+			col = cols[Cpalehold];
+	}else{
+		if(type == Selborder)
+			col = cols[Ctitle];
+		else
+			col = cols[Cltitle];
+	}
+	border(w->i, w->i->r, Selborder, col, ZP);
+}
+
+static void
+wsetcols(Window *w, int topped)
+{
+	if(w->holding)
+		if(topped)
+			w->cols[TEXT] = cols[Chold];
+		else
+			w->cols[TEXT] = cols[Clhold];
+	else
+		if(topped)
+			w->cols[TEXT] = cols[Ctext];
+		else
+			w->cols[TEXT] = cols[Cpaletext];
+}
+
+void
+wsetname(Window *w)
+{
+	int i, n;
+	char err[ERRMAX];
+	
+	n = snprint(w->name, sizeof(w->name)-2, "window.%d.%d", w->id, w->namecount++);
+	for(i='A'; i<='Z'; i++){
+		if(nameimage(w->i, 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, "rio: setname failed: %s\n", err);
+}
+
+static void
+wresize(Window *w, Image *i)
+{
+	Rectangle r;
+
+	w->i = i;
+	w->mc.image = i;
+	r = insetrect(i->r, Selborder+1);
+	w->scrollr = r;
+	w->scrollr.max.x = r.min.x+Scrollwid;
+	w->lastsr = ZR;
+	r.min.x += Scrollwid+Scrollgap;
+	frclear(w, FALSE);
+	frinit(w, r, w->font, w->i, cols);
+	wsetcols(w, w == input);
+	w->maxtab = maxtab*stringwidth(w->font, "0");
+	if(!w->mouseopen || !w->winnameread){
+		r = insetrect(w->i->r, Selborder);
+		draw(w->i, r, cols[BACK], nil, w->entire.min);
+		wfill(w);
+		wsetselect(w, w->q0, w->q1);
+		wscrdraw(w);
+	}
+	if(w == input)
+		wborder(w, Selborder);
+	else
+		wborder(w, Unselborder);
+	flushimage(display, 1);
+	wsetname(w);
+	w->topped = ++topped;
+	w->resized = TRUE;
+	w->winnameread = FALSE;
+	w->mc.buttons = 0;	/* avoid re-triggering clicks on resize */
+	w->mouse.counter++;
+	w->wctlready = 1;
+}
+
+static void
+wrepaint(Window *w)
+{
+	wsetcols(w, w == input);
+	if(!w->mouseopen || !w->winnameread)
+		frredraw(w);
+	if(w == input)
+		wborder(w, Selborder);
+	else
+		wborder(w, Unselborder);
+}
+
+static void
+wrefresh(Window *w)
+{
+	Rectangle r;
+
+	if(w == input)
+		wborder(w, Selborder);
+	else
+		wborder(w, Unselborder);
+	r = insetrect(w->i->r, Selborder);
+	draw(w->i, r, w->cols[BACK], nil, w->entire.min);
+	wfill(w);
+	w->ticked = 0;
+	if(w->p0 > 0)
+		frdrawsel(w, frptofchar(w, 0), 0, w->p0, 0);
+	if(w->p1 < w->nchars)
+		frdrawsel(w, frptofchar(w, w->p1), w->p1, w->nchars, 0);
+	frdrawsel(w, frptofchar(w, w->p0), w->p0, w->p1, 1);
+	w->lastsr = ZR;
+	wscrdraw(w);
+}
+
+/*
+ * 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);
+}
+
+typedef struct Completejob Completejob;
+struct Completejob
+{
+	char	*dir;
+	char	*str;
+	Window	*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);
+
+	wclose(job->win);
+
+	free(job->dir);
+	free(job->str);
+	free(job);
+}
+
+static int
+windfilewidth(Window *w, uint q0, int oneelement)
+{
+	uint q;
+	Rune r;
+
+	q = q0;
+	while(q > 0){
+		r = w->r[q-1];
+		if(r<=' ' || r=='=' || r=='^' || r=='(' || r=='{')
+			break;
+		if(oneelement && r=='/')
+			break;
+		--q;
+	}
+	return q0-q;
+}
+
+static void
+namecomplete(Window *w)
+{
+	int nstr, npath;
+	Rune *path, *str;
+	char *dir, *root;
+	Completejob *job;
+
+	/* control-f: filename completion; works back to white space or / */
+	if(w->q0<w->nr && w->r[w->q0]>' ')	/* must be at end of word */
+		return;
+	nstr = windfilewidth(w, w->q0, TRUE);
+	str = w->r+(w->q0-nstr);
+	npath = windfilewidth(w, w->q0-nstr, FALSE);
+	path = w->r+(w->q0-nstr-npath);
+
+	/* is path rooted? if not, we need to make it relative to window path */
+	if(npath>0 && path[0]=='/')
+		dir = runetobyte(path, npath, &npath);
+	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 = runetobyte(str, nstr, &nstr);
+	job->dir = cleanname(dir);
+	job->win = w;
+	incref(w);
+	proccreate(completeproc, job, STACK);
+}
+
+static void
+showcandidates(Window *w, Completion *c)
+{
+	int i;
+	Fmt f;
+	Rune *rp;
+	uint nr, qline;
+	char *s;
+
+	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(w->qh, w->q0);
+	while(qline>0 && w->r[qline-1] != '\n')
+		qline--;
+
+	if(qline == w->qh){
+		/* advance host point to avoid readback */
+		w->qh = winsert(w, rp, nr, qline)+nr;
+	} else {
+		winsert(w, rp, nr, qline);
+	}
+	free(rp);
+}
+
+static int
+wbswidth(Window *w, Rune c)
+{
+	uint q, eq, stop;
+	Rune r;
+	int skipping;
+
+	/* there is known to be at least one character to erase */
+	if(c == 0x08)	/* ^H: erase character */
+		return 1;
+	q = w->q0;
+	stop = 0;
+	if(q > w->qh)
+		stop = w->qh;
+	skipping = TRUE;
+	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; 
+		}
+		if(c == 0x17){
+			eq = isalnum(r);
+			if(eq && skipping)	/* found one; stop skipping */
+				skipping = FALSE;
+			else if(!eq && !skipping)
+				break;
+		}
+		--q;
+	}
+	return w->q0-q;
+}
+
+void
+wsetorigin(Window *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;
+	wfill(w);
+	wscrdraw(w);
+	wsetselect(w, w->q0, w->q1);
+	if(fixup && w->p1 > w->p0)
+		frdrawsel(w, frptofchar(w, w->p1-1), w->p1-1, w->p1, 1);
+}
+
+uint
+wbacknl(Window *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;
+}
+
+char*
+wcontents(Window *w, int *ip)
+{
+	return runetobyte(w->r, w->nr, ip);
+}
+
+void
+wshow(Window *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)))
+		wscrdraw(w);
+	else{
+		nl = 4*w->maxlines/5;
+		q = wbacknl(w, q0, nl);
+		/* avoid going backwards if trying to go forwards - long lines! */
+		if(!(q0>w->org && q<w->org))
+			wsetorigin(w, q, TRUE);
+		while(q0 > w->org+w->nchars)
+			wsetorigin(w, w->org+1, FALSE);
+	}
+}
+
+void
+wsnarf(Window *w)
+{
+	if(w->q1 == w->q0)
+		return;
+	nsnarf = w->q1-w->q0;
+	snarf = runerealloc(snarf, nsnarf);
+	snarfversion++;	/* maybe modified by parent */
+	runemove(snarf, w->r+w->q0, nsnarf);
+	putsnarf();
+}
+
+void
+wsend(Window *w)
+{
+	getsnarf();
+	wsnarf(w);
+	if(nsnarf == 0)
+		return;
+	if(w->rawing){
+		waddraw(w, snarf, nsnarf);
+		if(snarf[nsnarf-1]!='\n' && snarf[nsnarf-1]!='\004')
+			waddraw(w, L"\n", 1);
+	}else{
+		winsert(w, snarf, nsnarf, w->nr);
+		if(snarf[nsnarf-1]!='\n' && snarf[nsnarf-1]!='\004')
+			winsert(w, L"\n", 1, w->nr);
+	}
+	wsetselect(w, w->nr, w->nr);
+	wshow(w, w->nr);
+}
+
+static void
+wdelete(Window *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);
+		wfill(w);
+	}
+}
+
+void
+wcut(Window *w)
+{
+	if(w->q1 == w->q0)
+		return;
+	wdelete(w, w->q0, w->q1);
+	wsetselect(w, w->q0, w->q0);
+}
+
+void
+wpaste(Window *w)
+{
+	uint q0;
+
+	if(nsnarf == 0)
+		return;
+	wcut(w);
+	q0 = w->q0;
+	if(w->rawing && q0==w->nr){
+		waddraw(w, snarf, nsnarf);
+		wsetselect(w, q0, q0);
+	}else{
+		q0 = winsert(w, snarf, nsnarf, w->q0);
+		wsetselect(w, q0, q0+nsnarf);
+	}
+}
+
+void
+wlook(Window *w)
+{
+	int i, n, e;
+
+	i = w->q1;
+	n = i - w->q0;
+	e = w->nr - n;
+	if(n <= 0 || e < n)
+		return;
+
+	if(i > e)
+		i = 0;
+
+	while(runestrncmp(w->r+w->q0, w->r+i, n) != 0){
+		if(i < e)
+			i++;
+		else
+			i = 0;
+	}
+
+	wsetselect(w, i, i+n);
+	wshow(w, i);
+}
+
+void
+wplumb(Window *w)
+{
+	Plumbmsg *m;
+	static int fd = -2;
+	char buf[32];
+	uint p0, p1;
+	Cursor *c;
+
+	if(fd == -2)
+		fd = plumbopen("send", OWRITE|OCEXEC);
+	if(fd < 0)
+		return;
+	m = emalloc(sizeof(Plumbmsg));
+	m->src = estrdup("rio");
+	m->dst = nil;
+	m->wdir = estrdup(w->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 > messagesize-1024){
+		plumbfree(m);
+		return;	/* too large for 9P */
+	}
+	m->data = runetobyte(w->r+p0, p1-p0, &m->ndata);
+	if(plumbsend(fd, m) < 0){
+		c = lastcursor;
+		riosetcursor(&query);
+		sleep(300);
+		riosetcursor(c);
+	}
+	plumbfree(m);
+}
+
+static void
+wkeyctl(Window *w, Rune r)
+{
+	uint q0 ,q1;
+	int n, nb;
+	int *notefd;
+
+	switch(r){
+	case 0:
+	case Kcaps:
+	case Knum:
+	case Kshift:
+	case Kalt:
+	case Kctl:
+	case Kaltgr:
+		return;
+	}
+
+	if(w->i==nil)
+		return;
+	/* navigation keys work only when mouse and kbd is not open */
+	if(!w->mouseopen)
+		switch(r){
+		case Kdown:
+			n = shiftdown ? 1 : w->maxlines/3;
+			goto case_Down;
+		case Kscrollonedown:
+			n = mousescrollsize(w->maxlines);
+			if(n <= 0)
+				n = 1;
+			goto case_Down;
+		case Kpgdown:
+			n = 2*w->maxlines/3;
+		case_Down:
+			q0 = w->org+frcharofpt(w, Pt(w->Frame.r.min.x, w->Frame.r.min.y+n*w->font->height));
+			wsetorigin(w, q0, TRUE);
+			return;
+		case Kup:
+			n = shiftdown ? 1 : w->maxlines/3;
+			goto case_Up;
+		case Kscrolloneup:
+			n = mousescrollsize(w->maxlines);
+			if(n <= 0)
+				n = 1;
+			goto case_Up;
+		case Kpgup:
+			n = 2*w->maxlines/3;
+		case_Up:
+			q0 = wbacknl(w, w->org, n);
+			wsetorigin(w, q0, TRUE);
+			return;
+		case Kleft:
+			if(w->q0 > 0){
+				q0 = w->q0-1;
+				wsetselect(w, q0, q0);
+				wshow(w, q0);
+			}
+			return;
+		case Kright:
+			if(w->q1 < w->nr){
+				q1 = w->q1+1;
+				wsetselect(w, q1, q1);
+				wshow(w, q1);
+			}
+			return;
+		case Khome:
+			wshow(w, 0);
+			return;
+		case Kend:
+			wshow(w, w->nr);
+			return;
+		case Kscroll:
+			w->scrolling ^= 1;
+			wshow(w, w->nr);
+			return;
+		case Ksoh:	/* ^A: beginning of line */
+			if(w->q0==0 || w->q0==w->qh || w->r[w->q0-1]=='\n')
+				return;
+			nb = wbswidth(w, 0x15 /* ^U */);
+			wsetselect(w, w->q0-nb, w->q0-nb);
+			wshow(w, w->q0);
+			return;
+		case Kenq:	/* ^E: end of line */
+			q0 = w->q0;
+			while(q0 < w->nr && w->r[q0]!='\n')
+				q0++;
+			wsetselect(w, q0, q0);
+			wshow(w, w->q0);
+			return;
+		case Kstx:	/* ^B: output point */
+			wsetselect(w, w->qh, w->qh);
+			wshow(w, w->q0);
+			return;
+		}
+	if(w->rawing && (w->q0==w->nr || w->mouseopen)){
+		waddraw(w, &r, 1);
+		return;
+	}
+	if(r==Kesc || (w->holding && r==Kdel)){	/* toggle hold */
+		if(w->holding)
+			--w->holding;
+		else
+			w->holding++;
+		wsetcursor(w, FALSE);
+		wrepaint(w);
+		if(r == Kesc)
+			return;
+	}
+	if(r != Kdel){
+		wsnarf(w);
+		wcut(w);
+	}
+	switch(r){
+	case Kdel:	/* send interrupt */
+		w->qh = w->nr;
+		wshow(w, w->qh);
+		if(w->notefd < 0)
+			return;
+		notefd = emalloc(sizeof(int));
+		*notefd = dup(w->notefd, -1);
+		proccreate(interruptproc, notefd, 4096);
+		return;
+	case Kack:	/* ^F: file name completion */
+	case Kins:	/* Insert: file name completion */
+		namecomplete(w);
+		return;
+	case Kbs:	/* ^H: erase character */
+	case Knack:	/* ^U: erase line */
+	case Ketb:	/* ^W: erase word */
+		if(w->q0==0 || w->q0==w->qh)
+			return;
+		nb = wbswidth(w, r);
+		q1 = w->q0;
+		q0 = q1-nb;
+		if(q0 < w->org){
+			q0 = w->org;
+			nb = q1-q0;
+		}
+		if(nb > 0){
+			wdelete(w, q0, q0+nb);
+			wsetselect(w, q0, q0);
+		}
+		return;
+	}
+	/* otherwise ordinary character; just insert */
+	q0 = w->q0;
+	q0 = winsert(w, &r, 1, q0);
+	wshow(w, q0+1);
+}
+
+static Window	*clickwin;
+static uint	clickmsec;
+static Point	clickpt;
+static uint	clickcount;
+static Window	*selectwin;
+static uint	selectq;
+
+static void
+wframescroll(Window *w, int dl)
+{
+	uint q0;
+
+	if(dl == 0){
+		wscrsleep(w, 100);
+		return;
+	}
+	if(dl < 0){
+		q0 = wbacknl(w, w->org, -dl);
+		if(selectq > w->org+w->p0)
+			wsetselect(w, w->org+w->p0, selectq);
+		else
+			wsetselect(w, selectq, w->org+w->p0);
+	}else{
+		if(w->org+w->nchars == w->nr)
+			return;
+		q0 = w->org+frcharofpt(w, Pt(w->Frame.r.min.x, w->Frame.r.min.y+dl*w->font->height));
+		if(selectq >= w->org+w->p1)
+			wsetselect(w, w->org+w->p1, selectq);
+		else
+			wsetselect(w, selectq, w->org+w->p1);
+	}
+	wsetorigin(w, q0, TRUE);
+}
+
+/*
+ * called from frame library
+ */
+static void
+framescroll(Frame *f, int dl)
+{
+	if(f != &selectwin->Frame)
+		error("frameselect not right frame");
+	wframescroll(selectwin, dl);
+}
+
+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
+wclickmatch(Window *w, int cl, int cr, int dir, uint *q)
+{
+	Rune c;
+	int nest;
+
+	nest = 1;
+	for(;;){
+		if(dir > 0){
+			if(*q == w->nr)
+				break;
+			c = w->r[*q];
+			(*q)++;
+		}else{
+			if(*q == 0)
+				break;
+			(*q)--;
+			c = w->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) ? isalnum(r) : r && !isspace(r);
+}
+
+static void
+wstretchsel(Window *w, 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 = w->r[q-1];
+		p = strrune(l, c);
+		if(p != nil){
+			if(wclickmatch(w, c, r[p-l], 1, &q))
+				*q1 = q-(c!='\n');
+			return;
+		}
+		/* try matching character to right, looking left */
+		if(q == w->nr)
+			c = '\n';
+		else
+			c = w->r[q];
+		p = strrune(r, c);
+		if(p != nil){
+			if(wclickmatch(w, c, l[p-r], -1, &q)){
+				*q1 = *q0+(*q0<w->nr && c=='\n');
+				*q0 = q;
+				if(c!='\n' || q!=0 || w->r[0]=='\n')
+					(*q0)++;
+			}
+			return;
+		}
+	}
+	/* try filling out word to right */
+	while(*q1<w->nr && inmode(w->r[*q1], mode))
+		(*q1)++;
+	/* try filling out word to left */
+	while(*q0>0 && inmode(w->r[*q0-1], mode))
+		(*q0)--;
+}
+
+static void
+wselect(Window *w)
+{
+	uint q0, q1;
+	int b, x, y, dx, dy, mode, first;
+
+	first = 1;
+	selectwin = w;
+	/*
+	 * Double-click immediately if it might make sense.
+	 */
+	b = w->mc.buttons;
+	q0 = w->q0;
+	q1 = w->q1;
+	dx = abs(clickpt.x - w->mc.xy.x);
+	dy = abs(clickpt.y - w->mc.xy.y);
+	clickpt = w->mc.xy;
+	selectq = w->org+frcharofpt(w, w->mc.xy);
+	clickcount++;
+	if(w->mc.msec-clickmsec >= 500 || clickwin != w || clickcount > 3 || dx > 3 || dy > 3)
+		clickcount = 0;
+	if(clickwin == w && clickcount >= 1 && w->mc.msec-clickmsec < 500){
+		mode = (clickcount > 2) ? 2 : clickcount;
+		wstretchsel(w, selectq, &q0, &q1, mode);
+		wsetselect(w, q0, q1);
+		x = w->mc.xy.x;
+		y = w->mc.xy.y;
+		/* stay here until something interesting happens */
+		while(1){
+			readmouse(&w->mc);
+			dx = abs(w->mc.xy.x-x);
+			dy = abs(w->mc.xy.y-y);
+			if(w->mc.buttons != b || dx >= 3 && dy >= 3)
+				break;
+			clickcount++;
+			clickmsec = w->mc.msec;
+		}
+		w->mc.xy.x = x;	/* in case we're calling frselect */
+		w->mc.xy.y = y;
+		q0 = w->q0;	/* may have changed */
+		q1 = w->q1;
+		selectq = w->org+frcharofpt(w, w->mc.xy);
+	}
+	if(w->mc.buttons == b && clickcount == 0){
+		w->scroll = framescroll;
+		frselect(w, &w->mc);
+		/* horrible botch: while asleep, may have lost selection altogether */
+		if(selectq > w->nr)
+			selectq = w->org + w->p0;
+		w->Frame.scroll = nil;
+		if(selectq < w->org)
+			q0 = selectq;
+		else
+			q0 = w->org + w->p0;
+		if(selectq > w->org+w->nchars)
+			q1 = selectq;
+		else
+			q1 = w->org+w->p1;
+	}
+	if(q0 == q1){
+		mode = (clickcount > 2) ? 2 : clickcount;
+		if(q0==w->q0 && clickwin==w && w->mc.msec-clickmsec<500)
+			wstretchsel(w, selectq, &q0, &q1, mode);
+		else
+			clickwin = w;
+		clickmsec = w->mc.msec;
+	}
+	wsetselect(w, q0, q1);
+	while(w->mc.buttons){
+		w->mc.msec = 0;
+		b = w->mc.buttons;
+		if(b & 6){
+			if(b & 2){
+				wsnarf(w);
+				wcut(w);
+			}else{
+				if(first){
+					first = 0;
+					getsnarf();
+				}
+				wpaste(w);
+			}
+		}
+		wscrdraw(w);
+		while(w->mc.buttons == b)
+			readmouse(&w->mc);
+		if(w->mc.msec-clickmsec >= 500)
+			clickwin = nil;
+	}
+}
+
+/*
+ * Convert back to physical coordinates
+ */
+static void
+wmovemouse(Window *w, Point p)
+{
+	if(w != input || menuing || sweeping)
+		return;
+	p.x += w->screenr.min.x-w->i->r.min.x;
+	p.y += w->screenr.min.y-w->i->r.min.y;
+	moveto(mousectl, p);
+}
+
+
+Window*
+wmk(Image *i, Mousectl *mc, Channel *ck, Channel *cctl, int scrolling)
+{
+	static int id;
+
+	Window *w;
+	Rectangle r;
+
+	w = emalloc(sizeof(Window));
+	w->screenr = i->r;
+	r = insetrect(i->r, Selborder+1);
+	w->i = i;
+	w->mc = *mc;
+	w->ck = ck;
+	w->cctl = cctl;
+	w->cursorp = nil;
+	w->conswrite = chancreate(sizeof(Conswritemesg), 0);
+	w->consread =  chancreate(sizeof(Consreadmesg), 0);
+	w->kbdread =  chancreate(sizeof(Consreadmesg), 0);
+	w->mouseread =  chancreate(sizeof(Mousereadmesg), 0);
+	w->wctlread =  chancreate(sizeof(Consreadmesg), 0);
+	w->complete = chancreate(sizeof(Completion*), 0);
+	w->gone = chancreate(sizeof(char*), 0);
+	w->scrollr = r;
+	w->scrollr.max.x = r.min.x+Scrollwid;
+	w->lastsr = ZR;
+	r.min.x += Scrollwid+Scrollgap;
+	frinit(w, r, font, i, cols);
+	w->maxtab = maxtab*stringwidth(font, "0");
+	w->topped = ++topped;
+	w->id = ++id;
+	w->notefd = -1;
+	w->scrolling = scrolling;
+	w->dir = estrdup(startdir);
+	w->label = estrdup("<unnamed>");
+	r = insetrect(w->i->r, Selborder);
+	draw(w->i, r, cols[BACK], nil, w->entire.min);
+	wborder(w, Selborder);
+	wscrdraw(w);
+	incref(w);	/* ref will be removed after mounting; avoids delete before ready to be deleted */
+	return w;
+}
+
+static void
+wclosewin(Window *w)
+{
+	Image *i = w->i;
+	if(i == nil)
+		return;
+	w->i = nil;
+	/* move it off-screen to hide it, in case client is slow in letting it go */
+	originwindow(i, i->r.min, view->r.max);
+	freeimage(i);
+}
+
+static void
+wclunk(Window *w)
+{
+	int i;
+
+	if(w->deleted)
+		return;
+	w->deleted = TRUE;
+	if(w == input){
+		sendp(wintap, nil);
+		input = nil;
+		riosetcursor(nil);
+	}
+	if(w == wkeyboard)
+		wkeyboard = nil;
+	for(i=0; i<nhidden; i++)
+		if(hidden[i] == w){
+			--nhidden;
+			memmove(hidden+i, hidden+i+1, (nhidden-i)*sizeof(hidden[0]));
+			break;
+		}
+	for(i=0; i<nwindow; i++)
+		if(window[i] == w){
+			--nwindow;
+			memmove(window+i, window+i+1, (nwindow-i)*sizeof(window[0]));
+			break;
+		}
+}
+
+int
+wclose(Window *w)
+{
+	int i;
+
+	i = decref(w);
+	if(i > 0)
+		return 0;
+	if(i < 0)
+		error("negative ref count");
+	wclunk(w);
+	wsendctlmesg(w, Exited, ZR, nil);
+	return 1;
+}
+
+void
+wsendctlmesg(Window *w, int type, Rectangle r, void *p)
+{
+	Wctlmesg wcm;
+
+	wcm.type = type;
+	wcm.r = r;
+	wcm.p = p;
+	send(w->cctl, &wcm);
+}
+
+static int
+wctlmesg(Window *w, int m, Rectangle r, void *p)
+{
+	Image *i = p;
+
+	switch(m){
+	default:
+		error("unknown control message");
+		break;
+	case Wakeup:
+		break;
+	case Reshaped:
+		if(w->deleted){
+			freeimage(i);
+			break;
+		}
+		w->screenr = r;
+		wclosewin(w);
+		wresize(w, i);
+		wsetcursor(w, FALSE);
+		break;
+	case Topped:
+		if(w->deleted)
+			break;
+		w->wctlready = 1;
+		wsetcursor(w, FALSE);
+		/* fall thrugh for redraw after input change */
+	case Repaint:
+		if(p != nil){
+			/* sync with input change from wcurrent()/wuncurrent() */
+			Channel *c = p;
+			input = recvp(c);
+
+			/* when we lost input, release mouse buttons */
+			if(w->mc.buttons){
+				w->mc.buttons = 0;
+				w->mouse.counter++;
+			}
+			w->wctlready = 1;
+
+			sendp(c, w);
+		}
+		if(w->i==nil || Dx(w->screenr)<=0)
+			break;
+		wrepaint(w);
+		flushimage(display, 1);
+		break;
+	case Refresh:
+		if(w->i==nil || Dx(w->screenr)<=0)
+			break;
+		wrefresh(w);
+		flushimage(display, 1);
+		break;
+	case Movemouse:
+		if(w->i==nil || Dx(w->screenr)<=0 || !ptinrect(r.min, w->i->r))
+			break;
+		wmovemouse(w, r.min);
+	case Rawon:
+		break;
+	case Rawoff:
+		while(w->nraw > 0){
+			wkeyctl(w, w->raw[0]);
+			--w->nraw;
+			runemove(w->raw, w->raw+1, w->nraw);
+		}
+		break;
+	case Holdon:
+	case Holdoff:
+		if(w->i==nil)
+			break;
+		wsetcursor(w, FALSE);
+		wrepaint(w);
+		flushimage(display, 1);
+		break;
+	case Truncate:
+		wdelete(w, 0, w->nr);
+		break;
+	case Deleted:
+		wclunk(w);
+		if(w->notefd >= 0)
+			write(w->notefd, "hangup", 6);
+		wclosewin(w);
+		flushimage(display, 1);
+		break;
+	case Exited:
+		wclosewin(w);
+		frclear(w, TRUE);
+		flushimage(display, 1);
+		if(w->notefd >= 0)
+			close(w->notefd);
+		chanfree(w->mc.c);
+		chanfree(w->ck);
+		chanfree(w->cctl);
+		chanfree(w->conswrite);
+		chanfree(w->consread);
+		chanfree(w->mouseread);
+		chanfree(w->wctlread);
+		chanfree(w->kbdread);
+		chanfree(w->complete);
+		chanfree(w->gone);
+		free(w->raw);
+		free(w->r);
+		free(w->dir);
+		free(w->label);
+		free(w);
+		break;
+	}
+	return m;
+}
+
+static void
+wmousectl(Window *w)
+{
+	int but;
+
+	for(but=1;; but++){
+		if(but > 5)
+			return;
+		if(w->mc.buttons == 1<<(but-1))
+			break;
+	}
+
+	incref(w);		/* hold up window while we track */
+	if(w->i != nil){
+		if(shiftdown && but > 3)
+			wkeyctl(w, but == 4 ? Kscrolloneup : Kscrollonedown);
+		else if(ptinrect(w->mc.xy, w->scrollr) || (but > 3))
+			wscroll(w, but);
+		else if(but == 1)
+			wselect(w);
+	}
+	wclose(w);
+}
+
+void
+winctl(void *arg)
+{
+	Rune *rp, *up, r;
+	uint qh, q0;
+	int nr, nb, c, wid, i, npart, initial, lastb;
+	char *s, *t, part[3];
+	Window *w;
+	Mousestate *mp, m;
+	enum { WKbd, WKbdread, WMouse, WMouseread, WCtl, WCwrite, WCread, WWread, WComplete, Wgone, NWALT };
+	Alt alts[NWALT+1];
+	Consreadmesg crm;
+	Mousereadmesg mrm;
+	Conswritemesg cwm;
+	Stringpair pair;
+	Wctlmesg wcm;
+	Completion *cr;
+	char *kbdq[32], *kbds;
+	uint kbdqr, kbdqw;
+
+	w = arg;
+	threadsetname("winctl-id%d", w->id);
+
+	mrm.cm = chancreate(sizeof(Mouse), 0);
+	crm.c1 = chancreate(sizeof(Stringpair), 0);
+	crm.c2 = chancreate(sizeof(Stringpair), 0);
+	cwm.cw = chancreate(sizeof(Stringpair), 0);
+	
+	alts[WKbd].c = w->ck;
+	alts[WKbd].v = &kbds;
+	alts[WKbd].op = CHANRCV;
+	alts[WKbdread].c = w->kbdread;
+	alts[WKbdread].v = &crm;
+	alts[WKbdread].op = CHANSND;
+	alts[WMouse].c = w->mc.c;
+	alts[WMouse].v = &w->mc.Mouse;
+	alts[WMouse].op = CHANRCV;
+	alts[WMouseread].c = w->mouseread;
+	alts[WMouseread].v = &mrm;
+	alts[WMouseread].op = CHANSND;
+	alts[WCtl].c = w->cctl;
+	alts[WCtl].v = &wcm;
+	alts[WCtl].op = CHANRCV;
+	alts[WCwrite].c = w->conswrite;
+	alts[WCwrite].v = &cwm;
+	alts[WCwrite].op = CHANSND;
+	alts[WCread].c = w->consread;
+	alts[WCread].v = &crm;
+	alts[WCread].op = CHANSND;
+	alts[WWread].c = w->wctlread;
+	alts[WWread].v = &crm;
+	alts[WWread].op = CHANSND;
+	alts[WComplete].c = w->complete;
+	alts[WComplete].v = &cr;
+	alts[WComplete].op = CHANRCV;
+	alts[Wgone].c = w->gone;
+	alts[Wgone].v = "window deleted";
+	alts[Wgone].op = CHANNOP;
+	alts[NWALT].op = CHANEND;
+
+	kbdqr = kbdqw = 0;
+	npart = 0;
+	lastb = -1;
+	for(;;){
+		if(w->i==nil){
+			/* window deleted */
+			alts[Wgone].op = CHANSND;
+
+			alts[WKbdread].op = CHANNOP;
+			alts[WMouseread].op = CHANNOP;
+			alts[WCwrite].op = CHANNOP;
+			alts[WWread].op = CHANNOP;
+			alts[WCread].op = CHANNOP;
+		} else {
+			alts[WKbdread].op = (w->kbdopen && kbdqw != kbdqr) ?
+				CHANSND : CHANNOP;
+			alts[WMouseread].op = (w->mouseopen && w->mouse.counter != w->mouse.lastcounter) ? 
+				CHANSND : CHANNOP;
+			alts[WCwrite].op = w->scrolling || w->mouseopen || (w->qh <= w->org+w->nchars) ?
+				CHANSND : CHANNOP;
+			alts[WWread].op = w->wctlready ?
+				CHANSND : CHANNOP;
+			/* this code depends on NL and EOT fitting in a single byte */
+			/* kind of expensive for each loop; worth precomputing? */
+			if(w->holding)
+				alts[WCread].op = CHANNOP;
+			else if(npart || (w->rawing && w->nraw>0))
+				alts[WCread].op = CHANSND;
+			else{
+				alts[WCread].op = CHANNOP;
+				for(i=w->qh; i<w->nr; i++){
+					c = w->r[i];
+					if(c=='\n' || c=='\004'){
+						alts[WCread].op = CHANSND;
+						break;
+					}
+				}
+			}
+		}
+		switch(alt(alts)){
+		case WKbd:
+			if(kbdqw - kbdqr < nelem(kbdq))
+				kbdq[kbdqw++ % nelem(kbdq)] = kbds;
+			else
+				free(kbds);
+			if(w->kbdopen)
+				continue;
+			while(kbdqr != kbdqw){
+				kbds = kbdq[kbdqr++ % nelem(kbdq)];
+				if(*kbds == 'c'){
+					chartorune(&r, kbds+1);
+					if(r)
+						wkeyctl(w, r);
+				}
+				free(kbds);
+			}
+			break;
+		case WKbdread:
+			recv(crm.c1, &pair);
+			nb = 0;
+			while(kbdqr != kbdqw){
+				kbds = kbdq[kbdqr % nelem(kbdq)];
+				i = strlen(kbds)+1;
+				if(nb+i > pair.ns)
+					break;
+				memmove((char*)pair.s + nb, kbds, i);
+				free(kbds);
+				nb += i;
+				kbdqr++;
+			}
+			pair.ns = nb;
+			send(crm.c2, &pair);
+			continue;
+		case WMouse:
+			if(w->mouseopen) {
+				w->mouse.counter++;
+
+				/* queue click events */
+				if(!w->mouse.qfull && lastb != w->mc.buttons) {	/* add to ring */
+					mp = &w->mouse.queue[w->mouse.wi];
+					if(++w->mouse.wi == nelem(w->mouse.queue))
+						w->mouse.wi = 0;
+					if(w->mouse.wi == w->mouse.ri)
+						w->mouse.qfull = TRUE;
+					mp->Mouse = w->mc;
+					mp->counter = w->mouse.counter;
+					lastb = w->mc.buttons;
+				}
+			} else
+				wmousectl(w);
+			break;
+		case WMouseread:
+			/* send a queued event or, if the queue is empty, the current state */
+			/* if the queue has filled, we discard all the events it contained. */
+			/* the intent is to discard frantic clicking by the user during long latencies. */
+			w->mouse.qfull = FALSE;
+			if(w->mouse.wi != w->mouse.ri) {
+				m = w->mouse.queue[w->mouse.ri];
+				if(++w->mouse.ri == nelem(w->mouse.queue))
+					w->mouse.ri = 0;
+			} else
+				m = (Mousestate){w->mc.Mouse, w->mouse.counter};
+
+			w->mouse.lastcounter = m.counter;
+			send(mrm.cm, &m.Mouse);
+			continue;
+		case WCtl:
+			if(wctlmesg(w, wcm.type, wcm.r, wcm.p) == Exited){
+				while(kbdqr != kbdqw)
+					free(kbdq[kbdqr++ % nelem(kbdq)]);
+				chanfree(crm.c1);
+				chanfree(crm.c2);
+				chanfree(mrm.cm);
+				chanfree(cwm.cw);
+				threadexits(nil);
+			}
+			continue;
+		case WCwrite:
+			recv(cwm.cw, &pair);
+			rp = pair.s;
+			nr = pair.ns;
+			for(i=0; i<nr; i++)
+				if(rp[i] == '\b'){
+					up = rp+i;
+					initial = 0;
+					for(; i<nr; i++){
+						if(rp[i] == '\b'){
+							if(up == rp)
+								initial++;
+							else
+								up--;
+						}else
+							*up++ = rp[i];
+					}
+					if(initial){
+						if(initial > w->qh)
+							initial = w->qh;
+						qh = w->qh-initial;
+						wdelete(w, qh, qh+initial);
+						w->qh = qh;
+					}
+					nr = up - rp;
+					break;
+				}
+			w->qh = winsert(w, rp, nr, w->qh)+nr;
+			if(w->scrolling || w->mouseopen)
+				wshow(w, w->qh);
+			wsetselect(w, w->q0, w->q1);
+			wscrdraw(w);
+			free(rp);
+			break;
+		case WCread:
+			recv(crm.c1, &pair);
+			t = pair.s;
+			nb = pair.ns;
+			i = npart;
+			npart = 0;
+			if(i)
+				memmove(t, part, i);
+			while(i<nb && (w->qh<w->nr || w->nraw>0)){
+				if(w->qh == w->nr){
+					wid = runetochar(t+i, &w->raw[0]);
+					w->nraw--;
+					runemove(w->raw, w->raw+1, w->nraw);
+				}else
+					wid = runetochar(t+i, &w->r[w->qh++]);
+				c = t[i];	/* knows break characters fit in a byte */
+				i += wid;
+				if(!w->rawing && (c == '\n' || c=='\004')){
+					if(c == '\004')
+						i--;
+					break;
+				}
+			}
+			if(i==nb && w->qh<w->nr && w->r[w->qh]=='\004')
+				w->qh++;
+			if(i > nb){
+				npart = i-nb;
+				memmove(part, t+nb, npart);
+				i = nb;
+			}
+			pair.s = t;
+			pair.ns = i;
+			send(crm.c2, &pair);
+			continue;
+		case WWread:
+			w->wctlready = 0;
+			recv(crm.c1, &pair);
+			s = Dx(w->screenr) > 0 ? "visible" : "hidden";
+			t = "notcurrent";
+			if(w == input)
+				t = "current";
+			pair.ns = snprint(pair.s, pair.ns+1, "%11d %11d %11d %11d %11s %11s ",
+				w->i->r.min.x, w->i->r.min.y, w->i->r.max.x, w->i->r.max.y, t, s);
+			send(crm.c2, &pair);
+			continue;
+		case WComplete:
+			if(w->i!=nil){
+				if(!cr->advance)
+					showcandidates(w, cr);
+				if(cr->advance){
+					rp = runesmprint("%s", cr->string);
+					if(rp){
+						nr = runestrlen(rp);
+						q0 = w->q0;
+						q0 = winsert(w, rp, nr, q0);
+						wshow(w, q0+nr);
+						free(rp);
+					}
+				}
+			}
+			freecompletion(cr);
+			break;
+		}
+		if(w->i!=nil && Dx(w->screenr) > 0 && display->bufp > display->buf)
+			flushimage(display, 1);
+	}
+}
+
+void
+wsetpid(Window *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);
+			free(w->label);
+			w->label = estrdup(buf);
+		}
+		snprint(buf, sizeof(buf), "/proc/%lud/notepg", (ulong)pid);
+		w->notefd = open(buf, OWRITE|OCEXEC);
+	}
+	if(ofd >= 0)
+		close(ofd);
+}
+
+void
+winshell(void *args)
+{
+	Window *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(filsysmount(filsys, 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() */
+	}
+	if(wclose(w) == 0){	/* remove extra ref hanging from creation */
+		notify(nil);
+		dup(1, 2);
+		if(dir)
+			chdir(dir);
+		procexec(pidc, cmd, argv);
+		_exits("exec failed");
+	}
+}
--- /dev/null
+++ b/sys/src/cmd/bof/xfid.c
@@ -1,0 +1,921 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <cursor.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <frame.h>
+#include <fcall.h>
+#include <plumb.h>
+#include "dat.h"
+#include "fns.h"
+
+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";
+char Ewindow[] = 	"cannot make window";
+char Enowindow[] = 	"window has no image";
+char Ebadmouse[] = 	"bad format on /dev/mouse";
+char Enope[] =		"not appearing in this film";
+
+extern char Eperm[];
+extern char Enomem[];
+
+static	Xfid	*xfidfree;
+static	Xfid	*xfid;
+static	Channel	*cxfidalloc;	/* chan(Xfid*) */
+static	Channel	*cxfidfree;	/* chan(Xfid*) */
+
+static	char	*tsnarf;
+static	int	ntsnarf;
+
+void
+xfidallocthread(void*)
+{
+	Xfid *x;
+	enum { Alloc, Free, N };
+	static Alt alts[N+1];
+
+	alts[Alloc].c = cxfidalloc;
+	alts[Alloc].v = nil;
+	alts[Alloc].op = CHANRCV;
+	alts[Free].c = cxfidfree;
+	alts[Free].v = &x;
+	alts[Free].op = CHANRCV;
+	alts[N].op = CHANEND;
+	for(;;){
+		switch(alt(alts)){
+		case Alloc:
+			x = xfidfree;
+			if(x)
+				xfidfree = x->free;
+			else{
+				x = emalloc(sizeof(Xfid));
+				x->c = chancreate(sizeof(void(*)(Xfid*)), 0);
+				x->flushc = chancreate(sizeof(int), 0);	/* notification only; no data */
+				x->flushtag = -1;
+				x->next = xfid;
+				xfid = x;
+				threadcreate(xfidctl, x, 16384);
+			}
+			if(x->ref != 0){
+				fprint(2, "%p incref %ld\n", x, x->ref);
+				error("incref");
+			}
+			if(x->flushtag != -1)
+				error("flushtag in allocate");
+			incref(x);
+			sendp(cxfidalloc, x);
+			break;
+		case Free:
+			if(x->ref != 0){
+				fprint(2, "%p decref %ld\n", x, x->ref);
+				error("decref");
+			}
+			if(x->flushtag != -1)
+				error("flushtag in free");
+			x->free = xfidfree;
+			xfidfree = x;
+			break;
+		}
+	}
+}
+
+Channel*
+xfidinit(void)
+{
+	cxfidalloc = chancreate(sizeof(Xfid*), 0);
+	cxfidfree = chancreate(sizeof(Xfid*), 0);
+	threadcreate(xfidallocthread, nil, STACK);
+	return cxfidalloc;
+}
+
+void
+xfidctl(void *arg)
+{
+	Xfid *x;
+	void (*f)(Xfid*);
+
+	x = arg;
+	threadsetname("xfid.%p", x);
+	for(;;){
+		f = recvp(x->c);
+		if(f){
+			x->flushtag = x->tag;
+			(*f)(x);
+		}
+		if(decref(x) == 0)
+			sendp(cxfidfree, x);
+	}
+}
+
+void
+xfidflush(Xfid *x)
+{
+	Fcall t;
+	Xfid *xf;
+
+	for(xf=xfid; xf; xf=xf->next)
+		if(xf->flushtag == x->oldtag){
+			incref(xf);	/* to hold data structures up at tail of synchronization */
+			if(xf->ref == 1)
+				error("ref 1 in flush");
+			xf->flushtag = -1;
+			break;
+		}
+
+	/* take over flushtag so follow up flushes wait for us */
+	x->flushtag = x->oldtag;
+
+	/*
+	 * wakeup filsysflush() in the filsysproc so the next
+	 * flush can come in.
+	 */
+	sendul(x->fs->csyncflush, 0);
+
+	if(xf){
+		enum { Done, Flush, End };
+		Alt alts[End+1];
+		void *f;
+		int z;
+
+		z = 0;
+		f = nil;
+
+		alts[Done].c = xf->c;
+		alts[Done].v = &f;
+		alts[Done].op = CHANSND;
+		alts[Flush].c = xf->flushc;
+		alts[Flush].v = &z;
+		alts[Flush].op = CHANSND;
+		alts[End].op = CHANEND;
+
+		while(alt(alts) != Done)
+			;
+	}
+	if(nbrecv(x->flushc, nil)){
+		filsyscancel(x);
+		return;
+	}
+	filsysrespond(x->fs, x, &t, nil);
+}
+
+void
+xfidattach(Xfid *x)
+{
+	Fcall t;
+	int id, hideit, scrollit;
+	Window *w;
+	char *err, *n, *dir, errbuf[ERRMAX];
+	int pid, newlymade;
+	Rectangle r;
+	Image *i;
+
+	t.qid = x->f->qid;
+	qlock(&all);
+	w = nil;
+	err = Eunkid;
+	dir = nil;
+	newlymade = FALSE;
+	hideit = 0;
+	scrollit = scrolling;
+
+	if(x->aname[0] == 'N'){	/* N 100,100, 200, 200 - old syntax */
+		n = x->aname+1;
+		pid = strtoul(n, &n, 0);
+		if(*n == ',')
+			n++;
+		r.min.x = strtoul(n, &n, 0);
+		if(*n == ',')
+			n++;
+		r.min.y = strtoul(n, &n, 0);
+		if(*n == ',')
+			n++;
+		r.max.x = strtoul(n, &n, 0);
+		if(*n == ',')
+			n++;
+		r.max.y = strtoul(n, &n, 0);
+  Allocate:
+		if(!goodrect(r))
+			err = Ebadrect;
+		else{
+			if(hideit)
+				i = allocimage(display, r, screen->chan, 0, DNofill);
+			else
+				i = allocwindow(wscreen, r, Refbackup, DNofill);
+			if(i){
+				if(pid == 0)
+					pid = -1;	/* make sure we don't pop a shell! - UGH */
+				w = new(i, hideit, scrollit, pid, dir, nil, nil);
+				newlymade = TRUE;
+			}else
+				err = Ewindow;
+		}
+	}else if(strncmp(x->aname, "new", 3) == 0){	/* new -dx -dy - new syntax, as in wctl */
+		pid = 0;
+		if(parsewctl(nil, ZR, &r, &pid, nil, &hideit, &scrollit, &dir, x->aname, errbuf) < 0)
+			err = errbuf;
+		else
+			goto Allocate;
+	}else if(strncmp(x->aname, "none", 4) == 0){
+		x->f->w = nil;
+		goto Done;
+	}else{
+		id = atoi(x->aname);
+		w = wlookid(id);
+	}
+	x->f->w = w;
+	if(w == nil){
+		qunlock(&all);
+		x->f->busy = FALSE;
+		filsysrespond(x->fs, x, &t, err);
+		return;
+	}
+	if(!newlymade)	/* counteract dec() in winshell() */
+		incref(w);
+  Done:
+	qunlock(&all);
+	filsysrespond(x->fs, x, &t, nil);
+}
+
+void
+xfidopen(Xfid *x)
+{
+	Fcall t;
+	Window *w;
+	char *s;
+
+	w = x->f->w;
+	if(w != nil && w->deleted){
+		filsysrespond(x->fs, x, &t, Edeleted);
+		return;
+	}
+	switch(FILE(x->f->qid)){
+	case Qtext:
+		if(x->mode&OTRUNC)
+			wsendctlmesg(w, Truncate, ZR, nil);
+		break;
+	case Qconsctl:
+		if(w->ctlopen){
+			filsysrespond(x->fs, x, &t, Einuse);
+			return;
+		}
+		w->ctlopen = TRUE;
+		break;
+	case Qkbd:
+		if(w->kbdopen){
+			filsysrespond(x->fs, x, &t, Einuse);
+			return;
+		}
+		w->kbdopen = TRUE;
+		break;
+	case Qmouse:
+		if(w->mouseopen){
+			filsysrespond(x->fs, x, &t, Einuse);
+			return;
+		}
+		/*
+		 * Reshaped: there's a race if the appl. opens the
+		 * window, is resized, and then opens the mouse,
+		 * but that's rare.  The alternative is to generate
+		 * a resized event every time a new program starts
+		 * up in a window that has been resized since the
+		 * dawn of time.  We choose the lesser evil.
+		 */
+		w->resized = FALSE;
+		w->mouseopen = TRUE;
+		break;
+	case Qsnarf:
+		if(x->mode==ORDWR || x->mode==OWRITE)
+			ntsnarf = 0;
+		break;
+	case Qwctl:
+		if(w != nil && (x->mode==OREAD || x->mode==ORDWR)){
+			/*
+			 * It would be much nicer to implement fan-out for wctl reads,
+			 * so multiple people can see the resizings, but rio just isn't
+			 * structured for that.  It's structured for /dev/cons, which gives
+			 * alternate data to alternate readers.  So to keep things sane for
+			 * wctl, we compromise and give an error if two people try to
+			 * open it.  Apologies.
+			 */
+			if(w->wctlopen){
+				filsysrespond(x->fs, x, &t, Einuse);
+				return;
+			}
+			w->wctlopen = TRUE;
+			w->wctlready = 1;
+			wsendctlmesg(w, Wakeup, ZR, nil);
+		}
+		break;
+	case Qtap:
+		chanprint(ctltap, "%c%c", Tapon, x->mode);
+		s = recvp(resptap);
+		if(s == nil)
+			break;
+		filsysrespond(x->fs, x, &t, s);
+		return;
+	case Qdot:
+		if(x->mode == ORDWR || x->mode == OWRITE){
+			filsysrespond(x->fs, x, &t, Enope);
+			return;
+		}
+		break;
+	}
+	t.qid = x->f->qid;
+	t.iounit = messagesize-IOHDRSZ;
+	x->f->open = TRUE;
+	x->f->mode = x->mode;
+	filsysrespond(x->fs, x, &t, nil);
+}
+
+void
+xfidclose(Xfid *x)
+{
+	Fcall t;
+	Window *w;
+	int nb, nulls;
+
+	w = x->f->w;
+	switch(FILE(x->f->qid)){
+	case Qconsctl:
+		if(w->rawing){
+			w->rawing = FALSE;
+			wsendctlmesg(w, Rawoff, ZR, nil);
+		}
+		if(w->holding){
+			w->holding = FALSE;
+			wsendctlmesg(w, Holdoff, ZR, nil);
+		}
+		w->ctlopen = FALSE;
+		break;
+	case Qcursor:
+		w->cursorp = nil;
+		wsetcursor(w, FALSE);
+		break;
+	case Qkbd:
+		w->kbdopen = FALSE;
+		break;
+	case Qmouse:
+		w->resized = FALSE;
+		w->mouseopen = FALSE;
+		w->winnameread = FALSE;
+		if(w->i != nil)
+			wsendctlmesg(w, Refresh, w->i->r, nil);
+		break;
+	/* odd behavior but really ok: replace snarf buffer when /dev/snarf is closed */
+	case Qsnarf:
+		if(x->f->mode==ORDWR || x->f->mode==OWRITE){
+			snarf = runerealloc(snarf, ntsnarf+1);
+			cvttorunes(tsnarf, ntsnarf, snarf, &nb, &nsnarf, &nulls);
+			ntsnarf = 0;
+		}
+		break;
+	case Qwctl:
+		if(w != nil && (x->f->mode==OREAD || x->f->mode==ORDWR))
+			w->wctlopen = FALSE;
+		break;
+	case Qtap:
+		chanprint(ctltap, "%c%c", Tapoff, x->f->mode);
+		recvp(resptap);
+		break;
+	}
+	if(w)
+		wclose(w);
+	filsysrespond(x->fs, x, &t, nil);
+}
+
+void
+xfidwrite(Xfid *x)
+{
+	Fcall fc;
+	int cnt, qid, nb, nr;
+	char err[ERRMAX], *p, *e;
+	Point pt;
+	Window *w;
+	Rune *r;
+	Conswritemesg cwm;
+	Stringpair pair;
+	enum { CWdata, CWgone, CWflush, NCW };
+	Alt alts[NCW+1];
+
+	w = x->f->w;
+	if(w != nil && w->deleted){
+		filsysrespond(x->fs, x, &fc, Edeleted);
+		return;
+	}
+	qid = FILE(x->f->qid);
+	cnt = x->count;
+	x->data[cnt] = 0;
+	switch(qid){
+	case Qcons:
+	case Qtext:
+		alts[CWdata].c = w->conswrite;
+		alts[CWdata].v = &cwm;
+		alts[CWdata].op = CHANRCV;
+		alts[CWgone].c = w->gone;
+		alts[CWgone].v = nil;
+		alts[CWgone].op = CHANRCV;
+		alts[CWflush].c = x->flushc;
+		alts[CWflush].v = nil;
+		alts[CWflush].op = CHANRCV;
+		alts[NCW].op = CHANEND;
+	
+		switch(alt(alts)){
+		case CWdata:
+			break;
+		case CWgone:
+			filsysrespond(x->fs, x, &fc, Edeleted);
+			return;
+		case CWflush:
+			filsyscancel(x);
+			return;
+		}
+
+		nr = x->f->nrpart;
+		if(nr > 0){
+			memmove(x->data+nr, x->data, cnt);	/* there's room: see malloc in filsysproc */
+			memmove(x->data, x->f->rpart, nr);
+			cnt += nr;
+		}
+		r = runemalloc(cnt);
+		if(r == nil){
+			pair.ns = 0;
+			send(cwm.cw, &pair);
+			filsysrespond(x->fs, x, &fc, Enomem);
+			return;
+		}
+		x->f->nrpart = 0;
+		cvttorunes(x->data, cnt-UTFmax, r, &nb, &nr, nil);
+		/* approach end of buffer */
+		while(fullrune(x->data+nb, cnt-nb)){
+			nb += chartorune(&r[nr], x->data+nb);
+			if(r[nr])
+				nr++;
+		}
+		if(nb < cnt){
+			memmove(x->f->rpart, x->data+nb, cnt-nb);
+			x->f->nrpart = cnt-nb;
+		}
+
+		pair.s = r;
+		pair.ns = nr;
+		send(cwm.cw, &pair);
+		fc.count = x->count;
+		filsysrespond(x->fs, x, &fc, nil);
+		return;
+
+	case Qconsctl:
+		if(strncmp(x->data, "holdon", 6)==0){
+			if(w->holding++ == 0)
+				wsendctlmesg(w, Holdon, ZR, nil);
+			break;
+		}
+		if(strncmp(x->data, "holdoff", 7)==0 && w->holding){
+			if(--w->holding == 0)
+				wsendctlmesg(w, Holdoff, ZR, nil);
+			break;
+		}
+		if(strncmp(x->data, "rawon", 5)==0){
+			if(w->holding){
+				w->holding = 0;
+				wsendctlmesg(w, Holdoff, ZR, nil);
+			}
+			if(w->rawing++ == 0)
+				wsendctlmesg(w, Rawon, ZR, nil);
+			break;
+		}
+		if(strncmp(x->data, "rawoff", 6)==0 && w->rawing){
+			if(--w->rawing == 0)
+				wsendctlmesg(w, Rawoff, ZR, nil);
+			break;
+		}
+		filsysrespond(x->fs, x, &fc, "unknown control message");
+		return;
+
+	case Qcursor:
+		if(cnt < 2*4+2*2*16)
+			w->cursorp = nil;
+		else{
+			w->cursor.offset.x = BGLONG(x->data+0*4);
+			w->cursor.offset.y = BGLONG(x->data+1*4);
+			memmove(w->cursor.clr, x->data+2*4, 2*2*16);
+			w->cursorp = &w->cursor;
+		}
+		wsetcursor(w, TRUE);
+		break;
+
+	case Qlabel:
+		p = realloc(w->label, cnt+1);
+		if(p == nil){
+			filsysrespond(x->fs, x, &fc, Enomem);
+			return;
+		}
+		w->label = p;
+		w->label[cnt] = 0;
+		memmove(w->label, x->data, cnt);
+		break;
+
+	case Qmouse:
+		if(w!=input || Dx(w->screenr)<=0)
+			break;
+		if(x->data[0] != 'm'){
+			filsysrespond(x->fs, x, &fc, Ebadmouse);
+			return;
+		}
+		p = nil;
+		pt.x = strtoul(x->data+1, &p, 0);
+		if(p == nil){
+			filsysrespond(x->fs, x, &fc, Eshort);
+			return;
+		}
+		pt.y = strtoul(p, nil, 0);
+		if(w==input && wpointto(mouse->xy)==w)
+			wsendctlmesg(w, Movemouse, Rpt(pt, pt), nil);
+		break;
+
+	case Qsnarf:
+		if(cnt == 0)
+			break;
+		/* always append only */
+		if(ntsnarf > MAXSNARF){	/* avoid thrashing when people cut huge text */
+			filsysrespond(x->fs, x, &fc, Elong);
+			return;
+		}
+		p = realloc(tsnarf, ntsnarf+cnt+1);	/* room for NUL */
+		if(p == nil){
+			filsysrespond(x->fs, x, &fc, Enomem);
+			return;
+		}
+		tsnarf = p;
+		memmove(tsnarf+ntsnarf, x->data, cnt);
+		ntsnarf += cnt;
+		snarfversion++;
+		break;
+
+	case Qwdir:
+		if(cnt == 0)
+			break;
+		if(x->data[cnt-1] == '\n'){
+			if(cnt == 1)
+				break;
+			x->data[cnt-1] = '\0';
+		}
+		/* assume data comes in a single write */
+		if(x->data[0] == '/'){
+			p = smprint("%.*s", cnt, x->data);
+		}else{
+			p = smprint("%s/%.*s", w->dir, cnt, x->data);
+		}
+		if(p == nil){
+			filsysrespond(x->fs, x, &fc, Enomem);
+			return;
+		}
+		free(w->dir);
+		w->dir = cleanname(p);
+		break;
+
+	case Qwctl:
+		if(writewctl(x, err) < 0){
+			filsysrespond(x->fs, x, &fc, err);
+			return;
+		}
+		break;
+
+	case Qdot:
+		filsysrespond(x->fs, x, &fc, Enope);
+		return;
+
+	case Qtap:
+		if(cnt < 2){
+			filsysrespond(x->fs, x, &fc, "malformed key");
+			return;
+		}
+		e = x->data + cnt;
+		for(p = x->data; p < e; p += strlen(p)+1){
+			switch(*p){
+			case '\0':
+				fc.count = p - x->data;
+				filsysrespond(x->fs, x, &fc, "null message type");
+				return;
+			case Tapfocus:
+				/* cleanup our own pollution */
+				break;
+			default:
+				chanprint(fromtap, "%s", p);
+				break;	
+			}
+		}
+		break;
+
+	default:
+		fprint(2, "unknown qid %d in write\n", qid);
+		filsysrespond(x->fs, x, &fc, "unknown qid in write");
+		return;
+	}
+	fc.count = cnt;
+	filsysrespond(x->fs, x, &fc, nil);
+}
+
+int
+readwindow(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;
+}
+
+void
+xfidread(Xfid *x)
+{
+	Fcall fc;
+	int n, off, cnt, c;
+	uint qid;
+	char buf[128], *t;
+	char cbuf[30];
+	Window *w;
+	Mouse ms;
+	Rectangle r;
+	Image *i;
+	Channel *c1, *c2;	/* chan (tuple(char*, int)) */
+	Consreadmesg crm;
+	Mousereadmesg mrm;
+	Stringpair pair;
+	enum { Adata, Agone, Aflush, Aend };
+	Alt alts[Aend+1];
+
+	w = x->f->w;
+	if(w != nil && w->deleted){
+		filsysrespond(x->fs, x, &fc, Edeleted);
+		return;
+	}
+	qid = FILE(x->f->qid);
+	off = x->offset;
+	cnt = x->count;
+	switch(qid){
+	case Qwctl:
+		if(w == nil){
+			if(off >= 6*12){
+				filsysrespond(x->fs, x, &fc, Etooshort);
+				return;
+			}
+			n = sprint(buf, "%11d %11d %11d %11d nowindow    nowindow    ",
+				screen->r.min.x, screen->r.min.y, screen->r.max.x, screen->r.max.y);
+			t = estrdup(buf);
+			goto Text;
+		}
+		if(cnt < 4*12){
+			filsysrespond(x->fs, x, &fc, Etooshort);
+			return;
+		}
+		alts[Adata].c = w->wctlread;
+		goto Consmesg;
+
+	case Qkbd:
+		alts[Adata].c = w->kbdread;
+		goto Consmesg;
+
+	case Qcons:
+		alts[Adata].c = w->consread;
+
+	Consmesg:
+		alts[Adata].v = &crm;
+		alts[Adata].op = CHANRCV;
+		alts[Agone].c = w->gone;
+		alts[Agone].v = nil;
+		alts[Agone].op = CHANRCV;
+		alts[Aflush].c = x->flushc;
+		alts[Aflush].v = nil;
+		alts[Aflush].op = CHANRCV;
+		alts[Aend].op = CHANEND;
+
+		switch(alt(alts)){
+		case Adata:
+			break;
+		case Agone:
+			filsysrespond(x->fs, x, &fc, Edeleted);
+			return;
+		case Aflush:
+			filsyscancel(x);
+			return;
+		}
+		c1 = crm.c1;
+		c2 = crm.c2;
+		t = emalloc(cnt+UTFmax+1);	/* room to unpack partial rune plus */
+		pair.s = t;
+		pair.ns = cnt;
+		send(c1, &pair);
+		recv(c2, &pair);
+		fc.data = pair.s;
+		fc.count = min(cnt, pair.ns);
+		filsysrespond(x->fs, x, &fc, nil);
+		free(t);
+		break;
+
+	case Qtap:
+		alts[Adata].c = totap;
+		alts[Adata].v = &t;
+		alts[Adata].op = CHANRCV;
+		if(w != nil){
+			alts[Agone].c = w->gone;
+			alts[Agone].v = nil;
+			alts[Agone].op = CHANRCV;
+		} else
+			alts[Agone].op = CHANNOP;
+		alts[Aflush].c = x->flushc;
+		alts[Aflush].v = nil;
+		alts[Aflush].op = CHANRCV;
+		alts[Aend].op = CHANEND;
+
+		switch(alt(alts)){
+		case Adata:
+			break;
+		case Agone:
+			filsysrespond(x->fs, x, &fc, Edeleted);
+			return;
+		case Aflush:
+			filsyscancel(x);
+			return;
+		}
+		fc.data = t;
+		/* kbdproc ensures we're only dealing with one message */
+		fc.count = strlen(t)+1;
+		filsysrespond(x->fs, x, &fc, nil);
+		free(t);
+		break;
+
+	case Qlabel:
+		n = strlen(w->label);
+		if(off > n)
+			off = n;
+		if(off+cnt > n)
+			cnt = n-off;
+		fc.data = w->label+off;
+		fc.count = cnt;
+		filsysrespond(x->fs, x, &fc, nil);
+		break;
+
+	case Qmouse:
+		alts[Adata].c = w->mouseread;
+		alts[Adata].v = &mrm;
+		alts[Adata].op = CHANRCV;
+		alts[Agone].c = w->gone;
+		alts[Agone].v = nil;
+		alts[Agone].op = CHANRCV;
+		alts[Aflush].c = x->flushc;
+		alts[Aflush].v = nil;
+		alts[Aflush].op = CHANRCV;
+		alts[Aend].op = CHANEND;
+
+		switch(alt(alts)){
+		case Adata:
+			break;
+		case Agone:
+			filsysrespond(x->fs, x, &fc, Edeleted);
+			return;
+		case Aflush:
+			filsyscancel(x);
+			return;
+		}
+
+		recv(mrm.cm, &ms);
+		c = 'm';
+		if(w->resized)
+			c = 'r';
+		n = sprint(buf, "%c%11d %11d %11d %11ld ", c, ms.xy.x, ms.xy.y, ms.buttons, ms.msec);
+		w->resized = 0;
+		fc.data = buf;
+		fc.count = min(n, cnt);
+		filsysrespond(x->fs, x, &fc, nil);
+		break;
+
+	case Qcursor:
+		filsysrespond(x->fs, x, &fc, "cursor read not implemented");
+		break;
+
+	/* The algorithm for snarf and text is expensive but easy and rarely used */
+	case Qsnarf:
+		getsnarf();
+		if(nsnarf)
+			t = runetobyte(snarf, nsnarf, &n);
+		else {
+			t = nil;
+			n = 0;
+		}
+		goto Text;
+
+	case Qtext:
+		t = wcontents(w, &n);
+		goto Text;
+
+	case Qdot:
+		/* assumption on mountpoint */
+		n = snprint(buf, sizeof buf, "/dev/wsys/%d/text:#%d,#%d", w->id, w->q0, w->q1);
+		t = estrdup(buf);
+		goto Text;
+
+	Text:
+		if(off > n){
+			off = n;
+			cnt = 0;
+		}
+		if(off+cnt > n)
+			cnt = n-off;
+		fc.data = t+off;
+		fc.count = cnt;
+		filsysrespond(x->fs, x, &fc, nil);
+		free(t);
+		break;
+
+	case Qwdir:
+		t = estrdup(w->dir);
+		n = strlen(t);
+		goto Text;
+
+	case Qwinid:
+		n = sprint(buf, "%11d ", w->id);
+		t = estrdup(buf);
+		goto Text;
+
+
+	case Qwinname:
+		n = strlen(w->name);
+		if(n == 0){
+			filsysrespond(x->fs, x, &fc, "window has no name");
+			break;
+		}
+		t = estrdup(w->name);
+		w->winnameread = TRUE;
+		goto Text;
+
+	case Qwindow:
+		i = w->i;
+		if(i == nil){
+			filsysrespond(x->fs, x, &fc, Enowindow);
+			return;
+		}
+		r = i->r;
+		goto caseImage;
+
+	case Qscreen:
+		i = screen;
+		r = screen->r;
+
+	caseImage:
+		if(off < 5*12){
+			n = sprint(buf, "%11s %11d %11d %11d %11d ",
+				chantostr(cbuf, i->chan),
+				r.min.x, r.min.y, r.max.x, r.max.y);
+			t = estrdup(buf);
+			goto Text;
+		}
+		off -= 5*12;
+		n = -1;
+		t = malloc(cnt);
+		if(t){
+			fc.data = t;
+			n = readwindow(i, t, r, off, cnt);	/* careful; fc.count is unsigned */
+		}
+		if(n < 0){
+			buf[0] = 0;
+			errstr(buf, sizeof buf);
+			filsysrespond(x->fs, x, &fc, buf);
+		}else{
+			fc.count = n;
+			filsysrespond(x->fs, x, &fc, nil);
+		}
+		free(t);
+		return;
+
+	default:
+		fprint(2, "unknown qid %d in read\n", qid);
+		snprint(buf, sizeof(buf), "unknown qid in read");
+		filsysrespond(x->fs, x, &fc, buf);
+		break;
+	}
+}