shithub: plan9front

Download patch

ref: 166e8b3cf8d2e087258eb47fdba7e890d6a8432a
parent: 2a29b497f215f91d05b6718a0e852e8a9b6ce6da
parent: c6ae349e9d67ef24ce429f699c5232a9acdcb1ea
author: cinap_lenrek <cinap_lenrek@felloff.net>
date: Sat Oct 8 06:38:14 EDT 2022

merge

--- a/lib/font/bit/vga/unicode.font
+++ b/lib/font/bit/vga/unicode.font
@@ -71,6 +71,8 @@
 0x2329	0x232a	vga.2329-232A
 0x239b	0x23bd	vga.239B-23BD
 0x23ce	0x23ce	vga.23CE-23CE
+0x23ed	0x23ef	vga.23ED-23EF
+0x23f4	0x23fa	vga.23F4-23FA
 0x2409	0x240d	vga.2409-240D
 0x2423	0x2424	vga.2423-2424
 0x2426	0x2426	vga.2426-2426
binary files /tmp/diff100063423078 b/lib/font/bit/vga/vga.23ED-23EF differ
binary files /tmp/diff100063423081 b/lib/font/bit/vga/vga.23F4-23FA differ
--- a/lib/font/bit/vga/vga.font
+++ b/lib/font/bit/vga/vga.font
@@ -71,6 +71,8 @@
 0x2329	0x232A	vga.2329-232A
 0x239B	0x23BD	vga.239B-23BD
 0x23CE	0x23CE	vga.23CE-23CE
+0x23ED	0x23EF	vga.23ED-23EF
+0x23F4	0x23FA	vga.23F4-23FA
 0x2409	0x240D	vga.2409-240D
 0x2423	0x2424	vga.2423-2424
 0x2426	0x2426	vga.2426-2426
--- a/sys/include/keyboard.h
+++ b/sys/include/keyboard.h
@@ -44,6 +44,16 @@
 	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 */
+
 	Ksoh=	0x01,
 	Kstx=	0x02,
 	Ketx=	0x03,
--- a/sys/lib/git/common.rc
+++ b/sys/lib/git/common.rc
@@ -61,6 +61,7 @@
 		name=$user
 	if(~ $email '')
 		email=$user@$sysname
+	status=''
 }
 
 # merge1 out ours base theirs
--- a/sys/lib/kbmap/us
+++ b/sys/lib/kbmap/us
@@ -270,8 +270,8 @@
 2	13	0
 2	14	0
 2	15	0
-2	16	0
-2	17	0
+2	16	0xf022
+2	17	0xf028
 2	18	0
 2	19	0
 2	20	0
@@ -279,16 +279,16 @@
 2	22	0
 2	23	0
 2	24	0
-2	25	0
-2	26	0
+2	25	0xf023
+2	26	0xf029
 2	27	0
 2	28	^J
 2	29	0xf862
 2	30	0
 2	31	0
-2	32	0
+2	32	0xf027
 2	33	0
-2	34	0
+2	34	0xf024
 2	35	0
 2	36	0
 2	37	0
@@ -300,9 +300,9 @@
 2	43	0
 2	44	0
 2	45	0
-2	46	0
+2	46	0xf025
 2	47	0
-2	48	0
+2	48	0xf026
 2	49	0
 2	50	0
 2	51	0
--- a/sys/man/1/bar
+++ b/sys/man/1/bar
@@ -55,7 +55,7 @@
 Date and time format may be set using
 .I -d
 option, see
-.I tmdate(2).
+.IR tmdate (2).
 .SH EXAMPLES
 An example of how
 .I bar
--- a/sys/man/1/camv
+++ b/sys/man/1/camv
@@ -1,16 +1,55 @@
 .TH CAMV 1
 .SH NAME
-camv, camera display
+camv \- USB camera display
 .SH SYNOPSIS
-.B camv cam-device
+.B camv
+.I cam-device
 .SH DESCRIPTION
 .I Camv
-uses the underlying
+uses the
 .I cam-device
-file system to start a graphical camera display stream.
+file tree provided by
+.IR nusb (4)
+to use and control a USB camera.
+It opens the tree's
+.I video
+file to play the video stream directly in the window.
 .PP
-The file system is initialized using nusb/cam (see nusb(4)).
+The middle mouse button menu shows and allows editing the current picture settings.
+While it lists resolution, format and framerate per second,
+those must be set manually before starting
+.IR camv (1).
+One can quit the program from the right mouse button menu.
+.SH EXAMPLES
+Initialize and configure a camera before starting
+.I camv
+in its own window.
+.IP
+.EX
+% nusb/cam 5
+% window -dx 320 -dy 240 \\
+	'label cam
+	cat <<EOF >/dev/cam5.1/ctl
+format 320x240
+fps 30
+backlight-compensation 1
+brightness 20
+contrast 95
+saturation 40
+sharpness 7
+gamma 200
+EOF
+	camv /dev/cam5.1
+\'
+.EE
 .SH SOURCE
 .B /sys/src/cmd/camv.c
-.SH SEE ALSO
+.SH "SEE ALSO"
 .IR nusb (4)
+.SH HISTORY
+.I Camv
+first appeared in 9front (March, 2018).
+.SH BUGS
+Not all available picture settings can be set while
+.IR camv (1)
+is running, some possibly not at all.
--- a/sys/man/1/chdev
+++ b/sys/man/1/chdev
@@ -81,12 +81,6 @@
 bind '#|' /n/pipe
 chdev -r '|'
 .EE
-.SH DIAGNOSTICS
-.I Chdev
-is implemented through writes to
-.BR /dev/drivers ,
-served by
-.IR cons (3).
 .SH SOURCE
 .B /rc/bin/chdev
 .SH "SEE ALSO"
@@ -95,5 +89,11 @@
 .PP
 .IR intro (3),
 .IR cons (3)
+.SH DIAGNOSTICS
+.I Chdev
+is implemented through writes to
+.BR /dev/drivers ,
+served by
+.IR cons (3).
 .SH HISTORY
 Chdev first appeared in 9front (May, 2022).
--- a/sys/man/1/git
+++ b/sys/man/1/git
@@ -291,11 +291,6 @@
 option, the changes are pulled from
 .I upstream
 instead of the configured origin.
-when passed the
-.B -b
-.I branch
-option, it only pulls changes related to
-.IR branch .
 
 .PP
 .B Git/serve
--- a/sys/man/1/ktrans
+++ b/sys/man/1/ktrans
@@ -85,15 +85,6 @@
 .TP
 .B ctrl-v
 Vietnamese Telex input.
-.SH SOURCE
-.B /sys/src/cmd/ktrans
-.SH SEE ALSO
-.IR rio (4)
-.IR kbdfs (8)
-.br
-.IR /sys/src/cmd/ktrans/README.kenji
-.br
-.IR /sys/src/cmd/ktrans/READMEJ.kenji
 .SH EXAMPLES
 To type the following Japanese text:
 
@@ -112,6 +103,15 @@
 where [^\\] and [^l] indicate 'ctl-\\' and 'ctl-l',
 respectively.  See README.kenji for the details of this Japanese input
 method.
+.SH SOURCE
+.B /sys/src/cmd/ktrans
+.SH SEE ALSO
+.IR rio (4)
+.IR kbdfs (8)
+.br
+.IR /sys/src/cmd/ktrans/README.kenji
+.br
+.IR /sys/src/cmd/ktrans/READMEJ.kenji
 .SH BUGS
 .PP
 There is no way to generate the control characters literally.
--- a/sys/man/1/reform
+++ b/sys/man/1/reform
@@ -109,9 +109,14 @@
 .B cputemp
 Exposes the current temperature reading of the CPU.
 .TP
+.B kbdoled
+An image can be displayed on the keyboard OLED by writing a 126x32x1
+uncompressed Plan 9 image (with or without a header).  Zero-length
+write clears the display.
+.TP
 .B light
 Provides a way to control the backlight of the built-in LCD by
-writing \fIlcd [-+]N\fR,
+writing \fIlcd [-+]N\fR or \fIkbd [-+]N\fR,
 where
 .I N
 is expressed in percentage, either as an absolute value (0-100) or
@@ -145,20 +150,21 @@
 reform/shortcuts </dev/kbdtap >/dev/kbdtap
 .EE
 .PP
-.I Super+F1/F2
+.I Hyper+F1/F2
 decreases/increases LCD brightness,
-.I Super+F3/F4
-decreases/increases "master" volume,
-.I Super+Escape
-(un)mutes the audio.
-Optionally, a single step amount can be set with
+.I Hyper+F7/F8/F9
+skips to the previous track, (un)pauses or skips to the next track in
+.IR zuke (1),
+.I Hyper+F10
+(un)mutes the audio,
+.I Hyper+F11/F12
+decreases/increases "master" volume.  Optionally, a single step amount
+can be set with
 .I -l
 for LCD light level (default is 5) and
 .I -v
 for volume (default is 3).  Values can be negative to essentially swap
-.I F1
-with
-.IR F2 .
+the decrement and increment keys.
 .SH SOURCE
 .B /sys/src/cmd/reform
 .SH SEE ALSO
@@ -173,8 +179,9 @@
 .B Light
 was chosen as a shorter alternative to
 .BR brightness .
-In the future it might support controlling keyboard and trackball
-light levels.
+.PP
+Current keyboard light level reading is only an indication, there is
+no way to get the actual value from the keyboard.
 .PP
 Values displayed in the
 .B battery
--- a/sys/man/1/riow
+++ b/sys/man/1/riow
@@ -9,7 +9,7 @@
 .SH DESCRIPTION
 .I riow
 provides keyboard controlling for
-.I rio(1)
+.IR rio (1)
 in the manner of
 .IR i3 ,
 .I sway
@@ -18,13 +18,9 @@
 (see \fIrio\fR(4)) and
 .IR /dev/wsys .
 .SS Running
+Example of running
 .I riow
-filters all key combinations that include
-.B Kmod4
-modifier, so it has to be placed
-.I last
-in the chain of programs using
-.IR /dev/kbdtap :
+with other programs handling input:
 .EX
 	</dev/kbdtap ktrans | \\
 		reform/shortcuts | \\
@@ -56,7 +52,7 @@
 .TP
 .B Kmod4+enter
 Spawn a new
-.IR window(1) .
+.IR window (1).
 .TP
 .B Kmod4+h/j/k/l
 Focus left/down/up/right.
--- a/sys/man/2/rsa
+++ b/sys/man/2/rsa
@@ -62,7 +62,7 @@
 RSApub*	X509toRSApub(uchar *cert, int ncert, char *name, int nname)
 .PP
 .B
-RSApub* X509reqtoRSApub(uchar *req, int nreq, char *name*, int nname)
+RSApub* X509reqtoRSApub(uchar *req, int nreq, char *name, int nname)
 .PP
 .B
 RSApriv*	asn1toRSApriv(uchar *priv, int npriv)
--- a/sys/man/4/nusb
+++ b/sys/man/4/nusb
@@ -145,8 +145,12 @@
 .IR kbdfs (8)
 process them.
 Mouse events are sent to
-.BR /dev/mousein
+.B /dev/mousein
 in the same way.
+A file
+.BI /dev/hid N ctl
+supports setting keyboard repeat and delay setting, the unit is
+milliseconds.
 .SS Joysticks
 .I Joy
 parses data packets from a given endpoint and prints back
@@ -271,11 +275,34 @@
 .IR audio (3).
 .SS Camera devices
 .I Cam
-configures and manages a USB camera device.
-It implements a file system (normally seen under
-.BR /dev ),
-compatible with
-.IR camv (1).
+configures and exposes a USB camera device's capabilities,
+implementing a file system compatible with
+.IR camv (1),
+under a directory named
+.BI cam N [. M ].
+It provides the following files:
+.BR desc ,
+showing all of the device's internal descriptors and their values;
+.BR format ,
+listing admissible image resolutions and framerates;
+.BR ctl ,
+the picture settings control file;
+.BR frame ,
+which captures and outputs a single video frame as an
+.IR image (6)
+file;
+and
+.IR video ,
+streaming video in a preset format, resolution and framerate.
+In particular, reading from the
+.B ctl
+file yields a space-separated list of parameter settings,
+where the second and third columns correspond to key-value pairs,
+and an optional fourth column corresponds to the range of possible numerical values
+(formatted as
+.IR minimum / increment / maximum ).
+The same pairs can be written to the file to configure the camera.
+Currently, only the YUY2 video format is supported.
 .SH SOURCE
 .B /sys/src/cmd/nusb
 .SH "SEE ALSO"
@@ -288,11 +315,15 @@
 .IR uart (3),
 .IR usb (3),
 .IR shr (3),
+.IR image (6),
 .IR nusbrc (8),
 .IR kbdfs (8)
 .SH HISTORY
 .I Joy
 first appeared in 9front (March, 2014).
+.br
+.I Cam
+first appeared in 9front (March, 2018).
 .SH BUGS
 The various device drivers are generic USB drivers and
 may work only for certain devices of each class.
@@ -307,3 +338,9 @@
 signals and some of the extra features are not implemented.
 For Ftdi, only the Sheevaplug and Guruplug have been tried.
 There is support for the EHCI debug port, but it loses bytes.
+.PP
+USB video format settings cannot be changed while
+.IR camv (1)
+is running, and must be set manually by writing them to
+.IR cam (4)'s ctl
+file before starting the viewer.
--- a/sys/man/6/authsrv
+++ b/sys/man/6/authsrv
@@ -593,8 +593,8 @@
 .IR attach (5)).
 Other services, such as
 .IR rcpu (1),
-.IR rexport(1),
-.IR rimport(1)
+.IR rexport (1),
+.IR rimport (1)
 and
 .IR tlssrv (8)
 run
--- a/sys/man/8/acmed
+++ b/sys/man/8/acmed
@@ -144,7 +144,7 @@
 Then the
 .B acct.key
 must be loaded into
-.IR factotum(4).
+.IR factotum (4).
 It is recommended to put
 .B acct.key
 into
@@ -173,7 +173,7 @@
 .PP
 The certificate for the domain can now be fetched.
 This requires
-.IR webfs(4)
+.IR webfs (4)
 to be mounted as the ACME protocol uses HTTP
 to talk to the provider.
 .IP
--- a/sys/man/8/sol
+++ b/sys/man/8/sol
@@ -47,7 +47,7 @@
 flag connects to the VNC port instead.
 .SH EXAMPLE
 Connect to the KVM port with
-.IR vnc(1):
+.IR vnc (1):
 .IP
 .EX
 execnet && vncv 'exec!ip/sol -k host'
--- a/sys/src/cmd/aux/kbdfs/kbdfs.c
+++ b/sys/src/cmd/aux/kbdfs/kbdfs.c
@@ -124,12 +124,16 @@
 Channel *intchan;	/* chan(int) */
 
 /*
- * The codes at 0x79 and 0x7b are produced by the PFU Happy Hacking keyboard.
- * A 'standard' keyboard doesn't produce anything above 0x58.
+ * The codes at 0x79 and 0x7b are for the (無)変換 "(Mu)henkan" keys
+ * used by OADG 109(A) keyboards. The PFU Happy Hacking keyboard
+ * has only one layout and will produce these for otherwise unmarked
+ * keys. The default mappings for the HHKB on other systems map
+ * these to Kdown and Kup. The jp kbmap will instead map these
+ * (along with 0x70) to control characters that ktrans understands.
  */
 Rune kbtab[Nscan] = 
 {
-[0x00]	0,	0x1b,	'1',	'2',	'3',	'4',	'5',	'6',
+[0x00]	0,	Kesc,	'1',	'2',	'3',	'4',	'5',	'6',
 [0x08]	'7',	'8',	'9',	'0',	'-',	'=',	'\b',	'\t',
 [0x10]	'q',	'w',	'e',	'r',	't',	'y',	'u',	'i',
 [0x18]	'o',	'p',	'[',	']',	'\n',	Kctl,	'a',	's',
@@ -149,7 +153,7 @@
 
 Rune kbtabshift[Nscan] =
 {
-[0x00]	0,	0x1b,	'!',	'@',	'#',	'$',	'%',	'^',
+[0x00]	0,	Kesc,	'!',	'@',	'#',	'$',	'%',	'^',
 [0x08]	'&',	'*',	'(',	')',	'_',	'+',	'\b',	'\t',
 [0x10]	'Q',	'W',	'E',	'R',	'T',	'Y',	'U',	'I',
 [0x18]	'O',	'P',	'{',	'}',	'\n',	Kctl,	'A',	'S',
@@ -171,11 +175,11 @@
 {
 [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,	'\n',	Kctl,	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,	Kprint,
+[0x10]	Ksbwd,	Kbrtdn,	0,	0,	0,	0,	0,	0,
+[0x18]	0,	Ksfwd,	Kbrtup,	0,	'\n',	Kctl,	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,
 [0x38]	Kaltgr,	0,	0,	0,	0,	0,	0,	0,
 [0x40]	0,	0,	0,	0,	0,	0,	Kbreak,	Khome,
 [0x48]	Kup,	Kpgup,	0,	Kleft,	0,	Kright,	0,	Kend,
--- a/sys/src/cmd/bar.c
+++ b/sys/src/cmd/bar.c
@@ -200,9 +200,9 @@
 	Binit(&b, 0, OREAD);
 	for(;;){
 		s = Brdstr(&b, '\n', 1);
-		sendp(c, s ? s : strdup(""));
 		if(s == nil)
 			break;
+		sendp(c, s);
 	}
 	Bterm(&b);
 
--- a/sys/src/cmd/cc/pickle.c
+++ b/sys/src/cmd/cc/pickle.c
@@ -172,9 +172,9 @@
 	int n;
 	char *an;
 
-	if(!debug['P'])
+	if(!debug['Z'])
 		return;
-	if(debug['P'] > 1) {
+	if(debug['Z'] > 1) {
 		n = 0;
 		for(i=iostack; i; i=i->link)
 			n++;
@@ -221,9 +221,9 @@
 	Type *t;
 	Sym *s1, *s2;
 
-	if(!debug['P'] || debug['s'])
+	if(!debug['Z'] || debug['s'])
 		return;
-	if(debug['P'] > 1) {
+	if(debug['Z'] > 1) {
 		n = 0;
 		for(i=iostack; i; i=i->link)
 			n++;
--- a/sys/src/cmd/git/compat
+++ b/sys/src/cmd/git/compat
@@ -6,10 +6,8 @@
 args=()
 
 fn cmd_init{
-	while(~ $#* 0){
+	while(! ~ $#* 0){
 		switch($1){
-		case --bare
-			opts=(-b)
 		case -- 
 			# go likes to use these
 		case -*
@@ -19,7 +17,6 @@
 		}
 		shift
 	}
-	ls >[1=2]
 	git/init $opts $args
 }
 
--- a/sys/src/cmd/git/hist
+++ b/sys/src/cmd/git/hist
@@ -7,8 +7,8 @@
 fn dodiff {
 	while(t=`{read}){
 		h=$t(1)
-		o=$gitroot^.git/fs/object/`{git/query $h~}
-		c=$gitroot^.git/fs/object/$h
+		o=$gitfs/object/`{git/query $h~}
+		c=$gitfs/object/$h
 		echo 'Hash:' $h
 		echo -n 'Date: '; date `{walk -em $c/msg}
 		echo -n 'Author: '; cat $c/author
--- a/sys/src/cmd/git/import
+++ b/sys/src/cmd/git/import
@@ -96,8 +96,10 @@
 		}
 		git/walk -fRMA $files
 		if(~ $#nocommit 0){
-			if(hash=`{git/save -n $aname -e $amail -N $name -E $email -m $msg -d $date $parents $files})
+			if(hash=`{git/save -n $aname -e $amail -N $name -E $email -m $msg -d $date $parents $files}){
 				echo $hash > $refpath
+				rm -f .git/index9/removed/$files
+			}
 		}
 		status=''''
 	'
--- a/sys/src/cmd/ip/acmed.c
+++ /dev/null
@@ -1,905 +1,0 @@
-#include <u.h>
-#include <libc.h>
-#include <json.h>
-#include <mp.h>
-#include <libsec.h>
-#include <auth.h>
-#include <authsrv.h>
-
-typedef struct Hdr Hdr;
-
-#pragma varargck	type	"E"	char*
-
-struct Hdr {
-	char	*name;
-	char	*val;
-	int	nval;
-};
-
-#define Keyspec		"proto=rsa service=acme role=sign hash=sha256 acct=%s"
-#define Useragent	"useragent aclient-plan9"
-#define Contenttype	"contenttype application/jose+json"
-#define between(x,min,max)	(((min-1-x) & (x-max-1))>>8)
-int	debug;
-int	(*challengefn)(char*, char*, char*, int*);
-char	*keyspec;
-char	*provider = "https://acme-v02.api.letsencrypt.org/directory"; /* default endpoint */
-char	*challengecmd;
-char	*challengeout;
-char	*keyid;
-char	*epnewnonce;
-char	*epnewacct;
-char	*epneworder;
-char	*eprevokecert;
-char	*epkeychange;
-char	*jwsthumb;
-JSON	*jwskey;
-
-#define dprint(...) if(debug)fprint(2, __VA_ARGS__);
-
-char*
-evsmprint(char *fmt, va_list ap)
-{
-	char *r;
-
-	if((r = vsmprint(fmt, ap)) == nil)
-		abort();
-	return r;
-}
-
-char*
-esmprint(char *fmt, ...)
-{
-	va_list ap;
-	char *r;
-
-	va_start(ap, fmt);
-	r = evsmprint(fmt, ap);
-	va_end(ap);
-	return r;
-}
-
-int
-encurl64chr(int o)
-{
-	int c;
-
-	c  = between(o,  0, 25) & ('A'+o);
-	c |= between(o, 26, 51) & ('a'+(o-26));
-	c |= between(o, 52, 61) & ('0'+(o-52));
-	c |= between(o, 62, 62) & ('-');
-	c |= between(o, 63, 63) & ('_');
-	return c;
-}
-char*
-encurl64(void *in, int n)
-{
-	int lim;
-	char *out, *p;
-
-	lim = 4*n/3 + 5;
-	if((out = malloc(lim)) == nil)
-		abort();
-	enc64x(out, lim, in, n, encurl64chr);
-	if((p = strchr(out, '=')) != nil)
-		*p = 0;
-	return out;
-}
-
-char*
-signRS256(char *hdr, char *prot)
-{
-	uchar hash[SHA2_256dlen];
-	DigestState *s;
-	AuthRpc *rpc;
-	int afd;
-	char *r;
-
-	if((afd = open("/mnt/factotum/rpc", ORDWR|OCEXEC)) < 0)
-		return nil;
-	if((rpc = auth_allocrpc(afd)) == nil){
-		close(afd);
-		return nil;
-	}
-	if(auth_rpc(rpc, "start", keyspec, strlen(keyspec)) != ARok){
-		auth_freerpc(rpc);
-		close(afd);
-		return nil;
-	}
-
-	s = sha2_256((uchar*)hdr, strlen(hdr), nil, nil);
-	s = sha2_256((uchar*)".", strlen("."), nil, s);
-	sha2_256((uchar*)prot, strlen(prot), hash, s);
-
-	if(auth_rpc(rpc, "write", hash, sizeof(hash)) != ARok)
-		sysfatal("sign: write hash: %r");
-	if(auth_rpc(rpc, "read", nil, 0) != ARok)
-		sysfatal("sign: read sig: %r");
-	r = encurl64(rpc->arg, rpc->narg);
-	auth_freerpc(rpc);
-	close(afd);
-	return r;	
-}
-
-/*
- * Reads all available data from an fd.
- * guarantees returned value is terminated.
- */
-static void*
-slurp(int fd, int *n)
-{
-	char *b;
-	int r, sz;
-
-	*n = 0;
-	sz = 32;
-	if((b = malloc(sz)) == nil)
-		abort();
-	while(1){
-		if(*n + 1 == sz){
-			sz *= 2;
-			if((b = realloc(b, sz)) == nil)
-				abort();
-		}
-		r = read(fd, b + *n, sz - *n - 1);
-		if(r == 0)
-			break;
-		if(r == -1){
-			free(b);
-			return nil;
-		}
-		*n += r;
-	}
-	b[*n] = 0;
-	return b;
-}
-		
-static int
-webopen(char *url, char *dir, int ndir)
-{
-	char buf[16];
-	int n, cfd, conn;
-
-	if((cfd = open("/mnt/web/clone", ORDWR|OCEXEC)) == -1)
-		return -1;
-	if((n = read(cfd, buf, sizeof(buf)-1)) == -1)
-		goto Error;
-	buf[n] = 0;
-	conn = atoi(buf);
-
-	if(fprint(cfd, "url %s", url) == -1)
-		goto Error;
-	snprint(dir, ndir, "/mnt/web/%d", conn);
-	return cfd;
-Error:
-	close(cfd);
-	return -1;
-}
-
-static char*
-get(char *url, int *n)
-{
-	char *r, dir[64], path[80];
-	int cfd, dfd;
-
-	r = nil;
-	dfd = -1;
-	if((cfd = webopen(url, dir, sizeof(dir))) == -1)
-		goto Error;
-	snprint(path, sizeof(path), "%s/%s", dir, "body");
-	if((dfd = open(path, OREAD|OCEXEC)) == -1)
-		goto Error;
-	r = slurp(dfd, n);
-Error:
-	if(dfd != -1) close(dfd);
-	if(cfd != -1) close(cfd);
-	return r;
-}
-
-static char*
-post(char *url, char *buf, int nbuf, int *nret, Hdr *h)
-{
-	char *r, dir[64], path[80];
-	int cfd, dfd, hfd, ok;
-
-	r = nil;
-	ok = 0;
-	dfd = -1;
-	hfd = -1;
-	if((cfd = webopen(url, dir, sizeof(dir))) == -1)
-		goto Error;
-	if(write(cfd, Contenttype, strlen(Contenttype)) == -1)
-		goto Error;
-	snprint(path, sizeof(path), "%s/%s", dir, "postbody");
-	if((dfd = open(path, OWRITE|OCEXEC)) == -1)
-		goto Error;
-	if(write(dfd, buf, nbuf) != nbuf)
-		goto Error;
-	close(dfd);
-	snprint(path, sizeof(path), "%s/%s", dir, "body");
-	if((dfd = open(path, OREAD|OCEXEC)) == -1)
-		goto Error;
-	if(h != nil){
-		snprint(path, sizeof(path), "%s/%s", dir, h->name);
-		if((hfd = open(path, OREAD|OCEXEC)) == -1)
-			goto Error;
-		if((h->val = slurp(hfd, &h->nval)) == nil)
-			goto Error;
-	}
-	if((r = slurp(dfd, nret)) == nil)
-		goto Error;
-	ok = 1;
-Error:
-	if(hfd != -1) close(hfd);
-	if(dfd != -1) close(dfd);
-	if(cfd != -1) close(cfd);
-	if(!ok && h != nil){
-		free(h->val);
-		h->val = nil;
-		h->nval = 0;
-	}
-	return r;
-}
-
-static int
-endpoints(void)
-{
-	JSON *j;
-	JSONEl *e;
-	char *s;
-	int n;
-
-	if((s = get(provider, &n)) == nil)
-		sysfatal("get %s: %r", provider);
-	if((j = jsonparse(s)) == nil)
-		sysfatal("parse endpoints: %r");
-	if(j->t != JSONObject)
-		sysfatal("expected object");
-	for(e = j->first; e != nil; e = e->next){
-		if(e->val->t != JSONString)
-			continue;
-		if(strcmp(e->name, "keyChange") == 0)
-			epkeychange = strdup(e->val->s);
-		else if(strcmp(e->name, "newAccount") == 0)
-			epnewacct = strdup(e->val->s);
-		else if(strcmp(e->name, "newNonce") == 0)
-			epnewnonce = strdup(e->val->s);
-		else if(strcmp(e->name, "newOrder") == 0)
-			epneworder = strdup(e->val->s);
-		else if(strcmp(e->name, "revokeCert") == 0)
-			eprevokecert = strdup(e->val->s);
-	}
-	jsonfree(j);
-	free(s);
-	if(epnewnonce==nil|| epnewacct==nil || epneworder==nil
-	|| eprevokecert==nil || epkeychange==nil){
-		sysfatal("missing directory entries");
-		return -1;
-	}
-	return 0;
-}
-
-static char*
-getnonce(void)
-{
-	char *r, dir[64], path[80];
-	int n, cfd, dfd, hfd;
-
-	r = nil;
-	dfd = -1;
-	hfd = -1;
-	if((cfd = webopen(epnewnonce, dir, sizeof(dir))) == -1)
-		goto Error;
-	fprint(cfd, "request HEAD");
-
-	snprint(path, sizeof(path), "%s/%s", dir, "body");
-	if((dfd = open(path, OREAD|OCEXEC)) == -1)
-		goto Error;
-	snprint(path, sizeof(path), "%s/%s", dir, "replaynonce");
-	if((hfd = open(path, OREAD|OCEXEC)) == -1)
-		goto Error;
-	r = slurp(hfd, &n);
-Error:
-	if(hfd != -1)
-		close(hfd);
-	if(dfd != -1)
-		close(dfd);
-	close(cfd);
-	return r;
-}
-
-char*
-jwsenc(char *hdr, char *msg, int *nbuf)
-{
-	char *h, *m, *s, *r;
-
-	h = encurl64(hdr, strlen(hdr));
-	m = encurl64(msg, strlen(msg));
-	s = signRS256(h, m);
-	if(s == nil)
-		return nil;
-
-	r = esmprint(
-		"{\n"
-		"\"protected\": \"%s\",\n"
-		"\"payload\": \"%s\",\n"
-		"\"signature\": \"%s\"\n"
-		"}\n",
-		h, m, s);
-	*nbuf = strlen(r);
-	free(h);
-	free(m);
-	free(s);
-
-	return r;
-}
-
-char*
-jwsheader(char *url)
-{
-	char *nonce;
-
-	if((nonce = getnonce()) == nil)
-		sysfatal("get nonce: %r");
-	return esmprint(
-		"{"
-		"\"alg\": \"RS256\","
-		"\"nonce\": \"%E\","
-		"\"kid\": \"%E\","
-		"\"url\": \"%E\""
-		"}",
-		nonce, keyid, url);
-}
-
-char*
-jwsrequest(char *url, int *nresp, Hdr *h, char *fmt, ...)
-{
-	char *hdr, *msg, *req, *resp;
-	int nreq;
-	va_list ap;
-
-	va_start(ap, fmt);
-	hdr = jwsheader(url);
-	msg = evsmprint(fmt, ap);
-	req = jwsenc(hdr, msg, &nreq);
-	dprint("req=\"%s\"\n", req);
-	resp = post(url, req, nreq, nresp, h);
-	free(hdr);
-	free(req);
-	free(msg);
-	va_end(ap);
-	dprint("resp=%s\n", resp);
-	return resp;
-}
-
-static void
-mkaccount(char *addr)
-{
-	char *nonce, *hdr, *msg, *req, *resp;
-	int nreq, nresp;
-	Hdr loc = { "location" };
-
-	if((nonce = getnonce()) == nil)
-		sysfatal("get nonce: %r");
-	hdr = esmprint(
-		"{"
-		"\"alg\": \"RS256\","
-		"\"jwk\": %J,"
-		"\"nonce\": \"%E\","
-		"\"url\": \"%E\""
-		"}",
-		jwskey, nonce, epnewacct);
-	msg = esmprint(
-		"{"
-		"\"termsOfServiceAgreed\": true,"
-		"\"contact\": [\"mailto:%E\"]"
-		"}",
-		addr);
-	free(nonce);
-	if((req = jwsenc(hdr, msg, &nreq)) == nil)
-		sysfatal("failed to sign: %r");
-	dprint("req=\"%s\"\n", req);
-
-	if((resp = post(epnewacct, req, nreq, &nresp, &loc)) == nil)
-		sysfatal("failed req: %r");
-	dprint("resp=%s, loc=%s\n", resp, loc.val);
-	keyid = loc.val;
-}
-
-static JSON*
-submitorder(char **dom, int ndom, Hdr *hdr)
-{
-	char *req, *resp, *sep, rbuf[8192];
-	int nresp, i;
-	JSON *r;
-
-	sep = "";
-	req = seprint(rbuf, rbuf+sizeof(rbuf),
-		"{"
-		"  \"identifiers\": [");
-	for(i = 0; i < ndom; i++){
-		req = seprint(req, rbuf+sizeof(rbuf),
-			"%s{"
-			"  \"type\": \"dns\","
-			"  \"value\": \"%E\""
-			"}",
-			sep, dom[i]);
-		sep = ",";
-	}
-	req = seprint(req, rbuf+sizeof(rbuf),
-		"  ],"
-		"  \"wildcard\": false"
-		"}");
-	if(req - rbuf < 2)
-		sysfatal("truncated order");
-	resp = jwsrequest(epneworder, &nresp, hdr, "%s", rbuf);
-	if(resp == nil)
-		sysfatal("submit order: %r");
-	if((r = jsonparse(resp)) == nil)
-		sysfatal("parse order: %r");
-	free(resp);
-	return r;
-}
-
-static void
-hashauthbuf(char *buf, int nbuf)
-{
-	uchar hash[SHA2_256dlen];
-	char *enc;
-
-	sha2_256((uchar*)buf, strlen(buf), hash, nil);
-	if((enc = encurl64(hash, sizeof(hash))) == nil)
-		sysfatal("hashbuf: %r");
-	if(snprint(buf, nbuf, "%s", enc) != strlen(enc))
-		sysfatal("hashbuf: buffer too small, truncated");
-	free(enc);
-}
-
-static int
-runchallenge(char *ty, char *dom, char *tok, int *matched)
-{
-	char auth[1024];
-	Waitmsg *w;
-	int pid;
-
-	snprint(auth, sizeof(auth), "%s.%s", tok, jwsthumb);
-	if(strcmp(ty, "dns-01") == 0)
-		hashauthbuf(auth, sizeof(auth));
-
-	pid = fork();
-	switch(pid){
-	case -1:
-		return -1;
-	case 0:
-		dup(1, 2);
-		execl(challengecmd, challengecmd, ty, dom, tok, auth, nil);
-		sysfatal("%s: %r", challengecmd);
-	}
-
-	while((w = wait()) != nil){
-		if(w->pid != pid){
-			free(w);
-			continue;
-		}
-		if(w->msg[0] == '\0'){
-			free(w);
-			*matched = 1;
-			return 0;
-		}
-		werrstr("%s", w->msg);
-		free(w);
-		return -1;
-	}
-	return -1;
-}
-
-static int
-httpchallenge(char *ty, char *, char *tok, int *matched)
-{
-	char path[1024];
-	int fd, r;
-
-	if(strcmp(ty, "http-01") != 0)
-		return -1;
-	*matched = 1;
-
-	snprint(path, sizeof(path), "%s/%s", challengeout, tok);
-	if((fd = create(path, OWRITE|OCEXEC, 0666)) == -1)
-		return -1;
-	r = fprint(fd, "%s.%s\n", tok, jwsthumb);
-	close(fd);
-	return r;
-}
-
-static int
-dnschallenge(char *ty, char *dom, char *tok, int *matched)
-{
-	char auth[1024];
-	int fd;
-
-	if(strcmp(ty, "dns-01") != 0)
-		return -1;
-	*matched = 1;
-
-	snprint(auth, sizeof(auth), "%s.%s", tok, jwsthumb);
-	hashauthbuf(auth, sizeof(auth));
-
-	if((fd = create(challengeout, OWRITE|OCEXEC, 0666)) == -1){
-		werrstr("could not create challenge: %r");
-		return -1;
-	}
-	if(fprint(fd,"dom=_acme-challenge.%s soa=\n\ttxt=\"%s\"\n", dom, auth) == -1){
-		werrstr("could not write challenge: %r");
-		close(fd);
-		return -1;
-	}
-	close(fd);
-
-	if((fd = open("/net/dns", OWRITE|OCEXEC)) == -1){
-		werrstr("could not open dns ctl: %r");
-		return -1;
-	}
-	if(fprint(fd, "refresh") == -1){
-		werrstr("could not write dns refresh: %r");
-		close(fd);
-		return -1;
-	}
-	close(fd);
-
-	return 0;
-}
-
-static int
-challenge(JSON *j, char *authurl, JSON *id, char *dom[], int ndom, int *matched)
-{
-	JSON *dn, *ty, *url, *tok, *poll, *state;
-	char *resp;
-	int i, nresp;
-
-	if((dn = jsonbyname(id, "value")) == nil)
-		return -1;
-	if(dn->t != JSONString)
-		return -1;
-
-	/* make sure the identifier matches the csr */
-	for(i = 0; i < ndom; i++){
-		if(cistrcmp(dom[i], dn->s) == 0)
-			break;
-	}
-	if(i >= ndom){
-		werrstr("unknown challenge identifier '%s'", dn->s);
-		return -1;
-	}
-
-	if((ty = jsonbyname(j, "type")) == nil)
-		return -1;
-	if((url = jsonbyname(j, "url")) == nil)
-		return -1;
-	if((tok = jsonbyname(j, "token")) == nil)
-		return -1;
-
-	if(ty->t != JSONString || url->t != JSONString || tok->t != JSONString)
-		return -1;
-
-	dprint("trying challenge %s\n", ty->s);
-	if(challengefn(ty->s, dn->s, tok->s, matched) == -1){
-		dprint("challengefn failed: %r\n");
-		return -1;
-	}
-
-	if((resp = jwsrequest(url->s, &nresp, nil, "{}")) == nil)
-		sysfatal("challenge: post %s: %r", url->s);
-	free(resp);
-
-	for(i = 0; i < 60; i++){
-		sleep(1000);
-		if((resp = jwsrequest(authurl, &nresp, nil, "")) == nil)
-			sysfatal("challenge: post %s: %r", url->s);
-		if((poll = jsonparse(resp)) == nil){
-			free(resp);
-			return -1;
-		}
-		if((state = jsonbyname(poll, "status")) != nil && state->t == JSONString){
-			if(strcmp(state->s, "valid") == 0){
-				jsonfree(poll);
-				return 0;
-			}
-			else if(strcmp(state->s, "pending") != 0){
-				fprint(2, "error: %J", poll);
-				werrstr("status '%s'", state->s);
-				jsonfree(poll);
-				return -1;
-			}
-		}
-		jsonfree(poll);	
-	}
-	werrstr("timeout");
-	return -1;
-}
-
-static int
-dochallenges(char *dom[], int ndom, JSON *order)
-{
-	JSON *chals, *j, *cl, *id;
-	JSONEl *ae, *ce;
-	int nresp, matched;
-	char *resp;
-
-	if((j = jsonbyname(order, "authorizations")) == nil){
-		werrstr("parse response: missing authorizations");
-		return -1;
-	}
-	if(j->t != JSONArray){
-		werrstr("parse response: authorizations must be array");
-		return -1;
-	}
-	for(ae = j->first; ae != nil; ae = ae->next){
-		if(ae->val->t != JSONString){
-			werrstr("challenge: auth must be url");
-			return -1;
-		}
-		if((resp = jwsrequest(ae->val->s, &nresp, nil, "")) == nil){
-			werrstr("challenge: request %s: %r", ae->val->s);
-			return -1;
-		}
-		if((chals = jsonparse(resp)) == nil){
-			werrstr("invalid challenge: %r");
-			return -1;
-		}
-		if((id = jsonbyname(chals, "identifier")) == nil){
-			werrstr("missing identifier");
-			jsonfree(chals);
-			return -1;
-		}
-		if((cl = jsonbyname(chals, "challenges")) == nil){
-			werrstr("missing challenge");
-			jsonfree(chals);
-			return -1;
-		}
-		matched = 0;
-		for(ce = cl->first; ce != nil; ce = ce->next){
-			if(challenge(ce->val, ae->val->s, id, dom, ndom, &matched) == 0)
-				break;
-			if(matched)
-				werrstr("could not complete challenge: %r");
-		}
-		if(!matched)
-			sysfatal("no matching auth type");
-		jsonfree(chals);
-		free(resp);
-	}
-	return 0;
-}
-
-static int
-submitcsr(JSON *order, char *b64csr)
-{
-	char *resp;
-	int nresp;
-	JSON *j;
-
-	if((j = jsonbyname(order, "finalize")) == nil)
-		sysfatal("parse response: missing authorizations");
-	if(j->t != JSONString)
-		werrstr("parse response: finalizer must be string");
-	if((resp = jwsrequest(j->s, &nresp, nil, "{\"csr\":\"%E\"}", b64csr)) == nil)
-		sysfatal("submit csr: %r");
-	free(resp);
-	return 0;
-}
-
-static int
-fetchcert(char *url)
-{
-	JSON *cert, *poll, *state;
-	int i, r, nresp;
-	char *resp;
-
-	poll = nil;
-	for(i = 0; i < 60; i++){
-		sleep(1000);
-		if((resp = jwsrequest(url, &nresp, nil, "")) == nil)
-			return -1;
-		if((poll = jsonparse(resp)) == nil){
-			free(resp);
-			return -1;
-		}
-		free(resp);
-		if((state = jsonbyname(poll, "status")) != nil && state->t == JSONString){
-			if(strcmp(state->s, "valid") == 0)
-				break;
-			else if(strcmp(state->s, "pending") != 0 && strcmp(state->s, "processing") != 0){
-				fprint(2, "error: %J", poll);
-				werrstr("invalid request: %s", state->s);
-				jsonfree(poll);
-				return -1;
-				
-			}
-		}
-		jsonfree(poll);
-	}
-	if(poll == nil){
-		werrstr("timed out");
-		return -1;
-	}
-	if((cert = jsonbyname(poll, "certificate")) == nil || cert->t != JSONString){
-		werrstr("missing cert url in response");
-		jsonfree(poll);
-		return -1;
-	}
-	if((resp = jwsrequest(cert->s, &nresp, nil, "")) == nil){
-		jsonfree(poll);
-		return -1;
-	}
-	jsonfree(poll);
-	r = write(1, resp, nresp);
-	free(resp);
-	if(r != nresp)
-		return -1;
-	return 0;
-}
-
-static void
-getcert(char *csrpath)
-{
-	char *csr, *dom[64], name[2048];
-	uchar *der;
-	int nder, ndom, fd;
-	RSApub *rsa;
-	Hdr loc = { "location" };
-	JSON *o;
-
-	if((fd = open(csrpath, OREAD|OCEXEC)) == -1)
-		sysfatal("open %s: %r", csrpath);
-	if((der = slurp(fd, &nder)) == nil)
-		sysfatal("read %s: %r", csrpath);
-	if((rsa = X509reqtoRSApub(der, nder, name, sizeof(name))) == nil)
-		sysfatal("decode csr: %r");
-	if((csr = encurl64(der, nder)) == nil)
-		sysfatal("encode %s: %r", csrpath);
-	if((ndom = getfields(name, dom, nelem(dom), 1, ", ")) == nelem(dom))
-		sysfatal("too man domains");
-	rsapubfree(rsa);
-	close(fd);
-	free(der);
-
-	if((o = submitorder(dom, ndom, &loc)) == nil)
-		sysfatal("order: %r");
-	if(dochallenges(dom, ndom, o) == -1)
-		sysfatal("challenge: %r");
-	if(submitcsr(o, csr) == -1)
-		sysfatal("signing cert: %r");
-	if(fetchcert(loc.val) == -1)
-		sysfatal("saving cert: %r");
-	free(csr);
-}
-
-static int
-Econv(Fmt *f)
-{
-	char *s;
-	Rune r;
-	int w;
-
-	w = 0;
-	s = va_arg(f->args, char*);
-	while(*s){
-		s += chartorune(&r, s);
-		if(r == '\\' || r == '\"')
-			w += fmtrune(f, '\\');
-		w += fmtrune(f, r);
-	}
-	return w;
-}
-
-static int
-loadkey(char *path)
-{
-	uchar h[SHA2_256dlen];
-	char key[8192];
-	JSON *j, *e, *kty, *n;
-	DigestState *ds;
-	int fd, nr;
-
-	if((fd = open(path, OREAD|OCEXEC)) == -1)
-		return -1;
-	nr = readn(fd, key, sizeof(key));
-	close(fd);
-	if(nr == -1)
-		return -1;
-	key[nr] = 0;
-
-	if((j = jsonparse(key)) == nil)
-		return -1;
-	if((e = jsonbyname(j, "e")) == nil || e->t != JSONString)
-		return -1;
-	if((kty = jsonbyname(j, "kty")) == nil || kty->t != JSONString)
-		return -1;
-	if((n = jsonbyname(j, "n")) == nil || n->t != JSONString)
-		return -1;
-
-	ds = sha2_256((uchar*)"{\"e\":\"", 6, nil, nil);
-	ds = sha2_256((uchar*)e->s, strlen(e->s), nil, ds);
-	ds = sha2_256((uchar*)"\",\"kty\":\"", 9, nil, ds);
-	ds = sha2_256((uchar*)kty->s, strlen(kty->s), nil, ds);
-	ds = sha2_256((uchar*)"\",\"n\":\"", 7, nil, ds);
-	ds = sha2_256((uchar*)n->s, strlen(n->s), nil, ds);
-	sha2_256((uchar*)"\"}", 2, h, ds);
-	jwskey = j;
-	jwsthumb = encurl64(h, sizeof(h));
-	return 0;
-}
-
-static void
-usage(void)
-{
-	fprint(2, "usage: %s [-a acctkey] [-e cmd | -o chalout -t type] [-p provider] acct csr\n", argv0);
-	exits("usage");
-}
-
-void
-main(int argc, char **argv)
-{
-	char *acctkey, *ct, *co;
-
-	JSONfmtinstall();
-	fmtinstall('E', Econv);
-
-	ct = nil;
-	co = nil;
-	acctkey = nil;
-	ARGBEGIN{
-	case 'd':
-		debug++;
-		break;
-	case 'a':
-		acctkey = EARGF(usage());
-		break;
-	case 'e':
-		challengecmd = EARGF(usage());
-		break;
-	case 'o':
-		co = EARGF(usage());
-		break;
-	case 't':
-		ct = EARGF(usage());
-		break;
-	case 'p':
-		provider = EARGF(usage());
-		break;
-	default:
-		usage();
-		break;
-	}ARGEND;
-
-	if(challengecmd != nil){
-		if(ct != nil || co != nil)
-			usage();
-		challengeout = "/dev/null";
-		challengefn = runchallenge;
-	}else if(ct == nil || strcmp(ct, "http") == 0){
-		challengeout = (co != nil) ? co : "/usr/web/.well-known/acme-challenge";
-		challengefn = httpchallenge;
-	}else if(strcmp(ct, "dns") == 0){
-		challengeout = (co != nil) ? co : "/lib/ndb/dnschallenge";
-		challengefn = dnschallenge;
-	}else {
-		sysfatal("unknown challenge type '%s'", ct);
-	}
-
-	if(argc != 2)
-		usage();
-
-	if(acctkey == nil)
-		acctkey = esmprint("/sys/lib/tls/acmed/%s.pub", argv[0]);
-	if((keyspec = smprint(Keyspec, argv[0])) == nil)
-		sysfatal("smprint: %r");
-	if(loadkey(acctkey) == -1)
-		sysfatal("load key: %r");
-
-	if(endpoints() == -1)
-		sysfatal("endpoints: %r");
-	mkaccount(argv[0]);
-	getcert(argv[1]);
-	exits(nil);
-}
--- a/sys/src/cmd/ktrans/main.c
+++ b/sys/src/cmd/ktrans/main.c
@@ -2,6 +2,7 @@
 #include <libc.h>
 #include <ctype.h>
 #include <bio.h>
+#include <plumb.h>
 #include <thread.h>
 #include "hash.h"
 
@@ -275,32 +276,28 @@
 	return hmapget(*h, s, m);
 }
 
-typedef struct Msg Msg;
-struct Msg {
-	char code;
-	char buf[64];
-};
-static Channel *dictch;
-static Channel *output;
-static Channel *input;
-static char  backspace[64];
+enum   { Msgsize = 64 };
+static Channel	*dictch;
+static Channel	*output;
+static Channel	*input;
+static char	backspace[Msgsize];
 
 static int
 emitutf(Channel *out, char *u, int nrune)
 {
-	Msg m;
+	char b[Msgsize];
 	char *e;
 
-	m.code = 'c';
-	e = pushutf(m.buf, m.buf + sizeof m.buf, u, nrune);
-	send(out, &m);
-	return e - m.buf;
+	b[0] = 'c';
+	e = pushutf(b+1, b + Msgsize - 1, u, nrune);
+	send(out, b);
+	return e - b;
 }
 
 static void
 dictthread(void*)
 {
-	Msg m;
+	char m[Msgsize];
 	Rune r;
 	int n;
 	char *p;
@@ -325,8 +322,8 @@
 	resetstr(&last, &line, &okuri, nil);
 
 	threadsetname("dict");
-	while(recv(dictch, &m) != -1){
-		for(p = m.buf; *p; p += n){
+	while(recv(dictch, m) != -1){
+		for(p = m+1; *p; p += n){
 			n = chartorune(&r, p);
 			if(r != ''){
 				if(selected >= 0){
@@ -441,7 +438,7 @@
 	}
 }
 
-int
+static int
 telexlkup(Str *line, Str *out)
 {
 	Map lkup;
@@ -454,7 +451,6 @@
 	if(hmapget(telex, buf, &lkup) < 0)
 		return -1;
 
-	assert(lkup.leadstomore == 1);
 	if(utflen(line->b) < 2)
 		return 2;
 
@@ -480,7 +476,7 @@
 keythread(void*)
 {
 	int lang;
-	Msg m;
+	char m[Msgsize];
 	Map lkup;
 	char *p;
 	int n, ln, rn;
@@ -487,9 +483,7 @@
 	Rune r;
 	char peek[UTFmax+1];
 	Str line, tbuf;
-	int mode;
 
-	mode = 0;
 	peek[0] = lang = deflang;
 	resetstr(&line, nil);
 	if(lang == LangJP || lang == LangZH)
@@ -496,18 +490,18 @@
 		emitutf(dictch, peek, 1);
 
 	threadsetname("keytrans");
-	while(recv(input, &m) != -1){
-		if(m.code == 'z'){
+	while(recv(input, m) != -1){
+		if(m[0] == 'z'){
 			emitutf(dictch, "", 1);
 			resetstr(&line, nil);
 			continue;
 		}
-		if(m.code != 'c'){
-			send(output, &m);
+		if(m[0] != 'c'){
+			send(output, m);
 			continue;
 		}
 
-		for(p = m.buf; *p; p += n){
+		for(p = m+1; *p; p += n){
 			n = chartorune(&r, p);
 			if(checklang(&lang, r)){
 				emitutf(dictch, "", 1);
@@ -527,12 +521,8 @@
 					resetstr(&line, nil);
 					continue;
 				}
-				if(lang == LangJP && isupper(*p)){
+				if(lang == LangJP && isupper(*p))
 					*p = tolower(*p);
-					mode++;
-				} else {
-					mode = 0;
-				}
 			}
 
 			emitutf(output, p, 1);
@@ -590,10 +580,10 @@
 static int kbdin;
 static int kbdout;
 
-void
+static void
 kbdtap(void*)
 {
-	Msg msg;
+	char m[Msgsize];
 	char buf[128];
 	char *p, *e;
 	int n;
@@ -600,14 +590,12 @@
 
 	threadsetname("kbdtap");
 	for(;;){
-Drop:
+	Drop:
 		n = read(kbdin, buf, sizeof buf);
 		if(n < 0)
 			break;
 		for(p = buf; p < buf+n;){
-			msg.code = p[0];
-			p++;
-			switch(msg.code){
+			switch(*p){
 			case 'c': case 'k': case 'K':
 			case 'z':
 				break;
@@ -614,38 +602,73 @@
 			default:
 				goto Drop;
 			}
-			e = utfecpy(msg.buf, msg.buf + sizeof msg.buf, p);
-			p += e - msg.buf;
+			*m = *p++;
+			e = utfecpy(m+1, m + Msgsize - 1, p);
+			p += e - m;
 			p++;
-			if(send(input, &msg) == -1)
+			if(send(input, m) == -1)
 				return;
 		}
 	}
 }
 
-void
+static void
 kbdsink(void*)
 {
-	Msg m;
+	char in[Msgsize];
+	char out[Msgsize];
 	char *p;
+	int n;
 	Rune rn;
 
+	out[0] = 'c';
 	threadsetname("kbdsink");
-	while(recv(output, &m) != -1){
-		if(m.code != 'c'){
-			fprint(kbdout, "%c%s", m.code, m.buf);
+	while(recv(output, in) != -1){
+		if(in[0] != 'c'){
+			if(write(kbdout, in, strlen(in)+1) < 0)
+				break;
 			continue;
 		}
-		p = m.buf;
-		for(;;){
-			p += chartorune(&rn, p);
+
+		for(p = in+1; *p; p += n){
+			n = chartorune(&rn, p);
 			if(rn == Runeerror || rn == '\0')
 				break;
-			fprint(kbdout, "c%C", rn);
+			memmove(out+1, p, n);
+			out[1+n] = '\0';
+			if(write(kbdout, out, 1+n+1) < 0)
+				break;
 		}
 	}
 }
 
+static int plumbfd;
+
+static void
+plumbproc(void*)
+{
+	char m[Msgsize];
+	Plumbmsg *p;
+
+	threadsetname("plumbproc");
+	for(; p = plumbrecv(plumbfd); plumbfree(p)){
+		if(p->ndata > sizeof m - 1)
+			continue;
+		memmove(m, p->data, p->ndata);
+		m[p->ndata] = '\0';
+
+		m[1] = parselang(m);
+		if(m[1] == -1)
+			continue;
+		m[0] = 'c';
+		m[2] = '\0';
+
+		if(send(input, m) == -1)
+			break;
+	}
+	plumbfree(p);
+}
+
 void
 usage(void)
 {
@@ -704,9 +727,13 @@
 	hangul 	= openmap("/lib/ktrans/hangul.map");
 	telex	= openmap("/lib/ktrans/telex.map");
 
-	dictch 	= chancreate(sizeof(Msg), 0);
-	input 	= chancreate(sizeof(Msg), 0);
-	output 	= chancreate(sizeof(Msg), 0);
+	dictch 	= chancreate(Msgsize, 0);
+	input 	= chancreate(Msgsize, 0);
+	output 	= chancreate(Msgsize, 0);
+
+	plumbfd = plumbopen("lang", OREAD);
+	if(plumbfd >= 0)
+		proccreate(plumbproc, nil, mainstacksize);
 
 	proccreate(kbdtap, nil, mainstacksize);
 	proccreate(kbdsink, nil, mainstacksize);
--- a/sys/src/cmd/nusb/disk/disk.c
+++ b/sys/src/cmd/nusb/disk/disk.c
@@ -155,10 +155,10 @@
 	part = &lun->part[0];
 
 	fmtstrinit(&fmt);
-	fmtprint(&fmt, "dev %s\n", dev->dir);
-	fmtprint(&fmt, "lun %zd\n", lun - &ums->lun[0]);
 	if(lun->flags & Finqok)
 		fmtprint(&fmt, "inquiry %s\n", lun->inq);
+	fmtprint(&fmt, "dev %s\n", dev->dir);
+	fmtprint(&fmt, "lun %zd\n", lun - &ums->lun[0]);
 	if(lun->blocks > 0)
 		fmtprint(&fmt, "geometry %llud %ld\n", lun->blocks, lun->lbsize);
 	for (p = &part[Qdata+1]; p < &part[Qmax]; p++)
--- a/sys/src/cmd/nusb/kb/hid.h
+++ b/sys/src/cmd/nusb/kb/hid.h
@@ -4,6 +4,7 @@
 enum {
 
 	Stack = 32 * 1024,
+	Nkey = 64,
 
 	/* HID class subclass protocol ids */
 	PtrCSP		= 0x020103,	/* mouse.boot.hid */
--- a/sys/src/cmd/nusb/kb/kb.c
+++ b/sys/src/cmd/nusb/kb/kb.c
@@ -13,21 +13,21 @@
 
 #include <u.h>
 #include <libc.h>
+#include <ctype.h>
+#include <fcall.h>
 #include <thread.h>
+#include <9p.h>
 #include "usb.h"
 #include "hid.h"
 
 enum
 {
-	Awakemsg=0xdeaddead,
+	Awakemsg = 0xdeaddead,
 	Diemsg = 0xbeefbeef,
+	Rawon = 0x0defaced,
 };
 
-enum
-{
-	Kbdelay = 500,
-	Kbrepeat = 100,
-};
+char user[] = "kb";
 
 typedef struct Hiddev Hiddev;
 struct Hiddev
@@ -78,7 +78,7 @@
 	Hidslot	s[16];
 
 	int	nk;
-	uchar	k[64];
+	ushort	k[Nkey];
 
 	int	o;
 	uchar	*e;
@@ -92,6 +92,7 @@
 	/* Scan codes (see kbd.c) */
 	SCesc1		= 0xe0,		/* first of a 2-character sequence */
 	SCesc2		= 0xe1,
+	Consumer	= 0x100,	/* the key is on consumer page */
 	Keyup		= 0x80,		/* flag bit */
 	Keymask		= 0x7f,		/* regular scan code bits */
 };
@@ -101,12 +102,12 @@
  */
 #define isext(sc)	((sc) >= 0x80)
 
+static char sctab[2*256] =
+{
 /*
  * key code to scan code; for the page table used by
  * the logitech bluetooth keyboard.
  */
-static char sctab[256] = 
-{
 [0x00]	0x0,	0x0,	0x0,	0x0,	0x1e,	0x30,	0x2e,	0x20,
 [0x08]	0x12,	0x21,	0x22,	0x23,	0x17,	0x24,	0x25,	0x26,
 [0x10]	0x32,	0x31,	0x18,	0x19,	0x10,	0x13,	0x1f,	0x14,
@@ -124,7 +125,7 @@
 [0x70]	0xf8,	0xf9,	0xfa,	0xfb,	0x0,	0x0,	0x0,	0x0,
 [0x78]	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0xf1,
 [0x80]	0xf3,	0xf2,	0x0,	0x0,	0x0,	0xfc,	0x0,	0x0,
-[0x88]	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,
+[0x88]	0x70,	0x0,	0x79,	0x7b,	0x0,	0x0,	0x0,	0x0,
 [0x90]	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,
 [0x98]	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,
 [0xa0]	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,
@@ -135,10 +136,43 @@
 [0xc8]	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,
 [0xd0]	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,
 [0xd8]	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,
-[0xe0]	0x1d,	0x2a,	0x38,	0xdb,	0xe1,	0x36,	0xb8,	0xfe,
+[0xe0]	0x1d,	0x2a,	0x38,	0xdb,	0x9d,	0x36,	0xb8,	0xfe,
 [0xe8]	0x0,	0x0,	0x0,	0x0,	0x0,	0xf3,	0xf2,	0xf1,
 [0xf0]	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,
 [0xf8]	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,
+/* consumer page to scan code */
+[0x100]	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,
+[0x108]	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,
+[0x110]	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,
+[0x118]	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,
+[0x120]	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,
+[0x128]	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,
+[0x130]	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,
+[0x138]	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,
+[0x140]	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,
+[0x148]	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,
+[0x150]	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,
+[0x158]	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,
+[0x160]	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,
+[0x168]	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x9a,
+[0x170]	0x91,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,
+[0x178]	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,
+[0x180]	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,
+[0x188]	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,
+[0x190]	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,
+[0x198]	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,
+[0x1a0]	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,
+[0x1a8]	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,
+[0x1b0]	0x0,	0x0,	0x0,	0x0,	0x0,	0x99,	0x90,	0x0,
+[0x1b8]	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,
+[0x1c0]	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,
+[0x1c8]	0x0,	0x0,	0x0,	0x0,	0x0,	0xa2,	0x0,	0x0,
+[0x1d0]	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,
+[0x1d8]	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,
+[0x1e0]	0x0,	0x0,	0xa0,	0x0,	0x0,	0x0,	0x0,	0x0,
+[0x1e8]	0x0,	0xb0,	0xae,	0x0,	0x0,	0x0,	0x0,	0x0,
+[0x1f0]	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,
+[0x1f8]	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,	0x0,
 };
 
 static uchar kbdbootrep[] = {
@@ -165,6 +199,9 @@
 };
 
 static int debug = 0;
+static int kbdelay = 500;
+static int kbrepeat = 100;
+static int havekbd;
 
 static int
 signext(int v, int bits)
@@ -377,12 +414,6 @@
 	return usbcmd(f->dev, Rh2d|Rclass|Riface, Setproto, proto, iface->id, nil, 0);
 }
 
-static int
-setleds(Hiddev* f, int, uchar leds)
-{
-	return usbcmd(f->dev, Rh2d|Rclass|Riface, Setreport, Reportout, 0, &leds, 1);
-}
-
 static void
 hdfree(Hiddev *f)
 {
@@ -498,9 +529,9 @@
 			continue;
 		}
 		sc = l & 0xff;
-		t = Kbdelay;
+		t = kbdelay;
 		if(alt(a) == 1){
-			t = Kbrepeat;
+			t = kbrepeat;
 			while(alt(a) == 1)
 				putscan(f, sc, 0);
 		}
@@ -528,7 +559,7 @@
 {
 	Hidreport *p = a;
 	Hidslot *s = &p->s[p->ns];
-	int v, m;
+	int v, m, cp;
 
 	switch(t){
 	case Input:
@@ -571,11 +602,11 @@
 	if(debug > 1)
 		fprint(2, "hidparse: t=%x f=%x usage=%x v=%x\n", t, f, l[Usage], v);
 
-	if((l[Usage]>>16) == 0x07){	/* keycode */
+	if((cp = ((l[Usage]>>16) == 0x0c)) || (l[Usage]>>16) == 0x07){	/* consumer/keycode */
 		if((f & (Fvar|Farray)) == Fvar)
 			if(v != 0) v = l[Usage] & 0xFF;
 		if(p->nk < nelem(p->k) && v != 0)
-			p->k[p->nk++] = v;
+			p->k[p->nk++] = v | cp*Consumer;
 		return;
 	}
 
@@ -677,12 +708,23 @@
 	close(fd);
 }
 
+static ushort *
+keykey(ushort *a, ushort k, int n)
+{
+	while(n > 0){
+		if(*a++ == k)
+			return a-1;
+		n--;
+	}
+	return nil;
+}
+
 static void
 readerproc(void* a)
 {
 	char	err[ERRMAX], mbuf[80];
-	uchar	lastk[64], uk, dk;
-	int	i, c, nerrs, bpress, lastb, nlastk;
+	ushort	lastk[Nkey], uks[Nkey], dks[Nkey], nuks, ndks;
+	int	i, c, nerrs, bpress, lastb, nlastk, stopped;
 	int	abs, x, y, z, b;
 	Hidreport p;
 	Hidslot lasts[nelem(p.s)], *s, *l;
@@ -730,7 +772,7 @@
 			if(debug){
 				fprint(2, "kbd: ");
 				for(i = 0; i < p.nk; i++)
-					fprint(2, "%#2.2ux ", p.k[i]);
+					fprint(2, "%#4.4ux ", p.k[i]);
 				fprint(2, "\n");
 			}
 
@@ -745,25 +787,36 @@
 				proccreate(repeatproc, f, Stack);
 			}
 	
-			dk = uk = 0;
-			for(i=0; i<nlastk; i++){
-				if(memchr(p.k, lastk[i], p.nk) == nil){
-					uk = sctab[lastk[i]];
-					putscan(f, uk, Keyup);
-				}
+			/* collect key presses/releases */
+			for(i=nuks=0; i<nlastk; i++){
+				if(keykey(p.k, lastk[i], p.nk) == nil)
+					uks[nuks++] = sctab[lastk[i]];
 			}
-			for(i=0; i<p.nk; i++){
-				if(memchr(lastk, p.k[i], nlastk) == nil){
-					dk = sctab[p.k[i]];
-					putscan(f, dk, 0);
+			for(i=ndks=0; i<p.nk; i++){
+				if(keykey(lastk, p.k[i], nlastk) == nil)
+					dks[ndks++] = sctab[p.k[i]];
+			}
+
+			/*
+			 * stop the repeats first to avoid race condition when
+			 * the key is released but the repeat happens right after
+			 */
+			for(stopped = i = 0; i < nuks; i++){
+				if(ndks == 0 || keykey(dks, uks[i], ndks) != nil){
+					stoprepeat(f);
+					stopped = 1;
+					break;
 				}
 			}
-			if(uk != 0 && (dk == 0 || dk == uk))
-				stoprepeat(f);
-			else if(dk != 0)
-				startrepeat(f, dk);
+			for(i = 0; i < nuks; i++)
+				putscan(f, uks[i], Keyup);
+			for(i = 0; i < ndks; i++)
+				putscan(f, dks[i], 0);
 
-			memmove(lastk, p.k, nlastk = p.nk);
+			if(stopped == 0 && ndks > 0)
+				startrepeat(f, dks[ndks-1]);
+
+			memmove(lastk, p.k, (nlastk = p.nk)*sizeof(lastk[0]));
 			p.nk = 0;
 		}
 
@@ -871,7 +924,7 @@
 	}
 }
 
-static void
+static int
 hdsetup(Dev *d, Ep *ep)
 {
 	Hiddev *f;
@@ -896,12 +949,71 @@
 	}
 	quirks(f);
 	procrfork(readerproc, f, Stack, RFNOTEG);
-	return;
+	return 0;
 Err:
 	hdfree(f);
+	return -1;
 }
 
 static void
+fsread(Req *r)
+{
+	char msg[48], *s, *e;
+
+	s = msg;
+	e = msg+sizeof(msg);
+	*s = 0;
+	if(havekbd)
+		e = seprint(s, e, "repeat %d\ndelay %d\n", kbrepeat, kbdelay);
+	USED(e);
+	readstr(r, msg);
+	respond(r, nil);
+}
+
+static void
+fswrite(Req *r)
+{
+	char msg[256], *f[4];
+	void *data;
+	int nf, sz;
+	Dev *dev;
+
+	dev = r->fid->file->aux;
+	data = r->ifcall.data;
+	sz = r->ifcall.count;
+	if(r->fid->aux == (void*)Rawon){
+		if(sz == 6 && memcmp(data, "rawoff", 6) == 0)
+			r->fid->aux = nil;
+		else if(usbcmd(dev, Rh2d|Rclass|Riface, Setreport, Reportout, 0, data, sz) < 0){
+			responderror(r);
+			return;
+		}
+	}else{
+		snprint(msg, sizeof(msg), "%.*s", utfnlen(data, sz), data);
+		nf = tokenize(msg, f, nelem(f));
+		if(nf == 1 && strcmp(f[0], "rawon") == 0)
+			r->fid->aux = (void*)Rawon;
+		else if(nf == 2 && strcmp(f[0], "repeat") == 0)
+			kbrepeat = atoi(f[1]);
+		else if(nf == 2 && strcmp(f[0], "delay") == 0)
+			kbdelay = atoi(f[1]);
+		else if(nf == 2 && strcmp(f[0], "debug") == 0)
+			debug = atoi(f[1]);
+		else{
+			respond(r, "invalid ctl message");
+			return;
+		}
+	}
+	r->ofcall.count = sz;
+	respond(r, nil);
+}
+
+static Srv fs = {
+	.read = fsread,
+	.write = fswrite,
+};
+
+static void
 usage(void)
 {
 	fprint(2, "usage: %s [-d] devid\n", argv0);
@@ -911,10 +1023,11 @@
 void
 threadmain(int argc, char* argv[])
 {
-	int i;
+	int i, n;
 	Dev *d;
 	Ep *ep;
 	Usbdev *ud;
+	char buf[32];
 
 	ARGBEGIN{
 	case 'd':
@@ -929,7 +1042,7 @@
 	if(d == nil)
 		sysfatal("getdev: %r");
 	ud = d->usb;
-	for(i = 0; i < nelem(ud->ep); i++){
+	for(i = n = 0; i < nelem(ud->ep); i++){
 		if((ep = ud->ep[i]) == nil)
 			continue;
 		if(ep->type != Eintr || ep->dir != Ein)
@@ -936,13 +1049,21 @@
 			continue;
 		switch(ep->iface->csp){
 		case KbdCSP:
+			havekbd = 1;
 		case PtrCSP:
 		case PtrNonBootCSP:
 		case HidCSP:
-			hdsetup(d, ep);
+			n += hdsetup(d, ep) == 0;
 			break;
 		}
 	}
 	closedev(d);
+	if(n > 0){
+		fs.tree = alloctree(user, "usb", DMDIR|0555, nil);
+		snprint(buf, sizeof buf, "hidU%sctl", d->hname);
+		createfile(fs.tree->root, buf, user, 0666, d);
+		snprint(buf, sizeof buf, "%s.hid", d->hname);
+		threadpostsharesrv(&fs, nil, "usb", buf);
+	}
 	threadexits(nil);
 }
--- a/sys/src/cmd/patch.c
+++ b/sys/src/cmd/patch.c
@@ -393,12 +393,11 @@
 	char *tmp;
 	int fd;
 
-	if(strcmp(new, "/dev/null") == 0 && len != 0){
-		sysfatal("diff modifies removed file");
-		return;
-	}
 	tmp = nil;
-	if(!dryrun){
+	if(strcmp(new, "/dev/null") == 0){
+		if(len != 0)
+			sysfatal("diff modifies removed file");
+	}else if(!dryrun){
 		if(mkpath(new) == -1)
 			sysfatal("mkpath %s: %r", new);
 		if((tmp = smprint("%s.tmp%d", new, getpid())) == nil)
@@ -428,7 +427,7 @@
 	for(i = 0; i < nchanged; i++){
 		c = &changed[i];
 		if(!ok){
-			if(remove(c->tmp) == -1)
+			if(c->tmp != nil && remove(c->tmp) == -1)
 				fprint(2, "remove %s: %r\n", c->tmp);
 			goto Free;
 		}
@@ -460,7 +459,7 @@
 	free(changed);
 }
 
-int
+void
 slurp(Fbuf *f, char *path)
 {
 	int n, i, fd, sz, len, nlines, linesz;
@@ -503,7 +502,6 @@
 	f->lines = lines;
 	f->nlines = nlines;
 	f->lastln = -1;
-	return 0;
 }
 
 char*
@@ -517,7 +515,7 @@
 	for(fuzz = 0; scanning && fuzz <= nfuzz; fuzz++){
 		scanning = 0;
 		ln = h->oldln - fuzz;
-		if(ln > f->lastln){
+		if(ln > f->lastln && ln < f->nlines){
 			off = f->lines[ln];
 			if(off + len > f->len)
 				continue;
@@ -527,8 +525,8 @@
 				return f->buf + off;
 			}
 		}
-		ln = h->oldln + fuzz - 1;
-		if(ln <= f->nlines){
+		ln = h->oldln + fuzz + 1;
+		if(ln > f->lastln && ln < f->nlines){
 			off = f->lines[ln];
 			if(off + len >= f->len)
 				continue;
@@ -558,9 +556,9 @@
 int
 apply(Patch *p, char *fname)
 {
-	char *o, *s, *e, *curfile;
+	char *o, *s, *e, *curfile, *nextfile;
 	int i, osz;
-	Hunk *h;
+	Hunk *h, *prevh;
 	Fbuf f;
 
 	e = nil;
@@ -567,13 +565,26 @@
 	o = nil;
 	osz = 0;
 	curfile = nil;
+	h = nil;
+	prevh = nil;
 	for(i = 0; i < p->nhunk; i++){
 		h = &p->hunk[i];
-		if(curfile == nil || strcmp(curfile, h->newpath) != 0){
-			if(!dryrun && slurp(&f, h->oldpath) == -1)
-				sysfatal("slurp %s: %r", h->oldpath);
-			curfile = h->newpath;
-			e = f.buf;
+		if(strcmp(h->newpath, "/dev/null") == 0)
+			nextfile = h->oldpath;
+		else
+			nextfile = h->newpath;
+		if(curfile == nil || strcmp(curfile, nextfile) != 0){
+			if(curfile != nil){
+				if(!dryrun)
+					o = append(o, &osz, e, f.buf + f.len);
+				blat(prevh->oldpath, prevh->newpath, o, osz);
+				osz = 0;
+			}
+			if(!dryrun){
+				slurp(&f, h->oldpath);
+				e = f.buf;
+			}
+			curfile = nextfile;
 		}
 		if(!dryrun){
 			s = e;
@@ -582,11 +593,12 @@
 			o = append(o, &osz, h->new, h->new + h->newlen);
 			e += h->oldlen;
 		}
-		if(i+1 == p->nhunk || strcmp(curfile, p->hunk[i+1].newpath) != 0){
+		prevh = h;
+	}
+	if(curfile != nil){
+		if(!dryrun)
 			o = append(o, &osz, e, f.buf + f.len);
-			blat(h->oldpath, h->newpath, o, osz);
-			osz = 0;
-		}
+		blat(h->oldpath, h->newpath, o, osz);
 	}
 	free(o);
 	return 0;
--- a/sys/src/cmd/reform/pm.c
+++ b/sys/src/cmd/reform/pm.c
@@ -3,11 +3,14 @@
 #include <fcall.h>
 #include <thread.h>
 #include <9p.h>
+#include <draw.h>
+#include <memdraw.h>
 
 enum
 {
 	Mhz = 1000*1000,
 	Pwmsrcclk = 25*Mhz,
+	Kbdlightmax = 8,
 
 	Scharge = 0,
 	Sovervolted,
@@ -22,8 +25,15 @@
 	Light = 1,
 	Temp,
 	Battery,
+	Kbdoled,
 	Pmctl,
 
+	KbdoledW = 126,
+	KbdoledH = 32,
+
+	Lcd = 0,
+	Kbd,
+
 	PWMSAR = 0x0c/4,
 	PWMPR = 0x10/4,
 
@@ -68,9 +78,13 @@
 		STAT_RR = 1<<3,
 };
 
+static char *uid = "pm";
 static Reqqueue *lpcreq;
 static u32int *pwm2, *tmu, *spi2;
-static char *uid = "pm";
+static int kbdlight = 0;
+static int kbdhidfd = -1;
+static Memimage *kbdoled;
+static u8int kbdoledraw[4+KbdoledW*KbdoledH/8] = {'W', 'B', 'I', 'T', 0};
 
 static void
 wr(u32int *base, int reg, u32int v)
@@ -86,9 +100,97 @@
 	return base != nil ? base[reg] : -1;
 }
 
-static void
-setlight(int p)
+static char *
+readall(int f)
 {
+	int bufsz, sz, n;
+	char *s;
+
+	bufsz = 2047;
+	s = nil;
+	for(sz = 0;; sz += n){
+		if(bufsz-sz < 2048){
+			bufsz *= 2;
+			s = realloc(s, bufsz);
+		}
+		if((n = readn(f, s+sz, bufsz-sz-1)) < 1)
+			break;
+	}
+	if(n < 0 || sz < 1){
+		if(n == 0)
+			werrstr("empty");
+		free(s);
+		return nil;
+	}
+	s[sz] = 0;
+
+	return s;
+}
+
+static int
+openkbdhid(void)
+{
+	char path[32], *s, *k, *e;
+	int f;
+
+	if(kbdhidfd < 0 && (f = open("/dev/usb/ctl", OREAD)) >= 0){
+		if((s = readall(f)) != nil &&
+			(k = strstr(s, "MNT 'Reform Keyboard'")) != nil &&
+			(e = strchr(k+22, ' ')) != nil){
+			*e = 0;
+			snprint(path, sizeof(path), "/dev/hidU%sctl", k+22);
+			if((kbdhidfd = open(path, OWRITE)) >= 0 && write(kbdhidfd, "rawon", 5) != 5){
+				close(kbdhidfd);
+				kbdhidfd = -1;
+			}
+		}
+		free(s);
+		close(f);
+	}
+
+	return kbdhidfd < 0;
+}
+
+static int
+loadkbdoled(void *data, int size)
+{
+	int x, y, i, k, v, bpl;
+	u8int *p, q;
+
+	if(openkbdhid() != 0)
+		return -1;
+	if(size == 0)
+		return write(kbdhidfd, "WCLR", 4);
+
+	bpl = bytesperline(kbdoled->r, kbdoled->depth);
+	if(size == 60+bpl*KbdoledH){
+		data = (u8int*)data + 60;
+		size -= 60;
+	}else if(size != bpl*KbdoledH){
+		werrstr("invalid image: expected %dx%d GREY1 (%d bytes)", KbdoledW, KbdoledH, bpl*KbdoledH);
+		return -1;
+	}
+
+	k = loadmemimage(kbdoled, kbdoled->r, data, size);
+	if(k < 0 || openkbdhid() != 0)
+		return -1;
+	for(y = 0, i = 4; y < KbdoledH; y += 8){
+		for(x = v = 0; x < KbdoledW; x++, v = (v+1)&7){
+			SET(p);
+			if(v == 0)
+				p = byteaddr(kbdoled, Pt(x,y));
+			for(k = q = 0; k < 8; k++)
+				q |= ((p[bpl*k] >> (7-v)) & 1) << k;
+			kbdoledraw[i++] = q;
+		}
+	}
+
+	return write(kbdhidfd, kbdoledraw, sizeof(kbdoledraw));
+}
+
+static int
+setlight(int k, int p)
+{
 	u32int v;
 
 	if(p < 0)
@@ -96,17 +198,37 @@
 	if(p > 100)
 		p = 100;
 
-	v = Pwmsrcclk / rd(pwm2, PWMSAR);
-	wr(pwm2, PWMPR, (Pwmsrcclk/(v*p/100))-2);
+	if(k == Lcd){
+		v = Pwmsrcclk / rd(pwm2, PWMSAR);
+		wr(pwm2, PWMPR, (Pwmsrcclk/(v*p/100))-2);
+		return 0;
+	}else if(k == Kbd && openkbdhid() == 0){
+		v = Kbdlightmax*p/100;
+		if(fprint(kbdhidfd, "LITE%c", '0'+v) > 0){
+			kbdlight = v;
+			return 0;
+		}
+		close(kbdhidfd);
+		kbdhidfd = -1;
+		kbdlight = 0;
+	}
+
+	return -1;
 }
 
 static int
-getlight(void)
+getlight(int k)
 {
 	u32int m, v;
 
-	m = Pwmsrcclk / rd(pwm2, PWMSAR);
-	v = Pwmsrcclk / (rd(pwm2, PWMPR)+2);
+	SET(m, v);
+	if(k == Lcd){
+		m = Pwmsrcclk / rd(pwm2, PWMSAR);
+		v = Pwmsrcclk / (rd(pwm2, PWMPR)+2);
+	}else if(k == Kbd){
+		m = Kbdlightmax;
+		v = kbdlight;
+	}
 	return v*100/m;
 }
 
@@ -194,17 +316,17 @@
 	wr(spi2, SPIx_TXDATA, cmd);
 	wr(spi2, SPIx_TXDATA, arg);
 	wr(spi2, SPIx_CONREG, con | CON_XCH);
-	sleep(60);
 
-	/* LPC buffers 3 bytes without responding, ignore (including garbage) */
+	/*
+	 * LPC buffers 3 bytes without responding, but spends some time
+	 * to prepare the response. 50ms should be safe, add a bit more
+	 * to be sure LPC is blocked waiting for the chip select to go
+	 * active again.
+	 */
+	sleep(60);
 	while(rd(spi2, SPIx_STATREG) & STAT_RR)
 		rd(spi2, SPIx_RXDATA);
 
-	/*
-	 * at this point LPC hopefully is blocked waiting for
-	 * chip select to go active
-	 */
-
 	/* expecting 8 bytes, start the exchange */
 	for(i = 0; i < 8; i++)
 		wr(spi2, SPIx_TXDATA, 0);
@@ -315,7 +437,7 @@
 	if(r->ifcall.offset == 0){
 		aux = r->fid->file->aux;
 		if(aux == (void*)Light){
-			snprint(msg, sizeof(msg), "lcd %d\n", getlight());
+			snprint(msg, sizeof(msg), "lcd %d\nkbd %d\n", getlight(Lcd), getlight(Kbd));
 		}else if(aux == (void*)Temp){
 			if((c = getcputemp()) < 0){
 				responderror(r);
@@ -339,13 +461,24 @@
 fswrite(Req *r)
 {
 	char msg[256], *f[4];
-	int nf, v, p;
+	int nf, v, p, k;
 	void *aux;
 
-	snprint(msg, sizeof(msg), "%.*s",
-		utfnlen((char*)r->ifcall.data, r->ifcall.count), (char*)r->ifcall.data);
-	nf = tokenize(msg, f, nelem(f));
 	aux = r->fid->file->aux;
+
+	if(aux == (void*)Kbdoled){
+		if(loadkbdoled(r->ifcall.data, r->ifcall.count) < 0){
+Err:
+			responderror(r);
+			return;
+		}
+		r->ofcall.count = r->ifcall.count;
+		respond(r, nil);
+		return;
+	}
+
+	snprint(msg, sizeof(msg), "%.*s", utfnlen(r->ifcall.data, r->ifcall.count), r->ifcall.data);
+	nf = tokenize(msg, f, nelem(f));
 	if(aux == (void*)Light){
 		if(nf < 2){
 Bad:
@@ -352,16 +485,31 @@
 			respond(r, "invalid ctl message");
 			return;
 		}
-		if(strcmp(f[0], "lcd") == 0){
-			v = atoi(f[1]);
-			if(*f[1] == '+' || *f[1] == '-')
-				v += getlight();
-			setlight(v);
-		}
+		if(strcmp(f[0], "lcd") == 0)
+			k = Lcd;
+		else if(strcmp(f[0], "kbd") == 0)
+			k = Kbd;
+		else
+			goto Bad;
+		v = atoi(f[1]);
+		if(*f[1] == '+' || *f[1] == '-')
+			v += getlight(k);
+		if(setlight(k, v) != 0)
+			goto Err;
 	}else if(aux == (void*)Pmctl){
 		p = -1;
-		if(nf == 2 && strcmp(f[0], "power") == 0 && strcmp(f[1], "off") == 0)
-			p = Psomoff;
+		if(nf >= 2 && strcmp(f[0], "power") == 0){
+			if(nf == 2 && strcmp(f[1], "off") == 0){
+				/*
+				 * LPC firmware might not be up to date so try
+				 * shutting down through the keyboard first
+				 */
+				if(openkbdhid() == 0){
+					write(kbdhidfd, "PWR0", 4);
+					sleep(2000); /* give it a chance */
+				}
+			}
+		}
 		if(p < 0)
 			goto Bad;
 		lpccall('p', p, msg);
@@ -425,11 +573,16 @@
 	if((spi2 = segattach(0, "ecspi2", 0, 0x20)) == (void*)-1)
 		sysfatal("no spi2");
 	tmuinit();
+	if(memimageinit() != 0)
+		sysfatal("%r");
+	if((kbdoled = allocmemimage(Rect(0, 0, KbdoledW, KbdoledH), GREY1)) == nil)
+		sysfatal("%r");
 	lpcreq = reqqueuecreate();
 	fs.tree = alloctree(uid, uid, DMDIR|0555, nil);
-	createfile(fs.tree->root, "battery", uid, 0444,(void*)Battery);
+	createfile(fs.tree->root, "battery", uid, 0444, (void*)Battery);
 	createfile(fs.tree->root, "cputemp", uid, 0444, (void*)Temp);
 	createfile(fs.tree->root, "light", uid, 0666, (void*)Light);
+	createfile(fs.tree->root, "kbdoled", uid, 0222, (void*)Kbdoled);
 	createfile(fs.tree->root, "pmctl", uid, 0666, (void*)Pmctl);
 	threadpostmountsrv(&fs, srv, mtpt, MAFTER);
 
--- a/sys/src/cmd/reform/shortcuts.c
+++ b/sys/src/cmd/reform/shortcuts.c
@@ -1,22 +1,32 @@
 #include <u.h>
 #include <libc.h>
 #include <keyboard.h>
+#include <plumb.h>
 
 static int lightstep = 5, volstep = 3;
-static int light, vol, actl, mod;
+static int light, vol, actl;
 
 static void
+aplumb(char *s)
+{
+	int f;
+
+	if((f = plumbopen("send", OWRITE)) >= 0){
+		plumbsendtext(f, "shortcuts", "audio", "/", s);
+		close(f);
+	}
+}
+
+static void
 process(char *s)
 {
 	char b[128], *p;
-	int n, o;
+	int n, o, skip;
 	Rune r;
 
-	if(*s == 'K' && s[1] == 0)
-		mod = 0;
-
 	o = 0;
 	b[o++] = *s;
+
 	for(p = s+1; *p != 0; p += n){
 		if((n = chartorune(&r, p)) == 1 && r == Runeerror){
 			/* bail out */
@@ -27,31 +37,31 @@
 			break;
 		}
 
-		if(*s == 'k' && r == Kmod4){
-			mod = 1;
-		}else if(*s == 'K'){
-			if(mod && r >= (KF|1) && r <= (KF|4))
-				continue;
-			if(r == Kmod4)
-				mod = 0;
-		}else if(mod && ((r >= (KF|1) && r <= (KF|4) || r == Kesc))){
-			if(*s == 'c'){
-				if(r == (KF|1))
-					fprint(light, "lcd %+d", -lightstep);
-				else if(r == (KF|2))
-					fprint(light, "lcd %+d", lightstep);
-				else if(r == (KF|3))
-					fprint(vol, "master %+d", -volstep);
-				else if(r == (KF|4))
-					fprint(vol, "master %+d", volstep);
-				else if(r == Kesc)
-					fprint(actl, "master toggle");
-			}
-			continue;
+		if(skip = (*s == 'c')){
+			if(r == Kbrtdn)
+				fprint(light, "lcd %+d", -lightstep);
+			else if(r == Kbrtup)
+				fprint(light, "lcd %+d", lightstep);
+			else if(r == Kvoldn)
+				fprint(vol, "master %+d", -volstep);
+			else if(r == Kvolup)
+				fprint(vol, "master %+d", volstep);
+			else if(r == Kmute)
+				fprint(actl, "master toggle");
+			else if(r == Ksbwd)
+				aplumb("key <");
+			else if(r == Ksfwd)
+				aplumb("key >");
+			else if(r == Kpause)
+				aplumb("key p");
+			else
+				skip = 0;
 		}
 
-		memmove(b+o, p, n);
-		o += n;
+		if(!skip){
+			memmove(b+o, p, n);
+			o += n;
+		}
 	}
 
 	/* all runes filtered out - ignore completely */
--- a/sys/src/cmd/rio/rio.c
+++ b/sys/src/cmd/rio/rio.c
@@ -393,32 +393,16 @@
 
 	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;
+	Alt alts[NALT+1] = {
+		[Awin]	{.c = wintap, .v = &w, .op = CHANRCV},
+		[Actl]	{.c = ctltap, .v = &ctl, .op = CHANRCV},
+		[Afrom]	{.c = fromtap, .v = &s, .op = CHANRCV},
+		[Adev]	{.c = kbdchan, .v = &s, .op = CHANRCV},
+		[Ato]	{.c = totap, .v = &s, .op = CHANNOP},
+		[Ainp]	{.c = nil, .v = &s, .op = CHANNOP},
+		[Awatch]{.c = totap, .v = &watched, .op = CHANNOP},
+		[NALT]	{.op = CHANEND},
+	};
 
 	cur = nil;
 	watched = nil;
--- a/sys/src/cmd/riow.c
+++ b/sys/src/cmd/riow.c
@@ -2,6 +2,7 @@
 #include <libc.h>
 #include <draw.h>
 #include <keyboard.h>
+#include <ctype.h>
 
 typedef struct W W;
 
@@ -24,6 +25,7 @@
 	Rectangle r;
 	int vd;
 	int flags;
+	int stickyforced;
 };
 
 static int vd = 1; /* current virtual desktop */
@@ -86,6 +88,7 @@
 		w->r.max.y = atoi(t[3]);
 		w->vd = -1;
 		w->flags = 0;
+		w->stickyforced = 0;
 
 		/* move over the current state of the window */
 		for(k = 0, seen = 0; k < wsn; k++){
@@ -92,6 +95,7 @@
 			if(ws[k].id == w->id){
 				w->vd = ws[k].vd;
 				w->flags = ws[k].flags & ~(Fvisible|Fcurrent);
+				w->stickyforced = ws[k].stickyforced;
 				if(w->flags & Ffullscreen)
 					w->r = ws[k].r;
 				seen = 1;
@@ -117,17 +121,21 @@
 		}
 
 		/* because a different program can run in any window we have to re-read */
-		snprint(s, sizeof(s), "/dev/wsys/%d/label", w->id);
 		w->flags &= ~Fsticky;
-		if((f = open(s, OREAD)) >= 0){
-			n = read(f, s, sizeof(s)-1);
-			close(f);
-			if(n > 0){
-				s[n] = 0;
-				for(k = 0; k < nelem(sticky) && sticky[k] != nil; k++){
-					if(strcmp(sticky[k], s) == 0){
-						w->flags |= Fsticky;
-						break;
+		if(w->stickyforced){
+			w->flags |= Fsticky;
+		}else{
+			snprint(s, sizeof(s), "/dev/wsys/%d/label", w->id);
+			if((f = open(s, OREAD)) >= 0){
+				n = read(f, s, sizeof(s)-1);
+				close(f);
+				if(n > 0){
+					s[n] = 0;
+					for(k = 0; k < nelem(sticky) && sticky[k] != nil; k++){
+						if(strcmp(sticky[k], s) == 0){
+							w->flags |= Fsticky;
+							break;
+						}
 					}
 				}
 			}
@@ -153,6 +161,7 @@
 {
 	int f;
 
+	wsupdate();
 	if(wcur == nil || (f = wwctl(wcur->id, OWRITE)) < 0)
 		return;
 	wcur->flags ^= Ffullscreen;
@@ -166,8 +175,9 @@
 static void
 togglesticky(void)
 {
+	wsupdate();
 	if(wcur != nil)
-		wcur->flags ^= Fsticky;
+		wcur->stickyforced ^= 1;
 }
 
 static void
@@ -176,14 +186,15 @@
 	int f, wcurf;
 	W *w;
 
+	if(vd == nvd)
+		return;
+
+	wsupdate();
 	if(mod == Mmod4){
 		wcur = nil;
 		wcurf = -1;
 		vd2wcur[vd] = -1;
 		for(w = ws; w < ws+wsn; w++){
-			if((f = wwctl(w->id, OWRITE)) < 0)
-				continue;
-
 			if(w->flags & Fvisible)
 				w->vd = vd;
 			else if(w->vd == vd)
@@ -192,24 +203,33 @@
 			if(w->flags & Fcurrent)
 				vd2wcur[vd] = w->id;
 
-			if(w->vd != nvd && (w->flags & Fsticky) == 0){
-				fprint(f, "hide");
-			}else{
-				fprint(f, "unhide");
-				if(vd2wcur[nvd] == w->id && wcurf < 0){
-					wcur = w;
-					wcurf = f;
-					f = -1;
+			if(w->vd == nvd && (w->flags & Fsticky) == 0){
+				if((f = wwctl(w->id, OWRITE)) >= 0){
+					fprint(f, "unhide");
+					if(vd2wcur[nvd] == w->id && wcurf < 0){
+						wcur = w;
+						wcurf = f;
+					}else
+						close(f);
 				}
 			}
-			if(f >= 0)
-				close(f);
 		}
+
 		if(wcur != nil){
 			fprint(wcurf, "top");
 			fprint(wcurf, "current");
 			close(wcurf);
 		}
+
+		for(w = ws; w < ws+wsn; w++){
+			if(w->vd != nvd && (w->flags & (Fsticky|Fvisible)) == Fvisible){
+				if((f = wwctl(w->id, OWRITE)) >= 0){
+					fprint(f, "hide");
+					close(f);
+				}
+			}
+		}
+
 		vd = nvd;
 		fprint(3, "%d\n", vd);
 	}else if(mod == (Mmod4 | Mshift) && wcur != nil && wcur->vd != nvd){
@@ -228,6 +248,7 @@
 {
 	int f;
 
+	wsupdate();
 	if(wcur == nil || (f = wwctl(wcur->id, OWRITE)) < 0)
 		return;
 
@@ -258,6 +279,7 @@
 	int wcurid, i, f;
 	W *w, *w₀;
 
+	wsupdate();
 	wcurid = wcur == nil ? -1 : wcur->id;
 	cyclectx.x = x;
 	cyclectx.y = y;
@@ -286,49 +308,90 @@
 	wcur = w;
 }
 
-static void
-keyevent(Rune r)
+static int
+keyevent(char c, Rune r)
 {
-	wsupdate();
-
-	if(r == '\n')
-		spawn("window");
-	else if(r == 'f')
-		togglefullscreen();
-	else if(r == 's')
-		togglesticky();
-	else if(r >= '0' && r <= '9')
+	if(c == 'c'){
+		if(r == '\n' && mod == Mmod4){
+			spawn("window");
+			return 0;
+		}
+		if(r == 'f' && mod == Mmod4){
+			togglefullscreen();
+			return 0;
+		}
+		if(r == 's' && mod == Mmod4){
+			togglesticky();
+			return 0;
+		}
+		if(r == Kup){
+			arrowaction(0, -1);
+			return 0;
+		}
+		if(r == Kdown){
+			arrowaction(0, 1);
+			return 0;
+		}
+		if(r == Kleft){
+			arrowaction(-1, 0);
+			return 0;
+		}
+		if(r == Kright){
+			arrowaction(1, 0);
+			return 0;
+		}
+		if(r == 'h' && mod == Mmod4){
+			cycleaction(-1, 0);
+			return 0;
+		}
+		if(r == 'l' && mod == Mmod4){
+			cycleaction(1, 0);
+			return 0;
+		}
+		if(r == 'j' && mod == Mmod4){
+			cycleaction(0, 1);
+			return 0;
+		}
+		if(r == 'k' && mod == Mmod4){
+			cycleaction(0, -1);
+			return 0;
+		}
+		if(r >= '0' && r <= '9' && (mod & Mctl) == 0){
+			vdaction(r - '0');
+			return 0;
+		}
+	}
+	/* mod4 + shift + 1…0 yields a shifted value on 'c': workaround */
+	if(c == 'k' && mod == (Mmod4|Mshift) && r >= '0' && r <= '9'){
 		vdaction(r - '0');
-	else if(r == Kup)
-		arrowaction(0, -1);
-	else if(r == Kdown)
-		arrowaction(0, 1);
-	else if(r == Kleft)
-		arrowaction(-1, 0);
-	else if(r == Kright)
-		arrowaction(1, 0);
-	else if(r == 'h')
-		cycleaction(-1, 0);
-	else if(r == 'l')
-		cycleaction(1, 0);
-	else if(r == 'j')
-		cycleaction(0, 1);
-	else if(r == 'k')
-		cycleaction(0, -1);
+		return 0;
+	}
+	/* don't bother to properly deal with handling shifted digit keys */
+	if((mod & Mshift) != 0 && (c == 'c' || c == 'k' || c == 'K'))
+		return ispunct(r) ? 0 : -1;
+
+	return -1;
 }
 
 static void
 process(char *s)
 {
-	int n, o, oldmod;
 	char b[128], *p;
+	int n, o;
 	Rune r;
 
-	if(*s == 'K' && s[1] == 0)
-		mod = 0;
-
 	o = 0;
 	b[o++] = *s;
+	if(*s == 'k' || *s == 'K'){
+		mod = 0;
+		if(utfrune(s+1, Kmod4) != nil)
+			mod |= Mmod4;
+		if(utfrune(s+1, Kctl) != nil)
+			mod |= Mctl;
+		if(utfrune(s+1, Kshift) != nil)
+			mod |= Mshift;
+	}
+
 	for(p = s+1; *p != 0; p += n){
 		if((n = chartorune(&r, p)) == 1 && r == Runeerror){
 			/* bail out */
@@ -339,36 +402,10 @@
 			break;
 		}
 
-		oldmod = mod;
-
-		if(*s == 'c' && (mod & Mmod4) != 0){
-			keyevent(r);
-			continue;
+		if((mod & Mmod4) == 0 || keyevent(*s, r) != 0){
+			memmove(b+o, p, n);
+			o += n;
 		}
-
-		if(*s == 'k'){
-			if(r == Kmod4)
-				mod |= Mmod4;
-			else if(r == Kctl)
-				mod |= Mctl;
-			else if(r == Kshift)
-				mod |= Mshift;
-			else if(r >= '0' && r <= '9' && (mod & (Mshift|Mmod4)) == (Mshift|Mmod4))
-				keyevent(r);
-		}else if(*s == 'K'){
-			if(r == Kmod4)
-				mod &= ~Mmod4;
-			else if(r == Kctl)
-				mod &= ~Mctl;
-			else if(r == Kshift)
-				mod &= ~Mshift;
-		}
-
-		if((oldmod | mod) & Mmod4)
-			continue;
-
-		memmove(b+o, p, n);
-		o += n;
 	}
 
 	/* all runes filtered out - ignore completely */
--- a/sys/src/cmd/vt/main.c
+++ b/sys/src/cmd/vt/main.c
@@ -299,6 +299,7 @@
 
 	if(rfork(RFENVG) < 0)
 		sysfatal("rfork: %r");
+	doquote = needsrcquote;
 	quotefmtinstall();
 	notify(catch);
 	atexit(shutdown);