shithub: fork

Download patch

ref: d6e903846391bca5c5b5e683372c339b03eb7279
parent: 1cae79ca64cfff5b19120f8309fac320da736c76
author: qwx <qwx@sciops.net>
date: Tue Aug 5 06:13:06 EDT 2025

kbdfs: expose right shift and control keys

not changing kbmaps and vmx

--- /dev/null
+++ b/sys/include/keyboard.h
@@ -1,0 +1,79 @@
+#pragma src "/sys/src/libdraw"
+#pragma lib "libdraw.a"
+
+typedef struct 	Keyboardctl Keyboardctl;
+typedef struct	Channel	Channel;
+
+struct	Keyboardctl
+{
+	Channel	*c;	/* chan(Rune)[20] */
+
+	char		*file;
+	int		consfd;		/* to cons file */
+	int		ctlfd;		/* to ctl file */
+	int		pid;		/* of slave proc */
+};
+
+
+extern	Keyboardctl*	initkeyboard(char*);
+extern	int		ctlkeyboard(Keyboardctl*, char*);
+extern	void		closekeyboard(Keyboardctl*);
+
+enum {
+	KF=	0xF000,	/* Rune: beginning of private Unicode space */
+	Spec=	0xF800,
+	PF=	Spec|0x20,	/* num pad function key */
+	Kview=	Spec|0x00,	/* view (shift window up) */
+	/* KF|1, KF|2, ..., KF|0xC is *respectively* F1, F2, ..., F12 */
+	Khome=	KF|0x0D,
+	Kup=	KF|0x0E,
+	Kdown=	Kview,
+	Kpgup=	KF|0x0F,
+	Kprint=	KF|0x10,
+	Kleft=	KF|0x11,
+	Kright=	KF|0x12,
+	Kpgdown=	KF|0x13,
+	Kins=	KF|0x14,
+
+	Kalt=	KF|0x15,
+	Kshift=	KF|0x16,
+	Kctl=	KF|0x17,
+
+	Kend=	KF|0x18,
+	Kscroll=	KF|0x19,
+	Kscrolloneup=	KF|0x20,
+	Kscrollonedown=	KF|0x21,
+
+	/* multimedia keys - no refunds */
+	Ksbwd=	KF|0x22,	/* skip backwards */
+	Ksfwd=	KF|0x23,	/* skip forward */
+	Kpause=	KF|0x24,	/* play/pause */
+	Kvoldn=	KF|0x25,	/* volume decrement */
+	Kvolup=	KF|0x26,	/* volume increment */
+	Kmute=	KF|0x27,	/* (un)mute */
+	Kbrtdn=	KF|0x28,	/* brightness decrement */
+	Kbrtup=	KF|0x29,	/* brightness increment */
+
+	Krshift=	KF|0x30,
+	Krctl=	KF|0x31,
+
+	Ksoh=	0x01,
+	Kstx=	0x02,
+	Ketx=	0x03,
+	Keof=	0x04,
+	Kenq=	0x05,
+	Kack=	0x06,
+	Kbs=	0x08,
+	Knack=	0x15,
+	Ketb=	0x17,
+	Kdel=	0x7f,
+	Kesc=	0x1b,
+
+	Kbreak=	Spec|0x61,
+	Kcaps=	Spec|0x64,
+	Knum=	Spec|0x65,
+	Kmiddle=	Spec|0x66,
+	Kaltgr=	Spec|0x67,
+	Kmod4=	Spec|0x68,
+	Kmouse=	Spec|0x100,
+};
--- a/sys/src/cmd/aux/kbdfs/kbdfs.c
+++ b/sys/src/cmd/aux/kbdfs/kbdfs.c
@@ -167,7 +167,7 @@
 	[0x18]	'o',	'p',	'[',	']',	'\n',	Kctl,	'a',	's',
 	[0x20]	'd',	'f',	'g',	'h',	'j',	'k',	'l',	';',
 	[0x28]	'\'',	'`',	Kshift,	'\\',	'z',	'x',	'c',	'v',
-	[0x30]	'b',	'n',	'm',	',',	'.',	'/',	Kshift,	'*',
+	[0x30]	'b',	'n',	'm',	',',	'.',	'/',	Krshift,'*',
 	[0x38]	Kalt,	' ',	Kctl,	KF|1,	KF|2,	KF|3,	KF|4,	KF|5,
 	[0x40]	KF|6,	KF|7,	KF|8,	KF|9,	KF|10,	Knum,	Kscroll,'7',
 	[0x48]	'8',	'9',	'-',	'4',	'5',	'6',	'+',	'1',
@@ -187,7 +187,7 @@
 	[0x18]	'O',	'P',	'{',	'}',	'\n',	Kctl,	'A',	'S',
 	[0x20]	'D',	'F',	'G',	'H',	'J',	'K',	'L',	':',
 	[0x28]	'"',	'~',	Kshift,	'|',	'Z',	'X',	'C',	'V',
-	[0x30]	'B',	'N',	'M',	'<',	'>',	'?',	Kshift,	'*',
+	[0x30]	'B',	'N',	'M',	'<',	'>',	'?',	Krshift,'*',
 	[0x38]	Kalt,	' ',	Kctl,	KF|1,	KF|2,	KF|3,	KF|4,	KF|5,
 	[0x40]	KF|6,	KF|7,	KF|8,	KF|9,	KF|10,	Knum,	Kscroll,'7',
 	[0x48]	'8',	'9',	'-',	'4',	'5',	'6',	'+',	'1',
@@ -204,7 +204,7 @@
 	[0x00]	0,	0,	0,	0,	0,	0,	0,	0,
 	[0x08]	0,	0,	0,	0,	0,	0,	0,	0,
 	[0x10]	Ksbwd,	Kbrtdn,	0,	0,	0,	0,	0,	0,
-	[0x18]	0,	Ksfwd,	Kbrtup,	0,	'\n',	Kctl,	0,	0,
+	[0x18]	0,	Ksfwd,	Kbrtup,	0,	'\n',	Krctl,	0,	0,
 	[0x20]	Kmute,	0,	Kpause,	0,	0,	0,	0,	0,
 	[0x28]	0,	0,	0,	0,	0,	0,	Kvoldn,	0,
 	[0x30]	Kvolup,	0,	0,	0,	0,	'/',	0,	Kprint,
@@ -224,7 +224,7 @@
 	[0x00]	0,	0,	0,	0,	0,	0,	0,	0,
 	[0x08]	0,	0,	0,	0,	0,	0,	0,	0,
 	[0x10]	0,	0,	0,	0,	0,	0,	0,	0,
-	[0x18]	0,	0,	0,	0,	0,	0,	0,	0,
+	[0x18]	0,	0,	0,	0,	0,	Krctl,	0,	0,
 	[0x20]	0,	0,	0,	0,	0,	0,	0,	0,
 	[0x28]	0,	0,	0,	0,	0,	0,	0,	0,
 	[0x30]	0,	0,	0,	0,	0,	0,	0,	0,
@@ -244,7 +244,7 @@
 	[0x00]	0,	0,	0,	0,	0,	0,	0,	0,
 	[0x08]	0,	0,	0,	0,	0,	0,	0,	0,
 	[0x10]	0,	0,	0,	0,	0,	0,	0,	0,
-	[0x18]	0,	0,	0,	0,	0,	0,	0,	0,
+	[0x18]	0,	0,	0,	0,	0,	Krctl,	0,	0,
 	[0x20]	0,	0,	0,	0,	0,	0,	0,	0,
 	[0x28]	0,	0,	0,	0,	0,	0,	0,	0,
 	[0x30]	0,	0,	0,	0,	0,	0,	0,	0,
@@ -397,27 +397,6 @@
 }
 
 void
-emergencywarp(void)
-{
-	int fd;
-
-	if(debug)
-		return;
-
-	if(access("/srv/cwfs.cmd", AEXIST) == 0 && (fd = eopen("/srv/cwfs.cmd", OWRITE)) >= 0){
-		fprint(fd, "halt\n");
-		close(fd);
-	}
-	if(access("/srv/hjfs.cmd", AEXIST) == 0 && (fd = eopen("/srv/hjfs.cmd", OWRITE)) >= 0){
-		fprint(fd, "halt\n");
-		close(fd);
-	}
-	fprint(2, "emergency warp!\n");
-	sleep(1000);
-	reboot();
-}
-
-void
 shutdown(void)
 {
 	if(notefd >= 0)
@@ -496,9 +475,6 @@
 	if(scan->caps && key.r<='z' && key.r>='a')
 		key.r += 'A' - 'a';
 
-	if(scan->ctl && scan->altgr && key.r == Kdel)
-		emergencywarp();
-
 	if(scan->ctl && scan->alt && key.r == Kdel){
 		if(scan->shift)
 			shiftup();
@@ -511,9 +487,11 @@
 
 	switch(key.r){
 	case Kshift:
+	case Krshift:
 		scan->shift = key.down;
 		break;
 	case Kctl:
+	case Krctl:
 		scan->ctl = key.down;
 		break;
 	case Kaltgr:
@@ -576,22 +554,27 @@
 			if(kbtabs[Lnone][i] == k.r || kbtabs[Lshift][i] == k.r || (i >= 16 && kbtabs[Lctl][i] == k.r)){
 				/* assign button from kbtab */
 				k.b = kbtabs[Lnone][i];
+
 				/* handle ^X forms */
 				if(k.r == kbtabs[Lnone][i] && kbtabs[Lctl][i] && !a->shift && !a->altgr && a->ctl)
 					k.r = kbtabs[Lctl][i];
 				break;
+			} else if(kbtabs[Lesc1][i] == k.r || kbtabs[Lshiftesc1][i] == k.r){
+				/* check escaped scancodes too */
+				k.b = kbtabs[Lesc1][i];
+				break;
 			}
 		}
 		/* button unknown to kbtab, use rune if no modifier keys are active */
 		if(k.b == 0 && !a->shift && !a->altgr && !a->ctl)
 			k.b = k.r;
-		if(k.r == Kshift)
+		if(k.r == Kshift || k.r == Krshift)
 			a->shift = k.down;
 		else if(k.r == Kaltgr)
 			a->altgr = k.down;
 		else if(k.r == Kmod4)
 			a->mod4 = k.down;
-		else if(k.r == Kctl)
+		else if(k.r == Kctl || k.r == Krctl)
 			a->ctl = k.down;
 		send(keychan, &k);
 		break;
@@ -784,6 +767,7 @@
 		case Kcaps:
 		case Knum:
 		case Kshift:
+		case Krshift:
 		case Kaltgr:
 		case Kmod4:
 			/* ignore modifiers */
@@ -790,6 +774,7 @@
 			continue;
 
 		case Kctl:
+		case Krctl:
 		case Kalt:
 			/* composing escapes */
 			return 1;
@@ -828,7 +813,7 @@
 			continue;
 		}
 
-		if(r == Kctl){
+		if(r == Kctl || r == Krctl){
 			ctl = 1;
 			continue;
 		}
@@ -926,7 +911,7 @@
 				break;
 		}
 
-		if(mctlfd >= 0 && key.r == Kshift){
+		if(mctlfd >= 0 && (key.r == Kshift || key.r == Krshift)){
 			if(key.down){
 				fprint(mctlfd, "buttonmap 132");
 			} else {
--- /dev/null
+++ b/sys/src/cmd/evdump.c
@@ -1,0 +1,339 @@
+#include <u.h>
+#include <libc.h>
+#include <ctype.h>
+#include <bio.h>
+#include <draw.h>
+#include <keyboard.h>
+#include <mouse.h>
+#include <thread.h>
+
+typedef struct {
+	u8int e;
+	u8int c;
+	Rune r;
+}K;
+
+enum {
+	Kmbase = 0xf0000,
+};
+
+static K k[10*128];
+static int nk;
+static int kbd;
+static Biobuf *kbmap, *wctl;
+static char *layertab[] = {
+	"none", "shift", "esc", "altgr",
+	"ctl", "ctlesc", "shiftesc", "shiftaltgr",
+	"mod4", "altgrmod4",
+};
+
+static char *
+k2s(Rune r)
+{
+	switch(r){
+	case Ksbwd: return "Ksbwd";
+	case Ksfwd: return "Ksfwd";
+	case Kpause: return "Kpause";
+	case Kvoldn: return "Kvoldn";
+	case Kvolup: return "Kvolup";
+	case Kmute: return "Kmute";
+	case Kbrtdn: return "Kbrtdn";
+	case Kbrtup: return "Kbrtup";
+	case Kack: return "Kack";
+	case Kalt: return "Kalt";
+	case Kaltgr: return "Kaltgr";
+	case Kbreak: return "Kbreak";
+	case Kbs: return "Kbs";
+	case Kcaps: return "Kcaps";
+	case Kctl: return "Kctl";
+	case Kdel: return "Kdel";
+	case Kdown: return "Kdown";
+	case Kend: return "Kend";
+	case Kenq: return "Kenq";
+	case Keof: return "Keof";
+	case Kesc: return "Kesc";
+	case Ketb: return "Ketb";
+	case Ketx: return "Ketx";
+	case Khome: return "Khome";
+	case Kins: return "Kins";
+	case Kleft: return "Kleft";
+	case Kmiddle: return "Kmiddle";
+	case Kmod4: return "Kmod4";
+	case Knack: return "Knack";
+	case Knum: return "Knum";
+	case Kpgdown: return "Kpgdown";
+	case Kpgup: return "Kpgup";
+	case Kprint: return "Kprint";
+	case Krctl: return "Krctl";
+	case Kright: return "Kright";
+	case Krshift: return "Krshift";
+	case Kscroll: return "Kscroll";
+	case Kscrollonedown: return "Kscrollonedown";
+	case Kscrolloneup: return "Kscrolloneup";
+	case Kshift: return "Kshift";
+	case Ksoh: return "Ksoh";
+	case Kstx: return "Kstx";
+	case Kup: return "Kup";
+	case KF|1: return "F1";
+	case KF|2: return "F2";
+	case KF|3: return "F3";
+	case KF|4: return "F4";
+	case KF|5: return "F5";
+	case KF|6: return "F6";
+	case KF|7: return "F7";
+	case KF|8: return "F8";
+	case KF|9: return "F9";
+	case KF|10: return "F10";
+	case KF|11: return "F11";
+	case KF|12: return "F12";
+	case Kmouse|1: return "Kmouse1";
+	case Kmouse|2: return "Kmouse2";
+	case Kmouse|3: return "Kmouse3";
+	case Kmouse|4: return "Kmouse4";
+	case Kmouse|5: return "Kmouse5";
+	case '\n': return "\\n";
+	}
+
+	return nil;
+}
+
+static int
+kmreset(void *, char *)
+{
+	int i;
+
+	for(i = 0; i < nk; i++)
+		Bprint(kbmap, "%d\t%d\t%d\n", k[i].e, k[i].c, k[i].r);
+	Bflush(kbmap);
+
+	return 0;
+}
+
+static void
+kmset(void)
+{
+	int i;
+
+	for(i = 0; i < nk; i++)
+		Bprint(kbmap, "%d\t%d\t%d\n", k[i].e, k[i].c, Kmbase+i);
+	Bflush(kbmap);
+}
+
+static void
+key(Rune r, char *type)
+{
+	char *s, t[32];
+	Rune c;
+	K q;
+
+	if(r < Kmbase || r >= Kmbase+nk){
+		if((s = k2s(r)) != nil)
+			snprint(t, sizeof(t), "%s", s);
+		else if((r < 0x80 && isprint(r)) || r >= 0x20)
+			snprint(t, sizeof(t), "%C (0x%x)", r, r);
+		else
+			snprint(t, sizeof(t), "0x%x", r);
+		return;
+	}
+	q = k[r-Kmbase];
+	c = q.r;
+	if((s = k2s(c)) != nil)
+		snprint(t, sizeof(t), "%s", s);
+	else if((c < 0x80 && isprint(c)) || c >= 0x20)
+		snprint(t, sizeof(t), "%C (0x%x)", c, c);
+	else
+		snprint(t, sizeof(t), "0x%x", c);
+
+	print("key %s %s: %d %d 0x%ux\n", type, t, q.e, q.c, q.r);
+}
+
+static void
+wctlproc(void *)
+{
+	char s[256], *t[8];
+	int wctl, n;
+
+	if((wctl = open("/dev/wctl", OREAD)) < 0)
+		sysfatal("%r");
+	for(;;){
+		if((n = read(wctl, s, sizeof(s)-1)) <= 0)
+			break;
+		s[n] = 0;
+		if(tokenize(s, t, nelem(t)) < 6)
+			continue;
+
+		if(strcmp(t[4], "current") == 0)
+			kmset();
+		else if(strcmp(t[4], "notcurrent") == 0)
+			kmreset(nil, nil);
+
+		print("wctl %s %s\n", t[4], t[5]);
+	}
+	close(wctl);
+
+	threadexits(nil);
+}
+
+static void
+kbproc(void *)
+{
+	char *s, buf[128], buf2[128];
+	int kbd, n;
+	Rune r;
+
+	threadsetname("kbproc");
+	if((kbd = open("/dev/kbd", OREAD)) < 0)
+		sysfatal("/dev/kbd: %r");
+
+	buf2[0] = 0;
+	buf2[1] = 0;
+	buf[0] = 0;
+	for(;;){
+		if(buf[0] != 0){
+			n = strlen(buf)+1;
+			memmove(buf, buf+n, sizeof(buf)-n);
+		}
+		if(buf[0] == 0){
+			n = read(kbd, buf, sizeof(buf)-1);
+			if(n <= 0)
+				break;
+			buf[n-1] = 0;
+			buf[n] = 0;
+		}
+
+		switch(buf[0]){
+		case 'k':
+			for(s = buf+1; *s;){
+				s += chartorune(&r, s);
+				if(utfrune(buf2+1, r) == nil)
+					key(r, "down");
+			}
+			break;
+		case 'K':
+			for(s = buf2+1; *s;){
+				s += chartorune(&r, s);
+				if(utfrune(buf+1, r) == nil)
+					key(r, "up");
+			}
+			break;
+		case 'c':
+			if(chartorune(&r, buf+1) > 0 && r != Runeerror)
+				key(r, "repeat");
+		default:
+			continue;
+		}
+
+		strcpy(buf2, buf);
+	}
+
+	close(kbd);
+
+	threadexits(nil);
+}
+
+static void
+usage(void)
+{
+	fprint(2, "usage: %s\n", argv0);
+	threadexitsall("usage");
+}
+
+void
+threadmain(int argc, char **argv)
+{
+	Mousectl *mctl;
+	char tmp[32], *f[4], *s;
+	int nf, e;
+	Mouse m;
+	enum { Cmouse, Cresize, Numchan };
+	Alt a[Numchan+1] = {
+		[Cmouse] = { nil, &m, CHANRCV },
+		[Cresize] = { nil, nil, CHANRCV },
+		{ nil, nil, CHANEND },
+	};
+
+	ARGBEGIN{
+	default:
+		usage();
+	}ARGEND
+
+	if(argc != 0)
+		usage();
+
+	if((kbmap = Bopen("/dev/kbmap", OREAD)) == nil)
+		sysfatal("%r");
+
+	nk = 0;
+	for(;;){
+		if((s = Brdline(kbmap, '\n')) == nil)
+			break;
+		s[Blinelen(kbmap)-1] = '\0';
+		nf = getfields(s, f, nelem(f), 1, " \t");
+		if(nf < 3)
+			continue;
+		for(e = 0; e < nelem(layertab); e++)
+			if(strcmp(f[0], layertab[e]) == 0)
+				break;
+		if(e >= nelem(layertab)){
+			e = strtoul(f[0], &s, 0);
+			if(*s != '\0')
+				continue;
+		}
+		k[nk].e = e;
+		k[nk].c = strtoul(f[1], &s, 0);
+		if(*s != '\0')
+			continue;
+		k[nk].r = strtoul(f[2], &s, 0);
+		if(*s != '\0')
+			continue;
+		if(++nk >= nelem(k))
+			break;
+	}
+	Bterm(kbmap);
+
+	if((kbmap = Bopen("/dev/kbmap", OWRITE)) == nil)
+		sysfatal("%r");
+	threadnotify(kmreset, 1);
+
+	snprint(tmp, sizeof(tmp), "-pid %d -dx %d -dy %d", getpid(), 256, 256);
+	newwindow(tmp);
+
+	if(initdraw(nil, nil, "evdump") < 0)
+		sysfatal("initdraw: %r");
+	if((mctl = initmouse(nil, screen)) == nil)
+		sysfatal("initmouse: %r");
+	a[Cmouse].c = mctl->c;
+	a[Cresize].c = mctl->resizec;
+
+	proccreate(kbproc, nil, mainstacksize);
+	proccreate(wctlproc, nil, mainstacksize);
+
+	for(;;){
+		draw(screen, screen->r, display->black, nil, ZP);
+
+		switch(alt(a)){
+		case -1:
+			goto end;
+
+		case Cmouse:
+			print(
+				"mouse buttons 0x%x x %d y %d\n",
+				m.buttons,
+				m.xy.x, m.xy.y
+			);
+			break;
+
+		case Cresize:
+			getwindow(display, Refnone);
+			print(
+				"resize min %d %d max %d %d\n",
+				screen->r.min.x, screen->r.min.y,
+				screen->r.max.x, screen->r.max.y
+			);
+			break;
+		}
+	}
+
+end:
+	threadexitsall(nil);
+}
--- /dev/null
+++ b/sys/src/cmd/rio/wind.c
@@ -1,0 +1,1850 @@
+#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){
+		input = w;
+		return;
+	}
+	if(w == input)
+		return;
+	incref(input);
+	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 = holdcol;
+		else
+			col = paleholdcol;
+	}else{
+		if(type == Selborder)
+			col = titlecol;
+		else
+			col = lighttitlecol;
+	}
+	border(w->i, w->i->r, Selborder, col, ZP);
+}
+
+static void
+wsetcols(Window *w, int topped)
+{
+	if(w->holding)
+		if(topped)
+			w->cols[TEXT] = holdcol;
+		else
+			w->cols[TEXT] = lightholdcol;
+	else
+		if(topped)
+			w->cols[TEXT] = cols[TEXT];
+		else
+			w->cols[TEXT] = paletextcol;
+}
+
+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:
+	case Krctl:
+	case Krshift:
+		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, rc, lc;
+	Rune *r, *l, *p;
+	uint q;
+
+	if(mode){
+		lc = *q0 > 0     ? w->r[*q0-1] : '\n';
+		rc = *q1 < w->nr ? w->r[*q1]   : '\n';
+		for(i=0; left[i]; i++){
+			l = left[i];
+			r = right[i];
+			p = strrune(l, lc);
+			if(p && r[p-l] == rc){
+				*q0 -= *q0 > 0 && lc != '\n';
+				*q1 += *q1 < w->nr;
+				return;
+			}
+		}
+	}
+
+	*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){
+		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 and keyboard buttons */
+			if(w->mc.buttons){
+				w->mc.buttons = 0;
+				w->mouse.counter++;
+			}
+			w->keyup = w->kbdopen;
+			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 || w->keyup) ?
+				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++;
+			}
+			if(w->keyup && nb+2 <= pair.ns){
+				w->keyup = 0;
+				memmove((char*)pair.s + nb, "K", 2);
+				nb += 2;
+			}
+			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");
+	}
+}
--- a/sys/src/cmd/riow.c
+++ b/sys/src/cmd/riow.c
@@ -399,7 +399,7 @@
 		mod = 0;
 		if(utfrune(s+1, Kmod4) != nil)
 			mod |= Mmod4;
-		if(utfrune(s+1, Kctl) != nil)
+		if(utfrune(s+1, Kctl) != nil || utfrune(s+1, Krctl) != nil)
 			mod |= Mctl;
 		if(utfrune(s+1, Kshift) != nil)
 			mod |= Mshift;
--- /dev/null
+++ b/sys/src/cmd/vnc/kbds.c
@@ -1,0 +1,74 @@
+#include	<u.h>
+#include	<libc.h>
+#include	<keyboard.h>
+#include	"compat.h"
+#include	"kbd.h"
+#include   "ksym2utf.h"
+
+enum {
+	VKSpecial = 0xff00,
+};
+
+static Rune vnckeys[] =
+{
+[0x00]	0,	0,	0,	0,	0,	0,	0,	0,
+[0x08]	'\b',	'\t',	'\r',	0,	0,	'\n',	0,	0,
+[0x10]	0,	0,	0,	0,	Kscroll,0,	0,	0,
+[0x18]	0,	0,	0,	Kesc,	0,	0,	0,	0,
+[0x20]	0,	0,	0,	0,	0,	0,	0,	0,
+[0x28]	0,	0,	0,	0,	0,	0,	0,	0,
+[0x30]	0,	0,	0,	0,	0,	0,	0,	0,
+[0x38]	0,	0,	0,	0,	0,	0,	0,	0,
+[0x40]	0,	0,	0,	0,	0,	0,	0,	0,
+[0x48]	0,	0,	0,	0,	0,	0,	0,	0,
+[0x50]	Khome,	Kleft,	Kup,	Kright,	Kdown,	Kpgup,	Kpgdown,Kend,
+[0x58]	0,	0,	0,	0,	0,	0,	0,	0,
+[0x60]	0,	Kprint,	0,	Kins,	0,	0,	0,	0,
+[0x68]	0,	0,	0,	Kbreak,	0,	0,	0,	0,
+[0x70]	0,	0,	0,	0,	0,	0,	0,	0,
+[0x78]	0,	0,	0,	0,	0,	0,	0,	Knum,
+[0x80]	0,	0,	0,	0,	0,	0,	0,	0,
+[0x88]	0,	0,	0,	0,	0,	0,	0,	0,
+[0x90]	0,	0,	0,	0,	0,	0,	0,	0,
+[0x98]	0,	0,	0,	0,	0,	0,	0,	0,
+[0xa0]	0,	0,	0,	0,	0,	0,	0,	0,
+[0xa8]	0,	0,	'*',	'+',	0,	'-',	'.',	'/',
+[0xb0]	'0',	'1',	'2',	'3',	'4',	'5',	'6',	'7',
+[0xb8]	'8',	'9',	0,	0,	0,	'=',	0,	0,
+[0xc0]	0,	0,	0,	0,	0,	0,	0,	0,
+[0xc8]	0,	0,	0,	0,	0,	0,	0,	0,
+[0xd0]	0,	0,	0,	0,	0,	0,	0,	0,
+[0xd8]	0,	0,	0,	0,	0,	0,	0,	0,
+[0xe0]	0,	Kshift,	Krshift,	Kctl,	Krctl,	Kcaps,	Kcaps,	0,
+[0xe8]	0,	Kalt,	Kalt,	0,	0,	0,	0,	0,
+[0xf0]	0,	0,	0,	0,	0,	0,	0,	0,
+[0xf8]	0,	0,	0,	0,	0,	0,	0,	Kdel,
+};
+
+/*
+ *  keyboard interrupt
+ */
+void
+vncputc(int keyup, int c)
+{
+	char buf[16];
+
+	/*
+ 	 *  character mapping
+	 */
+	if((c & VKSpecial) == VKSpecial){
+		c = vnckeys[c & 0xff];
+		if(c == 0)
+			return;
+	}
+	/*
+	 * map an xkeysym onto a utf-8 char
+	 */
+	if((c & 0xff00) && c < nelem(ksym2utf) && ksym2utf[c] != 0)
+		c = ksym2utf[c];
+	snprint(buf, sizeof(buf), "r%C", c);
+	if(keyup)
+		buf[0] = 'R';
+	if(kbdin >= 0)
+		write(kbdin, buf, strlen(buf)+1);
+}
--- /dev/null
+++ b/sys/src/cmd/vnc/kbdv.c
@@ -1,0 +1,278 @@
+#include "vnc.h"
+#include <keyboard.h>
+#include "utf2ksym.h"
+
+enum {
+	Xshift = 0xFFE1,
+	Xrshift = 0xFFE2,
+	Xctl = 0xFFE3,
+	Xrctl = 0xFFE4,
+	Xmeta = 0xFFE7,
+	Xalt = 0xFFE9,
+	Xsuper = 0xFFEB,
+};
+
+static struct {
+	Rune kbdc;
+	ulong keysym;
+} ktab[] = {
+	{'\b',		0xff08},
+	{'\t',		0xff09},
+	{'\n',		0xff0d},
+	/* {0x0b, 0xff0b}, */
+	{'\r',		0xff0d},
+	{Kesc,	0xff1b},
+	{Kins,	0xff63},
+	{Kdel,	0xffff},
+	{Khome,	0xff50},
+	{Kend,	0xff57},
+	{Kpgup,	0xff55},
+	{Kpgdown,	0xff56},
+	{Kleft,	0xff51},
+	{Kup,	0xff52},
+	{Kright,	0xff53},
+	{Kdown,	0xff54},
+	{KF|1,	0xffbe},
+	{KF|2,	0xffbf},
+	{KF|3,	0xffc0},
+	{KF|4,	0xffc1},
+	{KF|5,	0xffc2},
+	{KF|6,	0xffc3},
+	{KF|7,	0xffc4},
+	{KF|8,	0xffc5},
+	{KF|9,	0xffc6},
+	{KF|10,	0xffc7},
+	{KF|11,	0xffc8},
+	{KF|12,	0xffc9},
+
+	{Kshift, Xshift},
+	{Krshift, Xrshift},
+	{Kalt, Xalt},
+	{Kaltgr, Xmeta},
+	{Kmod4, Xsuper},
+	{Kctl, Xctl},
+	{Krctl, Xrctl},
+};
+
+static char shiftkey[128] = {
+	0, 0, 0, 0, 0, 0, 0, 0, /* nul soh stx etx eot enq ack bel */
+	0, 0, 0, 0, 0, 0, 0, 0, /* bs ht nl vt np cr so si */
+	0, 0, 0, 0, 0, 0, 0, 0, /* dle dc1 dc2 dc3 dc4 nak syn etb */
+	0, 0, 0, 0, 0, 0, 0, 0, /* can em sub esc fs gs rs us */
+	0, 1, 1, 1, 1, 1, 1, 0, /* sp ! " # $ % & ' */
+	1, 1, 1, 1, 0, 0, 0, 0, /* ( ) * + , - . / */
+	0, 0, 0, 0, 0, 0, 0, 0, /* 0 1 2 3 4 5 6 7 */
+	0, 0, 1, 0, 1, 0, 1, 1, /* 8 9 : ; < = > ? */
+	1, 1, 1, 1, 1, 1, 1, 1, /* @ A B C D E F G */
+	1, 1, 1, 1, 1, 1, 1, 1, /* H I J K L M N O */
+	1, 1, 1, 1, 1, 1, 1, 1, /* P Q R S T U V W */
+	1, 1, 1, 0, 0, 0, 1, 1, /* X Y Z [ \ ] ^ _ */
+	0, 0, 0, 0, 0, 0, 0, 0, /* ` a b c d e f g */
+	0, 0, 0, 0, 0, 0, 0, 0, /* h i j k l m n o */
+	0, 0, 0, 0, 0, 0, 0, 0, /* p q r s t u v w */
+	0, 0, 0, 1, 1, 1, 1, 0, /* x y z { | } ~ del  */
+};
+
+ulong
+runetoksym(Rune r)
+{
+	int i;
+
+	for(i=0; i<nelem(ktab); i++)
+		if(ktab[i].kbdc == r)
+			return ktab[i].keysym;
+	return r;
+}
+
+static void
+keyevent(Vnc *v, ulong ksym, int down)
+{
+	vnclock(v);
+	vncwrchar(v, MKey);
+	vncwrchar(v, down);
+	vncwrshort(v, 0);
+	vncwrlong(v, ksym);
+	vncflush(v);
+	vncunlock(v);
+}
+
+static void
+readcons(Vnc *v)
+{
+	char buf[256], k[10];
+	ulong ks;
+	int ctlfd, fd, kr, kn, w, shift, ctl, alt, super;
+	Rune r;
+
+	snprint(buf, sizeof buf, "%s/cons", display->devdir);
+	if((fd = open(buf, OREAD)) < 0)
+		sysfatal("open %s: %r", buf);
+
+	snprint(buf, sizeof buf, "%s/consctl", display->devdir);
+	if((ctlfd = open(buf, OWRITE)) < 0)
+		sysfatal("open %s: %r", buf);
+	write(ctlfd, "rawon", 5);
+
+	kn = 0;
+	shift = alt = ctl = super = 0;
+	for(;;){
+		while(!fullrune(k, kn)){
+			kr = read(fd, k+kn, sizeof k - kn);
+			if(kr <= 0)
+				sysfatal("bad read from kbd");
+			kn += kr;
+		}
+		w = chartorune(&r, k);
+		kn -= w;
+		memmove(k, &k[w], kn);
+		ks = runetoksym(r);
+
+		switch(r){
+		case Kalt:
+			alt = !alt;
+			keyevent(v, Xalt, alt);
+			break;
+		case Kctl:
+		case Krctl:
+			ctl = !ctl;
+			keyevent(v, Xctl, ctl);
+			break;
+		case Kshift:
+		case Krshift:
+			shift = !shift;
+			keyevent(v, Xshift, shift);
+			break;
+		case Kmod4:
+			super = !super;
+			keyevent(v, Xsuper, super);
+			break;
+		default:
+			if(r == ks && r < 0x1A){	/* control key */
+				keyevent(v, Xctl, 1);
+				keyevent(v, r+0x60, 1);	/* 0x60: make capital letter */
+				keyevent(v, r+0x60, 0);
+				keyevent(v, Xctl, 0);
+			}else{
+				/*
+				 * to send an upper case letter or shifted
+				 * punctuation, mac os x vnc server,
+				 * at least, needs a `shift' sent first.
+				 */
+				if(!shift && r == ks && r < sizeof shiftkey && shiftkey[r]){
+					shift = 1;
+					keyevent(v, Xshift, 1);
+				}
+				/*
+				 * map an xkeysym onto a utf-8 char.
+				 * allows Xvnc to read us, see utf2ksym.h
+				 */
+				if((ks & 0xff00) && ks < nelem(utf2ksym) && utf2ksym[ks] != 0)
+					ks = utf2ksym[ks];
+				keyevent(v, ks, 1);
+				/*
+				 * up event needed by vmware inside linux vnc server,
+				 * perhaps others.
+				 */
+				keyevent(v, ks, 0);
+			}
+
+			if(alt){
+				keyevent(v, Xalt, 0);
+				alt = 0;
+			}
+			if(ctl){
+				keyevent(v, Xctl, 0);
+				ctl = 0;
+			}
+			if(shift){
+				keyevent(v, Xshift, 0);
+				shift = 0;
+			}
+			if(super){
+				keyevent(v, Xsuper, 0);
+				super = 0;
+			}
+			break;
+		}
+	}
+}
+
+ulong
+runetovnc(Rune r)
+{
+	ulong k;
+
+	k = runetoksym(r);
+	if((k & 0xff00) && k < nelem(utf2ksym) && utf2ksym[k] != 0)
+		k = utf2ksym[k];
+	return k;
+}
+
+void
+readkbd(Vnc *v)
+{
+	char buf[128], buf2[128], *s;
+	int fd, n;
+	Rune r;
+
+	if((fd = open("/dev/kbd", OREAD)) < 0){
+		readcons(v);
+		return;
+	}
+
+	buf2[0] = 0;
+	buf2[1] = 0;
+	buf[0] = 0;
+	for(;;){
+		if(buf[0] != 0){
+			n = strlen(buf)+1;
+			memmove(buf, buf+n, sizeof(buf)-n);
+		}
+		if(buf[0] == 0){
+			n = read(fd, buf, sizeof(buf)-1);
+			if(n <= 0)
+				break;
+			buf[n-1] = 0;
+			buf[n] = 0;
+		}
+		switch(buf[0]){
+		case 'k':
+			s = buf+1;
+			while(*s){
+				s += chartorune(&r, s);
+				if(utfrune(buf2+1, r) == nil)
+					if((r == Kshift) ||
+					   utfrune(buf+1, Krshift) || 
+					   utfrune(buf+1, Kctl) || 
+					   utfrune(buf+1, Krctl) || 
+					   utfrune(buf+1, Kalt) ||
+					   utfrune(buf+1, Kmod4) ||
+					   utfrune(buf+1, Kaltgr))
+						keyevent(v, runetovnc(r), 1);
+			}
+			break;
+		case 'K':
+			s = buf2+1;
+			while(*s){
+				s += chartorune(&r, s);
+				if(utfrune(buf+1, r) == nil)
+					keyevent(v, runetovnc(r), 0);
+			}
+			break;
+		case 'c':
+			if(utfrune(buf2+1, Kctl) ||
+			   utfrune(buf2+1, Kalt) ||
+			   utfrune(buf2+1, Kmod4) ||
+			   utfrune(buf2+1, Kaltgr))
+				continue;
+			chartorune(&r, buf+1);
+			keyevent(v, runetovnc(r), 1);
+			if(utfrune(buf2+1, r) == nil)
+				keyevent(v, runetovnc(r), 0);
+		default:
+			continue;
+		}
+		strcpy(buf2, buf);
+	}
+}
+
--