git: 9front

Download patch

ref: da54919d76f166f50c6f3605c9d9c3933ad225fc
parent: 136b3e2d5b1fc971a784c53c065af3d238662db3
author: cinap_lenrek <cinap_lenrek@localhost>
date: Thu Apr 14 13:27:24 EDT 2011

fill /acme

--- /dev/null
+++ b/acme/acid/Acid
@@ -1,0 +1,6 @@
+#!/bin/rc
+if(~ $#* 0){
+	echo usage: Acid pid >[2=1]
+	exit usage
+}
+win acid -l acme $*
--- /dev/null
+++ b/acme/acid/guide
@@ -1,0 +1,3 @@
+broke|rc    kill program|rc
+Acid pid
+Acid -l thread -l acidfile pid
--- /dev/null
+++ b/acme/bin/Battery
@@ -1,0 +1,30 @@
+#!/bin/rc
+
+if(! test -f /mnt/apm/battery){
+	echo no apm >[1=2]
+	exit 'no apm'
+}
+
+cd /mnt/acme/new
+echo name /dev/apm >ctl
+echo dump Battery >ctl
+
+awkscript='
+NR==1 {
+	if($3 != -1)
+		printf("%d%% %d:%02d %s", $2, $3/3600, ($3/60)%60, $1);
+	else
+		printf("%d%% %s", $2, $1);
+}
+'
+
+fn chk {
+	what=`{awk $awkscript /mnt/apm/battery}
+	echo cleartag >ctl || exit die
+	echo clean >ctl || exit die
+	echo ' '^$"what >tag || exit die
+}
+
+chk
+while(sleep 60)
+	chk
--- /dev/null
+++ b/acme/bin/Isspam
@@ -1,0 +1,13 @@
+#!/bin/rc
+
+if(! ~ $#* 0){
+	echo usage: Isspam >[1=2]
+	exit usage
+}
+
+if(! ~ `{pwd} /mail/fs/*/[0-9]* || ! test -f raw || ! test -f unixheader){
+	echo must run in mail directory >[1=2]
+	exit 'bad dir'
+}
+
+cat unixheader raw | upas/isspam
--- /dev/null
+++ b/acme/bin/Mail
@@ -1,0 +1,9 @@
+#!/bin/rc
+
+#/mail/fs is read-protected unless fs is mounted
+test -r /mail/fs || {
+	if(test -d /mnt/term/mail/fs/mbox) bind /mnt/term/mail/fs /mail/fs
+	if not upas/fs
+}
+
+exec /acme/bin/$objtype/Mail $*
--- /dev/null
+++ b/acme/bin/Perl
@@ -1,0 +1,7 @@
+#!/bin/rc
+
+# aperl:   
+# Executes perl command and alters stderr to produce Acme-friendly error messages
+# Created 02-JUL-1996, Luther Huffman,  lutherh@stratcom.com
+
+/bin/perl $* |[2]  /bin/perl -pe 's/ line (\d+)/:$1 /'  >[1=2]
--- /dev/null
+++ b/acme/bin/README
@@ -1,0 +1,3 @@
+The source directory should be called ./src instead of ./source,
+but this directory is bound into /bin and there is a command called
+src that the local directory would hide.
--- /dev/null
+++ b/acme/bin/Spam
@@ -1,0 +1,13 @@
+#!/bin/rc
+
+if(! ~ $#* 0){
+	echo usage: Spam >[1=2]
+	exit usage
+}
+
+if(! ~ `{pwd} /mail/fs/*/[0-9]* || ! test -f raw || ! test -f unixheader){
+	echo must run in mail directory >[1=2]
+	exit 'bad dir'
+}
+
+cat unixheader raw | upas/spam
--- /dev/null
+++ b/acme/bin/Unspam
@@ -1,0 +1,13 @@
+#!/bin/rc
+
+if(! ~ $#* 0){
+	echo usage: Unspam >[1=2]
+	exit usage
+}
+
+if(! ~ `{pwd} /mail/fs/*/[0-9]* || ! test -f raw || ! test -f unixheader){
+	echo must run in mail directory >[1=2]
+	exit 'bad dir'
+}
+
+cat unixheader raw | upas/unspam
--- /dev/null
+++ b/acme/bin/adiff
@@ -1,0 +1,24 @@
+#!/bin/rc
+
+if(~ $#* 0 1){
+	echo >[1=2] usage: adiff file1 file2
+	echo >[1=2] or adiff file1 file2... dir
+	exit usage
+}
+
+dir = /mnt/wsys
+if(! test -f $dir/cons)
+	dir = /mnt/term/$dir
+id=`{cat $dir/new/ctl}
+id=$id(1)
+
+l=$1
+r=$2
+if (test -d $1) l=$1/`{basename $2}
+if not if (test -d $2) r=$2/`{basename $1}
+
+echo 'name '^`{pwd}^/-diff-^`{basename $l} > $dir/$id/ctl
+
+diff $* | awk -v 'l='$l -v 'r='^$r '/^diff/ {l=$2; r=$3; next} /^[1-9]/ {sub("[acd]", " & " r ":"); sub("^", l ":", $0)}
+	{print $0}' > $dir/$id/body
+echo clean > $dir/$id/ctl
--- /dev/null
+++ b/acme/bin/agrep
@@ -1,0 +1,3 @@
+#!/bin/rc
+
+exec grep -n $* /dev/null
--- /dev/null
+++ b/acme/bin/ap
@@ -1,0 +1,12 @@
+#!/bin/rc
+args=''
+while(~ $1 -*) {
+	args=$args^' '^$1
+	shift 1
+}
+if (~ $#1 0)
+	sysname=alice
+if not
+	sysname=$1
+if (! test -f /n/$sysname/usr/spool/ap ) { 9fs $sysname }
+eval exec /acme/bin/$cputype/apread $args $sysname
--- /dev/null
+++ b/acme/bin/aspell
@@ -1,0 +1,43 @@
+#!/bin/rc
+
+spellflags=()
+fflag=''
+for(x){
+	switch($x){
+	case -[bcvx]
+		spellflags=($spellflags $x)
+	case -f
+		fflag=$x
+	case *
+		if(~ $fflag -f)	{
+			spellflags=($spellflags -f $x)
+			fflag=''
+		}
+		if not args = ($args $x)
+	}
+}
+
+dir = /mnt/wsys
+if(! test -f $dir/cons)
+	dir = /mnt/term/$dir
+id=`{cat $dir/new/ctl}
+id=$id(1)
+
+if(~ $#args 1 && ~ $args /*){
+	adir = `{basename -d $args}
+	args = `{basename $args}
+	echo 'name '^$adir^/-spell > $dir/$id/ctl
+	cd $adir
+}
+if not {
+	echo 'name '^`{pwd}^/-spell > $dir/$id/ctl
+}
+
+{
+	echo noscroll
+	if(~ $#args 0)
+		/acme/bin/$cputype/spout | sort  -t: -u +2 | sort  -t: +1.1n | aux/sprog -a $spellflags > $dir/$id/body
+	if not for(i in $args)
+		/acme/bin/$cputype/spout $i | sort  -t: -u +2 | sort  -t: +1.1n | aux/sprog -a $spellflags > $dir/$id/body
+	echo clean
+}> $dir/$id/ctl
--- /dev/null
+++ b/acme/bin/guide
@@ -1,0 +1,4 @@
+win
+aspell file
+adiff file1 file2
+adict -d oed
--- /dev/null
+++ b/acme/bin/ind
@@ -1,0 +1,3 @@
+#!/bin/rc
+
+sed 's/^/	/' $*
--- /dev/null
+++ b/acme/bin/new
@@ -1,0 +1,10 @@
+#!/bin/rc
+
+id=`{cat /mnt/acme/new/ctl}
+id=$id(1)
+cmd = $*
+if(~ $#cmd 0) cmd = cat
+
+echo 'name '^`{pwd}^/-^`{basename $cmd(1)} > /mnt/acme/$id/ctl
+$cmd > /mnt/acme/$id/body
+echo clean > /mnt/acme/$id/ctl
--- /dev/null
+++ b/acme/bin/quote
@@ -1,0 +1,3 @@
+#!/bin/rc
+
+sed 's/^/> /' $*
--- /dev/null
+++ b/acme/bin/source/acd/README
@@ -1,0 +1,33 @@
+This is a CD player for use under Acme.
+
+It is derived from my earlier cdplay, which
+was in turn derived from a 2nd edition player
+called vcd.  I think hardly any of the code from
+vcd is left anymore, but it's what got me started.
+Vcd was originally by David Hogan with additions
+by Alberto Nava.  David Hogan claims the only
+code left is the definition of struct Msf.
+
+Run it by executing "acd /dev/sdD0", where
+/dev/sdD0 is your CD reader.
+
+A window with a track list will appear, with
+tracks named Track 1, Track 2, etc.
+If it can be found in the freedb.org CD database,
+real track names will replace the boring
+ones before long.
+
+To start playing a track, right click the number.
+A "> " marks the currently playing track.
+When that track finishes, acd plays the track
+on the next line.  This means you can edit
+the window as thought it were a play list.
+
+If the next line is "repeat", acd will start again
+at the first song listed in the window.
+
+CD changes are handled gracefully.
+
+Russ Cox
+9 August 2000
+rsc@plan9.bell-labs.com
--- /dev/null
+++ b/acme/bin/source/acd/access
@@ -1,0 +1,243 @@
+TWO FORMS OF ACCESS TO THE FREEDB
+---------------------------------
+
+In the following document we will refer to CDDB instead of freedb, since 
+from a technical point of view, freedb is a CDDB-Server as it uses the 
+CDDB-protocol.
+
+If you are interested in incorporating the use of freedb in your
+software, there are two forms of access that you may consider.
+
+1. <a href="#local">Local access</a>
+
+   In this mode your software simply attempts to open local files on
+   the computer to access the CDDB.
+
+2. <a href="#remote">Remote access</a>
+
+   In this mode the software must connect to a freedb server on the
+   network to access the CDDB.  There is a CDDB server protocol that
+   the software (also known as the "client") must use to converse with
+   the server.
+
+You may choose to support either one, or both of these modes.
+
+
+CDDB DISCID
+-----------
+
+Both forms of CDDB access requires that the software computes a "disc
+ID" which is an identifier that is used to access the CDDB.  The disc
+ID is a 8-digit hexadecimal (base-16) number, computed using data from
+a CD's Table-of-Contents (TOC) in MSF (Minute Second Frame) form.  The
+algorithm is listed below in the <a href="http://freedb.freedb.org/sections.php?op=viewarticle&artid=6">DISCID Howto</a>.
+
+It is crucial that your software compute the disc ID correctly.  If it
+does not generate the disc ID, it will not be compatible with the
+CDDB.  Moreover, if your software submits CDDB entries with bad disc
+IDs to the freedb archives, it could compromise the integrity of the
+freedb.
+
+If you have access to a UNIX platform that xmcd supports, we suggest 
+installing xmcd, and then test the disc ID code in your software by
+comparing the disc ID generated by xmcd with that of your software,
+for as large a number of CDs as possible.
+
+
+<a name="local"></a>LOCAL CDDB ACCESS
+-----------------
+
+There are two forms of the CDDB archive available, the standard form
+and the alternate form.  Both forms are available for download from 
+various servers. You can always find an actual list of mirrors on the 
+freedb-homepage at <a href="http://freedb.freedb.org">http://freedb.freedb.org</a>.
+The standard form of the CDDB archive is released to the public as 
+a UNIX tar(1)-format archive, compressed with gzip.  The alternate 
+form  archive is in the .zip format that is popular on the Windows 
+platform.
+
+Standard Form:
+--------------
+
+Each CD entry is a separate file in the xmcd CDDB.  These files are
+organized in several directories, each directory is a category of
+music.  Currently the "official" categories are listed as follows:
+
+	blues
+	classical
+	country
+	data
+	folk
+	jazz
+	misc
+	newage
+	reggae
+	rock
+	soundtrack
+
+The individual CDDB files have a file name that is the 8-digit disc
+ID. For example, under the blues directory there may be the following
+files:
+
+	0511c012
+	060e7314
+	0c01e902
+	0f0c3112
+	...
+	fa0f6f10
+	fb0f8814
+	fd0e6013
+
+To access the CDDB entry associated with a CD, your software simply
+opens the appropriate file and reads the information.
+
+The content of each of these files is in a format described in the 
+<a href="http://freedb.freedb.org/software/old/DBFORMAT">database-format specification</a>.
+
+Different pressings of a particular CD title may contain differences
+in timings that can cause the computed disc ID to be different.
+The CDDB allows this by having multiple file names be links to
+the same file.  The links are implemented as actual filesystem links
+(see the ln(1) command) on UNIX systems.  For example, the following
+files in the rock directory are all links to the same file, and
+refer to the CD "Pink Floyd / The Division Bell".:
+
+	850f740b
+	850f950b
+	850f970b
+	860f960b
+	890f970b
+
+Xmcd and the CD database server use this form of the CDDB archive.  The
+benefit of the standard form of the CDDB archive is very fast access,
+and ease of add/delete/edit operations on entries.
+
+Alternate Form:
+---------------
+
+Due to limitations in the FAT file system used on Windows 9x and 
+Windows ME, it is unfeasible to use the standard format CDDB archive 
+due to the large number of files.  This is because such a filesystem 
+operates on fixed-size clusters and even a small file (and most CDDB
+files are 1KB or less) would consume the space of a full cluster
+(Depending upon disk size, a cluster can range from 4KB to 32KB in 
+size).  Thus, a tremendous amount of disk space would be wasted on
+these systems if the CDDB archive is used in its standard form.
+
+An alternate form of the CDDB archives was created for use by software
+that must operate on a system with the FAT limitations.
+
+The alterate form still use the separate category directories as the
+standard form, but concatenates many files into a smaller number of
+files under each category.  The first two digits of the CDDB file names
+is used as a key for concatenation, each file is allowed to grow to
+approximately 64KB in size before a new file is started.  The file name
+indicates what range of the digits are included in that file.  For
+example, under the blues category we may have the following files:
+
+	01to36
+	37to55
+	56to71
+	...
+	b2tod7
+	d8toff
+
+The 01to36 file contains all CDDB entries with disc ID 01xxxxxx,
+02xxxxxx, 03xxxxxx and so on, up to 36xxxxxx.
+
+Each entry in the concatenated file begins with the keyword
+
+#FILENAME=xxxxxxxx
+
+where discid is the 8-digit hexadecimal disc ID of that entry.  Your
+software must search through the appropriate file to locate the desired
+entry.  The CDDB entry is in the format described in Appendix B below.
+
+The alternate form avoids the problem of inefficient disk space
+utilization on FAT-based filesystems, but is slower to access than the
+standard form, and it is much more cumbersome to perform add/delete/edit
+operations on a CDDB entry.
+
+
+<a name="remote"></a>REMOTE CDDB ACCESS
+------------------
+
+Your software must be able to communicate with a remote CD server
+system via TCP/IP or HTTP.  
+There are a number of public freedb servers operating
+on the Internet.  The <a href="http://freedb.freedb.org/sections.php?op=viewarticle&artid=9">current list of public servers</a> is listed on the
+freedb web page at:
+
+    http://freedb.freedb.org.
+	
+It may also be obtained programmatically via the CDDB protocol "sites" 
+command.
+
+TCP/IP access:
+
+All current freedb servers answer at TCP port 888.  There may be future
+sites that deviate from this convention, however.
+
+HTTP access:
+
+The freedb-servers can be accessed via the cddb.cgi. This is resides at the
+following path: /~cddb/cddb.cgi 
+Thus, the URL for accessing the server at freedb.freedb.org is:
+http://freedb.freedb.org/~cddb/cddb.cgi
+
+You should make the freedb server host (or hosts) and port numbers
+user-configurable in your software.  Do not hard-wire the list of
+CD database servers into your code.  The list of active servers changes
+over time.
+
+The CDDB server protocol is described in the <a href="http://freedb.freedb.org/sections.php?op=viewarticle&artid=28">CDDB-protocol documentation</a>.
+
+The CDDB entry returned from the server via a "cddb read" command is in
+the format described <a href="http://freedb.freedb.org/software/old/DBFORMAT">database-format specification</a>.
+
+You may experiment with the freedb server by connecting to port 888 of
+the server host via the "telnet" program, and then typing the cddb
+protocol commands by hand.  For example:
+
+	telnet freedb.freedb.org 888
+
+connects you to the freedb server at freedb.freedb.org.
+
+Some additional notes for accessing freedb over the Internet:
+
+Your application should always specify the highest documented protocol
+level. The highest level currently supported is "3". Lower protocol 
+levels will work, but are only provided for compatibility with older 
+CDDB applications. If you do not use the highest available protocol 
+level, certain important features will not be available to your 
+application.
+
+Make sure to use the proper arguments with the "hello" command. The user
+and hostname arguments should be that of the user's email address, not
+some fixed hard-coded value. The application name and version should be
+that of your application, not that of another existing application.
+
+We consider the use of the "cddb query" command mandatory for all CDDB
+clients. It is not valid to issue a "cddb read" command without issuing
+a prior "cddb query" and receiving a good response, as it may yield incorrect
+results. In addition, it is clients should support close matches
+(aka "fuzzy" matches, or response code 211).
+
+The proper way to handle multiple fuzzy matches is to present the
+entire list of matches to the user and to let the user choose between them.
+Matches are listed in the order of best fit for the user's disc, so they
+should be presented to the user in the order they are listed by the server.
+
+The suggested algorithm for obtaining the list of server sites is
+as follows.  The application should offer to get the list from
+freedb.freedb.org with the "sites" command the first time the user runs 
+the program. Additionally the application should provide the user with 
+some method of downloading the list on-demand.
+
+We do strongly suggest that you provide your users with the capability of
+choosing freedb server sites as described above. However, for some 
+applications this may not be feasible. If you do not wish to offer this 
+functionality, you may safely hard-code "freedb.freedb.org" in your 
+application as the sole freedb site to access. This will deprive your users
+of the option to choose a site near their locale for optimal response, but
+that is your choice.
--- /dev/null
+++ b/acme/bin/source/acd/acd.h
@@ -1,0 +1,171 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <disk.h>
+#include <auth.h>
+#include <fcall.h>
+#include <thread.h>
+#include <9p.h>
+
+/* acme */
+typedef struct Event Event;
+typedef struct Window Window;
+
+enum
+{
+	STACK		= 16384,
+	EVENTSIZE	= 256,
+	NEVENT		= 5,
+};
+
+struct Event
+{
+	int	c1;
+	int	c2;
+	int	q0;
+	int	q1;
+	int	flag;
+	int	nb;
+	int	nr;
+	char	b[EVENTSIZE*UTFmax+1];
+	Rune	r[EVENTSIZE+1];
+};
+
+struct Window
+{
+	/* file descriptors */
+	int		ctl;
+	int		event;
+	int		addr;
+	int		data;
+	Biobuf	*body;
+
+	/* event input */
+	char		buf[512];
+	char		*bufp;
+	int		nbuf;
+	Event	e[NEVENT];
+
+	int		id;
+	int		open;
+	Channel	*cevent;	/* chan(Event*) */
+};
+
+extern	Window*	newwindow(void);
+extern	int		winopenfile(Window*, char*);
+extern	void		winopenbody(Window*, int);
+extern	void		winclosebody(Window*);
+extern	void		wintagwrite(Window*, char*, int);
+extern	void		winname(Window*, char*);
+extern	void		winwriteevent(Window*, Event*);
+extern	void		winread(Window*, uint, uint, char*);
+extern	int		windel(Window*, int);
+extern	void		wingetevent(Window*, Event*);
+extern	void		wineventproc(void*);
+extern	void		winwritebody(Window*, char*, int);
+extern	void		winclean(Window*);
+extern	int		winselect(Window*, char*, int);
+extern	int		winsetaddr(Window*, char*, int);
+extern	char*	winreadbody(Window*, int*);
+extern	void		windormant(Window*);
+extern	void		winsetdump(Window*, char*, char*);
+
+extern	char*	readfile(char*, char*, int*);
+extern	void		ctlprint(int, char*, ...);
+extern	void*	emalloc(uint);
+extern	char*	estrdup(char*);
+extern	char*	estrstrdup(char*, char*);
+extern	char*	egrow(char*, char*, char*);
+extern	char*	eappend(char*, char*, char*);
+extern	void		error(char*, ...);
+extern	int		tokenizec(char*, char**, int, char*);
+
+/* cd stuff */
+typedef struct Msf Msf;	/* minute, second, frame */
+struct Msf {
+	int m;
+	int s;
+	int f;
+};
+
+typedef struct Track Track;
+struct Track {
+	Msf start;
+	Msf end;
+	ulong bstart;
+	ulong bend;
+	char *title;
+};
+
+enum {
+	MTRACK = 64,
+};
+typedef struct Toc Toc;
+struct Toc {
+	int ntrack;
+	int nchange;
+	int changetime;
+	int track0;
+	Track track[MTRACK];
+	char *title;
+};
+
+extern int msfconv(Fmt*);
+
+#pragma	varargck	argpos	error	1
+#pragma	varargck	argpos	ctlprint	2
+#pragma	varargck	type		"M"	Msf
+
+enum {	/* state */
+	Sunknown,
+	Splaying,
+	Spaused,
+	Scompleted,
+	Serror,
+};
+
+typedef struct Cdstatus Cdstatus;
+struct Cdstatus {
+	int state;
+	int track;
+	int index;
+	Msf abs;
+	Msf rel;
+};
+
+typedef struct Drive Drive;
+struct Drive {
+	Window *w;
+	Channel *cstatus;	/* chan(Cdstatus) */
+	Channel *ctocdisp;	/* chan(Toc) */
+	Channel *cdbreq;	/* chan(Toc) */
+	Channel *cdbreply; /* chan(Toc) */
+	Scsi *scsi;
+	Toc toc;
+	Cdstatus status;
+};
+
+int gettoc(Scsi*, Toc*);
+void drawtoc(Window*, Drive*, Toc*);
+void redrawtoc(Window*, Toc*);
+void tocproc(void*);	/* Drive* */
+void cddbproc(void*);	/* Drive* */
+void cdstatusproc(void*);	/* Drive* */
+
+extern int debug;
+
+#define DPRINT if(debug)fprint
+void acmeevent(Drive*, Window*, Event*);
+
+int playtrack(Drive*, int, int);
+int pause(Drive*);
+int resume(Drive*);
+int stop(Drive*);
+int eject(Drive*);
+int ingest(Drive*);
+
+int markplay(Window*, ulong);
+int setplaytime(Window*, char*);
+void advancetrack(Drive*, Window*);
+
+
--- /dev/null
+++ b/acme/bin/source/acd/acme.c
@@ -1,0 +1,347 @@
+#include "acd.h"
+
+static int
+iscmd(char *s, char *cmd)
+{
+	int len;
+
+	len = strlen(cmd);
+	return strncmp(s, cmd, len)==0 && (s[len]=='\0' || s[len]==' ' || s[len]=='\t' || s[len]=='\n');
+}
+
+static char*
+skip(char *s, char *cmd)
+{
+	s += strlen(cmd);
+	while(*s==' ' || *s=='\t' || *s=='\n')
+		s++;
+	return s;
+}
+
+//#define PLAYSTRING "/^[0-9:]+>"
+//#define PLAYSTRINGSPACE "/^[0-9:]+> ?"
+//#define INITSTRING "0:00> "
+
+#define INITSTRING "> "
+#define PLAYSTRING "/^>"
+#define PLAYSTRINGSPACE "/^> ?"
+
+/*
+ * find the playing string, leave in addr
+ * if q0, q1 are non-nil, set them to the addr of the string.
+ */
+int
+findplay(Window *w, char *s, ulong *q0, ulong *q1)
+{
+	char xbuf[25];
+	if(w->data < 0)
+		w->data = winopenfile(w, "data");
+
+	if(!winsetaddr(w, "#0", 1) || !winsetaddr(w, s, 1))
+		return 0;
+
+	seek(w->addr, 0, 0);
+	if(read(w->addr, xbuf, 24) != 24)
+		return 0;
+	
+	xbuf[24] = 0;
+	if(q0)
+		*q0 = atoi(xbuf);
+	if(q1)
+		*q1 = atoi(xbuf+12);
+
+	return 1;
+}
+
+/*
+ * find the playing string and replace the time
+ */
+int
+setplaytime(Window *w, char *new)
+{
+	char buf[40];
+	ulong q0, q1;
+
+return 1;
+	if(!findplay(w, PLAYSTRING, &q0, &q1))
+		return 0;
+
+	q1--;	/* > */
+	sprint(buf, "#%lud,#%lud", q0, q1);
+	DPRINT(2, "setaddr %s\n", buf);
+	if(!winsetaddr(w, buf, 1))
+		return 0;
+	
+	if(write(w->data, new, strlen(new)) != strlen(new))
+		return 0;
+
+	return 1;
+}
+
+/*
+ * find the playing string, and remove it.
+ * return the string at the beginning of hte next line in buf
+ * (presumably a track number).
+ */
+static int
+unmarkplay(Window *w, char *buf, int n, ulong *q0, ulong *q1, ulong *qbegin)
+{
+	char xbuf[24];
+
+	if(!findplay(w, PLAYSTRINGSPACE, q0, q1))
+		return 0;
+
+	if(write(w->data, "", 0) < 0 || !winsetaddr(w, "+1+#0", 1))
+		return 0;
+
+	if(qbegin) {
+		seek(w->addr, 0, 0);
+		if(read(w->addr, xbuf, 24) != 24)
+			return 0;
+		*qbegin = atoi(xbuf);
+	}
+
+	if(buf) {
+		if((n = read(w->data, buf, n-1)) < 0)
+			return 0;
+	
+		buf[n] = '\0';
+	}
+
+	return 1;
+}
+
+int
+markplay(Window *w, ulong q0)
+{
+	char buf[20];
+
+	if(w->data < 0)
+		w->data = winopenfile(w, "data");
+
+	sprint(buf, "#%lud", q0);
+	DPRINT(2, "addr %s\n", buf);
+	if(!winsetaddr(w, buf, 1) || !winsetaddr(w, "-0", 1))
+		return 0;
+	if(write(w->data, INITSTRING, strlen(INITSTRING)) != strlen(INITSTRING))
+		return 0;
+	return 1;
+}
+
+/* return 1 if handled, 0 otherwise */
+int
+cdcommand(Window *w, Drive *d, char *s)
+{
+	s = skip(s, "");
+
+	if(iscmd(s, "Del")){
+		if(windel(w, 0))
+			threadexitsall(nil);
+		return 1;
+	}
+	if(iscmd(s, "Stop")){
+		unmarkplay(w, nil, 0, nil, nil, nil);
+		stop(d);
+		return 1;
+	}
+	if(iscmd(s, "Eject")){
+		unmarkplay(w, nil, 0, nil, nil, nil);
+		eject(d);
+		return 1;
+	}
+	if(iscmd(s, "Ingest")){
+		unmarkplay(w, nil, 0, nil, nil, nil);
+		ingest(d);
+		return 1;
+	}
+	if(iscmd(s, "Pause")){
+		pause(d);
+		return 1;
+	}
+	if(iscmd(s, "Resume")){
+		resume(d);
+		return 1;
+	}
+	return 0;
+}
+
+void
+drawtoc(Window *w, Drive *d, Toc *t)
+{
+	int i, playing;
+
+	if(w->data < 0)
+		w->data = winopenfile(w, "data");
+	if(!winsetaddr(w, ",", 1))
+		return;
+
+	fprint(w->data, "Title\n\n");
+	playing = -1;
+	if(d->status.state == Splaying || d->status.state == Spaused)
+		playing = d->status.track-t->track0;
+
+	for(i=0; i<t->ntrack; i++)
+		fprint(w->data, "%s%d/ Track %d\n", i==playing ? "> " : "", i+1, i+1);
+	fprint(w->data, "");
+}
+
+void
+redrawtoc(Window *w, Toc *t)
+{
+	int i;
+	char old[50];
+
+	if(w->data < 0)
+		w->data = winopenfile(w, "data");
+	if(t->title) {
+		if(winsetaddr(w, "/Title", 1))
+			write(w->data, t->title, strlen(t->title));
+	}
+	for(i=0; i<t->ntrack; i++) {
+		if(t->track[i].title) {
+			sprint(old, "/Track %d", i+1);
+			if(winsetaddr(w, old, 1))
+				write(w->data, t->track[i].title, strlen(t->track[i].title));
+		}
+	}
+}
+
+void
+advancetrack(Drive *d, Window *w)
+{
+	int n;
+	ulong q0, q1, qnext;
+	char buf[20];
+
+	q0 = q1 = 0;
+	if(!unmarkplay(w, buf, sizeof(buf), &q0, &q1, &qnext)) {
+		DPRINT(2, "unmark: %r\n");
+		return;
+	}
+
+	DPRINT(2, "buf: %s\n", buf);
+	if(strncmp(buf, "repeat", 6) == 0) {
+		if(!winsetaddr(w, "#0", 1) || !findplay(w, "/^[0-9]+\\/", &qnext, nil)) {
+			DPRINT(2, "set/find: %r\n");
+			return;
+		}
+		if(w->data < 0)
+			w->data = winopenfile(w, "data");
+		if((n = read(w->data, buf, sizeof(buf)-1)) <= 0) {
+			DPRINT(2, "read %d: %r\n", n);
+			return;
+		}
+		buf[n] = 0;
+		DPRINT(2, "buf: %s\n", buf);
+	}
+
+	if((n = atoi(buf)) == 0)
+		return;
+
+	if(!markplay(w, qnext))
+		DPRINT(2, "err: %r");
+
+	playtrack(d, n-1, n-1);
+}
+
+void
+acmeevent(Drive *d, Window *w, Event *e)
+{
+	Event *ea, *e2, *eq;
+	char *s, *t, *buf;
+	int n, na;
+	ulong q0, q1;
+
+	switch(e->c1){	/* origin of action */
+	default:
+	Unknown:
+		fprint(2, "unknown message %c%c\n", e->c1, e->c2);
+		break;
+
+	case 'E':	/* write to body or tag; can't affect us */
+		break;
+
+	case 'F':	/* generated by our actions; ignore */
+		break;
+
+	case 'K':	/* type away; we don't care */
+		break;
+
+	case 'M':	/* mouse event */
+		switch(e->c2){		/* type of action */
+		case 'x':	/* mouse: button 2 in tag */
+		case 'X':	/* mouse: button 2 in body */
+			ea = nil;
+		//	e2 = nil;
+			s = e->b;
+			if(e->flag & 2){	/* null string with non-null expansion */
+				e2 = recvp(w->cevent);
+				if(e->nb==0)
+					s = e2->b;
+			}
+			if(e->flag & 8){	/* chorded argument */
+				ea = recvp(w->cevent);	/* argument */
+				na = ea->nb;
+				recvp(w->cevent);		/* ignore origin */
+			}else
+				na = 0;
+			
+			/* append chorded arguments */
+			if(na){
+				t = emalloc(strlen(s)+1+na+1);
+				sprint(t, "%s %s", s, ea->b);
+				s = t;
+			}
+			/* if it's a known command, do it */
+			/* if it's a long message, it can't be for us anyway */
+			DPRINT(2, "exec: %s\n", s);
+			if(!cdcommand(w, d, s))	/* send it back */
+				winwriteevent(w, e);
+			if(na)
+				free(s);
+			break;
+
+		case 'l':	/* mouse: button 3 in tag */
+		case 'L':	/* mouse: button 3 in body */
+		//	buf = nil;
+			eq = e;
+			if(e->flag & 2){
+				e2 = recvp(w->cevent);
+				eq = e2;
+			}
+			s = eq->b;
+			if(eq->q1>eq->q0 && eq->nb==0){
+				buf = emalloc((eq->q1-eq->q0)*UTFmax+1);
+				winread(w, eq->q0, eq->q1, buf);
+				s = buf;
+			}
+			DPRINT(2, "load %s\n", s);
+			if((n = atoi(s)) != 0) {
+				DPRINT(2, "mark %d\n", n);
+				q0 = q1 = 0;
+				unmarkplay(w, nil, 0, &q0, &q1, nil);
+
+				/* adjust eq->q* for deletion */
+				if(eq->q0 > q1) {
+					eq->q0 -= (q1-q0);
+					eq->q1 -= (q1-q0);
+				}
+				if(!markplay(w, eq->q0))
+					DPRINT(2, "err: %r\n");
+
+				playtrack(d, n-1, n-1);
+			} else
+				winwriteevent(w, e);
+			break;
+
+		case 'i':	/* mouse: text inserted in tag */
+		case 'I':	/* mouse: text inserted in body */
+		case 'd':	/* mouse: text deleted from tag */
+		case 'D':	/* mouse: text deleted from body */
+			break;
+
+		default:
+			goto Unknown;
+		}
+	}
+}
--- /dev/null
+++ b/acme/bin/source/acd/cddb
@@ -1,0 +1,206 @@
+<html><html>
+<head>
+<title>::freedb.org::</title>
+</head>
+
+<body bgcolor="#FFFFFF" text="#000000" link="#101070" vlink="#101070">
+
+<center>
+
+<table cellpadding=0 cellspacing=0 border=0 width="99%" align=center><tr><td align=left>
+<a href="/">
+<table border=0>
+<td bgcolor="#ffffff">
+ <table border=0 width=100% cellpadding=0 cellspacing=0>
+  <td bgcolor=#101070>
+   <table border=0>
+    <td bgcolor=#ffffff><font face="Arial,Helvetica,Lucida" color="#101070" size=8><b>freedb</b></td>
+    <td bgcolor=#101070><font face="Arial,Helvetica,Lucida" color="#ffffff" size=8><b>.org</b></td>
+   </table>
+  </td>
+  <tr>
+  <td align=right><font face="Arial,Helvetica,Lucida" color="#101070" size=2><b>a free approach to cddbp
+ </table>
+</td>
+</table>
+</a>
+</td><td align=right width=100%>
+	<form action="search.php" method=post>
+	<font face=Arial,Helvetica size=2><input type=name name=query width=20 size=20 length=20>
+	</td>
+	<td align=right>&nbsp;&nbsp;<input type=image src=images/menu/english/search.gif border=0 align=middle></td>
+	</form>
+
+</td></tr></table><br>
+<table cellpadding=0 cellspacing=0 border=0 width="99%" bgcolor=101070><tr><td>
+<table cellpadding=5 cellspacing=1 border=0 width="100%" bgcolor=FFFFFF><tr><td>
+<font face=Lucida,Verdana,Arial,Helvetica size=2>freedb.org - a free approach to cddbp</td></tr></table></td></tr></table><P>
+
+<table width="99%" align=center cellpadding=0 cellspacing=0 border=0><tr>
+	<td valign=top rowspan=5>
+
+<table border=0><tr><td>
+
+	<table width="115" border="0" cellpadding="0" cellspacing="0"><tr valign="top" bgcolor="#101070">
+                 <td bgcolor="#FFFFFF"><img src="themes/SlashOcean/pix.gif" width="3" height="3"></td>
+                 <td><img src="themes/SlashOcean/cl.gif" width="7" height="10"></td>
+                 <td><font face="verdana,helvetica,arial" size="1" color="#ffffff"><B>Main Menu</B></font></td>
+                 <td align="right"><img src="themes/SlashOcean/cr.gif" width="7" height="10" alt=""></td>
+                 <td bgcolor="#FFFFFF" align="right"><img src="themes/SlashOcean/pix.gif" width="3" height="3"></td>
+	</tr></table>
+	<table width="100%" border="0" cellpadding="0" cellspacing="0">
+	<tr bgcolor="#101070"><td colspan="3"><img src="themes/SlashOcean/pix.gif" width="1" height="1"></td></tr>
+	<tr bgcolor="#ffffff">
+		<td background="themes/SlashOcean/sl.gif"><img src="themes/SlashOcean/pix.gif" width="3" height="3" alt=""></td>
+		<td width="100%">
+		<table width="100%" border="0" cellpadding="5" cellspacing="0"><tr>
+		<td><font face="verdana,helvetica,arial" size="1">
+		<li><a href=index.php>Home</a>
+<li><a href=topics.php>News-Topics</a>
+<li><a href=sections.php?op=listarticles&secid=1>About</a>
+<li><a href=sections.php?op=listarticles&secid=2>Developers</a>
+<li><a href=sections.php?op=listarticles&secid=3>Applications</a>
+<li><a href=sections.php?op=listarticles&secid=7>Download</a>
+<li><a href=forum/index.php>Forum</a>
+<li><a href=http://freedb.music.sk/search/>Web-based Search</a>
+<li><a href=links.php>Web Links</a>
+<li><a href=user.php>Your Account</a>
+<li><a href=submit.php>Submit News</a>
+
+		</font></td>
+		</tr></table>
+
+		
+	</td><td background="themes/SlashOcean/sr.gif" align="right"><img src="themes/SlashOcean/pix.gif" width="3" height="3" alt=""></td>
+      </tr>
+	<tr bgcolor="#101070"><td colspan="3"><img src="themes/SlashOcean/pix.gif" width="1" height="1"></td></tr>
+	</table>
+	</td>
+
+
+
+
+
+
+</tr></td></table>
+<br>
+<table border=0><tr><td>
+
+	<table width="115" border="0" cellpadding="0" cellspacing="0"><tr valign="top" bgcolor="#101070">
+                 <td bgcolor="#FFFFFF"><img src="themes/SlashOcean/pix.gif" width="3" height="3"></td>
+                 <td><img src="themes/SlashOcean/cl.gif" width="7" height="10"></td>
+                 <td><font face="verdana,helvetica,arial" size="1" color="#ffffff"><B>FAQ</B></font></td>
+                 <td align="right"><img src="themes/SlashOcean/cr.gif" width="7" height="10" alt=""></td>
+                 <td bgcolor="#FFFFFF" align="right"><img src="themes/SlashOcean/pix.gif" width="3" height="3"></td>
+	</tr></table>
+	<table width="100%" border="0" cellpadding="0" cellspacing="0">
+	<tr bgcolor="#101070"><td colspan="3"><img src="themes/SlashOcean/pix.gif" width="1" height="1"></td></tr>
+	<tr bgcolor="#ffffff">
+		<td background="themes/SlashOcean/sl.gif"><img src="themes/SlashOcean/pix.gif" width="3" height="3" alt=""></td>
+		<td width="100%">
+		<table width="100%" border="0" cellpadding="5" cellspacing="0"><tr>
+		<td><font face="verdana,helvetica,arial" size="1">
+		Our FAQ can be found <a href="http://freedb.freedb.org/sections.php?op=viewarticle&artid=26">here</a>.<br>
+Please read the FAQ before asking questions via email.		</font></td>
+		</tr></table>
+
+		
+	</td><td background="themes/SlashOcean/sr.gif" align="right"><img src="themes/SlashOcean/pix.gif" width="3" height="3" alt=""></td>
+      </tr>
+	<tr bgcolor="#101070"><td colspan="3"><img src="themes/SlashOcean/pix.gif" width="1" height="1"></td></tr>
+	</table>
+	</td>
+
+</tr></td></table>
+<br>
+<table border=0><tr><td>
+
+	<table width="115" border="0" cellpadding="0" cellspacing="0"><tr valign="top" bgcolor="#101070">
+                 <td bgcolor="#FFFFFF"><img src="themes/SlashOcean/pix.gif" width="3" height="3"></td>
+                 <td><img src="themes/SlashOcean/cl.gif" width="7" height="10"></td>
+                 <td><font face="verdana,helvetica,arial" size="1" color="#ffffff"><B>Contact</B></font></td>
+                 <td align="right"><img src="themes/SlashOcean/cr.gif" width="7" height="10" alt=""></td>
+                 <td bgcolor="#FFFFFF" align="right"><img src="themes/SlashOcean/pix.gif" width="3" height="3"></td>
+	</tr></table>
+	<table width="100%" border="0" cellpadding="0" cellspacing="0">
+	<tr bgcolor="#101070"><td colspan="3"><img src="themes/SlashOcean/pix.gif" width="1" height="1"></td></tr>
+	<tr bgcolor="#ffffff">
+		<td background="themes/SlashOcean/sl.gif"><img src="themes/SlashOcean/pix.gif" width="3" height="3" alt=""></td>
+		<td width="100%">
+		<table width="100%" border="0" cellpadding="5" cellspacing="0"><tr>
+		<td><font face="verdana,helvetica,arial" size="1">
+		General questions:<br>
+<a href="mailto:info@freedb.org">info@freedb.org</a><hr>
+Databaseupdates:<br>
+<a href="mailto:updates@freedb.org">updates@freedb.org</a><br>
+(<b>NOT</b> for submission!)<hr>
+Please keep in mind that we are NOT the Nero-Support and please do not send CD-submits to the adresses above.<br>
+Submits have to go to:<br>
+<a href="mailto:freedb-submit@freedb.org">freedb-submit@freedb.org</a>		</font></td>
+		</tr></table>
+
+		
+	</td><td background="themes/SlashOcean/sr.gif" align="right"><img src="themes/SlashOcean/pix.gif" width="3" height="3" alt=""></td>
+      </tr>
+	<tr bgcolor="#101070"><td colspan="3"><img src="themes/SlashOcean/pix.gif" width="1" height="1"></td></tr>
+	</table>
+	</td>
+
+</tr></td></table>
+<br>
+<table border=0><tr><td>
+
+	<table width="115" border="0" cellpadding="0" cellspacing="0"><tr valign="top" bgcolor="#101070">
+                 <td bgcolor="#FFFFFF"><img src="themes/SlashOcean/pix.gif" width="3" height="3"></td>
+                 <td><img src="themes/SlashOcean/cl.gif" width="7" height="10"></td>
+                 <td><font face="verdana,helvetica,arial" size="1" color="#ffffff"><B>Downloads</B></font></td>
+                 <td align="right"><img src="themes/SlashOcean/cr.gif" width="7" height="10" alt=""></td>
+                 <td bgcolor="#FFFFFF" align="right"><img src="themes/SlashOcean/pix.gif" width="3" height="3"></td>
+	</tr></table>
+	<table width="100%" border="0" cellpadding="0" cellspacing="0">
+	<tr bgcolor="#101070"><td colspan="3"><img src="themes/SlashOcean/pix.gif" width="1" height="1"></td></tr>
+	<tr bgcolor="#ffffff">
+		<td background="themes/SlashOcean/sl.gif"><img src="themes/SlashOcean/pix.gif" width="3" height="3" alt=""></td>
+		<td width="100%">
+		<table width="100%" border="0" cellpadding="5" cellspacing="0"><tr>
+		<td><font face="verdana,helvetica,arial" size="1">
+		The link to the database downloads is <a href="/sections.php?op=viewarticle&artid=12">here</a>		</font></td>
+		</tr></table>
+
+		
+	</td><td background="themes/SlashOcean/sr.gif" align="right"><img src="themes/SlashOcean/pix.gif" width="3" height="3" alt=""></td>
+      </tr>
+	<tr bgcolor="#101070"><td colspan="3"><img src="themes/SlashOcean/pix.gif" width="1" height="1"></td></tr>
+	</table>
+	</td>
+
+
+</tr></td></table>
+<td>&nbsp;</td><td valign="top" width="100%">
+
+<!-- columna de inicio -->
+<center>
+	<table border=0 cellpadding=1 cellspacing=0 width=100% bgcolor=000000><tr><td>
+	<table border=0 cellpadding=8 cellspacing=0 width=100% bgcolor=FFFFFF>
+	<tr><td align=left><font face=Arial,Helvetica size=3>
+	<b>Database-format specification</b><br>
+	<font size=2>
+	<br><br>
+	Due to problems with using backslashes on our Webpage, we cannot display the database-format specification directly here.<br>
+But you can find it <a href="http://www.freedb.org/software/old/DBFORMAT">here</a> as a text-document.
+	</tr></td>
+	<tr><td align=center><font face=Arial,Helvetica>
+	&nbsp;
+	</tr></td>
+	</table></tr></td></table></center></td><td>&nbsp;</td>
+
+
+</tr></table></td></tr></table><br><br>
+
+<font face=Arial,Helvetica size=1><center>
+<br>
+<br>
+<br>
+<br>
+</body>
+</html>
--- /dev/null
+++ b/acme/bin/source/acd/cddb.c
@@ -1,0 +1,197 @@
+#include "acd.h"
+#include <ctype.h>
+
+/* see CDDBPROTO */
+static ulong 
+cddb_sum(int n)
+{
+	int ret;
+	ret = 0;
+	while(n > 0) {
+		ret += n%10;
+		n /= 10;
+	}
+	return ret;
+}
+
+static ulong
+diskid(Toc *t)
+{
+	int i, n, tmp;
+	Msf *ms, *me;
+
+	n = 0;
+	for(i=0; i < t->ntrack; i++)
+		n += cddb_sum(t->track[i].start.m*60+t->track[i].start.s);
+
+	ms = &t->track[0].start;
+	me = &t->track[t->ntrack].start;
+	tmp = (me->m*60+me->s) - (ms->m*60+ms->s);
+
+	/*
+	 * the spec says n%0xFF rather than n&0xFF.  it's unclear which is correct.
+	 * most CDs are in the database under both entries.
+	 */
+	return ((n & 0xFF) << 24 | (tmp << 8) | t->ntrack);
+}
+
+static void
+append(char **d, char *s)
+{
+	char *r;
+	if (*d == nil)
+		*d = estrdup(s);
+	else {
+		r = emalloc(strlen(*d) + strlen(s) + 1);
+		strcpy(r, *d);
+		strcat(r, s);
+		free(*d);
+		*d = r;
+	}
+}
+
+static int
+cddbfilltoc(Toc *t)
+{
+	int fd;
+	int i;
+	char *p, *q;
+	Biobuf bin;
+	Msf *m;
+	char *f[10];
+	int nf;
+	char *id, *categ;
+	char gottrack[MTRACK];
+	int gottitle;
+
+	fd = dial("tcp!freedb.freedb.org!888", 0, 0, 0);
+	if(fd < 0) {
+		fprint(2, "cannot dial: %r\n");
+		return -1;
+	}
+	Binit(&bin, fd, OREAD);
+
+	if((p=Brdline(&bin, '\n')) == nil || atoi(p)/100 != 2) {
+	died:
+		close(fd);
+		Bterm(&bin);
+		fprint(2, "error talking to server\n");
+		if(p) {
+			p[Blinelen(&bin)-1] = 0;
+			fprint(2, "server says: %s\n", p);
+		}
+		return -1;
+	}
+
+	fprint(fd, "cddb hello gre plan9 9cd 1.0\r\n");
+	if((p = Brdline(&bin, '\n')) == nil || atoi(p)/100 != 2)
+		goto died;
+
+	fprint(fd, "cddb query %8.8lux %d", diskid(t), t->ntrack);
+	DPRINT(2, "cddb query %8.8lux %d", diskid(t), t->ntrack);
+	for(i=0; i<t->ntrack; i++) {
+		m = &t->track[i].start;
+		fprint(fd, " %d", (m->m*60+m->s)*75+m->f);
+		DPRINT(2, " %d", (m->m*60+m->s)*75+m->f);
+	}
+	m = &t->track[t->ntrack-1].end;
+	fprint(fd, " %d\r\n", m->m*60+m->s);
+	DPRINT(2, " %d\r\n", m->m*60+m->s);
+
+	if((p = Brdline(&bin, '\n')) == nil || atoi(p)/100 != 2)
+		goto died;
+	p[Blinelen(&bin)-1] = 0;
+	DPRINT(2, "cddb: %s\n", p);
+	nf = tokenize(p, f, nelem(f));
+	if(nf < 1)
+		goto died;
+
+	switch(atoi(f[0])) {
+	case 200:	/* exact match */
+		if(nf < 3)
+			goto died;
+		categ = f[1];
+		id = f[2];
+		break;
+	case 211:	/* close matches */
+		if((p = Brdline(&bin, '\n')) == nil)
+			goto died;
+		if(p[0] == '.')	/* no close matches? */
+			goto died;
+		p[Blinelen(&bin)-1] = '\0';
+
+		/* accept first match */
+		nf = tokenize(p, f, nelem(f));
+		if(nf < 2)
+			goto died;
+		categ = f[0];
+		id = f[1];
+
+		/* snarf rest of buffer */
+		while(p[0] != '.') {
+			if((p = Brdline(&bin, '\n')) == nil)
+				goto died;
+			p[Blinelen(&bin)-1] = '\0';
+			DPRINT(2, "cddb: %s\n", p);
+		}
+		break;
+	case 202: /* no match */
+	default:
+		goto died;
+	}
+
+	/* fetch results for this cd */
+	fprint(fd, "cddb read %s %s\r\n", categ, id);
+
+	memset(gottrack, 0, sizeof(gottrack));
+	gottitle = 0;
+	do {
+		if((p = Brdline(&bin, '\n')) == nil)
+			goto died;
+		q = p+Blinelen(&bin)-1;
+		while(isspace(*q))
+			*q-- = 0;
+DPRINT(2, "cddb %s\n", p);
+		if(strncmp(p, "DTITLE=", 7) == 0) {
+			if (gottitle)
+				append(&t->title, p + 7);
+			else
+				t->title = estrdup(p+7);
+			gottitle = 1;
+		} else if(strncmp(p, "TTITLE", 6) == 0 && isdigit(p[6])) {
+			i = atoi(p+6);
+			if(i < t->ntrack) {
+				p += 6;
+				while(isdigit(*p))
+					p++;
+				if(*p == '=')
+					p++;
+
+				if (gottrack[i])
+					append(&t->track[i].title, p);
+				else
+					t->track[i].title = estrdup(p);
+				gottrack[i] = 1;
+			}
+		} 
+	} while(*p != '.');
+
+	fprint(fd, "quit\r\n");
+	close(fd);
+	Bterm(&bin);
+
+	return 0;
+}
+
+void
+cddbproc(void *v)
+{
+	Drive *d;
+	Toc t;
+
+	threadsetname("cddbproc");
+	d = v;
+	while(recv(d->cdbreq, &t))
+		if(cddbfilltoc(&t) == 0)
+			send(d->cdbreply, &t);
+}
--- /dev/null
+++ b/acme/bin/source/acd/cddbproto
@@ -1,0 +1,894 @@
+<html><html>
+<head>
+<title>::freedb.org::</title>
+</head>
+
+<body bgcolor="#FFFFFF" text="#000000" link="#101070" vlink="#101070">
+
+<center>
+
+<table cellpadding=0 cellspacing=0 border=0 width="99%" align=center><tr><td align=left>
+<a href="/">
+<table border=0>
+<td bgcolor="#ffffff">
+ <table border=0 width=100% cellpadding=0 cellspacing=0>
+  <td bgcolor=#101070>
+   <table border=0>
+    <td bgcolor=#ffffff><font face="Arial,Helvetica,Lucida" color="#101070" size=8><b>freedb</b></td>
+    <td bgcolor=#101070><font face="Arial,Helvetica,Lucida" color="#ffffff" size=8><b>.org</b></td>
+   </table>
+  </td>
+  <tr>
+  <td align=right><font face="Arial,Helvetica,Lucida" color="#101070" size=2><b>a free approach to cddbp
+ </table>
+</td>
+</table>
+</a>
+</td><td align=right width=100%>
+	<form action="search.php" method=post>
+	<font face=Arial,Helvetica size=2><input type=name name=query width=20 size=20 length=20>
+	</td>
+	<td align=right>&nbsp;&nbsp;<input type=image src=images/menu/english/search.gif border=0 align=middle></td>
+	</form>
+
+</td></tr></table><br>
+<table cellpadding=0 cellspacing=0 border=0 width="99%" bgcolor=101070><tr><td>
+<table cellpadding=5 cellspacing=1 border=0 width="100%" bgcolor=FFFFFF><tr><td>
+<font face=Lucida,Verdana,Arial,Helvetica size=2>freedb.org - a free approach to cddbp</td></tr></table></td></tr></table><P>
+
+<table width="99%" align=center cellpadding=0 cellspacing=0 border=0><tr>
+	<td valign=top rowspan=5>
+
+<table border=0><tr><td>
+
+	<table width="115" border="0" cellpadding="0" cellspacing="0"><tr valign="top" bgcolor="#101070">
+                 <td bgcolor="#FFFFFF"><img src="themes/SlashOcean/pix.gif" width="3" height="3"></td>
+                 <td><img src="themes/SlashOcean/cl.gif" width="7" height="10"></td>
+                 <td><font face="verdana,helvetica,arial" size="1" color="#ffffff"><B>Main Menu</B></font></td>
+                 <td align="right"><img src="themes/SlashOcean/cr.gif" width="7" height="10" alt=""></td>
+                 <td bgcolor="#FFFFFF" align="right"><img src="themes/SlashOcean/pix.gif" width="3" height="3"></td>
+	</tr></table>
+	<table width="100%" border="0" cellpadding="0" cellspacing="0">
+	<tr bgcolor="#101070"><td colspan="3"><img src="themes/SlashOcean/pix.gif" width="1" height="1"></td></tr>
+	<tr bgcolor="#ffffff">
+		<td background="themes/SlashOcean/sl.gif"><img src="themes/SlashOcean/pix.gif" width="3" height="3" alt=""></td>
+		<td width="100%">
+		<table width="100%" border="0" cellpadding="5" cellspacing="0"><tr>
+		<td><font face="verdana,helvetica,arial" size="1">
+		<li><a href=index.php>Home</a>
+<li><a href=topics.php>News-Topics</a>
+<li><a href=sections.php?op=listarticles&secid=1>About</a>
+<li><a href=sections.php?op=listarticles&secid=2>Developers</a>
+<li><a href=sections.php?op=listarticles&secid=3>Applications</a>
+<li><a href=sections.php?op=listarticles&secid=7>Download</a>
+<li><a href=forum/index.php>Forum</a>
+<li><a href=http://freedb.music.sk/search/>Web-based Search</a>
+<li><a href=links.php>Web Links</a>
+<li><a href=user.php>Your Account</a>
+<li><a href=submit.php>Submit News</a>
+
+		</font></td>
+		</tr></table>
+
+		
+	</td><td background="themes/SlashOcean/sr.gif" align="right"><img src="themes/SlashOcean/pix.gif" width="3" height="3" alt=""></td>
+      </tr>
+	<tr bgcolor="#101070"><td colspan="3"><img src="themes/SlashOcean/pix.gif" width="1" height="1"></td></tr>
+	</table>
+	</td>
+
+
+
+
+
+
+</tr></td></table>
+<br>
+<table border=0><tr><td>
+
+	<table width="115" border="0" cellpadding="0" cellspacing="0"><tr valign="top" bgcolor="#101070">
+                 <td bgcolor="#FFFFFF"><img src="themes/SlashOcean/pix.gif" width="3" height="3"></td>
+                 <td><img src="themes/SlashOcean/cl.gif" width="7" height="10"></td>
+                 <td><font face="verdana,helvetica,arial" size="1" color="#ffffff"><B>FAQ</B></font></td>
+                 <td align="right"><img src="themes/SlashOcean/cr.gif" width="7" height="10" alt=""></td>
+                 <td bgcolor="#FFFFFF" align="right"><img src="themes/SlashOcean/pix.gif" width="3" height="3"></td>
+	</tr></table>
+	<table width="100%" border="0" cellpadding="0" cellspacing="0">
+	<tr bgcolor="#101070"><td colspan="3"><img src="themes/SlashOcean/pix.gif" width="1" height="1"></td></tr>
+	<tr bgcolor="#ffffff">
+		<td background="themes/SlashOcean/sl.gif"><img src="themes/SlashOcean/pix.gif" width="3" height="3" alt=""></td>
+		<td width="100%">
+		<table width="100%" border="0" cellpadding="5" cellspacing="0"><tr>
+		<td><font face="verdana,helvetica,arial" size="1">
+		Our FAQ can be found <a href="http://freedb.freedb.org/sections.php?op=viewarticle&artid=26">here</a>.<br>
+Please read the FAQ before asking questions via email.		</font></td>
+		</tr></table>
+
+		
+	</td><td background="themes/SlashOcean/sr.gif" align="right"><img src="themes/SlashOcean/pix.gif" width="3" height="3" alt=""></td>
+      </tr>
+	<tr bgcolor="#101070"><td colspan="3"><img src="themes/SlashOcean/pix.gif" width="1" height="1"></td></tr>
+	</table>
+	</td>
+
+</tr></td></table>
+<br>
+<table border=0><tr><td>
+
+	<table width="115" border="0" cellpadding="0" cellspacing="0"><tr valign="top" bgcolor="#101070">
+                 <td bgcolor="#FFFFFF"><img src="themes/SlashOcean/pix.gif" width="3" height="3"></td>
+                 <td><img src="themes/SlashOcean/cl.gif" width="7" height="10"></td>
+                 <td><font face="verdana,helvetica,arial" size="1" color="#ffffff"><B>Contact</B></font></td>
+                 <td align="right"><img src="themes/SlashOcean/cr.gif" width="7" height="10" alt=""></td>
+                 <td bgcolor="#FFFFFF" align="right"><img src="themes/SlashOcean/pix.gif" width="3" height="3"></td>
+	</tr></table>
+	<table width="100%" border="0" cellpadding="0" cellspacing="0">
+	<tr bgcolor="#101070"><td colspan="3"><img src="themes/SlashOcean/pix.gif" width="1" height="1"></td></tr>
+	<tr bgcolor="#ffffff">
+		<td background="themes/SlashOcean/sl.gif"><img src="themes/SlashOcean/pix.gif" width="3" height="3" alt=""></td>
+		<td width="100%">
+		<table width="100%" border="0" cellpadding="5" cellspacing="0"><tr>
+		<td><font face="verdana,helvetica,arial" size="1">
+		General questions:<br>
+<a href="mailto:info@freedb.org">info@freedb.org</a><hr>
+Databaseupdates:<br>
+<a href="mailto:updates@freedb.org">updates@freedb.org</a><br>
+(<b>NOT</b> for submission!)<hr>
+Please keep in mind that we are NOT the Nero-Support and please do not send CD-submits to the adresses above.<br>
+Submits have to go to:<br>
+<a href="mailto:freedb-submit@freedb.org">freedb-submit@freedb.org</a>		</font></td>
+		</tr></table>
+
+		
+	</td><td background="themes/SlashOcean/sr.gif" align="right"><img src="themes/SlashOcean/pix.gif" width="3" height="3" alt=""></td>
+      </tr>
+	<tr bgcolor="#101070"><td colspan="3"><img src="themes/SlashOcean/pix.gif" width="1" height="1"></td></tr>
+	</table>
+	</td>
+
+</tr></td></table>
+<br>
+<table border=0><tr><td>
+
+	<table width="115" border="0" cellpadding="0" cellspacing="0"><tr valign="top" bgcolor="#101070">
+                 <td bgcolor="#FFFFFF"><img src="themes/SlashOcean/pix.gif" width="3" height="3"></td>
+                 <td><img src="themes/SlashOcean/cl.gif" width="7" height="10"></td>
+                 <td><font face="verdana,helvetica,arial" size="1" color="#ffffff"><B>Downloads</B></font></td>
+                 <td align="right"><img src="themes/SlashOcean/cr.gif" width="7" height="10" alt=""></td>
+                 <td bgcolor="#FFFFFF" align="right"><img src="themes/SlashOcean/pix.gif" width="3" height="3"></td>
+	</tr></table>
+	<table width="100%" border="0" cellpadding="0" cellspacing="0">
+	<tr bgcolor="#101070"><td colspan="3"><img src="themes/SlashOcean/pix.gif" width="1" height="1"></td></tr>
+	<tr bgcolor="#ffffff">
+		<td background="themes/SlashOcean/sl.gif"><img src="themes/SlashOcean/pix.gif" width="3" height="3" alt=""></td>
+		<td width="100%">
+		<table width="100%" border="0" cellpadding="5" cellspacing="0"><tr>
+		<td><font face="verdana,helvetica,arial" size="1">
+		The link to the database downloads is <a href="/sections.php?op=viewarticle&artid=12">here</a>		</font></td>
+		</tr></table>
+
+		
+	</td><td background="themes/SlashOcean/sr.gif" align="right"><img src="themes/SlashOcean/pix.gif" width="3" height="3" alt=""></td>
+      </tr>
+	<tr bgcolor="#101070"><td colspan="3"><img src="themes/SlashOcean/pix.gif" width="1" height="1"></td></tr>
+	</table>
+	</td>
+
+
+</tr></td></table>
+<td>&nbsp;</td><td valign="top" width="100%">
+
+<!-- columna de inicio -->
+<center>
+	<table border=0 cellpadding=1 cellspacing=0 width=100% bgcolor=000000><tr><td>
+	<table border=0 cellpadding=8 cellspacing=0 width=100% bgcolor=FFFFFF>
+	<tr><td align=left><font face=Arial,Helvetica size=3>
+	<b>CDDB-protocol documentation</b><br>
+	<font size=2>
+	<br><br>
+	<pre>
+				CDDB Protocol
+
+			  By Steve Scherf and Ti Kan
+		          --------------------------
+
+Revision: $Id: CDDBPROTO,v 1.6 1997/05/14 07:53:52 steve Exp steve $
+
+
+Notation:
+-&gt; : client to server
+&lt;- : server to client
+
+terminating marker: `.' character in the beginning of a line
+
+
+Server response code (three digit code):
+
+First digit:
+1xx	Informative message
+2xx	Command OK
+3xx	Command OK so far, continue
+4xx	Command OK, but cannot be performed for some specified reasons
+5xx	Command unimplemented, incorrect, or program error
+ 
+Second digit:
+x0x	Ready for further commands
+x1x	More server-to-client output follows (until terminating marker)
+x2x	More client-to-server input follows (until terminating marker)
+x3x	Connection will close
+
+Third digit:
+xx[0-9]	Command-specific code
+
+
+CDDB Protocol Level 1:
+----------------------
+
+Server sign-on banner:
+----------------------
+&lt;- code hostname CDDBP server version ready at date
+
+    code:
+	200	OK, read/write allowed
+	201	OK, read only
+	432	No connections allowed: permission denied
+	433	No connections allowed: X users allowed, Y currently active
+	434	No connections allowed: system load too high
+    hostname:
+	Server host name.  Example: xyz.fubar.com
+    version:
+	Version number of server software.  Example: v1.0PL0
+    date:
+	Current date and time.  Example: Wed Mar 13 00:41:34 1996
+
+
+Initial client-server handshake:
+--------------------------------
+Note: This handshake must occur before other cddb commands
+      are accepted by the server.
+
+Client command:
+-&gt; cddb hello username hostname clientname version
+
+    username:
+	Login name of user.  Example: johndoe
+    hostname:
+	Host name of client.  Example: abc.fubar.com
+    clientname:
+	The name of the connecting client.  Example: xmcd, cda, EasyCD,
+	et cetera. Do not use the name of another client which already
+	exists.
+    version:
+	Version number of client software.  Example: v1.0PL0
+
+Server response:
+&lt;- code hello and welcome username@hostname running clientname version
+
+    code:
+	200	Handshake successful
+	431	Handshake not successful, closing connection
+	402	Already shook hands
+
+
+CDDB query:
+-----------
+Client command:
+-&gt; cddb query discid ntrks off1 off2 ... nsecs
+
+    discid:
+	CD disc ID number.  Example: f50a3b13
+    ntrks:
+	Total number of tracks on CD.
+    off1, off2, ...:
+	Frame offset of the starting location of each track.
+    nsecs:
+	Total playing length of CD in seconds.
+
+Server response:
+&lt;- code categ discid dtitle
+	or
+&lt;- code close matches found
+&lt;- categ discid dtitle
+&lt;- categ discid dtitle
+&lt;- (more matches...)
+&lt;- .
+
+    code:
+	200	Found exact match
+	211	Found inexact matches, list follows (until terminating marker)
+	202	No match found
+	403	Database entry is corrupt
+	409	No handshake
+    categ:
+	CD category.  Example: rock
+    discid:
+	CD disc ID number of the found entry.  Example: f50a3b13
+    dtitle:
+	The Disc Artist and Disc Title (The DTITLE line).  For example:
+	Pink Floyd / The Dark Side of the Moon
+
+
+CDDB read:
+----------
+Client command:
+-&gt; cddb read categ discid
+
+    categ:
+	CD category.  Example: rock
+    discid:
+	CD disc ID number.  Example: f50a3b13
+
+Server response:
+&lt;- code categ discid
+&lt;- # xmcd 2.0 CD database file
+&lt;- # ...
+&lt;- (CDDB data...)
+&lt;- .
+	or
+&lt;- code categ discid No such CD entry in database
+
+    code:
+	210	OK, CDDB database entry follows (until terminating marker)
+	401	Specified CDDB entry not found.
+	402	Server error.
+	403	Database entry is corrupt.
+	409	No handshake.
+    categ:
+	CD category.  Example: rock
+    discid:
+	CD disc ID number.  Example: f50a3b13
+
+
+CDDB search: (command not yet implemented in freedb-serversoftware!)
+------------
+Client command:
+-&gt; cddb srch key search_type ... search_type
+
+    key:
+	Pseudo-regular expression to match. Expressions should meet the
+	following description:
+
+	- No white space.
+	- Printable characters only.
+	- Case is ignored.
+    search_type:
+	CDDB fields to search through.  Example: title
+	Supported types: artist, title, extd, ext, trk
+    categ:
+	CD category.  Example: rock
+
+Server response:
+&lt;- code matches found
+&lt;- categ discid dtitle
+&lt;- categ discid dtitle
+&lt;- (more matches...)
+&lt;- .
+
+    code:
+	210	OK, matches found, list follows (until terminating marker)
+	401	No match found.
+	409	No handshake.
+    categ:
+	CD category.  Example: rock
+    dtitle:
+	The Disc Artist and Disc Title (The DTITLE line).  For example:
+	Pink Floyd / The Dark Side of the Moon
+
+
+CDDB write:
+-----------
+Client command:
+-&gt; cddb write categ discid
+
+    categ:
+	CD category.  Example: rock
+    discid:
+	CD disc ID number.  Example: f50a3b13
+
+Server response:
+&lt;- code categ discid
+
+    code:
+	320	OK, input CDDB data (until terminating marker)
+	401	Permission denied.
+	402	Server file system full/file access failed.
+	409	No handshake.
+	501	Entry rejected: reason for rejection.
+    categ:
+	CD category.  Example: rock
+    discid:
+	CD disc ID number.  Example: f50a3b13
+
+Client data:
+-&gt; # xmcd 2.0 CD database file
+-&gt; # ...
+-&gt; (CDDB data)
+-&gt; .
+
+Server response:
+&lt;- code message
+
+    code:
+	200	CDDB entry accepted
+	401	CDDB entry rejected: reason why
+    message:
+	Message string to indicate write status:
+	CDDB entry accepted, or CDDB entry rejected.
+
+
+Help information:
+-----------------
+Client command:
+-&gt; help
+	or
+-&gt; help cmd
+
+    cmd:
+	CDDB command.  Example: quit
+
+	or
+
+-&gt; help cmd subcmd
+
+    cmd:
+	CDDB command.  Example: cddb
+    subcmd:
+	CDDB command argument.  Example: query
+
+Server response:
+&lt;- code Help information follows
+&lt;- (help data ...)
+&lt;- .
+	or
+&lt;- code no help information available
+
+    code:
+	210	OK, help information follows (until terminating marker)
+	401	No help information available
+
+
+Log statistics:
+---------------
+Client command:
+-&gt; log [[-l lines] [start date [end date]] | [day [days]] | [get"]]
+
+    lines:
+	The maximum number of lines to print for each data list in the
+	log statistics.
+    start date:
+	The date after which statistics should be calculated. Date is
+	of the format: hh[mm[ss[MM[DD[[CC]YY]]]]]
+
+	E.g.:	201200053196 for 8:12 PM on May 31, 1996.
+		20120005312096 for 8:12 PM on May 31, 2096.
+		080530 for today at at 8:15 and 30 seconds.
+
+	If the century ("CC") is omitted, a reasonable guess is made. If
+	this argument is omitted, all messages are considered.
+    end date:
+	The date after which statistics should not be calculated. If
+	omitted, the end date is assumed to be the current date.
+    day:
+	The string "day". This solitary argument will cause a log search
+	of messages generated within the last day.
+    days:
+	A positive numerical argument which modifies the number of days'
+        messages to searh. If this argument is left out, the default is 1.
+    get:
+	The string "get". This solitary argument will cause the server
+	to send the contents of the log file.
+
+Server response:
+&lt;- code Log summary follows
+&lt;- (log stats)
+&lt;- .
+	or
+&lt;- code Log follows
+&lt;- (log stats)
+&lt;- .
+
+    code:
+	210	OK, log summary follows (until terminating marker)
+	211	OK, log follows (until terminating marker)
+	401	Permission denied
+	402	No log information available
+	501	Invalid start/end date
+
+
+Message of the day:
+------------------
+Client command:
+-&gt; motd
+
+Server response:
+&lt;- code Last modified: date MOTD follows (until terminating marker)
+&lt;- (message text)
+&lt;- .
+
+    code:
+	210	Last modified: 05/31/96 06:31:14 MOTD follows (until terminating marker)
+	401	No message of the day available
+    date:
+	The date the text of the message of the day was modified. The date
+	appears in the following format:
+
+		05/31/96 06:31:14
+
+	This value may be used by client software as a message timestamp
+	for purposes of determining if it has already been displayed. This
+	format was chosen because it is more easily parsed than the standard
+	ctime() format.
+
+
+Server protocol level:
+----------------------
+Client command:
+-&gt; proto [level]
+
+    level:
+	The (numerical) protocol level to set the server to.
+
+Server response:
+&lt;- code CDDB protocol level: current cur_level, supported supported_level
+	or
+&lt;- code OK, protocol version now: cur_level
+
+    code:
+	200	CDDB protocol level: current cur_level, supported supp_level
+	201	OK, protocol version now: cur_level
+	501	Illegal protocol level.
+	502	Protocol level already cur_level.
+    cur_level:
+	The current protocol level at which the server is running.
+    supported_level:
+	The maximum supported protocol level.
+
+
+Server sites:
+--------------
+Client command:
+-&gt; sites
+
+Server response:
+&lt;- code OK, site information follows (until terminating `.')
+&lt;- (data)
+&lt;- .
+
+    code:
+	210	Ok, site information follows
+	401	No site information available.
+
+    The data format is as follows:
+	site port latitude longitude description
+
+    The fields are as follows:
+	site:
+	    The Internet address of the remote site.
+	port:
+	    The port at which the server resides on that site.
+	latitude:
+	    The latitude of the server site. The format is as follows:
+		CDDD.MM
+	    Where "C" is the compass direction (N, S), "DDD" is the
+	    degrees, and "MM" is the minutes.
+	longitude:
+	    The longitude of the server site. Format is as above, except
+	    the compass direction must be one of (E, W).
+	description:
+	    A short description of the geographical location of the site.
+
+    Example:
+	cddb.moonsoft.com 888 N037.23 W122.01 Fremont, CA USA
+
+
+Server status:
+--------------
+Client command:
+-&gt; stat
+
+Server response:
+&lt;- code OK, status information follows (until terminating `.')
+&lt;- (data)
+&lt;- .
+
+    code:
+	210	Ok, status information follows
+
+    The possible data is as follows:
+	current proto: &lt;current_level&gt;
+	    An integer representing the server's current operating protocol
+	    level.
+	max proto:     &lt;max_level&gt;
+	    The maximum supported protocol level.
+	gets:          &lt;yes | no&gt;
+	    Whether or not the client is allowed to get log information,
+	    according to the string "yes" or "no".
+	updates:       &lt;yes | no&gt;
+	    Whether or not the client is allowed to initiate a database
+	    update, according to the string "yes" or "no".
+	posting:       &lt;yes | no&gt;
+	    Whether or not the client is allowed to post new entries,
+	    according to the string "yes" or "no".
+	quotes:        &lt;yes | no&gt;
+	    Whether or not quoted arguments are enabled, according to
+	    the string "yes" or "no".
+	current users: &lt;num_users&gt;
+	    The number of users currently connected to the server.
+	max users:     &lt;num_max_users&gt;
+	    The number of users that can concurrently connect to the server.
+	strip ext:	&lt;yes | no&gt;
+	    Whether or not extended data is stripped by the server before
+	    presented to the user.
+	Database entries: &lt;num_db_entries&gt;
+	    The total number of entries in the database.
+	Database entries by category:
+	    This field is followed by a list of catgories and the number
+	    of entries in that category. Each entry is of the following
+	    format:
+
+		&lt;white space&gt;catgory: &lt;num_db_entries&gt;
+
+	    The list of entries is terminated by the first line that does
+	    not begin with white space.
+
+	Pending file transmissions:
+	    This field is followed by a list of sites that are fed new
+	    database entries at periodic intervals, and the number of
+	    entries that have yet to be transmitted to that site.
+	    Each entry is of the following format:
+
+		&lt;white space&gt;site: &lt;num_db_entries&gt;
+
+	    The list of entries is terminated by the first line that does
+	    not begin with white space.
+
+	This list may grow as needed, so clients must expect possible
+	unrecognizable data. Also, additional fields may be added to
+	the currently existing lines, although no existing fields will
+	be removed or change position.
+	
+
+Server version:
+---------------
+Client command:
+-&gt; ver
+
+Server response:
+&lt;- code servername version copyright
+	or
+&lt;- code Version information follows
+
+    code:
+	200	Version information.
+	211	OK, version information follows (until terminating marker)
+    version:
+	Server version.  Example: v1.0PL0
+    copyright:
+	Copyright string.  Example: Copyright (c) 1996 Steve Scherf
+
+
+Database update:
+----------------
+Client command:
+-&gt; update
+
+Server response:
+&lt;- code Updating the database.
+	or
+&lt;- code Permission denied.
+	or
+&lt;- code Unable to update the database.
+
+    code:
+	200 Updating the database.
+	401 Permission denied.
+	402 Unable to update the database.
+
+
+Server users:
+-------------
+Client command:
+-&gt; whom
+
+Server response:
+&lt;- code User list follows
+
+    code:
+	210	OK, user list follows (until terminating marker)
+	401	No user information available.
+
+
+Client sign-off:
+----------------
+Client command:
+-&gt; quit
+
+Server response:
+&lt;- code hostname closing connection.  Goodbye.
+
+    code:
+	230	OK, goodbye.
+    hostname:
+	Server host name.  Example: xyz.fubar.com
+
+
+General errors:
+---------------
+
+Server response:
+&lt;- code error
+    code:
+	402	Server error.
+	408	CGI environment error.
+	500	Command syntax error, command unknown, command unimplemented.
+	530	Server error, server timeout.
+
+
+Reserved errors:
+----------------
+
+The following error codes are reserved, and will never be returned as a
+response to a CDDB protocol command. They are intended to be used internally
+by clients that have a need for generating pseudo-responses.
+
+	600-699
+
+
+CDDB Protocol Level 2:
+----------------------
+
+In all respects, protocol level 2 is the same as level 1, with the exceptions
+listed below.
+
+Arguments to commands may be surrounded by double quotes. All characters
+within the quotes, including white space, are included in the argument. All
+white space is replaced by the `_' (2Dh) character by the server. White space
+is defined as ` ' (20h) and `^I' (control-I, or 09h).
+
+Arguments containing quotes that should not be interpreted with the special
+meaning described above should be escaped with a preceding backslash character,
+or '' (5Ch). If an actual backslash appears in an argument, it should be
+escaped with a preceding backslash. In both cases, the preceding backslash
+will be removed from the input before being interpreted.
+
+
+CDDB Protocol Level 3:
+----------------------
+
+Protocol level 3 is the same as level 2, with the exception listed below.
+
+The output of the "sites" command has changed to meet the folowing description:
+
+    The data format is as follows:
+	site protocol port address latitude longitude description
+
+    The fields are as follows:
+	site:
+	    The Internet address of the remote site.
+	protocol:
+	    The transfer protocol used to access the site.
+	port:
+	    The port at which the server resides on that site.
+	address:
+	    Any additional addressing information needed to access the
+	    server. For example, for HTTP protocol servers, this would be
+	    the path to the CDDB server CGI script. This field will be
+	    "-" if no additional addressing information is needed.
+	latitude:
+	    The latitude of the server site. The format is as follows:
+		CDDD.MM
+	    Where "C" is the compass direction (N, S), "DDD" is the
+	    degrees, and "MM" is the minutes.
+	longitude:
+	    The longitude of the server site. Format is as above, except
+	    the compass direction must be one of (E, W).
+	description:
+	    A short description of the geographical location of the site.
+
+    Example:
+	cddb.moonsoft.com cddbp 888 - N037.23 W122.01 Fremont, CA USA
+	cddb.moonsoft.com http 80 /~cddb/cddb.cgi N037.23 W122.01 Fremont,CA USA
+
+Note that a site may appear once for each type of protocol it supports for
+accessing the server.
+
+
+Addendum A: Proper use of CDDBP:
+--------------------------------
+
+There are a few guidelines that must be followed in order to make proper use
+of CDDBP:
+
+- When handshaking with the server via the "cddb hello" command, the client
+  must specify its own name and version, not that of some other client (such
+  as xmcd). Also, the "username" and "hostname" must be that of the actual
+  user running the program, not some hardwired value.
+
+- Before performing a "cddb read", the client program MUST perform a
+  "cddb query". Failure to do so may result in the client program receiving
+  incorrect CDDB data from the server. Also, without performing a query, the
+  client program will not benefit from close matches in the event of the
+  lack of an exact match in the database.
+
+- For accounting purposes, it is best if client programs only perform a single
+  "cddb query" for a particular disc before performing a "cddb read" for that
+  disc.
+
+
+Addendum B: CDDBP under HTTP:
+-----------------------------
+
+Accessing a server as a CGI script is done in much the same way as through
+direct interaction. The command set is identical, though the method of
+communication is through CDDBP commands encapsulated in the HTTP protocol.
+The only limitation is that a single command may be executed per connection,
+since HTTP is not truly interactive. For the server to be accessed in this
+way, it must reside on the target host at a known URL which is accessible by
+the host HTTP server. The client program must connect to the HTTP server on
+the target host and issue an HTTP command with the appropriate CDDBP command
+encapsulated within.
+
+Commands may be submitted to servers in CGI mode using either the "GET" or
+"POST" HTTP commands. Both methods are supported, and there is no real
+difference between how both are to be used other than the syntactical
+difference between the two methods. The "POST" method may provide the ability
+to issue longer commands, though, depending on the architecture of the system
+on which the server resides.
+
+The server command must be sent as part of the "Request-URI" in the case
+of the "GET" method, and as the "Entity-Body" in the case of the "POST"
+method. In both cases, the command must be of the following form:
+
+cmd=server+command&amp;hello=joe+my.host.com+clientname+version&amp;proto=1
+
+Where the text following the "cmd=" represents the CDDBP command to be
+executed, the text following the "hello=" represents the arguments to
+the "cddb hello" command that is implied by this operation, and the
+text following the "proto=" represents the argument to the "proto" command
+that is implied by this operation.
+
+The "+" characters in the input represent spaces, and will be translated
+by the server before performing the request. Special characters may be
+represented by the sequence "%XX" where "XX" is a two-digit hex number
+corresponding to the ASCII (ISO-8859-1) sequence of that character. The
+"&amp;" characters denote separations between the command, hello and proto
+arguments. Newlines and carriage returns must not appear anywhere in the
+string except at the end.
+
+All CDDBP commands are supported under HTTP, except for "cddb hello",
+"cddb write", "proto" and "quit".
+
+For example, should user "joe" on system "my.host.com" be running xmcd 2.1,
+a read request for his currenly playing CD might look like this:
+
+cmd=cddb+read+rock+12345678&amp;hello=joe+my.host.com+xmcd+2.1&amp;proto=1
+
+The server will perform the implied "proto" and "cddb hello" commands,
+and then perform the requested "cddb read" command.
+
+Server response to the command is encapsulated in the HTTP server response,
+and appears in the "Entity-Body" exactly as it would appear using the CDDBP
+protocol. Note that the HTTP response "Entity-Header" is not guaranteed to
+contain a "Content-Length" field, so clients should be prepared to accept
+variable length input. This is no different from operation under CDDBP. The
+header will always contain a Mime "Content-Type" field which describes the
+body of data as "text/plain".
+
+For more detailed information on HTTP and Mime, see RFC 1945 and RFC 1521.
+</pre>
+	</tr></td>
+	<tr><td align=center><font face=Arial,Helvetica>
+	&nbsp;
+	</tr></td>
+	</table></tr></td></table></center></td><td>&nbsp;</td>
+
+
+</tr></table></td></tr></table><br><br>
+
+<font face=Arial,Helvetica size=1><center>
+<br>
+<br>
+<br>
+<br>
+</body>
+</html>
--- /dev/null
+++ b/acme/bin/source/acd/discid
@@ -1,0 +1,159 @@
+
+CDDB DISCID
+-----------
+
+Both forms of CDDB access require that the software compute a "disc
+ID" which is an identifier that is used to access the CDDB.  The disc
+ID is a 8-digit hexadecimal (base-16) number, computed using data from
+a CD's Table-of-Contents (TOC) in MSF (Minute Second Frame) form.  The
+algorithm is listed below in Appendix A.
+
+It is crucial that your software compute the disc ID correctly.  If it
+does not generate the correct disc ID, it will not be compatible with CDDB.
+Moreover, if your software submits CDDB entries with bad disc IDs to the
+CDDB archives, it could compromise the integrity of the CDDB.
+
+[...]
+
+APPENDIX A - CDDB DISCID ALGORITHM
+----------------------------------
+
+The following is a C code example that illustrates how to generate the
+CDDB disc ID. [...] A text description
+of the algorithm follows, which should contain the necessary information
+to code the algorithm in any programming language.
+
+
+struct toc {
+        int     min;
+        int     sec;
+        int     frame;
+};
+
+struct toc cdtoc[100];
+
+int
+read_cdtoc_from_drive(void)
+{
+        /* Do whatever is appropriate to read the TOC of the CD
+         * into the cdtoc[] structure array.
+         */
+        return (tot_trks);
+}
+
+int
+cddb_sum(int n)
+{
+        int     ret;
+
+        /* For backward compatibility this algorithm must not change */
+
+        ret = 0;
+
+        while (n > 0) {
+                ret = ret + (n % 10);
+                n = n / 10;
+        }
+
+        return (ret);
+}
+
+unsigned long
+cddb_discid(int tot_trks)
+{
+        int     i,
+                t = 0,
+                n = 0;
+
+        /* For backward compatibility this algorithm must not change */
+
+        i = 0;
+
+        while (i < tot_trks) {
+                n = n + cddb_sum((cdtoc[i].min * 60) + cdtoc[i].sec);
+                i++;
+        }
+
+        t = ((cdtoc[tot_trks].min * 60) + cdtoc[tot_trks].sec) -
+            ((cdtoc[0].min * 60) + cdtoc[0].sec);
+
+        return ((n % 0xff) << 24 | t << 8 | tot_trks);
+}
+
+main()
+{
+        int tot_trks;
+
+        tot_trks = read_cdtoc_from_drive();
+        printf("The discid is %08x", cddb_discid(tot_trks));
+}
+
+
+This code assumes that your compiler and architecture support 32-bit
+integers.
+
+The cddb_discid function computes the discid based on the CD's TOC data
+in MSF form.  The frames are ignored for this purpose.  The function is
+passed a parameter of tot_trks (which is the total number of tracks on
+the CD), and returns the discid integer number.
+
+It is assumed that cdtoc[] is an array of data structures (records)
+containing the fields min, sec and frame, which are the minute, second
+and frame offsets (the starting location) of each track.  This
+information is read from the TOC of the CD.  There are actually
+tot_trks + 1 "active" elements in the array, the last one being the
+offset of the lead-out (also known as track 0xAA).
+
+The function loops through each track in the TOC, and for each track
+it takes the (M * 60) + S (total offset in seconds) of the track and
+feeds it to cddb_sum() function, which simply adds the value of each digit
+in the decimal string representation of the number. A running sum of this
+result for each track is kept in the variable n.
+
+At the end of the loop:
+1. t is calculated by subtracting the (M * 60) + S offset of the lead-out
+minus the (M * 60) + S offset of first track (yielding the length of
+the disc in seconds).
+
+2. The result of (n modulo FFh) is left-shifted by 24 bits.
+
+3. t is left shifted by 8.
+
+The bitwise-OR operation of result 2., 3. and the tot_trks number is
+used as the discid.
+
+The discid is represented in hexadecimal form for the purpose of
+xmcd cddb file names and the DISCID= field in the xmcd cddb file itself.
+If the hexadecimal string is less than 8 characters long, it is
+zero-padded to 8 characters (i.e., 3a8f07 becomes 003a8f07).  All
+alpha characters in the string should be in lower case, where
+applicable.
+
+Important note for clients using the MS-Windows MCI interface:
+
+The Windows MCI interface does not provide the MSF location of the
+lead-out.  Thus, you must compute the lead-out location by taking the
+starting position of the last track and add the length of the last track
+to it.  However, the MCI interface returns the length of the last track
+as ONE FRAME SHORT of the actual length found in the CD's TOC.  In most
+cases this does not affect the disc ID generated, because we truncate
+the frame count when computing the disc ID anyway.  However, if the
+lead-out track has an actual a frame count of 0, the computed quantity
+(based on the MSF data returned from the MCI interface) would result in
+the seconds being one short and the frame count be 74.  For example,
+a CD with the last track at an offset of 48m 32s 12f and having a
+track length of 2m 50s 63f has a lead-out offset of 51m 23s 0f. Windows
+MCI incorrectly reports the length as 2m 50s 62f, which would yield a
+lead-out offset of 51m 22s 74f, which causes the resulting truncated
+disc length to be off by one second.  This will cause an incorrect disc
+ID to be generated. You should thus add one frame to the length of the
+last track when computing the location of the lead-out.
+
+The easiest way for Windows clients to compute the lead-out given information
+in MSF format is like this:
+
+(offset_minutes * 60 * 75) + (offset_seconds * 75) + offset_frames +
+(length_minutes * 60 * 75) + (length_seconds * 75) + length_frames + 1 = X
+
+Where X is the offset of the lead-out in frames. To find the lead-out in
+seconds, simply divide by 75 and discard the remainder.
--- /dev/null
+++ b/acme/bin/source/acd/mailinglist
@@ -1,0 +1,220 @@
+<html><html>
+<head>
+<title>::freedb.org::</title>
+</head>
+
+<body bgcolor="#FFFFFF" text="#000000" link="#101070" vlink="#101070">
+
+<center>
+
+<table cellpadding=0 cellspacing=0 border=0 width="99%" align=center><tr><td align=left>
+<a href="/">
+<table border=0>
+<td bgcolor="#ffffff">
+ <table border=0 width=100% cellpadding=0 cellspacing=0>
+  <td bgcolor=#101070>
+   <table border=0>
+    <td bgcolor=#ffffff><font face="Arial,Helvetica,Lucida" color="#101070" size=8><b>freedb</b></td>
+    <td bgcolor=#101070><font face="Arial,Helvetica,Lucida" color="#ffffff" size=8><b>.org</b></td>
+   </table>
+  </td>
+  <tr>
+  <td align=right><font face="Arial,Helvetica,Lucida" color="#101070" size=2><b>a free approach to cddbp
+ </table>
+</td>
+</table>
+</a>
+</td><td align=right width=100%>
+	<form action="search.php" method=post>
+	<font face=Arial,Helvetica size=2><input type=name name=query width=20 size=20 length=20>
+	</td>
+	<td align=right>&nbsp;&nbsp;<input type=image src=images/menu/english/search.gif border=0 align=middle></td>
+	</form>
+
+</td></tr></table><br>
+<table cellpadding=0 cellspacing=0 border=0 width="99%" bgcolor=101070><tr><td>
+<table cellpadding=5 cellspacing=1 border=0 width="100%" bgcolor=FFFFFF><tr><td>
+<font face=Lucida,Verdana,Arial,Helvetica size=2>freedb.org - a free approach to cddbp</td></tr></table></td></tr></table><P>
+
+<table width="99%" align=center cellpadding=0 cellspacing=0 border=0><tr>
+	<td valign=top rowspan=5>
+
+<table border=0><tr><td>
+
+	<table width="115" border="0" cellpadding="0" cellspacing="0"><tr valign="top" bgcolor="#101070">
+                 <td bgcolor="#FFFFFF"><img src="themes/SlashOcean/pix.gif" width="3" height="3"></td>
+                 <td><img src="themes/SlashOcean/cl.gif" width="7" height="10"></td>
+                 <td><font face="verdana,helvetica,arial" size="1" color="#ffffff"><B>Main Menu</B></font></td>
+                 <td align="right"><img src="themes/SlashOcean/cr.gif" width="7" height="10" alt=""></td>
+                 <td bgcolor="#FFFFFF" align="right"><img src="themes/SlashOcean/pix.gif" width="3" height="3"></td>
+	</tr></table>
+	<table width="100%" border="0" cellpadding="0" cellspacing="0">
+	<tr bgcolor="#101070"><td colspan="3"><img src="themes/SlashOcean/pix.gif" width="1" height="1"></td></tr>
+	<tr bgcolor="#ffffff">
+		<td background="themes/SlashOcean/sl.gif"><img src="themes/SlashOcean/pix.gif" width="3" height="3" alt=""></td>
+		<td width="100%">
+		<table width="100%" border="0" cellpadding="5" cellspacing="0"><tr>
+		<td><font face="verdana,helvetica,arial" size="1">
+		<li><a href=index.php>Home</a>
+<li><a href=topics.php>News-Topics</a>
+<li><a href=sections.php?op=listarticles&secid=1>About</a>
+<li><a href=sections.php?op=listarticles&secid=2>Developers</a>
+<li><a href=sections.php?op=listarticles&secid=3>Applications</a>
+<li><a href=sections.php?op=listarticles&secid=7>Download</a>
+<li><a href=forum/index.php>Forum</a>
+<li><a href=http://freedb.music.sk/search/>Web-based Search</a>
+<li><a href=links.php>Web Links</a>
+<li><a href=user.php>Your Account</a>
+<li><a href=submit.php>Submit News</a>
+
+		</font></td>
+		</tr></table>
+
+		
+	</td><td background="themes/SlashOcean/sr.gif" align="right"><img src="themes/SlashOcean/pix.gif" width="3" height="3" alt=""></td>
+      </tr>
+	<tr bgcolor="#101070"><td colspan="3"><img src="themes/SlashOcean/pix.gif" width="1" height="1"></td></tr>
+	</table>
+	</td>
+
+
+
+
+
+
+</tr></td></table>
+<br>
+<table border=0><tr><td>
+
+	<table width="115" border="0" cellpadding="0" cellspacing="0"><tr valign="top" bgcolor="#101070">
+                 <td bgcolor="#FFFFFF"><img src="themes/SlashOcean/pix.gif" width="3" height="3"></td>
+                 <td><img src="themes/SlashOcean/cl.gif" width="7" height="10"></td>
+                 <td><font face="verdana,helvetica,arial" size="1" color="#ffffff"><B>FAQ</B></font></td>
+                 <td align="right"><img src="themes/SlashOcean/cr.gif" width="7" height="10" alt=""></td>
+                 <td bgcolor="#FFFFFF" align="right"><img src="themes/SlashOcean/pix.gif" width="3" height="3"></td>
+	</tr></table>
+	<table width="100%" border="0" cellpadding="0" cellspacing="0">
+	<tr bgcolor="#101070"><td colspan="3"><img src="themes/SlashOcean/pix.gif" width="1" height="1"></td></tr>
+	<tr bgcolor="#ffffff">
+		<td background="themes/SlashOcean/sl.gif"><img src="themes/SlashOcean/pix.gif" width="3" height="3" alt=""></td>
+		<td width="100%">
+		<table width="100%" border="0" cellpadding="5" cellspacing="0"><tr>
+		<td><font face="verdana,helvetica,arial" size="1">
+		Our FAQ can be found <a href="http://freedb.freedb.org/sections.php?op=viewarticle&artid=26">here</a>.<br>
+Please read the FAQ before asking questions via email.		</font></td>
+		</tr></table>
+
+		
+	</td><td background="themes/SlashOcean/sr.gif" align="right"><img src="themes/SlashOcean/pix.gif" width="3" height="3" alt=""></td>
+      </tr>
+	<tr bgcolor="#101070"><td colspan="3"><img src="themes/SlashOcean/pix.gif" width="1" height="1"></td></tr>
+	</table>
+	</td>
+
+</tr></td></table>
+<br>
+<table border=0><tr><td>
+
+	<table width="115" border="0" cellpadding="0" cellspacing="0"><tr valign="top" bgcolor="#101070">
+                 <td bgcolor="#FFFFFF"><img src="themes/SlashOcean/pix.gif" width="3" height="3"></td>
+                 <td><img src="themes/SlashOcean/cl.gif" width="7" height="10"></td>
+                 <td><font face="verdana,helvetica,arial" size="1" color="#ffffff"><B>Contact</B></font></td>
+                 <td align="right"><img src="themes/SlashOcean/cr.gif" width="7" height="10" alt=""></td>
+                 <td bgcolor="#FFFFFF" align="right"><img src="themes/SlashOcean/pix.gif" width="3" height="3"></td>
+	</tr></table>
+	<table width="100%" border="0" cellpadding="0" cellspacing="0">
+	<tr bgcolor="#101070"><td colspan="3"><img src="themes/SlashOcean/pix.gif" width="1" height="1"></td></tr>
+	<tr bgcolor="#ffffff">
+		<td background="themes/SlashOcean/sl.gif"><img src="themes/SlashOcean/pix.gif" width="3" height="3" alt=""></td>
+		<td width="100%">
+		<table width="100%" border="0" cellpadding="5" cellspacing="0"><tr>
+		<td><font face="verdana,helvetica,arial" size="1">
+		General questions:<br>
+<a href="mailto:info@freedb.org">info@freedb.org</a><hr>
+Databaseupdates:<br>
+<a href="mailto:updates@freedb.org">updates@freedb.org</a><br>
+(<b>NOT</b> for submission!)<hr>
+Please keep in mind that we are NOT the Nero-Support and please do not send CD-submits to the adresses above.<br>
+Submits have to go to:<br>
+<a href="mailto:freedb-submit@freedb.org">freedb-submit@freedb.org</a>		</font></td>
+		</tr></table>
+
+		
+	</td><td background="themes/SlashOcean/sr.gif" align="right"><img src="themes/SlashOcean/pix.gif" width="3" height="3" alt=""></td>
+      </tr>
+	<tr bgcolor="#101070"><td colspan="3"><img src="themes/SlashOcean/pix.gif" width="1" height="1"></td></tr>
+	</table>
+	</td>
+
+</tr></td></table>
+<br>
+<table border=0><tr><td>
+
+	<table width="115" border="0" cellpadding="0" cellspacing="0"><tr valign="top" bgcolor="#101070">
+                 <td bgcolor="#FFFFFF"><img src="themes/SlashOcean/pix.gif" width="3" height="3"></td>
+                 <td><img src="themes/SlashOcean/cl.gif" width="7" height="10"></td>
+                 <td><font face="verdana,helvetica,arial" size="1" color="#ffffff"><B>Downloads</B></font></td>
+                 <td align="right"><img src="themes/SlashOcean/cr.gif" width="7" height="10" alt=""></td>
+                 <td bgcolor="#FFFFFF" align="right"><img src="themes/SlashOcean/pix.gif" width="3" height="3"></td>
+	</tr></table>
+	<table width="100%" border="0" cellpadding="0" cellspacing="0">
+	<tr bgcolor="#101070"><td colspan="3"><img src="themes/SlashOcean/pix.gif" width="1" height="1"></td></tr>
+	<tr bgcolor="#ffffff">
+		<td background="themes/SlashOcean/sl.gif"><img src="themes/SlashOcean/pix.gif" width="3" height="3" alt=""></td>
+		<td width="100%">
+		<table width="100%" border="0" cellpadding="5" cellspacing="0"><tr>
+		<td><font face="verdana,helvetica,arial" size="1">
+		The link to the database downloads is <a href="/sections.php?op=viewarticle&artid=12">here</a>		</font></td>
+		</tr></table>
+
+		
+	</td><td background="themes/SlashOcean/sr.gif" align="right"><img src="themes/SlashOcean/pix.gif" width="3" height="3" alt=""></td>
+      </tr>
+	<tr bgcolor="#101070"><td colspan="3"><img src="themes/SlashOcean/pix.gif" width="1" height="1"></td></tr>
+	</table>
+	</td>
+
+
+</tr></td></table>
+<td>&nbsp;</td><td valign="top" width="100%">
+
+<!-- columna de inicio -->
+<center>
+	<table border=0 cellpadding=1 cellspacing=0 width=100% bgcolor=000000><tr><td>
+	<table border=0 cellpadding=8 cellspacing=0 width=100% bgcolor=FFFFFF>
+	<tr><td align=left><font face=Arial,Helvetica size=3>
+	<b>Mailinglists</b><br>
+	<font size=2>
+	<br><br>
+	There are a couple of mailinglists available:
+
+<h4>fdb-apps@freedb.org</h4>
+
+This mailinglist is intended for all developers that want to
+exchange tips and tricks, codesnippets and so on. Subcribe
+to this list by sending an email to<br>
+<a href="mailto:fdb-apps-request@freedb.org">fdb-apps-request@freedb.org</a><br>
+with the word "subscribe" in the body of the email.
+<h4>fdb-dev@freedb.org</h4>
+
+This list is for anyone interested in developing the freedb.org
+server software. Subcribe
+to this list by sending an email to<br>
+<a href="mailto:fdb-dev-request@freedb.org">fdb-dev-request@freedb.org</a><br>
+with the word "subscribe" in the body of the email.
+	</tr></td>
+	<tr><td align=center><font face=Arial,Helvetica>
+	&nbsp;
+	</tr></td>
+	</table></tr></td></table></center></td><td>&nbsp;</td>
+
+
+</tr></table></td></tr></table><br><br>
+
+<font face=Arial,Helvetica size=1><center>
+<br>
+<br>
+<br>
+<br>
+</body>
+</html>
--- /dev/null
+++ b/acme/bin/source/acd/main.c
@@ -1,0 +1,135 @@
+#include "acd.h"
+
+int debug;
+
+void
+usage(void)
+{
+	fprint(2, "usage: acd dev\n");
+	threadexitsall("usage");
+}
+
+Alt
+mkalt(Channel *c, void *v, int op)
+{
+	Alt a;
+
+	memset(&a, 0, sizeof(a));
+	a.c = c;
+	a.v = v;
+	a.op = op;
+	return a;
+}
+
+void
+freetoc(Toc *t)
+{
+	int i;
+
+	free(t->title);
+	for(i=0; i<t->ntrack; i++)
+		free(t->track[i].title);
+}
+
+void
+eventwatcher(Drive *d)
+{
+	enum { STATUS, WEVENT, TOCDISP, DBREQ, DBREPLY, NALT };
+	Alt alts[NALT+1];
+	Toc nt, tdb;
+	Event *e;
+	Window *w;
+	Cdstatus s;
+	char buf[40];
+
+	w = d->w;
+
+	alts[STATUS] = mkalt(d->cstatus, &s, CHANRCV);
+	alts[WEVENT] = mkalt(w->cevent, &e, CHANRCV);
+	alts[TOCDISP] = mkalt(d->ctocdisp, &nt, CHANRCV);
+	alts[DBREQ] = mkalt(d->cdbreq, &tdb, CHANNOP);
+	alts[DBREPLY] = mkalt(d->cdbreply, &nt, CHANRCV);
+	alts[NALT] = mkalt(nil, nil, CHANEND);
+	for(;;) {
+		switch(alt(alts)) {
+		case STATUS:
+			//DPRINT(2, "s...");
+			d->status = s;
+			if(s.state == Scompleted) {
+				s.state = Sunknown;
+				advancetrack(d, w);
+			}
+			//DPRINT(2, "status %d %d %d %M %M\n", s.state, s.track, s.index, s.abs, s.rel);
+			sprint(buf, "%d:%2.2d", s.rel.m, s.rel.s);
+			setplaytime(w, buf);
+			break;
+		case WEVENT:
+			//DPRINT(2, "w...");
+			acmeevent(d, w, e);
+			break;
+		case TOCDISP:
+			//DPRINT(2,"td...");
+			freetoc(&d->toc);
+			d->toc = nt;
+			drawtoc(w, d, &d->toc);
+			tdb = nt;
+			alts[DBREQ].op = CHANSND;
+			break;
+		case DBREQ:	/* sent */
+			//DPRINT(2,"dreq...");
+			alts[DBREQ].op = CHANNOP;
+			break;
+		case DBREPLY:
+			//DPRINT(2,"drep...");
+			freetoc(&d->toc);
+			d->toc = nt;
+			redrawtoc(w, &d->toc);
+			break;
+		}
+	}
+}
+
+void
+threadmain(int argc, char **argv)
+{
+	Scsi *s;
+	Drive *d;
+	char buf[80];
+
+	ARGBEGIN{
+	case 'v':
+		debug++;
+		scsiverbose++;
+	}ARGEND
+
+	if(argc != 1)
+		usage();
+
+	fmtinstall('M', msfconv);
+
+	if((s = openscsi(argv[0])) == nil)
+		error("opening scsi: %r");
+
+	d = malloc(sizeof(*d));
+	if(d == nil)
+		error("out of memory");
+	memset(d, 0, sizeof d);
+
+	d->scsi = s;
+	d->w = newwindow();
+	d->ctocdisp = chancreate(sizeof(Toc), 0);
+	d->cdbreq = chancreate(sizeof(Toc), 0);
+	d->cdbreply = chancreate(sizeof(Toc), 0);
+	d->cstatus = chancreate(sizeof(Cdstatus), 0);
+
+	proccreate(wineventproc, d->w, STACK);
+	proccreate(cddbproc, d, STACK);
+	proccreate(cdstatusproc, d, STACK);
+
+	cleanname(argv[0]);
+	snprint(buf, sizeof(buf), "%s/", argv[0]);
+	winname(d->w, buf);
+
+	wintagwrite(d->w, "Stop Pause Resume Eject Ingest ", 5+6+7+6+7);
+	eventwatcher(d);
+}
--- /dev/null
+++ b/acme/bin/source/acd/mkfile
@@ -1,0 +1,22 @@
+</$objtype/mkfile
+
+TARG=acd
+BIN=/acme/bin/$objtype
+
+OFILES=\
+	acme.$O\
+	cddb.$O\
+	main.$O\
+	mmc.$O\
+	util.$O\
+	win.$O\
+
+HFILES=acd.h
+
+UPDATE=\
+	mkfile\
+	$HFILES\
+	${OFILES:%.$O=%.c}\
+	${TARG:%=/acme/bin/386/%}\
+
+</sys/src/cmd/mkone
--- /dev/null
+++ b/acme/bin/source/acd/mmc.c
@@ -1,0 +1,303 @@
+#include "acd.h"
+
+int
+msfconv(Fmt *fp)
+{
+	Msf m;
+
+	m = va_arg(fp->args, Msf);
+	fmtprint(fp, "%d.%d.%d", m.m, m.s, m.f);
+	return 0;
+}
+
+static int
+status(Drive *d)
+{
+	uchar cmd[12];
+
+	memset(cmd, 0, sizeof cmd);
+	cmd[0] = 0xBD;
+	return scsi(d->scsi, cmd, sizeof cmd, nil, 0, Snone);
+}
+
+static int
+playmsf(Drive *d, Msf start, Msf end)
+{
+	uchar cmd[12];
+
+	memset(cmd, 0, sizeof cmd);
+	cmd[0] = 0x47;
+	cmd[3] = start.m;
+	cmd[4] = start.s;
+	cmd[5] = start.f;
+	cmd[6] = end.m;
+	cmd[7] = end.s;
+	cmd[8] = end.f;
+
+	return scsi(d->scsi, cmd, sizeof cmd, nil, 0, Snone);
+}
+
+int
+playtrack(Drive *d, int start, int end)
+{
+	Toc *t;
+
+	t = &d->toc;
+
+	if(t->ntrack == 0)
+		return -1;
+
+	if(start < 0)
+		start = 0;
+	if(end >= t->ntrack)
+		end = t->ntrack-1;
+	if(end < start)
+		end = start;
+
+	return playmsf(d, t->track[start].start, t->track[end].end);
+}
+
+int
+resume(Drive *d)
+{
+	uchar cmd[12];
+
+	memset(cmd, 0, sizeof cmd);
+	cmd[0] = 0x4B;
+	cmd[8] = 0x01;
+	return scsi(d->scsi, cmd, sizeof cmd, nil, 0, Snone);
+}
+
+int
+pause(Drive *d)
+{
+	uchar cmd[12];
+
+	memset(cmd, 0, sizeof cmd);
+	cmd[0] = 0x4B;
+	return scsi(d->scsi, cmd, sizeof cmd, nil, 0, Snone);
+}
+
+int
+stop(Drive *d)
+{
+	uchar cmd[12];
+
+	memset(cmd, 0, sizeof cmd);
+	cmd[0] = 0x4E;
+	return scsi(d->scsi, cmd, sizeof cmd, nil, 0, Snone);
+}
+
+int
+eject(Drive *d)
+{
+	uchar cmd[12];
+
+	memset(cmd, 0, sizeof cmd);
+	cmd[0] = 0x1B;
+	cmd[1] = 1;
+	cmd[4] = 2;
+	return scsi(d->scsi, cmd, sizeof cmd, nil, 0, Snone);
+}
+
+int
+ingest(Drive *d)
+{
+	uchar cmd[12];
+
+	memset(cmd, 0, sizeof cmd);
+	cmd[0] = 0x1B;
+	cmd[1] = 1;
+	cmd[4] = 3;
+	return scsi(d->scsi, cmd, sizeof cmd, nil, 0, Snone);
+}
+
+static Msf
+rdmsf(uchar *p)
+{
+	Msf msf;
+
+	msf.m = p[0];
+	msf.s = p[1];
+	msf.f = p[2];
+	return msf;
+}
+
+static ulong
+rdlba(uchar *p)
+{
+	return (p[0]<<16) | (p[1]<<8) | p[2];
+}
+
+/* not a Drive, so that we don't accidentally touch Drive.toc */
+int
+gettoc(Scsi *s, Toc *t)
+{
+	int i, n;
+	uchar cmd[12];
+	uchar resp[1024];
+
+Again:
+	memset(t, 0, sizeof(*t));
+	memset(cmd, 0, sizeof cmd);
+	cmd[0] = 0x43;
+	cmd[1] = 0x02;
+	cmd[7] = sizeof(resp)>>8;
+	cmd[8] = sizeof(resp);
+
+	s->changetime = 1;
+	/* scsi sets nchange, changetime */
+	if(scsi(s, cmd, sizeof cmd, resp, sizeof(resp), Sread) < 4)
+		return -1;
+
+	if(s->changetime == 0) {
+		t->ntrack = 0;
+		werrstr("no media");
+		return -1;
+	}
+
+	if(t->nchange == s->nchange && t->changetime != 0)
+		return 0;
+
+	t->nchange = s->nchange;
+	t->changetime = s->changetime;
+
+	if(t->ntrack > MTRACK)
+		t->ntrack = MTRACK;
+
+DPRINT(2, "%d %d\n", resp[3], resp[2]);
+	t->ntrack = resp[3]-resp[2]+1;
+	t->track0 = resp[2];
+
+	n = ((resp[0]<<8) | resp[1])+2;
+	if(n < 4+8*(t->ntrack+1)) {
+		werrstr("bad read0 %d %d", n, 4+8*(t->ntrack+1));
+		return -1;
+	}
+
+	for(i=0; i<=t->ntrack; i++)		/* <=: track[ntrack] = end */
+		t->track[i].start = rdmsf(resp+4+i*8+5);
+
+	for(i=0; i<t->ntrack; i++)
+		t->track[i].end = t->track[i+1].start;
+
+	memset(cmd, 0, sizeof cmd);
+	cmd[0] = 0x43;
+	cmd[7] = sizeof(resp)>>8;
+	cmd[8] = sizeof(resp);
+	if(scsi(s, cmd, sizeof cmd, resp, sizeof(resp), Sread) < 4)
+		return -1;
+
+	if(s->changetime != t->changetime || s->nchange != t->nchange) {
+		fprint(2, "disk changed underfoot; repeating\n");
+		goto Again;
+	}
+
+	n = ((resp[0]<<8) | resp[1])+2;
+	if(n < 4+8*(t->ntrack+1)) {
+		werrstr("bad read");
+		return -1;
+	}
+
+	for(i=0; i<=t->ntrack; i++)
+		t->track[i].bstart = rdlba(resp+4+i*8+5);
+
+	for(i=0; i<t->ntrack; i++)
+		t->track[i].bend = t->track[i+1].bstart;
+
+	return 0;
+}
+
+static void
+dumptoc(Toc *t)
+{
+	int i;
+
+	fprint(1, "%d tracks\n", t->ntrack);
+	for(i=0; i<t->ntrack; i++)
+		print("%d. %M-%M (%lud-%lud)\n", i+1,
+			t->track[i].start, t->track[i].end,
+			t->track[i].bstart, t->track[i].bend);
+}
+
+static void
+ping(Drive *d)
+{
+	uchar cmd[12];
+
+	memset(cmd, 0, sizeof cmd);
+	cmd[0] = 0x43;
+	scsi(d->scsi, cmd, sizeof(cmd), nil, 0, Snone);
+}
+
+static int
+playstatus(Drive *d, Cdstatus *stat)
+{
+	uchar cmd[12], resp[16];
+
+	memset(cmd, 0, sizeof cmd);
+	cmd[0] = 0x42;
+	cmd[1] = 0x02;
+	cmd[2] = 0x40;
+	cmd[3] = 0x01;
+	cmd[7] = sizeof(resp)>>8;
+	cmd[8] = sizeof(resp);
+	if(scsi(d->scsi, cmd, sizeof(cmd), resp, sizeof(resp), Sread) < 0)
+		return -1;
+
+	switch(resp[1]){
+	case 0x11:
+		stat->state = Splaying;
+		break;
+	case 0x12:
+		stat->state = Spaused;
+		break;
+	case 0x13:
+		stat->state = Scompleted;
+		break;
+	case 0x14:
+		stat->state = Serror;
+		break;
+	case 0x00:	/* not supported */
+	case 0x15:	/* no current status to return */
+	default:
+		stat->state = Sunknown;
+		break;
+	}
+
+	stat->track = resp[6];
+	stat->index = resp[7];
+	stat->abs = rdmsf(resp+9);
+	stat->rel = rdmsf(resp+13);
+	return 0;
+}
+
+void
+cdstatusproc(void *v)
+{
+	Drive *d;
+	Toc t;
+	Cdstatus s;
+
+	t.changetime = ~0;
+	t.nchange = ~0;
+
+	threadsetname("cdstatusproc");
+	d = v;
+	DPRINT(2, "cdstatus %d\n", getpid());
+	for(;;) {
+		ping(d);
+	//DPRINT(2, "d %d %d t %d %d\n", d->scsi->changetime, d->scsi->nchange, t.changetime, t.nchange);
+		if(playstatus(d, &s) == 0)
+			send(d->cstatus, &s);
+		if(d->scsi->changetime != t.changetime || d->scsi->nchange != t.nchange) {
+			if(gettoc(d->scsi, &t) == 0) {
+				DPRINT(2, "sendtoc...\n");
+				if(debug) dumptoc(&t);
+				send(d->ctocdisp, &t);
+			} else
+				DPRINT(2, "error: %r\n");
+		}
+		sleep(1000);
+	}
+}
--- /dev/null
+++ b/acme/bin/source/acd/outline
@@ -1,0 +1,32 @@
+acd is composed of four procs
+
+wineventproc (win.c:/^wineventproc)
+	reads acme window events, sends them along w->cevent.
+
+cdstatusproc (mmc.c:/^cdstatusproc)
+	reads cd status once per second, sending
+		status updates to d->cstatus.
+	detects disk changes, sends new tocs to d->ctocdisp.
+
+cddbproc (cddb.c:/^cddbproc)
+	reads tocs from d->cdbreq, if it finds
+		translations in the cddb, sends new tocs to d->cdbreply.
+
+eventwatcher (main.c:/^eventwatcher)
+	the main event loop.
+		reads status from d->cstatus.
+		reads events from w->cevent.
+		reads new tocs to display from d->ctocdisp.
+		sends new tocs to translate to d->cdbreq.
+		reads new translated tocs from d->cdbreply.
+
+an interesting bug in the original design:
+	both cdstatusproc and the eventwatcher proc
+	issue scsi commands.  (the eventwatcher responds to
+	things such as Play, Stop, etc., as well as advancing the track.)
+
+	the sd(3) driver did not expect overlapped commands,
+	and crashed.
+
+	this has been fixed by making the scsi(2) commands threadsafe,
+	and making the sd(3) driver more robust.
--- /dev/null
+++ b/acme/bin/source/acd/submit
@@ -1,0 +1,220 @@
+CDDB SUBMISSION
+---------------
+
+Your software may allow users to enter CDDB data and then submit them
+to the freedb archive.  
+There are two methods of submission: <a href="#email">via e-mail</a> or <a href="#http">via http</a> using submit.cgi
+
+<a name="email"></a>1. Submission via e-mail
+------------------------
+
+Your software has to send the entry to the
+following address:
+
+	freedb-submit@freedb.org
+
+You may implement a button or somesuch in your software's user-interface
+to facilitate this.  The destination e-mail address should be made
+user-configurable.
+
+There should be one e-mail message per freedb entry.  The mail Subject
+line should be in the form "cddb category discid".  For example:
+
+Subject: cddb rock 850f970b
+
+The body of the e-mail message should be in the format of a CDDB file
+entry as described <a href="http://freedb.freedb.org/software/old/DBFORMAT">here</a>.  The messages should contain only
+plain ASCII text.  Do not attach encoded information or add special
+escape sequences.
+
+Note that the disc ID specified in the mail Subject line should
+also appear in the list of disc IDs in the DISCID= field of the
+CDDB file entry.  If not, it is considered an error and the submission
+will be rejected.
+
+You should only allow categories that are currently supported by the
+freedb (blues, classical, country, data, folk, jazz, misc, newage,
+reggae, rock, soundtrack).  Submissions specifying unsupported
+categories will be rejected.
+
+Please do not allow a user to submit CD database entries that
+have completely unfilled contents (i.e., blank information in the
+disc artist/title as well as the track titles, or filled with
+useless default information like "track 1", "track 2", etc.).
+While the current CD database server checks and rejects submissions
+that have a blank DTITLE line, it doesn't (and can't feasibly) check
+the track titles effectively, nor can it check any of these fields
+if they are filled with a default string.  If it were, it would
+have to be hacked to know about the default strings of every possible
+client.
+
+Thus, please design your client with this in mind.  This is a somewhat
+tricky thing to do, as some CDs contain blank tracks with no titles
+and you need to allow for that.  An example minimum requirement
+that a CD player client should meet is listed below:
+
+1. Don't allow the "send" or "submit" feature to be activated if
+   the CD database information form is not edited at all.
+2. Check that the disc artist/title contains something (that the user
+   typed in).
+3. Check that all of the tracks have a title filled in by the user 
+   (some (but not all!) may be blank, but not the default string).
+
+This should minimize the number of useless garbage being submitted
+into the CD database.
+
+Before you release your software, please be sure that it produces
+submissions that adheres to the CDDB file format, and that the frame
+offset, disc length, and disc ID information are correctly computed.
+For testing, please make your software send submissions to the
+following e-mail address (rather than the real submission site at
+freedb-submit@freedb.org):
+
+	test-submit@freedb.org
+
+The test address performs sanity checking on the CDDB submission and
+sends back pass/fail confirmation, but does not actually deposit the
+entry in the CD database.
+
+<a name="http"></a>2. Submission via http
+----------------------
+
+For submit via http, your application has to transmit the entry to the
+database through a CGI program at the following URL:
+
+http://freedb.freedb.org/~cddb/submit.cgi
+
+Submissions are made through the CGI program as follows. You must only use
+the "POST" method of sending data; "GET" is not supported. There are several
+HTTP "Entity-Header" fields that must be included in the data followed by a
+blank line, followed by the "Entity-Body" (a.k.a the CDDB entry) in the
+format described in Appendix B below. The required header fields are:
+
+Category: CDDB_category
+Discid: CDDB_discid
+User-Email: user@domain
+Submit-Mode: test_or_submit
+Content-Length: length_of_CDDB_entry
+
+Where:
+
+- "CDDB_category" is one of the valid CDDB categories (blues, classical,
+  country, data, folk, jazz, misc, newage, reggae, rock, soundtrack).
+  Invalid categories will result in the entry being rejected.
+
+- "CDDB_discid" is the 8-digit hex CDDB disc ID of the entry as described in
+  the "<a href="http://freedb.freedb.org/sections.php?op=viewarticle&artid=6">Discid howto</a>" section. This must be the same disc ID that appears
+  in the "DISCID=" section of the entry being submitted. If not, the entry
+  will be rejected.
+
+- "user@domain" is the valid email address of the user submitting the entry.
+  This is required in case a submission failure notice must be sent to the
+  user.
+
+- "test_or_submit" is the word "test" or "submit" (without the surrounding
+  quotes) to indicate whether the submission is a test submission or a real
+  submission to the database, respectively. See <a href="#testsubmission">below</a> for an explanation of
+  test submissions.
+
+- "length_of_CDDB_entry" is the size in bytes of the CDDB entry being
+  submitted. This number does not include the length of the header or the
+  blank line separating the HTTP header and the CDDB entry.
+
+There are several additional optional HTTP header fields that may also
+be specified (but which are currently not used by the freedb):
+
+Charset: character_set_of_CDDB_entry
+X-Cddbd-Note: message for user
+
+Where:
+
+- "character_set_of_CDDB_entry" is one of ISO-8859-1 or US-ASCII (lower case
+  may be used if desired). This specifies to the CDDB server which character
+  set the CDDB entry has been encoded in. If your application knows the
+  user's character set, then you should specify it here. Only these two
+  character sets are supported currently. DO NOT specify the character set
+  if your application does not have any way of verifying the user's character
+  set (i.e. do not guess; it's better not to specify it at all).
+
+- "message for user" is an arbitrary message to be included at the top of
+  any rejection notice that may be sent to the submitting user.
+
+An example submission showing the HTTP command, "Entity-Header" and "Entity-
+Body" follows:
+
+POST /~cddb/submit.cgi HTTP/1.0
+Category: rock
+Discid: 2a09310a
+User-Email: joe@joeshost.joesdomain.com
+Submit-Mode: submit
+Charset: ISO-8859-1
+X-Cddbd-Note: Problems with Super CD Player? Send email to support@supercd.com.
+Content-Length: 820
+
+# xmcd
+#
+# Track frame offsets:
+[ data omitted in this example for brevity ]
+PLAYORDER=
+
+Note the blank line between the "Content-Length" header field and the
+"# xmcd" which marks the beginning of the CDDB entry.
+
+When your application submits an entry through the CGI program, it will
+respond with a 3-digit response code indicating whether or not the entry has
+been forwarded to the freedb server for inclusion in the database, followed
+by a textual description of the response code. For example:
+
+200 OK, submission has been sent.
+400 Internal error: failed to forward submission.
+500 Missing required header information.
+
+These are but a few of the possible responses. 
+See the description of the <a href="http://freedb.freedb.org/sections.php?op=viewarticle&artid=28">CDDB server protocol</a> for more information on 
+handling response codes.
+
+The body of the freedb entry being submitted should be sent verbatim as
+described in the <a href="http://freedb.freedb.org/software/old/DBFORMAT">database-format specification</a>. DO NOT encode the data in any 
+way before transmitting it; data must be sent as raw text. For example, 
+Windows programmers should not use the Windows URL encode function prior to
+calling the submit CGI program. Doing so may lead to corrupt data being sent
+and also possibly to rejected submissions.
+
+You may implement a button or somesuch in your software's user interface
+to initiate submissions. Rejected submissions are automatically returned
+via email to the sender specified in the "User-Email" header field with an
+explanation of the reason for the rejection.
+
+Please do not allow a user to submit CD database entries that
+have completely unfilled contents (i.e., blank information in the
+disc artist/title as well as the track titles, or filled with
+useless default information like "track 1", "track 2", etc.).
+While the current CD database server checks and rejects submissions
+that have a blank DTITLE line, it doesn't (and can't feasibly) check
+the track titles effectively, nor can it check any of these fields
+if they are filled with a default string.  If it were, it would
+have to be hacked to know about the default strings of every possible
+client.
+
+Thus, please design your client with this in mind.  This is a somewhat
+tricky thing to do, as some CDs contain blank tracks with no titles
+and you need to allow for that.  An example minimum requirement
+that a CD player client should meet is listed below:
+
+1. Don't allow the "send" or "submit" feature to be activated if
+   the CD database information form is not edited at all.
+2. Check that the disc artist/title contains something (that the user
+   typed in).
+3. Check that all of the tracks have a title filled in by the user.
+   (some (but not all!) may be blank, but not the default string).
+	
+Before you release your software, please be sure that it produces
+submissions that adhere to the CDDB file format, and that the frame
+offset, disc length, and disc ID information are correctly computed.
+For testing, please make your software send submissions with the
+"Submit-Mode" HTTP header field set to "test".
+
+<a name="testsubmission"></a>CDDB submissions sent in test mode will be sanity-checked by the freedb server
+and pass/fail confirmation sent back to the submitter, but will not actually
+be deposited in the CD database. Please DO NOT send submisions in "submit"
+mode until you have tested your program with several different CD's.
--- /dev/null
+++ b/acme/bin/source/acd/toc.c
@@ -1,0 +1,59 @@
+#include "acd.h"
+
+Toc thetoc;
+
+void
+tocthread(void *v)
+{
+	Drive *d;
+
+	threadsetname("tocthread");
+	d = v;
+	DPRINT(2, "recv ctocdisp?...");
+	while(recv(d->ctocdisp, &thetoc) == 1) {
+		DPRINT(2, "recv ctocdisp!...");
+		drawtoc(d->w, &thetoc);
+		DPRINT(2, "send dbreq...\n");
+		send(d->ctocdbreq, &thetoc);
+	}
+}
+
+void
+freetoc(Toc *t)
+{
+	int i;
+
+	free(t->title);
+	for(i=0; i<t->ntrack; i++)
+		free(t->track[i].title);
+}
+
+void
+cddbthread(void *v)
+{
+	Drive *d;
+	Toc t;
+
+	threadsetname("cddbthread");
+	d = v;
+	while(recv(d->ctocdbreply, &t) == 1) {
+		if(thetoc.nchange == t.nchange) {
+			freetoc(&thetoc);
+			thetoc = t;
+			redrawtoc(d->w, &thetoc);
+		}
+	}
+}
+
+void
+cdstatusthread(void *v)
+{
+	Drive *d;
+	Cdstatus s;
+
+	d = v;
+	
+	for(;;)
+		recv(d->cstat, &s);
+
+}
--- /dev/null
+++ b/acme/bin/source/acd/util.c
@@ -1,0 +1,89 @@
+#include "acd.h"
+
+void*
+emalloc(uint n)
+{
+	void *p;
+
+	p = malloc(n);
+	if(p == nil)
+		error("can't malloc: %r");
+	memset(p, 0, n);
+	return p;
+}
+
+char*
+estrdup(char *s)
+{
+	char *t;
+
+	t = emalloc(strlen(s)+1);
+	strcpy(t, s);
+	return t;
+}
+
+char*
+estrstrdup(char *s, char *t)
+{
+	char *u;
+
+	u = emalloc(strlen(s)+strlen(t)+1);
+	strcpy(u, s);
+	strcat(u, t);
+	return u;
+}
+
+char*
+eappend(char *s, char *sep, char *t)
+{
+	char *u;
+
+	if(t == nil)
+		u = estrstrdup(s, sep);
+	else{
+		u = emalloc(strlen(s)+strlen(sep)+strlen(t)+1);
+		strcpy(u, s);
+		strcat(u, sep);
+		strcat(u, t);
+	}
+	free(s);
+	return u;
+}
+
+char*
+egrow(char *s, char *sep, char *t)
+{
+	s = eappend(s, sep, t);
+	free(t);
+	return s;
+}
+
+void
+error(char *fmt, ...)
+{
+	int n;
+	va_list arg;
+	char buf[256];
+
+	fprint(2, "Mail: ");
+	va_start(arg, fmt);
+	n = vsnprint(buf, sizeof buf, fmt, arg);
+	va_end(arg);
+	write(2, buf, n);
+	write(2, "\n", 1);
+	threadexitsall(fmt);
+}
+
+void
+ctlprint(int fd, char *fmt, ...)
+{
+	int n;
+	va_list arg;
+	char buf[256];
+
+	va_start(arg, fmt);
+	n = vsnprint(buf, sizeof buf, fmt, arg);
+	va_end(arg);
+	if(write(fd, buf, n) != n)
+		error("control file write error: %r");
+}
--- /dev/null
+++ b/acme/bin/source/acd/win.c
@@ -1,0 +1,320 @@
+#include "acd.h"
+
+Window*
+newwindow(void)
+{
+	char buf[12];
+	Window *w;
+
+	w = emalloc(sizeof(Window));
+	w->ctl = open("/mnt/wsys/new/ctl", ORDWR|OCEXEC);
+	if(w->ctl<0 || read(w->ctl, buf, 12)!=12)
+		error("can't open window ctl file: %r");
+	ctlprint(w->ctl, "noscroll\n");
+	w->id = atoi(buf);
+	w->event = winopenfile(w, "event");
+	w->addr = -1;	/* will be opened when needed */
+	w->body = nil;
+	w->data = -1;
+	w->cevent = chancreate(sizeof(Event*), 0);
+	if(w->cevent == nil)
+		error("cevent is nil: %r");
+	return w;
+}
+
+void
+winsetdump(Window *w, char *dir, char *cmd)
+{
+	if(dir != nil)
+		ctlprint(w->ctl, "dumpdir %s\n", dir);
+	if(cmd != nil)
+		ctlprint(w->ctl, "dump %s\n", cmd);
+}
+
+void
+wineventproc(void *v)
+{
+	Window *w;
+	int i;
+
+	threadsetname("wineventproc");
+	w = v;
+	for(i=0; ; i++){
+		if(i >= NEVENT)
+			i = 0;
+		wingetevent(w, &w->e[i]);
+		sendp(w->cevent, &w->e[i]);
+	}
+}
+
+int
+winopenfile(Window *w, char *f)
+{
+	char buf[64];
+	int fd;
+
+	sprint(buf, "/mnt/wsys/%d/%s", w->id, f);
+	fd = open(buf, ORDWR|OCEXEC);
+	if(fd < 0)
+		error("can't open window file %s: %r", f);
+	return fd;
+}
+
+void
+wintagwrite(Window *w, char *s, int n)
+{
+	int fd;
+
+	fd = winopenfile(w, "tag");
+	if(write(fd, s, n) != n)
+		error("tag write: %r");
+	close(fd);
+}
+
+void
+winname(Window *w, char *s)
+{
+	ctlprint(w->ctl, "name %s\n", s);
+}
+
+void
+winopenbody(Window *w, int mode)
+{
+	char buf[256];
+
+	sprint(buf, "/mnt/wsys/%d/body", w->id);
+	w->body = Bopen(buf, mode|OCEXEC);
+	if(w->body == nil)
+		error("can't open window body file: %r");
+}
+
+void
+winclosebody(Window *w)
+{
+	if(w->body != nil){
+		Bterm(w->body);
+		w->body = nil;
+	}
+}
+
+void
+winwritebody(Window *w, char *s, int n)
+{
+	if(w->body == nil)
+		winopenbody(w, OWRITE);
+	if(Bwrite(w->body, s, n) != n)
+		error("write error to window: %r");
+}
+
+int
+wingetec(Window *w)
+{
+	if(w->nbuf == 0){
+		w->nbuf = read(w->event, w->buf, sizeof w->buf);
+		if(w->nbuf <= 0){
+			/* probably because window has exited, and only called by wineventproc, so just shut down */
+			threadexits(nil);
+		}
+		w->bufp = w->buf;
+	}
+	w->nbuf--;
+	return *w->bufp++;
+}
+
+int
+wingeten(Window *w)
+{
+	int n, c;
+
+	n = 0;
+	while('0'<=(c=wingetec(w)) && c<='9')
+		n = n*10+(c-'0');
+	if(c != ' ')
+		error("event number syntax");
+	return n;
+}
+
+int
+wingeter(Window *w, char *buf, int *nb)
+{
+	Rune r;
+	int n;
+
+	r = wingetec(w);
+	buf[0] = r;
+	n = 1;
+	if(r >= Runeself) {
+		while(!fullrune(buf, n))
+			buf[n++] = wingetec(w);
+		chartorune(&r, buf);
+	} 
+	*nb = n;
+	return r;
+}
+
+void
+wingetevent(Window *w, Event *e)
+{
+	int i, nb;
+
+	e->c1 = wingetec(w);
+	e->c2 = wingetec(w);
+	e->q0 = wingeten(w);
+	e->q1 = wingeten(w);
+	e->flag = wingeten(w);
+	e->nr = wingeten(w);
+	if(e->nr > EVENTSIZE)
+		error("event string too long");
+	e->nb = 0;
+	for(i=0; i<e->nr; i++){
+		e->r[i] = wingeter(w, e->b+e->nb, &nb);
+		e->nb += nb;
+	}
+	e->r[e->nr] = 0;
+	e->b[e->nb] = 0;
+	if(wingetec(w) != '\n')
+		error("event syntax error");
+}
+
+void
+winwriteevent(Window *w, Event *e)
+{
+	fprint(w->event, "%c%c%d %d\n", e->c1, e->c2, e->q0, e->q1);
+}
+
+static int
+nrunes(char *s, int nb)
+{
+	int i, n;
+	Rune r;
+
+	n = 0;
+	for(i=0; i<nb; n++)
+		i += chartorune(&r, s+i);
+	return n;
+}
+
+void
+winread(Window *w, uint q0, uint q1, char *data)
+{
+	int m, n, nr;
+	char buf[256];
+
+	if(w->addr < 0)
+		w->addr = winopenfile(w, "addr");
+	if(w->data < 0)
+		w->data = winopenfile(w, "data");
+	m = q0;
+	while(m < q1){
+		n = sprint(buf, "#%d", m);
+		if(write(w->addr, buf, n) != n)
+			error("error writing addr: %r");
+		n = read(w->data, buf, sizeof buf);
+		if(n <= 0)
+			error("reading data: %r");
+		nr = nrunes(buf, n);
+		while(m+nr >q1){
+			do; while(n>0 && (buf[--n]&0xC0)==0x80);
+			--nr;
+		}
+		if(n == 0)
+			break;
+		memmove(data, buf, n);
+		data += n;
+		*data = 0;
+		m += nr;
+	}
+}
+
+void
+windormant(Window *w)
+{
+	if(w->addr >= 0){
+		close(w->addr);
+		w->addr = -1;
+	}
+	if(w->body != nil){
+		Bterm(w->body);
+		w->body = nil;
+	}
+	if(w->data >= 0){
+		close(w->data);
+		w->data = -1;
+	}
+}
+
+
+int
+windel(Window *w, int sure)
+{
+	if(sure)
+		write(w->ctl, "delete\n", 7);
+	else if(write(w->ctl, "del\n", 4) != 4)
+		return 0;
+	/* event proc will die due to read error from event file */
+	windormant(w);
+	close(w->ctl);
+	w->ctl = -1;
+	close(w->event);
+	w->event = -1;
+	return 1;
+}
+
+void
+winclean(Window *w)
+{
+	if(w->body)
+		Bflush(w->body);
+	ctlprint(w->ctl, "clean\n");
+}
+
+int
+winsetaddr(Window *w, char *addr, int errok)
+{
+	if(w->addr < 0)
+		w->addr = winopenfile(w, "addr");
+	if(write(w->addr, addr, strlen(addr)) < 0){
+		if(!errok)
+			error("error writing addr(%s): %r", addr);
+		return 0;
+	}
+	return 1;
+}
+
+int
+winselect(Window *w, char *addr, int errok)
+{
+	if(winsetaddr(w, addr, errok)){
+		ctlprint(w->ctl, "dot=addr\n");
+		return 1;
+	}
+	return 0;
+}
+
+char*
+winreadbody(Window *w, int *np)	/* can't use readfile because acme doesn't report the length */
+{
+	char *s;
+	int m, na, n;
+
+	if(w->body != nil)
+		winclosebody(w);
+	winopenbody(w, OREAD);
+	s = nil;
+	na = 0;
+	n = 0;
+	for(;;){
+		if(na < n+512){
+			na += 1024;
+			s = realloc(s, na+1);
+		}
+		m = Bread(w->body, s+n, na-n);
+		if(m <= 0)
+			break;
+		n += m;
+	}
+	s[n] = 0;
+	winclosebody(w);
+	*np = n;
+	return s;
+}
--- /dev/null
+++ b/acme/bin/source/adict/_adict.c
@@ -1,0 +1,584 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include "win.h"
+#include "adict.h"
+
+char *prog = "adict";
+char *lprog = "/bin/adict";
+char *xprog  = "/bin/dict";
+char *dict, *pattern, *curaddr[MAXMATCH], *curone, *args[6], buffer[80];
+char abuffer[80], fbuffer[80], pbuffer[80];
+int curindex, count, Eopen, Mopen;
+Win Mwin, Ewin, Dwin;
+
+void openwin(char*, char*, Win*, int);
+void  handle(Win*, int);
+void	rexec(void*);
+void	pexec(void*);
+int getaddr(char*);
+
+void
+usage(void)
+{
+		threadprint(2, "usage: %s [-d dictname] [pattern]\n", argv0);
+		threadexitsall(nil);
+}
+
+void
+threadmain(int argc, char** argv)
+{
+	ARGBEGIN{
+	case 'd':
+		dict = strdup(ARGF());
+		break;
+	default:
+		usage();
+	}ARGEND
+
+	/* if running as other name, note that fact */
+	if(access(argv0, AEXIST) == 0)
+		lprog = argv0;
+
+	switch(argc){
+	case 1:
+		pattern = pbuffer;
+		strcpy(pattern,argv[0]);
+		if(dict == nil)
+			dict = "oed";
+		break;
+	case 0:
+		break;
+	default:
+		usage();
+	}
+
+	if ((dict == nil) && (pattern == nil))
+		openwin(prog,"", &Dwin, Dictwin);
+	if (pattern == nil)
+		openwin(prog,"",&Ewin, Entrywin);
+	if ((count = getaddr(pattern)) <= 1)
+		openwin(prog,"Prev Next", &Ewin, Entrywin);
+	else
+		openwin(prog, "", &Mwin, Matchwin);
+}
+
+static int
+procrexec(char *xprog, ...)
+{
+	int fpipe[2];
+	void *rexarg[4];
+	Channel *c;
+	va_list va;
+	int i;
+	char *p;
+
+	pipe(fpipe);
+	va_start(va, xprog);
+	p = xprog;
+	for(i=0; p && i+1<nelem(args); i++){
+		args[i] = p;
+		p = va_arg(va, char*);
+	}
+	args[i] = nil;
+
+	c = chancreate(sizeof(ulong), 0);
+	rexarg[0] = xprog;
+	rexarg[1] = args;
+	rexarg[2] = fpipe;
+	rexarg[3] = c;
+
+	proccreate(rexec, rexarg, 8192);
+	recvul(c);
+	chanfree(c);
+	close(fpipe[1]);
+	return fpipe[0];
+}
+
+int
+getaddr(char *pattern)
+{
+	/* Get char offset into dictionary of matches. */
+
+	int fd, i;
+	Biobuf inbuf;
+	char *bufptr;
+char *obuf;
+
+	if (pattern == nil) {
+		curone = nil;
+		curindex = 0;
+		curaddr[curindex] = nil;
+		return 0;
+	}
+
+	sprint(buffer,"/%s/A", pattern);
+	fd = procrexec(xprog, "-d", dict, "-c", buffer, nil);
+	Binit(&inbuf, fd, OREAD);
+	i = 0;
+	curindex = 0;
+	while ((bufptr = Brdline(&inbuf, '\n')) != nil && (i < (MAXMATCH-1))) {
+		bufptr[Blinelen(&inbuf)-1] = 0;
+obuf=bufptr;
+		while (bufptr[0] != '#' && bufptr[0] != 0) bufptr++;
+if(bufptr[0] == 0)
+	print("whoops buf «%s»\n", obuf);
+		curaddr[i] = malloc(strlen(bufptr));
+		strcpy(curaddr[i], bufptr);
+		i++;
+	}
+	curaddr[i] = nil;
+	if (i == MAXMATCH)
+		threadprint(2, "Too many matches!\n");
+	Bterm(&inbuf);
+	close(fd);
+
+	curone = curaddr[curindex];
+	return(i);
+}
+
+char*
+getpattern(char *addr)
+{
+	/* Get the pattern corresponding to an absolute address.*/
+	int fd;
+	char *res, *t;
+
+	res = nil;
+	sprint(buffer,"%sh", addr);
+	fd = procrexec(xprog, "-d", dict, "-c", buffer, nil);
+	if (read(fd, pbuffer, 80) > 80)
+		threadprint(2, "Error in getting addres from dict.\n");
+	else {
+		t = pbuffer;
+		/* remove trailing whitespace, newline */
+		if (t != nil){
+			while(*t != 0 && *t != '\n')
+				t++;
+			if(t == 0 && t > pbuffer)
+				t--;
+			while(t >= pbuffer && (*t==' ' || *t=='\n' || *t=='\t' || *t=='\r'))
+				*t-- = 0;
+		}
+		res = pbuffer;
+	}
+	close(fd);
+	return(res);
+}
+
+char*
+chgaddr(int dir)
+{
+	/* Increment or decrement the current address (curone). */
+
+	int fd;
+	char *res, *t;
+
+	res = nil;
+	if (dir < 0)
+		sprint(buffer,"%s-a", curone);
+	else
+		sprint(buffer,"%s+a", curone);
+	fd = procrexec(xprog, "-d", dict, "-c", buffer, nil);
+	if (read(fd, abuffer, 80) > 80)
+		threadprint(2, "Error in getting addres from dict.\n");
+	else {
+		res = abuffer;
+		while (*res != '#') res++;
+		t = res;
+		while ((*t != '\n') && (t != nil)) t++;
+		if (t != nil) *t = 0;
+	}
+	close(fd);
+	return(res);
+}
+
+void
+dispdicts(Win *cwin)
+{
+	/* Display available dictionaries in window. */
+
+	int fd, nb, i;
+	char buf[1024], *t;
+
+	fd = procrexec(xprog, "-d", "?", nil);
+	wreplace(cwin, "0,$","",0);	/* Clear window */
+	while ((nb = read(fd, buf, 1024)) > 0) {
+		t = buf;
+		i = 0;
+		if (strncmp("Usage", buf, 5) == 0) {	/* Remove first line. */
+			while (t[0] != '\n') {
+				t++; 
+				i++;
+			}
+			t++; 
+			i++;
+		}
+		wwritebody(cwin, t, nb-i);
+	}
+	close(fd);
+	wclean(cwin);
+}
+
+void
+dispentry(Win *cwin)
+{
+	/* Display the current selection in window. */
+
+	int fd, nb;
+	char buf[BUFSIZE];
+
+	if (curone == nil) {
+		if (pattern != nil) {
+			sprint(buf,"Pattern not found.\n");
+			wwritebody(cwin, buf, 19);
+			wclean(cwin);
+		}
+		return;
+	}
+	sprint(buffer,"%sp", curone);
+	fd = procrexec(xprog, "-d", dict, "-c", buffer, nil);
+	wreplace(cwin, "0,$","",0);	/* Clear window */
+	while ((nb = read(fd, buf, BUFSIZE)) > 0) {
+		wwritebody(cwin, buf, nb);
+	}
+	close(fd);
+	wclean(cwin);
+}
+
+void
+dispmatches(Win *cwin)
+{
+	/* Display the current matches. */
+
+	int fd, nb;
+	char buf[BUFSIZE];
+
+	sprint(buffer,"/%s/H", pattern);
+	fd = procrexec(xprog, "-d", dict, "-c", buffer, nil);
+	while ((nb = read(fd, buf, BUFSIZE)) > 0)
+		wwritebody(cwin, buf, nb);
+	close(fd);
+	wclean(cwin);
+}
+
+char*
+format(char *s)
+{
+	/* Format a string to be written in window tag.  Acme doesn't like */
+	/* non alpha-num's in the tag line. */
+
+	char *t, *h;
+
+	t = fbuffer;
+	if (s == nil) {
+		*t = 0;
+		return t;
+	}
+	strcpy(t, s);
+	h = t;
+	while (*t != 0) {
+		if (!(((*t >= 'a') && (*t <= 'z')) || 
+		    ((*t >= 'A') && (*t <= 'Z')) ||
+		    ((*t >= '0') && (*t <= '9'))))
+			*t = '_';
+		t++;
+	}
+	if (strlen(h) > MAXTAG)
+		h[MAXTAG] = 0;
+	if (strcmp(s,h) == 0) return s;
+	return h;
+}
+
+void
+openwin(char *name, char *buttons, Win *twin, int wintype)
+{
+	char buf[80];
+
+	wnew(twin);
+	if (wintype == Dictwin)
+		sprint(buf,"%s",name);
+	else
+		if ((wintype == Entrywin) && (count > 1))
+			sprint(buf,"%s/%s/%s/%d",name, dict, format(pattern), curindex+1);
+		else
+			sprint(buf,"%s/%s/%s",name, dict, format(pattern));
+	wname(twin, buf);
+	wtagwrite(twin, buttons, strlen(buttons));
+	wclean(twin);
+	wdormant(twin);
+	if (wintype == Dictwin)
+		dispdicts(twin);
+	if (wintype == Matchwin) {
+		Mopen = True;
+		dispmatches(twin);
+	}
+	if (wintype == Entrywin) {
+		Eopen = True;
+		dispentry(twin);
+	}
+	handle(twin, wintype);
+}
+
+void
+vopenwin(void *v)
+{
+	void **arg;
+	char *name, *buttons;
+	Win *twin;
+	int wintype;
+
+	arg = v;
+	name = arg[0];
+	buttons = arg[1];
+	twin = arg[2];
+	wintype = (int)arg[3];
+	sendul(arg[4], 0);
+
+	openwin(name, buttons, twin, wintype);
+	threadexits(nil);
+}
+	
+void
+procopenwin(char *name, char *buttons, Win *twin, int wintype)
+{
+	void *arg[5];
+	Channel *c;
+
+	c = chancreate(sizeof(ulong), 0);
+	arg[0] = name;
+	arg[1] = buttons;
+	arg[2] = twin;
+	arg[3] = (void*)wintype;
+	arg[4] = c;
+	proccreate(vopenwin, arg, 8192);
+	recvul(c);
+	chanfree(c);
+}
+
+void
+rexec(void *v)
+{
+	void **arg;
+	char *prog;
+	char **args;
+	int *fd;
+	Channel *c;
+
+	arg = v;
+	prog = arg[0];
+	args = arg[1];
+	fd = arg[2];
+	c = arg[3];
+
+	rfork(RFENVG|RFFDG);
+	dup(fd[1], 1);
+	close(fd[1]);
+	close(fd[0]);
+	procexec(c, prog, args);
+	threadprint(2, "Remote pipe execution failed: %s %r\n", prog);
+abort();
+	threadexits(nil);
+}
+
+void
+pexec(void *v)
+{
+	void **arg;
+	char *prog;
+	char **args;
+	Channel *c;
+
+	arg = v;
+	prog = arg[0];
+	args = arg[1];
+	c = arg[2];
+
+	procexec(c, prog, args);
+	threadprint(2, "Remote execution failed: %s %r\n", prog);
+abort();
+	threadexits(nil);
+}
+
+void
+procpexec(char *prog, char **args)
+{
+	void *rexarg[4];
+	Channel *c;
+
+	c = chancreate(sizeof(ulong), 0);
+	rexarg[0] = prog;
+	rexarg[1] = args;
+	rexarg[2] = c;
+
+	proccreate(pexec, rexarg, 8192);
+	recvul(c);
+	chanfree(c);
+}
+
+void
+kill(void)
+{
+	/* Kill all processes related to this one. */
+	int fd;
+
+	sprint(buffer, "/proc/%d/notepg", getpid());
+	fd = open(buffer, OWRITE);
+	rfork(RFNOTEG);
+	write(fd, "kill", 4);
+}
+
+int
+command(char *com, Win *w, int wintype)
+{
+	char *buf;
+
+	if (strncmp(com, "Del", 3) == 0) {
+		switch(wintype){
+		case Entrywin:
+			if (wdel(w)) {
+				Eopen = False;
+				threadexits(nil);
+			}
+			break;
+		case Dictwin:
+			if (wdel(w))
+				threadexits(nil);
+			break;
+		case Matchwin:
+			kill();
+			if (Eopen)
+				if (~wdel(&Ewin))	/* Remove the entry window */
+					wdel(&Ewin);
+			if (!wdel(w))
+				wdel(w);
+			threadexits(nil);
+			break;
+		}
+		return True;
+	}
+	if (strncmp(com, "Next", 4) == 0){
+		if (curone != nil) {
+			curone = chgaddr(1);
+			buf = getpattern(curone);
+			sprint(buffer,"%s/%s/%s", prog, dict, format(buf));
+			wname(w, buffer);
+			dispentry(w);
+		}
+		return True;
+	}
+	if (strncmp(com, "Prev",4) == 0){
+		if (curone != nil) {
+			curone = chgaddr(-1);
+			buf = getpattern(curone);
+			sprint(buffer,"%s/%s/%s", prog, dict, format(buf));
+			wname(w, buffer);
+			dispentry(w);
+		}
+		return True;
+	}
+	if (strncmp(com, "Nmatch",6) == 0){
+		if (curaddr[++curindex] == nil)
+			curindex = 0;
+		curone = curaddr[curindex];
+		if (curone != nil) {
+			sprint(buffer,"%s/%s/%s/%d",prog,dict,format(pattern),curindex+1);
+			wname(w, buffer);
+			dispentry(w);
+		}
+		return True;
+	}
+	return False;
+}
+
+void
+handle(Win *w, int wintype)
+{
+	Event e, e2, ea, etoss;
+	char *s, *t, buf[80];
+	int tmp, na;
+
+	while (True) {
+		wevent(w, &e);
+		switch(e.c2){
+		default:
+			/* threadprint(2,"unknown message %c%c\n", e.c1, e.c2); */
+			break;
+		case 'i':
+			/* threadprint(2,"'%s' inserted in tag at %d\n", e.b, e.q0);*/
+			break;
+		case 'I':
+			/* threadprint(2,"'%s' inserted in body at %d\n", e.b, e.q0);*/
+			break;
+		case 'd':
+			/* threadprint(2, "'%s' deleted in tag at %d\n", e.b, e.q0);*/
+			break;
+		case 'D':
+			/* threadprint(2, "'%s' deleted in body at %d\n", e.b, e.q0);*/
+			break;
+		case 'x':
+		case 'X':				/* Execute command. */
+			if (e.flag & 2)
+				wevent(w, &e2);
+			if(e.flag & 8){
+				wevent(w, &ea);
+				wevent(w, &etoss);
+				na = ea.nb;
+			} else
+				na = 0;
+			s = e.b;
+			if ((e.flag & 2) && e.nb == 0)
+				s = e2.b;
+			if(na){
+				t = malloc(strlen(s)+1+na+1);
+				snprint(t, strlen(s)+1+na+1, "%s %s", s, ea.b);
+				s = t;
+			}
+			/* if it's a long message, it can't be for us anyway */
+			if(!command(s, w, wintype))	/* send it back */
+				wwriteevent(w, &e);
+			if(na)
+				free(s);
+			break;
+		case 'l':
+		case 'L':				/* Look for something. */
+			if (e.flag & 2)
+				wevent(w, &e);
+			wclean(w);		/* Set clean bit. */
+			if (wintype == Dictwin) {
+				strcpy(buf, e.b);
+				args[0] = lprog;
+				args[1] = "-d";
+				args[2] = buf;
+				args[3] = nil;
+				procpexec(lprog, args);	/* New adict with chosen dict. */
+			}
+			if (wintype == Entrywin) {
+				strcpy(buf, e.b);
+				args[0] = lprog;
+				args[1] = "-d";
+				args[2] = dict;
+				args[3] = buf;
+				args[4] = nil;
+				procpexec(lprog, args); /* New adict with chosen pattern. */
+			}
+			if (wintype == Matchwin) {
+				tmp = atoi(e.b) - 1;
+				if ((tmp >= 0) && (tmp < MAXMATCH) && (curaddr[tmp] != nil)) {
+					curindex = tmp;
+					curone = curaddr[curindex];
+					/* Display selected match. */
+					if (Eopen) {
+						sprint(buf,"%s/%s/%s/%d",prog,dict,format(pattern),curindex+1);
+						wname(&Ewin, buf);
+						dispentry(&Ewin);
+					}
+					else
+						procopenwin(prog,"Nmatch Prev Next", &Ewin, Entrywin);
+				}
+			}
+			break;
+		}
+	}
+}
--- /dev/null
+++ b/acme/bin/source/adict/_win.c
@@ -1,0 +1,315 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include "win.h"
+
+void*
+erealloc(void *p, uint n)
+{
+	p = realloc(p, n);
+	if(p == nil)
+		threadprint(2, "realloc failed: %r");
+	return p;
+}
+
+void
+wnew(Win *w)
+{
+	char buf[12];
+
+	w->ctl = open("/mnt/acme/new/ctl", ORDWR);
+	if(w->ctl<0 || read(w->ctl, buf, 12)!=12)
+		 threadprint (2, "can't open window ctl file: %r");
+	ctlwrite(w, "noscroll\n");
+	w->winid = atoi(buf);
+	w->event = openfile(w, "event");
+	w->addr = -1;	/* will be opened when needed */
+	w->body = nil;
+	w->data = -1;
+}
+
+int
+openfile(Win *w, char *f)
+{
+	char buf[64];
+	int fd;
+
+	sprint(buf, "/mnt/acme/%d/%s", w->winid, f);
+	fd = open(buf, ORDWR|OCEXEC);
+	if(fd < 0)
+		 threadprint (2,"can't open window %s file: %r", f);
+	return fd;
+}
+
+void
+openbody(Win *w, int mode)
+{
+	char buf[64];
+
+	sprint(buf, "/mnt/acme/%d/body", w->winid);
+	w->body = Bopen(buf, mode|OCEXEC);
+	if(w->body == nil)
+		 threadprint(2,"can't open window body file: %r");
+}
+
+void
+wwritebody(Win *w, char *s, int n)
+{
+	if(w->body == nil)
+		openbody(w, OWRITE);
+	if(Bwrite(w->body, s, n) != n)
+		  threadprint(2,"write error to window: %r");
+	Bflush(w->body);
+}
+
+void
+wreplace(Win *w, char *addr, char *repl, int nrepl)
+{
+	if(w->addr < 0)
+		w->addr = openfile(w, "addr");
+	if(w->data < 0)
+		w->data = openfile(w, "data");
+	if(write(w->addr, addr, strlen(addr)) < 0){
+		threadprint(2, "mail: warning: badd address %s:%r\n", addr);
+		return;
+	}
+	if(write(w->data, repl, nrepl) != nrepl)
+		 threadprint(2, "writing data: %r");
+}
+
+static int
+nrunes(char *s, int nb)
+{
+	int i, n;
+	Rune r;
+
+	n = 0;
+	for(i=0; i<nb; n++)
+		i += chartorune(&r, s+i);
+	return n;
+}
+
+void
+wread(Win *w, uint q0, uint q1, char *data)
+{
+	int m, n, nr;
+	char buf[256];
+
+	if(w->addr < 0)
+		w->addr = openfile(w, "addr");
+	if(w->data < 0)
+		w->data = openfile(w, "data");
+	m = q0;
+	while(m < q1){
+		n = sprint(buf, "#%d", m);
+		if(write(w->addr, buf, n) != n)
+			  threadprint(2,"writing addr: %r");
+		n = read(w->data, buf, sizeof buf);
+		if(n <= 0)
+			  threadprint(2,"reading data: %r");
+		nr = nrunes(buf, n);
+		while(m+nr >q1){
+			do; while(n>0 && (buf[--n]&0xC0)==0x80);
+			--nr;
+		}
+		if(n == 0)
+			break;
+		memmove(data, buf, n);
+		data += n;
+		*data = 0;
+		m += nr;
+	}
+}
+
+void
+wselect(Win *w, char *addr)
+{
+	if(w->addr < 0)
+		w->addr = openfile(w, "addr");
+	if(write(w->addr, addr, strlen(addr)) < 0)
+		  threadprint(2,"writing addr");
+	ctlwrite(w, "dot=addr\n");
+}
+
+void
+wtagwrite(Win *w, char *s, int n)
+{
+	int fd;
+
+	fd = openfile(w, "tag");
+	if(write(fd, s, n) != n)
+		  threadprint(2,"tag write: %r");
+	close(fd);
+}
+
+void
+ctlwrite(Win *w, char *s)
+{
+	int n;
+
+	n = strlen(s);
+	if(write(w->ctl, s, n) != n)
+		 threadprint(2,"write error to ctl file: %r");
+}
+
+int
+wdel(Win *w)
+{
+	if(write(w->ctl, "del\n", 4) != 4)
+		return False;
+	wdormant(w);
+	close(w->ctl);
+	w->ctl = -1;
+	close(w->event);
+	w->event = -1;
+	return True;
+}
+
+void
+wname(Win *w, char *s)
+{
+	char buf[128];
+
+	sprint(buf, "name %s\n", s);
+	ctlwrite(w, buf);
+}
+
+void
+wclean(Win *w)
+{
+	if(w->body)
+		Bflush(w->body);
+	ctlwrite(w, "clean\n");
+}
+
+void
+wdormant(Win *w)
+{
+	if(w->addr >= 0){
+		close(w->addr);
+		w->addr = -1;
+	}
+	if(w->body != nil){
+		Bterm(w->body);
+		w->body = nil;
+	}
+	if(w->data >= 0){
+		close(w->data);
+		w->data = -1;
+	}
+}
+
+int
+getec(Win *w)
+{
+	if(w->nbuf == 0){
+		w->nbuf = read(w->event, w->buf, sizeof w->buf);
+		if(w->nbuf <= 0)
+			  threadprint(2,"event read error: %r");
+		w->bufp = w->buf;
+	}
+	w->nbuf--;
+	return *w->bufp++;
+}
+
+int
+geten(Win *w)
+{
+	int n, c;
+
+	n = 0;
+	while('0'<=(c=getec(w)) && c<='9')
+		n = n*10+(c-'0');
+	if(c != ' ')
+		 threadprint(2, "event number syntax");
+	return n;
+}
+
+int
+geter(Win *w, char *buf, int *nb)
+{
+	Rune r;
+	int n;
+
+	r = getec(w);
+	buf[0] = r;
+	n = 1;
+	if(r < Runeself)
+		goto Return;
+	while(!fullrune(buf, n))
+		buf[n++] = getec(w);
+	chartorune(&r, buf);
+    Return:
+	*nb = n;
+	return r;
+}
+
+
+void
+wevent(Win *w, Event *e)
+{
+	int i, nb;
+
+	e->c1 = getec(w);
+	e->c2 = getec(w);
+	e->q0 = geten(w);
+	e->q1 = geten(w);
+	e->flag = geten(w);
+	e->nr = geten(w);
+	if(e->nr > EVENTSIZE)
+		  threadprint(2, "wevent: event string too long");
+	e->nb = 0;
+	for(i=0; i<e->nr; i++){
+		e->r[i] = geter(w, e->b+e->nb, &nb);
+		e->nb += nb;
+	}
+	e->r[e->nr] = 0;
+	e->b[e->nb] = 0;
+	if(getec(w) != '\n')
+		 threadprint(2, "wevent: event syntax 2");
+}
+
+void
+wslave(Win *w, Channel *ce)
+{
+	Event e;
+
+	while(recv(ce, &e) >= 0)
+		wevent(w, &e);
+}
+
+void
+wwriteevent(Win *w, Event *e)
+{
+	threadprint(w->event, "%c%c%d %d\n", e->c1, e->c2, e->q0, e->q1);
+}
+
+int
+wreadall(Win *w, char **sp)
+{
+	char *s;
+	int m, na, n;
+
+	if(w->body != nil)
+		Bterm(w->body);
+	openbody(w, OREAD);
+	s = nil;
+	na = 0;
+	n = 0;
+	for(;;){
+		if(na < n+512){
+			na += 1024;
+			s = erealloc(s, na+1);
+		}
+		m = Bread(w->body, s+n, na-n);
+		if(m <= 0)
+			break;
+		n += m;
+	}
+	s[n] = 0;
+	Bterm(w->body);
+	w->body = nil;
+	*sp = s;
+	return n;
+}
--- /dev/null
+++ b/acme/bin/source/adict/adict.c
@@ -1,0 +1,591 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include "win.h"
+#include "adict.h"
+
+enum
+{
+	STACK = 8192,
+};
+
+char *prog = "adict";
+char *lprog = "/bin/adict";
+char *xprog  = "/bin/dict";
+char *dict, *pattern, *curaddr[MAXMATCH], *curone, *args[6], buffer[80];
+char abuffer[80], fbuffer[80], pbuffer[80];
+int curindex, count, Eopen, Mopen;
+Win Mwin, Ewin, Dwin;
+
+void openwin(char*, char*, Win*, int);
+void  handle(Win*, int);
+void	rexec(void*);
+void	pexec(void*);
+int getaddr(char*);
+
+void
+usage(void)
+{
+		fprint(2, "usage: %s [-d dictname] [pattern]\n", argv0);
+		threadexitsall(nil);
+}
+
+int mainstacksize = STACK;
+
+void
+threadmain(int argc, char** argv)
+{
+	ARGBEGIN{
+	case 'd':
+		dict = strdup(ARGF());
+		break;
+	default:
+		usage();
+	}ARGEND
+
+	/* if running as other name, note that fact */
+	if(access(argv0, AEXIST) == 0)
+		lprog = argv0;
+
+	switch(argc){
+	case 1:
+		pattern = pbuffer;
+		strcpy(pattern,argv[0]);
+		if(dict == nil)
+			dict = "pgw";
+		break;
+	case 0:
+		break;
+	default:
+		usage();
+	}
+
+	if ((dict == nil) && (pattern == nil))
+		openwin(prog,"", &Dwin, Dictwin);
+	if (pattern == nil)
+		openwin(prog,"",&Ewin, Entrywin);
+	if ((count = getaddr(pattern)) <= 1)
+		openwin(prog,"Prev Next", &Ewin, Entrywin);
+	else
+		openwin(prog, "", &Mwin, Matchwin);
+}
+
+static int
+procrexec(char *xprog, ...)
+{
+	int fpipe[2];
+	void *rexarg[4];
+	Channel *c;
+	va_list va;
+	int i;
+	char *p;
+
+	pipe(fpipe);
+	va_start(va, xprog);
+	p = xprog;
+	for(i=0; p && i+1<nelem(args); i++){
+		args[i] = p;
+		p = va_arg(va, char*);
+	}
+	args[i] = nil;
+
+	c = chancreate(sizeof(ulong), 0);
+	rexarg[0] = xprog;
+	rexarg[1] = args;
+	rexarg[2] = fpipe;
+	rexarg[3] = c;
+
+	proccreate(rexec, rexarg, STACK);
+	recvul(c);
+	chanfree(c);
+	close(fpipe[1]);
+	return fpipe[0];
+}
+
+int
+getaddr(char *pattern)
+{
+	/* Get char offset into dictionary of matches. */
+
+	int fd, i;
+	Biobuf inbuf;
+	char *bufptr;
+char *obuf;
+
+	if (pattern == nil) {
+		curone = nil;
+		curindex = 0;
+		curaddr[curindex] = nil;
+		return 0;
+	}
+
+	sprint(buffer,"/%s/A", pattern);
+	fd = procrexec(xprog, "-d", dict, "-c", buffer, nil);
+	Binit(&inbuf, fd, OREAD);
+	i = 0;
+	curindex = 0;
+	while ((bufptr = Brdline(&inbuf, '\n')) != nil && (i < (MAXMATCH-1))) {
+		bufptr[Blinelen(&inbuf)-1] = 0;
+obuf=bufptr;
+		while (bufptr[0] != '#' && bufptr[0] != 0) bufptr++;
+if(bufptr[0] == 0)
+	print("whoops buf «%s»\n", obuf);
+		curaddr[i] = malloc(strlen(bufptr));
+		strcpy(curaddr[i], bufptr);
+		i++;
+	}
+	curaddr[i] = nil;
+	if (i == MAXMATCH)
+		fprint(2, "Too many matches!\n");
+	Bterm(&inbuf);
+	close(fd);
+
+	curone = curaddr[curindex];
+	return(i);
+}
+
+char*
+getpattern(char *addr)
+{
+	/* Get the pattern corresponding to an absolute address.*/
+	int fd;
+	char *res, *t;
+
+	res = nil;
+	sprint(buffer,"%sh", addr);
+	fd = procrexec(xprog, "-d", dict, "-c", buffer, nil);
+	if (read(fd, pbuffer, 80) > 80)
+		fprint(2, "Error in getting addres from dict.\n");
+	else {
+		t = pbuffer;
+		/* remove trailing whitespace, newline */
+		if (t != nil){
+			while(*t != 0 && *t != '\n')
+				t++;
+			if(t == 0 && t > pbuffer)
+				t--;
+			while(t >= pbuffer && (*t==' ' || *t=='\n' || *t=='\t' || *t=='\r'))
+				*t-- = 0;
+		}
+		res = pbuffer;
+	}
+	close(fd);
+	return(res);
+}
+
+char*
+chgaddr(int dir)
+{
+	/* Increment or decrement the current address (curone). */
+
+	int fd;
+	char *res, *t;
+
+	res = nil;
+	if (dir < 0)
+		sprint(buffer,"%s-a", curone);
+	else
+		sprint(buffer,"%s+a", curone);
+	fd = procrexec(xprog, "-d", dict, "-c", buffer, nil);
+	if (read(fd, abuffer, 80) > 80)
+		fprint(2, "Error in getting addres from dict.\n");
+	else {
+		res = abuffer;
+		while (*res != '#') res++;
+		t = res;
+		while ((*t != '\n') && (t != nil)) t++;
+		if (t != nil) *t = 0;
+	}
+	close(fd);
+	return(res);
+}
+
+void
+dispdicts(Win *cwin)
+{
+	/* Display available dictionaries in window. */
+
+	int fd, nb, i;
+	char buf[1024], *t;
+
+	fd = procrexec(xprog, "-d", "?", nil);
+	wreplace(cwin, "0,$","",0);	/* Clear window */
+	while ((nb = read(fd, buf, 1024)) > 0) {
+		t = buf;
+		i = 0;
+		if (strncmp("Usage", buf, 5) == 0) {	/* Remove first line. */
+			while (t[0] != '\n') {
+				t++; 
+				i++;
+			}
+			t++; 
+			i++;
+		}
+		wwritebody(cwin, t, nb-i);
+	}
+	close(fd);
+	wclean(cwin);
+}
+
+void
+dispentry(Win *cwin)
+{
+	/* Display the current selection in window. */
+
+	int fd, nb;
+	char buf[BUFSIZE];
+
+	if (curone == nil) {
+		if (pattern != nil) {
+			sprint(buf,"Pattern not found.\n");
+			wwritebody(cwin, buf, 19);
+			wclean(cwin);
+		}
+		return;
+	}
+	sprint(buffer,"%sp", curone);
+	fd = procrexec(xprog, "-d", dict, "-c", buffer, nil);
+	wreplace(cwin, "0,$","",0);	/* Clear window */
+	while ((nb = read(fd, buf, BUFSIZE)) > 0) {
+		wwritebody(cwin, buf, nb);
+	}
+	close(fd);
+	wclean(cwin);
+}
+
+void
+dispmatches(Win *cwin)
+{
+	/* Display the current matches. */
+
+	int fd, nb;
+	char buf[BUFSIZE];
+
+	sprint(buffer,"/%s/H", pattern);
+	fd = procrexec(xprog, "-d", dict, "-c", buffer, nil);
+	while ((nb = read(fd, buf, BUFSIZE)) > 0)
+		wwritebody(cwin, buf, nb);
+	close(fd);
+	wclean(cwin);
+}
+
+char*
+format(char *s)
+{
+	/* Format a string to be written in window tag.  Acme doesn't like */
+	/* non alpha-num's in the tag line. */
+
+	char *t, *h;
+
+	t = fbuffer;
+	if (s == nil) {
+		*t = 0;
+		return t;
+	}
+	strcpy(t, s);
+	h = t;
+	while (*t != 0) {
+		if (!(((*t >= 'a') && (*t <= 'z')) || 
+		    ((*t >= 'A') && (*t <= 'Z')) ||
+		    ((*t >= '0') && (*t <= '9'))))
+			*t = '_';
+		t++;
+	}
+	if (strlen(h) > MAXTAG)
+		h[MAXTAG] = 0;
+	if (strcmp(s,h) == 0) return s;
+	return h;
+}
+
+void
+openwin(char *name, char *buttons, Win *twin, int wintype)
+{
+	char buf[80];
+
+	wnew(twin);
+	if (wintype == Dictwin)
+		sprint(buf,"%s",name);
+	else
+		if ((wintype == Entrywin) && (count > 1))
+			sprint(buf,"%s/%s/%s/%d",name, dict, format(pattern), curindex+1);
+		else
+			sprint(buf,"%s/%s/%s",name, dict, format(pattern));
+	wname(twin, buf);
+	wtagwrite(twin, buttons, strlen(buttons));
+	wclean(twin);
+	wdormant(twin);
+	if (wintype == Dictwin)
+		dispdicts(twin);
+	if (wintype == Matchwin) {
+		Mopen = True;
+		dispmatches(twin);
+	}
+	if (wintype == Entrywin) {
+		Eopen = True;
+		dispentry(twin);
+	}
+	handle(twin, wintype);
+}
+
+void
+vopenwin(void *v)
+{
+	void **arg;
+	char *name, *buttons;
+	Win *twin;
+	int wintype;
+
+	arg = v;
+	name = arg[0];
+	buttons = arg[1];
+	twin = arg[2];
+	wintype = (int)arg[3];
+	sendul(arg[4], 0);
+
+	openwin(name, buttons, twin, wintype);
+	threadexits(nil);
+}
+	
+void
+procopenwin(char *name, char *buttons, Win *twin, int wintype)
+{
+	void *arg[5];
+	Channel *c;
+
+	c = chancreate(sizeof(ulong), 0);
+	arg[0] = name;
+	arg[1] = buttons;
+	arg[2] = twin;
+	arg[3] = (void*)wintype;
+	arg[4] = c;
+	proccreate(vopenwin, arg, STACK);
+	recvul(c);
+	chanfree(c);
+}
+
+void
+rexec(void *v)
+{
+	void **arg;
+	char *prog;
+	char **args;
+	int *fd;
+	Channel *c;
+
+	arg = v;
+	prog = arg[0];
+	args = arg[1];
+	fd = arg[2];
+	c = arg[3];
+
+	rfork(RFENVG|RFFDG);
+	dup(fd[1], 1);
+	close(fd[1]);
+	close(fd[0]);
+	procexec(c, prog, args);
+	fprint(2, "Remote pipe execution failed: %s %r\n", prog);
+abort();
+	threadexits(nil);
+}
+
+void
+pexec(void *v)
+{
+	void **arg;
+	char *prog;
+	char **args;
+	Channel *c;
+
+	arg = v;
+	prog = arg[0];
+	args = arg[1];
+	c = arg[2];
+
+	procexec(c, prog, args);
+	fprint(2, "Remote execution failed: %s %r\n", prog);
+abort();
+	threadexits(nil);
+}
+
+void
+procpexec(char *prog, char **args)
+{
+	void *rexarg[4];
+	Channel *c;
+
+	c = chancreate(sizeof(ulong), 0);
+	rexarg[0] = prog;
+	rexarg[1] = args;
+	rexarg[2] = c;
+
+	proccreate(pexec, rexarg, STACK);
+	recvul(c);
+	chanfree(c);
+}
+
+void
+kill(void)
+{
+	/* Kill all processes related to this one. */
+	int fd;
+
+	sprint(buffer, "/proc/%d/notepg", getpid());
+	fd = open(buffer, OWRITE);
+	rfork(RFNOTEG);
+	write(fd, "kill", 4);
+}
+
+int
+command(char *com, Win *w, int wintype)
+{
+	char *buf;
+
+	if (strncmp(com, "Del", 3) == 0) {
+		switch(wintype){
+		case Entrywin:
+			if (wdel(w)) {
+				Eopen = False;
+				threadexits(nil);
+			}
+			break;
+		case Dictwin:
+			if (wdel(w))
+				threadexits(nil);
+			break;
+		case Matchwin:
+			kill();
+			if (Eopen)
+				if (~wdel(&Ewin))	/* Remove the entry window */
+					wdel(&Ewin);
+			if (!wdel(w))
+				wdel(w);
+			threadexits(nil);
+			break;
+		}
+		return True;
+	}
+	if (strncmp(com, "Next", 4) == 0){
+		if (curone != nil) {
+			curone = chgaddr(1);
+			buf = getpattern(curone);
+			sprint(buffer,"%s/%s/%s", prog, dict, format(buf));
+			wname(w, buffer);
+			dispentry(w);
+		}
+		return True;
+	}
+	if (strncmp(com, "Prev",4) == 0){
+		if (curone != nil) {
+			curone = chgaddr(-1);
+			buf = getpattern(curone);
+			sprint(buffer,"%s/%s/%s", prog, dict, format(buf));
+			wname(w, buffer);
+			dispentry(w);
+		}
+		return True;
+	}
+	if (strncmp(com, "Nmatch",6) == 0){
+		if (curaddr[++curindex] == nil)
+			curindex = 0;
+		curone = curaddr[curindex];
+		if (curone != nil) {
+			sprint(buffer,"%s/%s/%s/%d",prog,dict,format(pattern),curindex+1);
+			wname(w, buffer);
+			dispentry(w);
+		}
+		return True;
+	}
+	return False;
+}
+
+void
+handle(Win *w, int wintype)
+{
+	Event e, e2, ea, etoss;
+	char *s, *t, buf[80];
+	int tmp, na;
+
+	while (True) {
+		wevent(w, &e);
+		switch(e.c2){
+		default:
+			/* fprint(2,"unknown message %c%c\n", e.c1, e.c2); */
+			break;
+		case 'i':
+			/* fprint(2,"'%s' inserted in tag at %d\n", e.b, e.q0);*/
+			break;
+		case 'I':
+			/* fprint(2,"'%s' inserted in body at %d\n", e.b, e.q0);*/
+			break;
+		case 'd':
+			/* fprint(2, "'%s' deleted in tag at %d\n", e.b, e.q0);*/
+			break;
+		case 'D':
+			/* fprint(2, "'%s' deleted in body at %d\n", e.b, e.q0);*/
+			break;
+		case 'x':
+		case 'X':				/* Execute command. */
+			if (e.flag & 2)
+				wevent(w, &e2);
+			if(e.flag & 8){
+				wevent(w, &ea);
+				wevent(w, &etoss);
+				na = ea.nb;
+			} else
+				na = 0;
+			s = e.b;
+			if ((e.flag & 2) && e.nb == 0)
+				s = e2.b;
+			if(na){
+				t = malloc(strlen(s)+1+na+1);
+				snprint(t, strlen(s)+1+na+1, "%s %s", s, ea.b);
+				s = t;
+			}
+			/* if it's a long message, it can't be for us anyway */
+			if(!command(s, w, wintype))	/* send it back */
+				wwriteevent(w, &e);
+			if(na)
+				free(s);
+			break;
+		case 'l':
+		case 'L':				/* Look for something. */
+			if (e.flag & 2)
+				wevent(w, &e);
+			wclean(w);		/* Set clean bit. */
+			if (wintype == Dictwin) {
+				strcpy(buf, e.b);
+				args[0] = lprog;
+				args[1] = "-d";
+				args[2] = buf;
+				args[3] = nil;
+				procpexec(lprog, args);	/* New adict with chosen dict. */
+			}
+			if (wintype == Entrywin) {
+				strcpy(buf, e.b);
+				args[0] = lprog;
+				args[1] = "-d";
+				args[2] = dict;
+				args[3] = buf;
+				args[4] = nil;
+				procpexec(lprog, args); /* New adict with chosen pattern. */
+			}
+			if (wintype == Matchwin) {
+				tmp = atoi(e.b) - 1;
+				if ((tmp >= 0) && (tmp < MAXMATCH) && (curaddr[tmp] != nil)) {
+					curindex = tmp;
+					curone = curaddr[curindex];
+					/* Display selected match. */
+					if (Eopen) {
+						sprint(buf,"%s/%s/%s/%d",prog,dict,format(pattern),curindex+1);
+						wname(&Ewin, buf);
+						dispentry(&Ewin);
+					}
+					else
+						procopenwin(prog,"Nmatch Prev Next", &Ewin, Entrywin);
+				}
+			}
+			break;
+		}
+	}
+}
--- /dev/null
+++ b/acme/bin/source/adict/adict.h
@@ -1,0 +1,10 @@
+enum
+{
+	Matchwin,
+	Entrywin,
+	Dictwin
+};
+
+#define MAXTAG	20
+#define MAXMATCH 100
+#define BUFSIZE	4096
--- /dev/null
+++ b/acme/bin/source/adict/man
@@ -1,0 +1,26 @@
+adict [-d dictionary] [pattern]
+
+	adict with no arguments opens a window that displays all the currently
+available dictionaries.  To select a dictionary, click the right mouse button on
+its name.
+
+-d dictionary	Opens a window that interfaces to the specified dictionary.  To
+	look up a word, enter it in the window, and click the right mouse button on it.
+
+[pattern]	If no dictionary is specified, adict looks up the pattern in "oed" (Oxford
+	English Dictionary).  If more than one entry is found, adict opens a window
+	displaying the headers of the matching entries.  To display a particular entry
+	click the right mouse button on the number to its left.
+
+Quit		Exit and remove all windows associated with this one.
+
+Nmatch	Display the next matching entry.
+
+Next		Display the next entry in the dictionary.
+
+Prev		Display the previous entry in the dictionary.
+
+	Nmatch works independently of Prev and Next.
+
+	Any word in the window displaying an entry can be looked up in the selected
+dictionary by clicking the right mouse button on that word.
\ No newline at end of file
--- /dev/null
+++ b/acme/bin/source/adict/mkfile
@@ -1,0 +1,11 @@
+</$objtype/mkfile
+
+TARG=adict
+
+HFILES=win.h
+
+OFILES=adict.$O\
+		win.$O\
+
+BIN= /acme/bin/$objtype
+</sys/src/cmd/mkone
--- /dev/null
+++ b/acme/bin/source/adict/win.c
@@ -1,0 +1,315 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include "win.h"
+
+void*
+erealloc(void *p, uint n)
+{
+	p = realloc(p, n);
+	if(p == nil)
+		fprint(2, "realloc failed: %r");
+	return p;
+}
+
+void
+wnew(Win *w)
+{
+	char buf[12];
+
+	w->ctl = open("/mnt/acme/new/ctl", ORDWR);
+	if(w->ctl<0 || read(w->ctl, buf, 12)!=12)
+		 fprint (2, "can't open window ctl file: %r");
+	ctlwrite(w, "noscroll\n");
+	w->winid = atoi(buf);
+	w->event = openfile(w, "event");
+	w->addr = -1;	/* will be opened when needed */
+	w->body = nil;
+	w->data = -1;
+}
+
+int
+openfile(Win *w, char *f)
+{
+	char buf[64];
+	int fd;
+
+	sprint(buf, "/mnt/acme/%d/%s", w->winid, f);
+	fd = open(buf, ORDWR|OCEXEC);
+	if(fd < 0)
+		 fprint (2,"can't open window %s file: %r", f);
+	return fd;
+}
+
+void
+openbody(Win *w, int mode)
+{
+	char buf[64];
+
+	sprint(buf, "/mnt/acme/%d/body", w->winid);
+	w->body = Bopen(buf, mode|OCEXEC);
+	if(w->body == nil)
+		 fprint(2,"can't open window body file: %r");
+}
+
+void
+wwritebody(Win *w, char *s, int n)
+{
+	if(w->body == nil)
+		openbody(w, OWRITE);
+	if(Bwrite(w->body, s, n) != n)
+		  fprint(2,"write error to window: %r");
+	Bflush(w->body);
+}
+
+void
+wreplace(Win *w, char *addr, char *repl, int nrepl)
+{
+	if(w->addr < 0)
+		w->addr = openfile(w, "addr");
+	if(w->data < 0)
+		w->data = openfile(w, "data");
+	if(write(w->addr, addr, strlen(addr)) < 0){
+		fprint(2, "mail: warning: badd address %s:%r\n", addr);
+		return;
+	}
+	if(write(w->data, repl, nrepl) != nrepl)
+		 fprint(2, "writing data: %r");
+}
+
+static int
+nrunes(char *s, int nb)
+{
+	int i, n;
+	Rune r;
+
+	n = 0;
+	for(i=0; i<nb; n++)
+		i += chartorune(&r, s+i);
+	return n;
+}
+
+void
+wread(Win *w, uint q0, uint q1, char *data)
+{
+	int m, n, nr;
+	char buf[256];
+
+	if(w->addr < 0)
+		w->addr = openfile(w, "addr");
+	if(w->data < 0)
+		w->data = openfile(w, "data");
+	m = q0;
+	while(m < q1){
+		n = sprint(buf, "#%d", m);
+		if(write(w->addr, buf, n) != n)
+			  fprint(2,"writing addr: %r");
+		n = read(w->data, buf, sizeof buf);
+		if(n <= 0)
+			  fprint(2,"reading data: %r");
+		nr = nrunes(buf, n);
+		while(m+nr >q1){
+			do; while(n>0 && (buf[--n]&0xC0)==0x80);
+			--nr;
+		}
+		if(n == 0)
+			break;
+		memmove(data, buf, n);
+		data += n;
+		*data = 0;
+		m += nr;
+	}
+}
+
+void
+wselect(Win *w, char *addr)
+{
+	if(w->addr < 0)
+		w->addr = openfile(w, "addr");
+	if(write(w->addr, addr, strlen(addr)) < 0)
+		  fprint(2,"writing addr");
+	ctlwrite(w, "dot=addr\n");
+}
+
+void
+wtagwrite(Win *w, char *s, int n)
+{
+	int fd;
+
+	fd = openfile(w, "tag");
+	if(write(fd, s, n) != n)
+		  fprint(2,"tag write: %r");
+	close(fd);
+}
+
+void
+ctlwrite(Win *w, char *s)
+{
+	int n;
+
+	n = strlen(s);
+	if(write(w->ctl, s, n) != n)
+		 fprint(2,"write error to ctl file: %r");
+}
+
+int
+wdel(Win *w)
+{
+	if(write(w->ctl, "del\n", 4) != 4)
+		return False;
+	wdormant(w);
+	close(w->ctl);
+	w->ctl = -1;
+	close(w->event);
+	w->event = -1;
+	return True;
+}
+
+void
+wname(Win *w, char *s)
+{
+	char buf[128];
+
+	sprint(buf, "name %s\n", s);
+	ctlwrite(w, buf);
+}
+
+void
+wclean(Win *w)
+{
+	if(w->body)
+		Bflush(w->body);
+	ctlwrite(w, "clean\n");
+}
+
+void
+wdormant(Win *w)
+{
+	if(w->addr >= 0){
+		close(w->addr);
+		w->addr = -1;
+	}
+	if(w->body != nil){
+		Bterm(w->body);
+		w->body = nil;
+	}
+	if(w->data >= 0){
+		close(w->data);
+		w->data = -1;
+	}
+}
+
+int
+getec(Win *w)
+{
+	if(w->nbuf == 0){
+		w->nbuf = read(w->event, w->buf, sizeof w->buf);
+		if(w->nbuf <= 0)
+			  fprint(2,"event read error: %r");
+		w->bufp = w->buf;
+	}
+	w->nbuf--;
+	return *w->bufp++;
+}
+
+int
+geten(Win *w)
+{
+	int n, c;
+
+	n = 0;
+	while('0'<=(c=getec(w)) && c<='9')
+		n = n*10+(c-'0');
+	if(c != ' ')
+		 fprint(2, "event number syntax");
+	return n;
+}
+
+int
+geter(Win *w, char *buf, int *nb)
+{
+	Rune r;
+	int n;
+
+	r = getec(w);
+	buf[0] = r;
+	n = 1;
+	if(r < Runeself)
+		goto Return;
+	while(!fullrune(buf, n))
+		buf[n++] = getec(w);
+	chartorune(&r, buf);
+    Return:
+	*nb = n;
+	return r;
+}
+
+
+void
+wevent(Win *w, Event *e)
+{
+	int i, nb;
+
+	e->c1 = getec(w);
+	e->c2 = getec(w);
+	e->q0 = geten(w);
+	e->q1 = geten(w);
+	e->flag = geten(w);
+	e->nr = geten(w);
+	if(e->nr > EVENTSIZE)
+		  fprint(2, "wevent: event string too long");
+	e->nb = 0;
+	for(i=0; i<e->nr; i++){
+		e->r[i] = geter(w, e->b+e->nb, &nb);
+		e->nb += nb;
+	}
+	e->r[e->nr] = 0;
+	e->b[e->nb] = 0;
+	if(getec(w) != '\n')
+		 fprint(2, "wevent: event syntax 2");
+}
+
+void
+wslave(Win *w, Channel *ce)
+{
+	Event e;
+
+	while(recv(ce, &e) >= 0)
+		wevent(w, &e);
+}
+
+void
+wwriteevent(Win *w, Event *e)
+{
+	fprint(w->event, "%c%c%d %d\n", e->c1, e->c2, e->q0, e->q1);
+}
+
+int
+wreadall(Win *w, char **sp)
+{
+	char *s;
+	int m, na, n;
+
+	if(w->body != nil)
+		Bterm(w->body);
+	openbody(w, OREAD);
+	s = nil;
+	na = 0;
+	n = 0;
+	for(;;){
+		if(na < n+512){
+			na += 1024;
+			s = erealloc(s, na+1);
+		}
+		m = Bread(w->body, s+n, na-n);
+		if(m <= 0)
+			break;
+		n += m;
+	}
+	s[n] = 0;
+	Bterm(w->body);
+	w->body = nil;
+	*sp = s;
+	return n;
+}
--- /dev/null
+++ b/acme/bin/source/adict/win.h
@@ -1,0 +1,59 @@
+enum
+{
+	False,
+	True,
+	EVENTSIZE=256,
+};
+
+
+typedef struct Event Event;
+struct Event
+{
+	int	c1;
+	int	c2;
+	int	q0;
+	int	q1;
+	int	flag;
+	int	nb;
+	int	nr;
+	char	b[EVENTSIZE*UTFmax+1];
+	Rune	r[EVENTSIZE+1];
+};
+
+
+typedef struct Win Win;
+struct Win
+{
+	int	winid;
+	int	addr;
+	Biobuf *body;
+	int	ctl;
+	int	data;
+	int	event;
+	char	buf[512];
+	char	*bufp;
+	int	nbuf;
+};
+
+int     dead(Win*);
+void	wnew(Win*);
+void	wwritebody(Win*, char *s, int n);
+void	wread(Win*, uint, uint, char*);
+void	wclean(Win*);
+void	wname(Win*, char*);
+void	wdormant(Win*);
+void	wevent(Win*, Event*);
+void	wtagwrite(Win*, char*, int);
+void	wwriteevent(Win*, Event*);
+void	wslave(Win*, Channel*);	/* chan(Event) */
+void	wreplace(Win*, char*, char*, int);
+void	wselect(Win*, char*);
+int	wdel(Win*);
+int	wreadall(Win*, char**);
+
+void	ctlwrite(Win*, char*);
+int	getec(Win*);
+int	geten(Win*);
+int	geter(Win*, char*, int*);
+int	openfile(Win*, char*);
+void	openbody(Win*, int);
--- /dev/null
+++ b/acme/bin/source/mkfile
@@ -1,0 +1,27 @@
+</$objtype/mkfile
+
+TARG=\
+	mkwnew\
+	spout\
+
+OFILES=
+HFILES=
+LIB=
+
+DIRS=win
+
+BIN=../$objtype
+
+</sys/src/cmd/mkmany
+
+all:V:		all.dirs
+install:V:	install.dirs
+clean:V:	clean.dirs
+nuke:V:		nuke.dirs
+
+%.dirs:VQ:
+	for (i in $DIRS) @{
+		echo mk $i
+		cd $i
+		mk $stem
+	}
--- /dev/null
+++ b/acme/bin/source/mkwnew.c
@@ -1,0 +1,45 @@
+#include <u.h>
+#include <libc.h>
+
+void
+main(int argc, char *argv[])
+{
+	int i, fd, pid, n;
+	char wdir[256];
+	int dflag;
+
+	dflag = 0;
+	ARGBEGIN{
+	case 'd':
+		dflag = 1;
+		break;
+	default:
+		fprint(2, "usage: wnew [-d] [label]\n");
+	}ARGEND
+
+	pid = getpid();
+	wdir[0] = '\0';
+	if(!dflag)
+		getwd(wdir, sizeof wdir);
+	if(argc>0)
+		for(i=0; i<argc; i++)
+			snprint(wdir, sizeof wdir, "%s%c%s", wdir, i==0? '/' : '-', argv[i]);
+	else
+		snprint(wdir, sizeof wdir, "%s/-win", wdir);
+
+	if((fd = open("/dev/wnew", ORDWR)) < 0)
+		sysfatal("wnew: can't open /dev/wnew: %r");
+
+	if(fprint(fd, "%d %s", pid, wdir+dflag) < 0)
+		sysfatal("wnew: can't create window: %r");
+
+	if(seek(fd, 0, 0) != 0)
+		sysfatal("wnew: can't seek: %r");
+
+	if((n=read(fd, wdir, sizeof wdir-1)) < 0)
+		sysfatal("wnew: can't read window id: %r");
+	wdir[n] = '\0';
+
+	print("%s\n", wdir);
+	exits(nil);
+}
--- /dev/null
+++ b/acme/bin/source/spout.c
@@ -1,0 +1,123 @@
+#include <u.h>
+#include <libc.h>
+#include <ctype.h>
+#include <bio.h>
+
+void	spout(int, char*);
+
+Biobuf bout;
+
+void
+main(int argc, char *argv[])
+{
+	int i, fd;
+
+	Binit(&bout, 1, OWRITE);
+	if(argc == 1)
+		spout(0, "");
+	else
+		for(i=1; i<argc; i++){
+			fd = open(argv[i], OREAD);
+			if(fd < 0){
+				fprint(2, "spell: can't open %s: %r\n", argv[i]);
+				continue;
+			}
+			spout(fd, argv[i]);
+			close(fd);
+		}
+	exits(nil);
+}
+
+Biobuf b;
+
+void
+spout(int fd, char *name)
+{
+	char *s, *t, *w;
+	Rune r;
+	int inword, wordchar;
+	int n, wn, wid, c, m;
+	char buf[1024];
+
+	Binit(&b, fd, OREAD);
+	n = 0;
+	wn = 0;
+	while((s = Brdline(&b, '\n')) != nil){
+		if(s[0] == '.')
+			for(c=0; c<3 && *s>' '; c++){
+				n++;
+				s++;
+			}
+		inword = 0;
+		w = s;
+		t = s;
+		do{
+			c = *(uchar*)t;
+			if(c < Runeself)
+				wid = 1;
+			else{
+				wid = chartorune(&r, t);
+				c = r;
+			}
+			wordchar = 0;
+			if(isalpha(c))
+				wordchar = 1;
+			if(inword && !wordchar){
+				if(c=='\'' && isalpha(t[1]))
+					goto Continue;
+				m = t-w;
+				if(m > 1){
+					memmove(buf, w, m);
+					buf[m] = 0;
+					Bprint(&bout, "%s:#%d,#%d:%s\n", name, wn, n, buf);
+				}
+				inword = 0;
+			}else if(!inword && wordchar){
+				wn = n;
+				w = t;
+				inword = 1;
+			}
+			if(c=='\\' && (isalpha(t[1]) || t[1]=='(')){
+				switch(t[1]){
+				case '(':
+					m = 4;
+					break;
+				case 'f':
+					if(t[2] == '(')
+						m = 5;
+					else
+						m = 3;
+					break;
+				case 's':
+					if(t[2] == '+' || t[2]=='-'){
+						if(t[3] == '(')
+							m = 6;
+						else
+							m = 4;
+					}else{
+						if(t[2] == '(')
+							m = 5;
+						else if(t[2]=='1' || t[2]=='2' || t[2]=='3')
+							m = 4;
+						else
+							m = 3;
+					}
+					break;
+				default:
+					m = 2;
+				}
+				while(m-- > 0){
+					if(*t == '\n')
+						break;
+					n++;
+					t++;
+				}
+				continue;
+			}
+	Continue:
+			n++;
+			t += wid;
+		}while(c != '\n');
+	}
+	Bterm(&b);
+}
--- /dev/null
+++ b/acme/bin/source/win/_fs.c
@@ -1,0 +1,146 @@
+#include <u.h>
+#include <libc.h>
+#include <fcall.h>
+#include <thread.h>
+#include <9p.h>
+#include "dat.h"
+
+Channel *fschan;
+Channel *writechan;
+
+static File *devcons, *devnew;
+
+static void
+fsread(Req *r)
+{
+	Fsevent e;
+
+	if(r->fid->file == devnew){
+		if(r->fid->aux==nil){
+			respond(r, "phase error");
+			return;
+		}
+		readstr(r, r->fid->aux);
+		respond(r, nil);
+		return;
+	}
+
+	assert(r->fid->file == devcons);
+	e.type = 'r';
+	e.r = r;
+	send(fschan, &e);
+}
+
+static void
+fsflush(Req *r)
+{
+	Fsevent e;
+
+	e.type = 'f';
+	e.r = r;
+	send(fschan, &e);
+}
+
+static void
+fswrite(Req *r)
+{
+	static Event *e[4];
+	Event *ep;
+	int i, j, nb, wid, pid;
+	Rune rune;
+	char *s;
+	char tmp[UTFmax], *t;
+	static int n, partial;
+
+	if(r->fid->file == devnew){
+		if(r->fid->aux){
+			respond(r, "already created a window");
+			return;
+		}
+		s = emalloc(r->ifcall.count+1);
+		memmove(s, r->ifcall.data, r->ifcall.count);
+		s[r->ifcall.count] = 0;
+		pid = strtol(s, &t, 0);
+		if(*t==' ')
+			t++;
+		i = newpipewin(pid, t);
+		free(s);
+		s = emalloc(32);
+		sprint(s, "%lud", (ulong)i);
+		r->fid->aux = s;
+		r->ofcall.count = r->ifcall.count;
+		respond(r, nil);
+		return;
+	}
+
+	assert(r->fid->file == devcons);
+
+	if(e[0] == nil){
+		for(i=0; i<nelem(e); i++){
+			e[i] = emalloc(sizeof(Event));
+			e[i]->c1 = 'S';
+		}
+	}
+
+	ep = e[n];
+	n = (n+1)%nelem(e);
+	assert(r->ifcall.count <= 8192);	/* is this guaranteed by lib9p? */
+	nb = r->ifcall.count;
+	memmove(ep->b+partial, r->ifcall.data, nb);
+	nb += partial;
+	ep->b[nb] = '\0';
+	if(strlen(ep->b) < nb){	/* nulls in data */
+		t = ep->b;
+		for(i=j=0; i<nb; i++)
+			if(ep->b[i] != '\0')
+				t[j++] = ep->b[i];
+		nb = j;
+		t[j] = '\0';
+	}
+	/* process bytes into runes, transferring terminal partial runes into next buffer */
+	for(i=j=0; i<nb && fullrune(ep->b+i, nb-i); i+=wid,j++)
+		wid = chartorune(&rune, ep->b+i);
+	memmove(tmp, ep->b+i, nb-i);
+	partial = nb-i;
+	ep->nb = i;
+	ep->nr = j;
+	ep->b[i] = '\0';
+	if(i != 0){
+		sendp(win->cevent, ep);
+		recvp(writechan);
+	}
+	partial = nb-i;
+	memmove(e[n]->b, tmp, partial);
+	r->ofcall.count = r->ifcall.count;
+	respond(r, nil);
+}
+
+void
+fsdestroyfid(Fid *fid)
+{
+	if(fid->aux)
+		free(fid->aux);
+}
+
+Srv fs = {
+.read=	fsread,
+.write=	fswrite,
+.flush=	fsflush,
+.destroyfid=	fsdestroyfid,
+.leavefdsopen=	1,
+};
+
+void
+mountcons(void)
+{
+	fschan = chancreate(sizeof(Fsevent), 0);
+	writechan = chancreate(sizeof(void*), 0);
+	fs.tree = alloctree("win", "win", DMDIR|0555, nil);
+	devcons = createfile(fs.tree->root, "cons", "win", 0666, nil);
+	if(devcons == nil)
+		sysfatal("creating /dev/cons: %r");
+	devnew = createfile(fs.tree->root, "wnew", "win", 0666, nil);
+	if(devnew == nil)
+		sysfatal("creating /dev/wnew: %r");
+	threadpostmountsrv(&fs, nil, "/dev", MBEFORE);
+}
--- /dev/null
+++ b/acme/bin/source/win/_main.c
@@ -1,0 +1,651 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include <fcall.h>
+#include <9p.h>
+#include <ctype.h>
+#include "dat.h"
+
+void	mainctl(void*);
+void	startcmd(char *[], int*);
+void	stdout2body(void*);
+
+int	debug;
+int	notepg;
+int	eraseinput;
+int	dirty = 0;
+
+Window *win;		/* the main window */
+
+void
+usage(void)
+{
+	fprint(2, "usage: win [command]\n");
+	threadexitsall("usage");
+}
+
+void
+threadmain(int argc, char *argv[])
+{
+	int i, j;
+	char *dir, *tag, *name;
+	char buf[1024], **av;
+
+	quotefmtinstall();
+	rfork(RFNAMEG);
+	ARGBEGIN{
+	case 'd':
+		debug = 1;
+		chatty9p++;
+		break;
+	case 'e':
+		eraseinput = 1;
+		break;
+	case 'D':
+{extern int _threaddebuglevel;
+		_threaddebuglevel = 1<<20;
+}
+	}ARGEND
+
+	if(argc == 0){
+		av = emalloc(3*sizeof(char*));
+		av[0] = "rc";
+		av[1] = "-i";
+		name = getenv("sysname");
+	}else{
+		av = argv;
+		name = utfrrune(av[0], '/');
+		if(name)
+			name++;
+		else
+			name = av[0];
+	}
+
+	if(getwd(buf, sizeof buf) == 0)
+		dir = "/";
+	else
+		dir = buf;
+	dir = estrdup(dir);
+	tag = estrdup(dir);
+	tag = eappend(estrdup(tag), "/-", name);
+	win = newwindow();
+	snprint(buf, sizeof buf, "%d", win->id);
+	putenv("winid", buf);
+	winname(win, tag);
+	wintagwrite(win, "Send Noscroll", 5+8);
+	threadcreate(mainctl, win, STACK);
+	mountcons();
+	threadcreate(fsloop, nil, STACK);
+	startpipe();
+	startcmd(av, &notepg);
+
+	strcpy(buf, "win");
+	j = 3;
+	for(i=0; i<argc && j+1+strlen(argv[i])+1<sizeof buf; i++){
+		strcpy(buf+j, " ");
+		strcpy(buf+j+1, argv[i]);
+		j += 1+strlen(argv[i]);
+	}
+
+	ctlprint(win->ctl, "scroll");
+	winsetdump(win, dir, buf);
+}
+
+int
+EQUAL(char *s, char *t)
+{
+	while(tolower(*s) == tolower(*t++))
+		if(*s++ == '\0')
+			return 1;
+	return 0;
+}
+
+int
+command(Window *w, char *s)
+{
+	while(*s==' ' || *s=='\t' || *s=='\n')
+		s++;
+	if(strcmp(s, "Delete")==0){
+		windel(w, 1);
+		threadexitsall(nil);
+		return 1;
+	}
+	if(strcmp(s, "Del")==0){
+		if(windel(w, 0))
+			threadexitsall(nil);
+		return 1;
+	}
+	if(EQUAL(s, "scroll")){
+		ctlprint(w->ctl, "scroll\nshow");
+		return 1;
+	}
+	if(EQUAL(s, "noscroll")){
+		ctlprint(w->ctl, "noscroll");
+		return 1;
+	}
+	return 0;
+}
+
+static long
+utfncpy(char *to, char *from, int n)
+{
+	char *end, *e;
+
+	e = to+n;
+	if(to >= e)
+		return 0;
+	end = memccpy(to, from, '\0', e - to);
+	if(end == nil){
+		end = e;
+		if(end[-1]&0x80){
+			if(end-2>=to && (end[-2]&0xE0)==0xC0)
+				return end-to;
+			if(end-3>=to && (end[-3]&0xF0)==0xE0)
+				return end-to;
+			while(end>to && (*--end&0xC0)==0x80)
+				;
+		}
+	}else
+		end--;
+	return end - to;
+}
+
+/* sendinput and fsloop run in the same proc (can't interrupt each other). */
+static Req *q;
+static Req **eq;
+static int
+__sendinput(Window *w, ulong q0, ulong q1)
+{
+	char *s, *t;
+	int n, nb, eofchar;
+	static int partial;
+	static char tmp[UTFmax];
+	Req *r;
+	Rune rune;
+
+	if(!q)
+		return 0;
+
+	r = q;
+	n = 0;
+	if(partial){
+	Partial:
+		nb = partial;
+		if(nb > r->ifcall.count)
+			nb = r->ifcall.count;
+		memmove(r->ofcall.data, tmp, nb);
+		if(nb!=partial)
+			memmove(tmp, tmp+nb, partial-nb);
+		partial -= nb;
+		q = r->aux;
+		if(q == nil)
+			eq = &q;
+		r->aux = nil;
+		r->ofcall.count = nb;
+		if(debug)
+			fprint(2, "satisfy read with partial\n");
+		respond(r, nil);
+		return n;
+	}
+	if(q0==q1)
+		return 0;
+	s = emalloc((q1-q0)*UTFmax+1);
+	n = winread(w, q0, q1, s);
+	s[n] = '\0';
+	t = strpbrk(s, "\n\004");
+	if(t == nil){
+		free(s);
+		return 0;
+	}
+	r = q;
+	eofchar = 0;
+	if(*t == '\004'){
+		eofchar = 1;
+		*t = '\0';
+	}else
+		*++t = '\0';
+	nb = utfncpy((char*)r->ofcall.data, s, r->ifcall.count);
+	if(nb==0 && s<t && r->ifcall.count > 0){
+		partial = utfncpy(tmp, s, UTFmax);
+		assert(partial > 0);
+		chartorune(&rune, tmp);
+		partial = runelen(rune);
+		free(s);
+		n = 1;
+		goto Partial;
+	}
+	n = utfnlen(r->ofcall.data, nb);
+	if(nb==strlen(s) && eofchar)
+		n++;
+	r->ofcall.count = nb;
+	q = r->aux;
+	if(q == nil)
+		eq = &q;
+	r->aux = nil;
+	if(debug)
+		fprint(2, "read returns %lud-%lud: %.*q\n", q0, q0+n, n, r->ofcall.data);
+	respond(r, nil);
+	return n;
+}
+
+static int
+_sendinput(Window *w, ulong q0, ulong *q1)
+{
+	char buf[32];
+	int n;
+
+	n = __sendinput(w, q0, *q1);
+	if(!n || !eraseinput)
+		return n;
+	/* erase q0 to q0+n */
+	sprint(buf, "#%lud,#%lud", q0, q0+n);
+	winsetaddr(w, buf, 0);
+	write(w->data, buf, 0);
+	*q1 -= n;
+	return 0;
+}
+
+int
+sendinput(Window *w, ulong q0, ulong *q1)
+{
+	ulong n;
+	Req *oq;
+
+	n = 0;
+	do {
+		oq = q;
+		n += _sendinput(w, q0+n, q1);
+	} while(q != oq);
+	return n;
+}
+
+Event esendinput;
+void
+fsloop(void*)
+{
+	Fsevent e;
+	Req **l, *r;
+
+	eq = &q;
+	memset(&esendinput, 0, sizeof esendinput);
+	esendinput.c1 = 'C';
+	for(;;){
+		while(recv(fschan, &e) == -1)
+			;
+		r = e.r;
+		switch(e.type){
+		case 'r':
+			*eq = r;
+			r->aux = nil;
+			eq = &r->aux;
+			/* call sendinput with hostpt and endpt */
+			sendp(win->cevent, &esendinput);
+			break;
+		case 'f':
+			for(l=&q; *l; l=&(*l)->aux){
+				if(*l == r->oldreq){
+					*l = (*l)->aux;
+					if(*l == nil)
+						eq = l;
+					respond(r->oldreq, "interrupted");
+					break;
+				}
+			}
+			respond(r, nil);
+			break;
+		}
+	}
+}	
+
+void
+sendit(char *s)
+{
+//	char tmp[32];
+
+	write(win->body, s, strlen(s));
+/*
+ * RSC: The problem here is that other procs can call sendit,
+ * so we lose our single-threadedness if we call sendinput.
+ * In fact, we don't even have the right queue memory,
+ * I think that we'll get a write event from the body write above,
+ * and we can do the sendinput then, from our single thread.
+ *
+ * I still need to figure out how to test this assertion for
+ * programs that use /srv/win*
+ *
+	winselect(win, "$", 0);
+	seek(win->addr, 0UL, 0);
+	if(read(win->addr, tmp, 2*12) == 2*12)
+		hostpt += sendinput(win, hostpt, atol(tmp), );
+ */
+}
+
+void
+execevent(Window *w, Event *e, int (*command)(Window*, char*))
+{
+	Event *ea, *e2;
+	int n, na, len, needfree;
+	char *s, *t;
+
+	ea = nil;
+	e2 = nil;
+	if(e->flag & 2)
+		e2 = recvp(w->cevent);
+	if(e->flag & 8){
+		ea = recvp(w->cevent);
+		na = ea->nb;
+		recvp(w->cevent);
+	}else
+		na = 0;
+
+	needfree = 0;
+	s = e->b;
+	if(e->nb==0 && (e->flag&2)){
+		s = e2->b;
+		e->q0 = e2->q0;
+		e->q1 = e2->q1;
+		e->nb = e2->nb;
+	}
+	if(e->nb==0 && e->q0<e->q1){
+		/* fetch data from window */
+		s = emalloc((e->q1-e->q0)*UTFmax+2);
+		n = winread(w, e->q0, e->q1, s);
+		s[n] = '\0';
+		needfree = 1;
+	}else 
+	if(na){
+		t = emalloc(strlen(s)+1+na+2);
+		sprint(t, "%s %s", s, ea->b);
+		if(needfree)
+			free(s);
+		s = t;
+		needfree = 1;
+	}
+
+	/* if it's a known command, do it */
+	/* if it's a long message, it can't be for us anyway */
+	if(!command(w, s) && s[0]!='\0'){	/* send it as typed text */
+		/* if it's a built-in from the tag, send it back */
+		if(e->flag & 1)
+			fprint(w->event, "%c%c%d %d\n", e->c1, e->c2, e->q0, e->q1);
+		else{	/* send text to main window */
+			len = strlen(s);
+			if(len>0 && s[len-1]!='\n' && s[len-1]!='\004'){
+				if(!needfree){
+					/* if(needfree), we left room for a newline before */
+					t = emalloc(len+2);
+					strcpy(t, s);
+					s = t;
+					needfree = 1;
+				}
+				s[len++] = '\n';
+				s[len] = '\0';
+			}
+			sendit(s);
+		}
+	}
+	if(needfree)
+		free(s);
+}
+
+int
+hasboundary(Rune *r, int nr)
+{
+	int i;
+
+	for(i=0; i<nr; i++)
+		if(r[i]=='\n' || r[i]=='\004')
+			return 1;
+	return 0;
+}
+
+void
+mainctl(void *v)
+{
+	Window *w;
+	Event *e;
+	int delta, pendingS, pendingK;
+	ulong hostpt, endpt;
+	char tmp[32];
+
+	w = v;
+	proccreate(wineventproc, w, STACK);
+
+	hostpt = 0;
+	endpt = 0;
+	winsetaddr(w, "0", 0);
+	pendingS = 0;
+	pendingK = 0;
+	for(;;){
+		if(debug)
+			fprint(2, "input range %lud-%lud\n", hostpt, endpt);
+		e = recvp(w->cevent);
+		if(debug)
+			fprint(2, "msg: %C %C %d %d %d %d %q\n",
+				e->c1 ? e->c1 : ' ', e->c2 ? e->c2 : ' ', e->q0, e->q1, e->flag, e->nb, e->b);
+		switch(e->c1){
+		default:
+		Unknown:
+			fprint(2, "unknown message %c%c\n", e->c1, e->c2);
+			break;
+
+		case 'C':	/* input needed for /dev/cons */
+			if(pendingS)
+				pendingK = 1;
+			else
+				hostpt += sendinput(w, hostpt, &endpt);
+			break;
+
+		case 'S':	/* output to stdout */
+			sprint(tmp, "#%lud", hostpt);
+			winsetaddr(w, tmp, 0);
+			write(w->data, e->b, e->nb);
+			pendingS += utfnlen(e->b, e->nb);
+			break;
+	
+		case 'E':	/* write to tag or body; body happens due to sendit */
+			delta = e->q1-e->q0;
+			if(e->c2=='I'){
+				endpt += delta;
+				if(e->q0 < hostpt)
+					hostpt += delta;
+				else
+					hostpt += sendinput(w, hostpt, &endpt);
+				break;
+			}
+			if(!islower(e->c2))
+				fprint(2, "win msg: %C %C %d %d %d %d %q\n",
+					e->c1, e->c2, e->q0, e->q1, e->flag, e->nb, e->b);
+			break;
+	
+		case 'F':	/* generated by our actions (specifically case 'S' above) */
+			delta = e->q1-e->q0;
+			if(e->c2=='D'){
+				/* we know about the delete by _sendinput */
+				break;
+			}
+			if(e->c2=='I'){
+				pendingS -= e->q1 - e->q0;
+				if(pendingS < 0)
+					fprint(2, "win: pendingS = %d\n", pendingS);
+				if(e->q0 != hostpt)
+					fprint(2, "win: insert at %d expected %lud\n", e->q0, hostpt);
+				endpt += delta;
+				hostpt += delta;
+				sendp(writechan, nil);
+				if(pendingS == 0 && pendingK){
+					pendingK = 0;
+					hostpt += sendinput(w, hostpt, &endpt);
+				}
+				break;
+			}
+			if(!islower(e->c2))
+				fprint(2, "win msg: %C %C %d %d %d %d %q\n",
+					e->c1, e->c2, e->q0, e->q1, e->flag, e->nb, e->b);
+			break;
+
+		case 'K':
+			delta = e->q1-e->q0;
+			switch(e->c2){
+			case 'D':
+				endpt -= delta;
+				if(e->q1 < hostpt)
+					hostpt -= delta;
+				else if(e->q0 < hostpt)
+					hostpt = e->q0;
+				break;
+			case 'I':
+				delta = e->q1 - e->q0;
+				endpt += delta;
+				if(endpt < e->q1)	/* just in case */
+					endpt = e->q1;
+				if(e->q0 < hostpt)
+					hostpt += delta;
+				if(e->nr>0 && e->r[e->nr-1]==0x7F){
+					write(notepg, "interrupt", 9);
+					hostpt = endpt;
+					break;
+				}
+				if(e->q0 >= hostpt
+				&& hasboundary(e->r, e->nr)){
+					/*
+					 * If we are between the S message (which
+					 * we processed by inserting text in the
+					 * window) and the F message notifying us
+					 * that the text has been inserted, then our
+					 * impression of the hostpt and acme's
+					 * may be different.  This could be seen if you
+					 * hit enter a bunch of times in a con
+					 * session.  To work around the unreliability,
+					 * only send input if we don't have an S pending.
+					 * The same race occurs between when a character
+					 * is typed and when we get notice of it, but
+					 * since characters tend to be typed at the end
+					 * of the buffer, we don't run into it.  There's
+					 * no workaround possible for this typing race,
+					 * since we can't tell when the user has typed
+					 * something but we just haven't been notified.
+					 */
+					if(pendingS)
+						pendingK = 1;
+					else
+						hostpt += sendinput(w, hostpt, &endpt);
+				}
+				break;
+			}
+			break;
+	
+		case 'M':	/* mouse */
+			delta = e->q1-e->q0;
+			switch(e->c2){
+			case 'x':
+			case 'X':
+				execevent(w, e, command);
+				break;
+	
+			case 'l':	/* reflect all searches back to acme */
+			case 'L':
+				if(e->flag & 2)
+					recvp(w->cevent);
+				winwriteevent(w, e);
+				break;
+	
+			case 'I':
+				endpt += delta;
+				if(e->q0 < hostpt)
+					hostpt += delta;
+				else
+					hostpt += sendinput(w, hostpt, &endpt);
+				break;
+
+			case 'D':
+				endpt -= delta;
+				if(e->q1 < hostpt)
+					hostpt -= delta;
+				else if(e->q0 < hostpt)
+					hostpt = e->q0;
+				break;
+			case 'd':	/* modify away; we don't care */
+			case 'i':
+				break;
+	
+			default:
+				goto Unknown;
+			}
+		}
+	}
+}
+
+enum
+{
+	NARGS		= 100,
+	NARGCHAR	= 8*1024,
+	EXECSTACK 	= STACK+(NARGS+1)*sizeof(char*)+NARGCHAR
+};
+
+struct Exec
+{
+	char		**argv;
+	Channel	*cpid;
+};
+
+int
+lookinbin(char *s)
+{
+	if(s[0] == '/')
+		return 0;
+	if(s[0]=='.' && s[1]=='/')
+		return 0;
+	if(s[0]=='.' && s[1]=='.' && s[2]=='/')
+		return 0;
+	return 1;
+}
+
+/* adapted from mail.  not entirely free of details from that environment */
+void
+execproc(void *v)
+{
+	struct Exec *e;
+	char *cmd, **av;
+	Channel *cpid;
+
+	e = v;
+	rfork(RFCFDG|RFNOTEG);
+	av = e->argv;
+	close(0);
+	open("/dev/cons", OREAD);
+	close(1);
+	open("/dev/cons", OWRITE);
+	dup(1, 2);
+	cpid = e->cpid;
+	free(e);
+	procexec(cpid, av[0], av);
+	if(lookinbin(av[0])){
+		cmd = estrstrdup("/bin/", av[0]);
+		procexec(cpid, cmd, av);
+	}
+	error("can't exec %s: %r", av[0]);
+}
+
+void
+startcmd(char *argv[], int *notepg)
+{
+	struct Exec *e;
+	Channel *cpid;
+	char buf[64];
+	int pid;
+
+	e = emalloc(sizeof(struct Exec));
+	e->argv = argv;
+	cpid = chancreate(sizeof(ulong), 0);
+	e->cpid = cpid;
+	sprint(buf, "/mnt/wsys/%d", win->id);
+	bind(buf, "/dev/acme", MREPL);
+	proccreate(execproc, e, EXECSTACK);
+	do
+		pid = recvul(cpid);
+	while(pid == -1);
+	sprint(buf, "/proc/%d/notepg", pid);
+	*notepg = open(buf, OWRITE);
+}
--- /dev/null
+++ b/acme/bin/source/win/dat.h
@@ -1,0 +1,95 @@
+typedef struct Fsevent Fsevent;
+typedef struct Event Event;
+typedef struct Message Message;
+typedef struct Window Window;
+
+enum
+{
+	STACK		= 8192,
+	NPIPEDATA	= 8000,
+	NPIPE		= NPIPEDATA+32,
+	/* EVENTSIZE is really 256 in acme, but we use events internally and want bigger buffers */
+	EVENTSIZE	= 8192,
+	NEVENT		= 5,
+};
+
+struct Fsevent
+{
+	int	type;
+	void	*r;
+};
+
+struct Event
+{
+	int	c1;
+	int	c2;
+	int	q0;
+	int	q1;
+	int	flag;
+	int	nb;
+	int	nr;
+	char	b[EVENTSIZE*UTFmax+1];
+	Rune	r[EVENTSIZE+1];
+};
+
+struct Window
+{
+	/* file descriptors */
+	int		ctl;
+	int		event;
+	int		addr;
+	int		data;
+	int		body;
+
+	/* event input */
+	char		buf[512];
+	char		*bufp;
+	int		nbuf;
+	Event	e[NEVENT];
+
+	int		id;
+	int		open;
+	Channel	*cevent;
+};
+
+extern	Window*	newwindow(void);
+extern	int		winopenfile(Window*, char*);
+extern	void		wintagwrite(Window*, char*, int);
+extern	void		winname(Window*, char*);
+extern	void		winwriteevent(Window*, Event*);
+extern	int		winread(Window*, uint, uint, char*);
+extern	int		windel(Window*, int);
+extern	void		wingetevent(Window*, Event*);
+extern	void		wineventproc(void*);
+extern	void		winclean(Window*);
+extern	int		winselect(Window*, char*, int);
+extern	int		winsetaddr(Window*, char*, int);
+extern	void		windormant(Window*);
+extern	void		winsetdump(Window*, char*, char*);
+
+extern	void		ctlprint(int, char*, ...);
+extern	void*	emalloc(uint);
+extern	char*	estrdup(char*);
+extern	char*	estrstrdup(char*, char*);
+extern	char*	egrow(char*, char*, char*);
+extern	char*	eappend(char*, char*, char*);
+extern	void		error(char*, ...);
+
+extern	void		startpipe(void);
+extern	void		sendit(char*);
+extern	void		execevent(Window *w, Event *e, int (*)(Window*, char*));
+
+extern	void		mountcons(void);
+extern	void		fsloop(void*);
+
+extern	int		newpipewin(int, char*);
+extern	void		startpipe(void);
+extern	int		pipecommand(Window*, char*);
+extern	void		pipectl(void*);
+
+#pragma	varargck	argpos	error	1
+#pragma	varargck	argpos	ctlprint	2
+
+extern	Window	*win;
+extern	Channel	*fschan, *writechan;
+
--- /dev/null
+++ b/acme/bin/source/win/fs.c
@@ -1,0 +1,147 @@
+#include <u.h>
+#include <libc.h>
+#include <fcall.h>
+#include <thread.h>
+#include <9p.h>
+#include "dat.h"
+
+Channel *fschan;
+Channel *writechan;
+
+static File *devcons, *devnew;
+
+static void
+fsread(Req *r)
+{
+	Fsevent e;
+
+	if(r->fid->file == devnew){
+		if(r->fid->aux==nil){
+			respond(r, "phase error");
+			return;
+		}
+		readstr(r, r->fid->aux);
+		respond(r, nil);
+		return;
+	}
+
+	assert(r->fid->file == devcons);
+	e.type = 'r';
+	e.r = r;
+	send(fschan, &e);
+}
+
+static void
+fsflush(Req *r)
+{
+	Fsevent e;
+
+	e.type = 'f';
+	e.r = r;
+	send(fschan, &e);
+}
+
+static void
+fswrite(Req *r)
+{
+	static Event *e[4];
+	Event *ep;
+	int i, j, ei, nb, wid, pid;
+	Rune rune;
+	char *s;
+	char tmp[UTFmax], *t;
+	static int n, partial;
+
+	if(r->fid->file == devnew){
+		if(r->fid->aux){
+			respond(r, "already created a window");
+			return;
+		}
+		s = emalloc(r->ifcall.count+1);
+		memmove(s, r->ifcall.data, r->ifcall.count);
+		s[r->ifcall.count] = 0;
+		pid = strtol(s, &t, 0);
+		if(*t==' ')
+			t++;
+		i = newpipewin(pid, t);
+		free(s);
+		s = emalloc(32);
+		sprint(s, "%lud", (ulong)i);
+		r->fid->aux = s;
+		r->ofcall.count = r->ifcall.count;
+		respond(r, nil);
+		return;
+	}
+
+	assert(r->fid->file == devcons);
+
+	if(e[0] == nil){
+		for(i=0; i<nelem(e); i++){
+			e[i] = emalloc(sizeof(Event));
+			e[i]->c1 = 'S';
+		}
+	}
+
+	ep = e[n];
+	n = (n+1)%nelem(e);
+	assert(r->ifcall.count <= 8192);	/* is this guaranteed by lib9p? */
+	nb = r->ifcall.count;
+	memmove(ep->b+partial, r->ifcall.data, nb);
+	nb += partial;
+	ep->b[nb] = '\0';
+	if(strlen(ep->b) < nb){	/* nulls in data */
+		t = ep->b;
+		for(i=j=0; i<nb; i++)
+			if(ep->b[i] != '\0')
+				t[j++] = ep->b[i];
+		nb = j;
+		t[j] = '\0';
+	}
+	ei = nb>8192? 8192 : nb;
+	/* process bytes into runes, transferring terminal partial runes into next buffer */
+	for(i=j=0; i<ei && fullrune(ep->b+i, ei-i); i+=wid,j++)
+		wid = chartorune(&rune, ep->b+i);
+	memmove(tmp, ep->b+i, nb-i);
+	partial = nb-i;
+	ep->nb = i;
+	ep->nr = j;
+	ep->b[i] = '\0';
+	if(i != 0){
+		sendp(win->cevent, ep);
+		recvp(writechan);
+	}
+	partial = nb-i;
+	memmove(e[n]->b, tmp, partial);
+	r->ofcall.count = r->ifcall.count;
+	respond(r, nil);
+}
+
+void
+fsdestroyfid(Fid *fid)
+{
+	if(fid->aux)
+		free(fid->aux);
+}
+
+Srv fs = {
+.read=	fsread,
+.write=	fswrite,
+.flush=	fsflush,
+.destroyfid=	fsdestroyfid,
+.leavefdsopen=	1,
+};
+
+void
+mountcons(void)
+{
+	fschan = chancreate(sizeof(Fsevent), 0);
+	writechan = chancreate(sizeof(void*), 0);
+	fs.tree = alloctree("win", "win", DMDIR|0555, nil);
+	devcons = createfile(fs.tree->root, "cons", "win", 0666, nil);
+	if(devcons == nil)
+		sysfatal("creating /dev/cons: %r");
+	devnew = createfile(fs.tree->root, "wnew", "win", 0666, nil);
+	if(devnew == nil)
+		sysfatal("creating /dev/wnew: %r");
+	threadpostmountsrv(&fs, nil, "/dev", MBEFORE);
+}
--- /dev/null
+++ b/acme/bin/source/win/main.c
@@ -1,0 +1,646 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include <fcall.h>
+#include <9p.h>
+#include <ctype.h>
+#include "dat.h"
+
+void	mainctl(void*);
+void	startcmd(char *[], int*);
+void	stdout2body(void*);
+
+int	debug;
+int	notepg;
+int	eraseinput;
+int	dirty = 0;
+
+Window *win;		/* the main window */
+
+void
+usage(void)
+{
+	fprint(2, "usage: win [command]\n");
+	threadexitsall("usage");
+}
+
+void
+threadmain(int argc, char *argv[])
+{
+	int i, j;
+	char *dir, *tag, *name;
+	char buf[1024], **av;
+
+	quotefmtinstall();
+	rfork(RFNAMEG);
+	ARGBEGIN{
+	case 'd':
+		debug = 1;
+		chatty9p++;
+		break;
+	case 'e':
+		eraseinput = 1;
+		break;
+	case 'D':
+{extern int _threaddebuglevel;
+		_threaddebuglevel = 1<<20;
+}
+	}ARGEND
+
+	if(argc == 0){
+		av = emalloc(3*sizeof(char*));
+		av[0] = "rc";
+		av[1] = "-i";
+		name = getenv("sysname");
+	}else{
+		av = argv;
+		name = utfrrune(av[0], '/');
+		if(name)
+			name++;
+		else
+			name = av[0];
+	}
+
+	if(getwd(buf, sizeof buf) == 0)
+		dir = "/";
+	else
+		dir = buf;
+	dir = estrdup(dir);
+	tag = estrdup(dir);
+	tag = eappend(estrdup(tag), "/-", name);
+	win = newwindow();
+	snprint(buf, sizeof buf, "%d", win->id);
+	putenv("winid", buf);
+	winname(win, tag);
+	wintagwrite(win, "Send Noscroll", 5+8);
+	threadcreate(mainctl, win, STACK);
+	mountcons();
+	threadcreate(fsloop, nil, STACK);
+	startpipe();
+	startcmd(av, &notepg);
+
+	strcpy(buf, "win");
+	j = 3;
+	for(i=0; i<argc && j+1+strlen(argv[i])+1<sizeof buf; i++){
+		strcpy(buf+j, " ");
+		strcpy(buf+j+1, argv[i]);
+		j += 1+strlen(argv[i]);
+	}
+
+	ctlprint(win->ctl, "scroll");
+	winsetdump(win, dir, buf);
+}
+
+int
+EQUAL(char *s, char *t)
+{
+	while(tolower(*s) == tolower(*t++))
+		if(*s++ == '\0')
+			return 1;
+	return 0;
+}
+
+int
+command(Window *w, char *s)
+{
+	while(*s==' ' || *s=='\t' || *s=='\n')
+		s++;
+	if(strcmp(s, "Delete")==0 || strcmp(s, "Del")==0){
+		windel(w, 1);
+		threadexitsall(nil);
+		return 1;
+	}
+	if(EQUAL(s, "scroll")){
+		ctlprint(w->ctl, "scroll\nshow");
+		return 1;
+	}
+	if(EQUAL(s, "noscroll")){
+		ctlprint(w->ctl, "noscroll");
+		return 1;
+	}
+	return 0;
+}
+
+static long
+utfncpy(char *to, char *from, int n)
+{
+	char *end, *e;
+
+	e = to+n;
+	if(to >= e)
+		return 0;
+	end = memccpy(to, from, '\0', e - to);
+	if(end == nil){
+		end = e;
+		if(end[-1]&0x80){
+			if(end-2>=to && (end[-2]&0xE0)==0xC0)
+				return end-to;
+			if(end-3>=to && (end[-3]&0xF0)==0xE0)
+				return end-to;
+			while(end>to && (*--end&0xC0)==0x80)
+				;
+		}
+	}else
+		end--;
+	return end - to;
+}
+
+/* sendinput and fsloop run in the same proc (can't interrupt each other). */
+static Req *q;
+static Req **eq;
+static int
+__sendinput(Window *w, ulong q0, ulong q1)
+{
+	char *s, *t;
+	int n, nb, eofchar;
+	static int partial;
+	static char tmp[UTFmax];
+	Req *r;
+	Rune rune;
+
+	if(!q)
+		return 0;
+
+	r = q;
+	n = 0;
+	if(partial){
+	Partial:
+		nb = partial;
+		if(nb > r->ifcall.count)
+			nb = r->ifcall.count;
+		memmove(r->ofcall.data, tmp, nb);
+		if(nb!=partial)
+			memmove(tmp, tmp+nb, partial-nb);
+		partial -= nb;
+		q = r->aux;
+		if(q == nil)
+			eq = &q;
+		r->aux = nil;
+		r->ofcall.count = nb;
+		if(debug)
+			fprint(2, "satisfy read with partial\n");
+		respond(r, nil);
+		return n;
+	}
+	if(q0==q1)
+		return 0;
+	s = emalloc((q1-q0)*UTFmax+1);
+	n = winread(w, q0, q1, s);
+	s[n] = '\0';
+	t = strpbrk(s, "\n\004");
+	if(t == nil){
+		free(s);
+		return 0;
+	}
+	r = q;
+	eofchar = 0;
+	if(*t == '\004'){
+		eofchar = 1;
+		*t = '\0';
+	}else
+		*++t = '\0';
+	nb = utfncpy((char*)r->ofcall.data, s, r->ifcall.count);
+	if(nb==0 && s<t && r->ifcall.count > 0){
+		partial = utfncpy(tmp, s, UTFmax);
+		assert(partial > 0);
+		chartorune(&rune, tmp);
+		partial = runelen(rune);
+		free(s);
+		n = 1;
+		goto Partial;
+	}
+	n = utfnlen(r->ofcall.data, nb);
+	if(nb==strlen(s) && eofchar)
+		n++;
+	r->ofcall.count = nb;
+	q = r->aux;
+	if(q == nil)
+		eq = &q;
+	r->aux = nil;
+	if(debug)
+		fprint(2, "read returns %lud-%lud: %.*q\n", q0, q0+n, n, r->ofcall.data);
+	respond(r, nil);
+	return n;
+}
+
+static int
+_sendinput(Window *w, ulong q0, ulong *q1)
+{
+	char buf[32];
+	int n;
+
+	n = __sendinput(w, q0, *q1);
+	if(!n || !eraseinput)
+		return n;
+	/* erase q0 to q0+n */
+	sprint(buf, "#%lud,#%lud", q0, q0+n);
+	winsetaddr(w, buf, 0);
+	write(w->data, buf, 0);
+	*q1 -= n;
+	return 0;
+}
+
+int
+sendinput(Window *w, ulong q0, ulong *q1)
+{
+	ulong n;
+	Req *oq;
+
+	n = 0;
+	do {
+		oq = q;
+		n += _sendinput(w, q0+n, q1);
+	} while(q != oq);
+	return n;
+}
+
+Event esendinput;
+void
+fsloop(void*)
+{
+	Fsevent e;
+	Req **l, *r;
+
+	eq = &q;
+	memset(&esendinput, 0, sizeof esendinput);
+	esendinput.c1 = 'C';
+	for(;;){
+		while(recv(fschan, &e) == -1)
+			;
+		r = e.r;
+		switch(e.type){
+		case 'r':
+			*eq = r;
+			r->aux = nil;
+			eq = &r->aux;
+			/* call sendinput with hostpt and endpt */
+			sendp(win->cevent, &esendinput);
+			break;
+		case 'f':
+			for(l=&q; *l; l=&(*l)->aux){
+				if(*l == r->oldreq){
+					*l = (*l)->aux;
+					if(*l == nil)
+						eq = l;
+					respond(r->oldreq, "interrupted");
+					break;
+				}
+			}
+			respond(r, nil);
+			break;
+		}
+	}
+}	
+
+void
+sendit(char *s)
+{
+//	char tmp[32];
+
+	write(win->body, s, strlen(s));
+/*
+ * RSC: The problem here is that other procs can call sendit,
+ * so we lose our single-threadedness if we call sendinput.
+ * In fact, we don't even have the right queue memory,
+ * I think that we'll get a write event from the body write above,
+ * and we can do the sendinput then, from our single thread.
+ *
+ * I still need to figure out how to test this assertion for
+ * programs that use /srv/win*
+ *
+	winselect(win, "$", 0);
+	seek(win->addr, 0UL, 0);
+	if(read(win->addr, tmp, 2*12) == 2*12)
+		hostpt += sendinput(win, hostpt, atol(tmp), );
+ */
+}
+
+void
+execevent(Window *w, Event *e, int (*command)(Window*, char*))
+{
+	Event *ea, *e2;
+	int n, na, len, needfree;
+	char *s, *t;
+
+	ea = nil;
+	e2 = nil;
+	if(e->flag & 2)
+		e2 = recvp(w->cevent);
+	if(e->flag & 8){
+		ea = recvp(w->cevent);
+		na = ea->nb;
+		recvp(w->cevent);
+	}else
+		na = 0;
+
+	needfree = 0;
+	s = e->b;
+	if(e->nb==0 && (e->flag&2)){
+		s = e2->b;
+		e->q0 = e2->q0;
+		e->q1 = e2->q1;
+		e->nb = e2->nb;
+	}
+	if(e->nb==0 && e->q0<e->q1){
+		/* fetch data from window */
+		s = emalloc((e->q1-e->q0)*UTFmax+2);
+		n = winread(w, e->q0, e->q1, s);
+		s[n] = '\0';
+		needfree = 1;
+	}else 
+	if(na){
+		t = emalloc(strlen(s)+1+na+2);
+		sprint(t, "%s %s", s, ea->b);
+		if(needfree)
+			free(s);
+		s = t;
+		needfree = 1;
+	}
+
+	/* if it's a known command, do it */
+	/* if it's a long message, it can't be for us anyway */
+	if(!command(w, s) && s[0]!='\0'){	/* send it as typed text */
+		/* if it's a built-in from the tag, send it back */
+		if(e->flag & 1)
+			fprint(w->event, "%c%c%d %d\n", e->c1, e->c2, e->q0, e->q1);
+		else{	/* send text to main window */
+			len = strlen(s);
+			if(len>0 && s[len-1]!='\n' && s[len-1]!='\004'){
+				if(!needfree){
+					/* if(needfree), we left room for a newline before */
+					t = emalloc(len+2);
+					strcpy(t, s);
+					s = t;
+					needfree = 1;
+				}
+				s[len++] = '\n';
+				s[len] = '\0';
+			}
+			sendit(s);
+		}
+	}
+	if(needfree)
+		free(s);
+}
+
+int
+hasboundary(Rune *r, int nr)
+{
+	int i;
+
+	for(i=0; i<nr; i++)
+		if(r[i]=='\n' || r[i]=='\004')
+			return 1;
+	return 0;
+}
+
+void
+mainctl(void *v)
+{
+	Window *w;
+	Event *e;
+	int delta, pendingS, pendingK;
+	ulong hostpt, endpt;
+	char tmp[32];
+
+	w = v;
+	proccreate(wineventproc, w, STACK);
+
+	hostpt = 0;
+	endpt = 0;
+	winsetaddr(w, "0", 0);
+	pendingS = 0;
+	pendingK = 0;
+	for(;;){
+		if(debug)
+			fprint(2, "input range %lud-%lud\n", hostpt, endpt);
+		e = recvp(w->cevent);
+		if(debug)
+			fprint(2, "msg: %C %C %d %d %d %d %q\n",
+				e->c1 ? e->c1 : ' ', e->c2 ? e->c2 : ' ', e->q0, e->q1, e->flag, e->nb, e->b);
+		switch(e->c1){
+		default:
+		Unknown:
+			fprint(2, "unknown message %c%c\n", e->c1, e->c2);
+			break;
+
+		case 'C':	/* input needed for /dev/cons */
+			if(pendingS)
+				pendingK = 1;
+			else
+				hostpt += sendinput(w, hostpt, &endpt);
+			break;
+
+		case 'S':	/* output to stdout */
+			sprint(tmp, "#%lud", hostpt);
+			winsetaddr(w, tmp, 0);
+			write(w->data, e->b, e->nb);
+			pendingS += e->nr;
+			break;
+	
+		case 'E':	/* write to tag or body; body happens due to sendit */
+			delta = e->q1-e->q0;
+			if(e->c2=='I'){
+				endpt += delta;
+				if(e->q0 < hostpt)
+					hostpt += delta;
+				else
+					hostpt += sendinput(w, hostpt, &endpt);
+				break;
+			}
+			if(!islower(e->c2))
+				fprint(2, "win msg: %C %C %d %d %d %d %q\n",
+					e->c1, e->c2, e->q0, e->q1, e->flag, e->nb, e->b);
+			break;
+	
+		case 'F':	/* generated by our actions (specifically case 'S' above) */
+			delta = e->q1-e->q0;
+			if(e->c2=='D'){
+				/* we know about the delete by _sendinput */
+				break;
+			}
+			if(e->c2=='I'){
+				pendingS -= e->q1 - e->q0;
+				if(pendingS < 0)
+					fprint(2, "win: pendingS = %d\n", pendingS);
+				if(e->q0 != hostpt)
+					fprint(2, "win: insert at %d expected %lud\n", e->q0, hostpt);
+				endpt += delta;
+				hostpt += delta;
+				sendp(writechan, nil);
+				if(pendingS == 0 && pendingK){
+					pendingK = 0;
+					hostpt += sendinput(w, hostpt, &endpt);
+				}
+				break;
+			}
+			if(!islower(e->c2))
+				fprint(2, "win msg: %C %C %d %d %d %d %q\n",
+					e->c1, e->c2, e->q0, e->q1, e->flag, e->nb, e->b);
+			break;
+
+		case 'K':
+			delta = e->q1-e->q0;
+			switch(e->c2){
+			case 'D':
+				endpt -= delta;
+				if(e->q1 < hostpt)
+					hostpt -= delta;
+				else if(e->q0 < hostpt)
+					hostpt = e->q0;
+				break;
+			case 'I':
+				delta = e->q1 - e->q0;
+				endpt += delta;
+				if(endpt < e->q1)	/* just in case */
+					endpt = e->q1;
+				if(e->q0 < hostpt)
+					hostpt += delta;
+				if(e->nr>0 && e->r[e->nr-1]==0x7F){
+					write(notepg, "interrupt", 9);
+					hostpt = endpt;
+					break;
+				}
+				if(e->q0 >= hostpt
+				&& hasboundary(e->r, e->nr)){
+					/*
+					 * If we are between the S message (which
+					 * we processed by inserting text in the
+					 * window) and the F message notifying us
+					 * that the text has been inserted, then our
+					 * impression of the hostpt and acme's
+					 * may be different.  This could be seen if you
+					 * hit enter a bunch of times in a con
+					 * session.  To work around the unreliability,
+					 * only send input if we don't have an S pending.
+					 * The same race occurs between when a character
+					 * is typed and when we get notice of it, but
+					 * since characters tend to be typed at the end
+					 * of the buffer, we don't run into it.  There's
+					 * no workaround possible for this typing race,
+					 * since we can't tell when the user has typed
+					 * something but we just haven't been notified.
+					 */
+					if(pendingS)
+						pendingK = 1;
+					else
+						hostpt += sendinput(w, hostpt, &endpt);
+				}
+				break;
+			}
+			break;
+	
+		case 'M':	/* mouse */
+			delta = e->q1-e->q0;
+			switch(e->c2){
+			case 'x':
+			case 'X':
+				execevent(w, e, command);
+				break;
+	
+			case 'l':	/* reflect all searches back to acme */
+			case 'L':
+				if(e->flag & 2)
+					recvp(w->cevent);
+				winwriteevent(w, e);
+				break;
+	
+			case 'I':
+				endpt += delta;
+				if(e->q0 < hostpt)
+					hostpt += delta;
+				else
+					hostpt += sendinput(w, hostpt, &endpt);
+				break;
+
+			case 'D':
+				endpt -= delta;
+				if(e->q1 < hostpt)
+					hostpt -= delta;
+				else if(e->q0 < hostpt)
+					hostpt = e->q0;
+				break;
+			case 'd':	/* modify away; we don't care */
+			case 'i':
+				break;
+	
+			default:
+				goto Unknown;
+			}
+		}
+	}
+}
+
+enum
+{
+	NARGS		= 100,
+	NARGCHAR	= 8*1024,
+	EXECSTACK 	= STACK+(NARGS+1)*sizeof(char*)+NARGCHAR
+};
+
+struct Exec
+{
+	char		**argv;
+	Channel	*cpid;
+};
+
+int
+lookinbin(char *s)
+{
+	if(s[0] == '/')
+		return 0;
+	if(s[0]=='.' && s[1]=='/')
+		return 0;
+	if(s[0]=='.' && s[1]=='.' && s[2]=='/')
+		return 0;
+	return 1;
+}
+
+/* adapted from mail.  not entirely free of details from that environment */
+void
+execproc(void *v)
+{
+	struct Exec *e;
+	char *cmd, **av;
+	Channel *cpid;
+
+	e = v;
+	rfork(RFCFDG|RFNOTEG);
+	av = e->argv;
+	close(0);
+	open("/dev/cons", OREAD);
+	close(1);
+	open("/dev/cons", OWRITE);
+	dup(1, 2);
+	cpid = e->cpid;
+	free(e);
+	procexec(cpid, av[0], av);
+	if(lookinbin(av[0])){
+		cmd = estrstrdup("/bin/", av[0]);
+		procexec(cpid, cmd, av);
+	}
+	error("can't exec %s: %r", av[0]);
+}
+
+void
+startcmd(char *argv[], int *notepg)
+{
+	struct Exec *e;
+	Channel *cpid;
+	char buf[64];
+	int pid;
+
+	e = emalloc(sizeof(struct Exec));
+	e->argv = argv;
+	cpid = chancreate(sizeof(ulong), 0);
+	e->cpid = cpid;
+	sprint(buf, "/mnt/wsys/%d", win->id);
+	bind(buf, "/dev/acme", MREPL);
+	proccreate(execproc, e, EXECSTACK);
+	do
+		pid = recvul(cpid);
+	while(pid == -1);
+	sprint(buf, "/proc/%d/notepg", pid);
+	*notepg = open(buf, OWRITE);
+}
--- /dev/null
+++ b/acme/bin/source/win/mkfile
@@ -1,0 +1,25 @@
+</$objtype/mkfile
+
+TARG=win
+OFILES=\
+	fs.$O\
+	main.$O\
+	pipe.$O\
+	util.$O\
+	win.$O
+
+HFILES=dat.h
+LIB=/$objtype/lib/lib9p.a
+
+BIN=/acme/bin/$objtype
+</sys/src/cmd/mkone
+
+UPDATE=\
+	mkfile\
+	$HFILES\
+	${OFILES:%.$O=%.c}\
+	${TARG:%=/acme/bin/$objtype/%}\
+	
+syms:V:
+	8c -a main.c	>syms
+	8c -aa util.c win.c 	>>syms
--- /dev/null
+++ b/acme/bin/source/win/pipe.c
@@ -1,0 +1,175 @@
+#include <u.h>
+#include <libc.h>
+#include <fcall.h>
+#include <thread.h>
+#include <9p.h>
+#include "dat.h"
+
+typedef struct Wpid Wpid;
+struct Wpid
+{
+	int		pid;
+	Window	*w;
+	Wpid		*next;
+};
+
+void	pipectl(void*);
+
+int	pipefd;
+Wpid	*wpid;
+int	snarffd;
+Channel *newpipechan;
+
+int
+newpipewin(int pid, char *p)
+{
+	int id;
+	Window *w;
+	Wpid *wp;
+
+	w = newwindow();
+	winname(w, p);
+	wintagwrite(w, "Send ", 5);
+	wp = emalloc(sizeof(Wpid));
+	wp->pid = pid;
+	wp->w = w;
+	wp->next = wpid;	/* BUG: this happens in fsread proc (we don't use wpid, so it's okay) */
+	wpid = wp;
+	id = w->id;
+	sendp(newpipechan, w);
+	return id;
+}
+
+int
+pipecommand(Window *w, char *s)
+{
+	ulong q0, q1;
+	char tmp[32], *t;
+	int n, k;
+
+	while(*s==' ' || *s=='\t' || *s=='\n')
+		s++;
+	if(strcmp(s, "Delete")==0){
+		windel(w, 1);
+		threadexits(nil);
+		return 1;
+	}
+	if(strcmp(s, "Del")==0){
+		if(windel(w, 0))
+			threadexits(nil);
+		return 1;
+	}
+	if(strcmp(s, "Send") == 0){
+		if(w->addr < 0)
+			w->addr = winopenfile(w, "addr");
+		ctlprint(w->ctl, "addr=dot\n");
+		seek(w->addr, 0UL, 0);
+		if(read(w->addr, tmp, 2*12) == 2*12){
+			q0 = atol(tmp+0*12);
+			q1 = atol(tmp+1*12);
+			if(q0 == q1){
+				t = nil;
+				k = 0;
+				if(snarffd > 0){
+					seek(0, snarffd, 0);
+					for(;;){
+						t = realloc(t, k+8192+2);
+						if(t == nil)
+							error("alloc failed: %r\n");
+						n = read(snarffd, t+k, 8192);
+						if(n <= 0)
+							break;
+						k += n;
+					}
+					t[k] = 0;
+				}
+			}else{
+				t = emalloc((q1-q0)*UTFmax+2);
+				winread(w, q0, q1, t);
+				k = strlen(t);
+			}
+			if(t!=nil && t[0]!='\0'){
+				if(t[k-1]!='\n' && t[k-1]!='\004'){
+					t[k++] = '\n';
+					t[k] = '\0';
+				}
+				sendit(t);
+			}
+			free(t);
+		}
+		return 1;
+	}
+	return 0;
+}
+
+void
+pipectl(void *v)
+{
+	Window *w;
+	Event *e;
+
+	w = v;
+	proccreate(wineventproc, w, STACK);
+
+	windormant(w);
+	winsetaddr(w, "0", 0);
+	for(;;){
+		e = recvp(w->cevent);
+		switch(e->c1){
+		default:
+		Unknown:
+			fprint(2, "unknown message %c%c\n", e->c1, e->c2);
+			break;
+
+		case 'E':	/* write to body; can't affect us */
+			break;
+	
+		case 'F':	/* generated by our actions; ignore */
+			break;
+	
+		case 'K':	/* ignore */
+			break;
+	
+		case 'M':
+			switch(e->c2){
+			case 'x':
+			case 'X':
+				execevent(w, e, pipecommand);
+				break;
+	
+			case 'l':	/* reflect all searches back to acme */
+			case 'L':
+				if(e->flag & 2)
+					recvp(w->cevent);
+				winwriteevent(w, e);
+				break;
+	
+			case 'I':	/* modify away; we don't care */
+			case 'i':
+			case 'D':
+			case 'd':
+				break;
+	
+			default:
+				goto Unknown;
+			}
+		}
+	}
+}
+
+void
+newpipethread(void*)
+{
+	Window *w;
+
+	while(w = recvp(newpipechan))
+		threadcreate(pipectl, w, STACK);
+}
+
+void
+startpipe(void)
+{
+	newpipechan = chancreate(sizeof(Window*), 0);
+	threadcreate(newpipethread, nil, STACK);
+	snarffd = open("/dev/snarf", OREAD|OCEXEC);
+}
--- /dev/null
+++ b/acme/bin/source/win/util.c
@@ -1,0 +1,90 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include "dat.h"
+
+void*
+emalloc(uint n)
+{
+	void *p;
+
+	p = malloc(n);
+	if(p == nil)
+		error("can't malloc: %r");
+	memset(p, 0, n);
+	return p;
+}
+
+char*
+estrdup(char *s)
+{
+	char *t;
+
+	t = emalloc(strlen(s)+1);
+	strcpy(t, s);
+	return t;
+}
+
+char*
+estrstrdup(char *s, char *t)
+{
+	char *u;
+
+	u = emalloc(strlen(s)+strlen(t)+1);
+	sprint(u, "%s%s", s, t);
+	return u;
+}
+
+char*
+eappend(char *s, char *sep, char *t)
+{
+	char *u;
+
+	if(t == nil)
+		u = estrstrdup(s, sep);
+	else{
+		u = emalloc(strlen(s)+strlen(sep)+strlen(t)+1);
+		sprint(u, "%s%s%s", s, sep, t);
+	}
+	free(s);
+	return u;
+}
+
+char*
+egrow(char *s, char *sep, char *t)
+{
+	s = eappend(s, sep, t);
+	free(t);
+	return s;
+}
+
+void
+error(char *fmt, ...)
+{
+	Fmt f;
+	char buf[64];
+	va_list arg;
+
+	fmtfdinit(&f, 2, buf, sizeof buf);
+	fmtprint(&f, "win: ");
+	va_start(arg, fmt);
+	fmtvprint(&f, fmt, arg);
+	va_end(arg);
+	fmtprint(&f, "\n");
+	fmtfdflush(&f);
+	threadexitsall(fmt);
+}
+
+void
+ctlprint(int fd, char *fmt, ...)
+{
+	int n;
+	va_list arg;
+
+	va_start(arg, fmt);
+	n = vfprint(fd, fmt, arg);
+	va_end(arg);
+	if(n <= 0)
+		error("control file write error: %r");
+}
--- /dev/null
+++ b/acme/bin/source/win/win.c
@@ -1,0 +1,264 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include "dat.h"
+
+Window*
+newwindow(void)
+{
+	char buf[12];
+	Window *w;
+
+	w = emalloc(sizeof(Window));
+	w->ctl = open("/mnt/wsys/new/ctl", ORDWR|OCEXEC);
+	if(w->ctl<0 || read(w->ctl, buf, 12)!=12)
+		error("can't open window ctl file: %r");
+	ctlprint(w->ctl, "noscroll\n");
+	w->id = atoi(buf);
+	w->event = winopenfile(w, "event");
+	w->addr = winopenfile(w, "addr");
+	w->body = winopenfile(w, "body");
+	w->data = winopenfile(w, "data");
+	w->cevent = chancreate(sizeof(Event*), 0);
+	return w;
+}
+
+void
+winsetdump(Window *w, char *dir, char *cmd)
+{
+	if(dir != nil)
+		ctlprint(w->ctl, "dumpdir %s\n", dir);
+	if(cmd != nil)
+		ctlprint(w->ctl, "dump %s\n", cmd);
+}
+
+void
+wineventproc(void *v)
+{
+	Window *w;
+	int i;
+
+	w = v;
+	for(i=0; ; i++){
+		if(i >= NEVENT)
+			i = 0;
+		wingetevent(w, &w->e[i]);
+		sendp(w->cevent, &w->e[i]);
+	}
+}
+
+int
+winopenfile(Window *w, char *f)
+{
+	char buf[64];
+	int fd;
+
+	sprint(buf, "/mnt/wsys/%d/%s", w->id, f);
+	fd = open(buf, ORDWR|OCEXEC);
+	if(fd < 0)
+		error("can't open window file %s: %r", f);
+	return fd;
+}
+
+void
+wintagwrite(Window *w, char *s, int n)
+{
+	int fd;
+
+	fd = winopenfile(w, "tag");
+	if(write(fd, s, n) != n)
+		error("tag write: %r");
+	close(fd);
+}
+
+void
+winname(Window *w, char *s)
+{
+	ctlprint(w->ctl, "name %s\n", s);
+}
+
+int
+wingetec(Window *w)
+{
+	if(w->nbuf == 0){
+		w->nbuf = read(w->event, w->buf, sizeof w->buf);
+		if(w->nbuf <= 0){
+			/* probably because window has exited, and only called by wineventproc, so just shut down */
+			threadexits(nil);
+		}
+		w->bufp = w->buf;
+	}
+	w->nbuf--;
+	return *w->bufp++;
+}
+
+int
+wingeten(Window *w)
+{
+	int n, c;
+
+	n = 0;
+	while('0'<=(c=wingetec(w)) && c<='9')
+		n = n*10+(c-'0');
+	if(c != ' ')
+		error("event number syntax");
+	return n;
+}
+
+int
+wingeter(Window *w, char *buf, int *nb)
+{
+	Rune r;
+	int n;
+
+	r = wingetec(w);
+	buf[0] = r;
+	n = 1;
+	if(r >= Runeself) {
+		while(!fullrune(buf, n))
+			buf[n++] = wingetec(w);
+		chartorune(&r, buf);
+	} 
+	*nb = n;
+	return r;
+}
+
+void
+wingetevent(Window *w, Event *e)
+{
+	int i, nb;
+
+	e->c1 = wingetec(w);
+	e->c2 = wingetec(w);
+	e->q0 = wingeten(w);
+	e->q1 = wingeten(w);
+	e->flag = wingeten(w);
+	e->nr = wingeten(w);
+	if(e->nr > EVENTSIZE)
+		error("event string too long");
+	e->nb = 0;
+	for(i=0; i<e->nr; i++){
+		e->r[i] = wingeter(w, e->b+e->nb, &nb);
+		e->nb += nb;
+	}
+	e->r[e->nr] = 0;
+	e->b[e->nb] = 0;
+	if(wingetec(w) != '\n')
+		error("event syntax error");
+}
+
+void
+winwriteevent(Window *w, Event *e)
+{
+	fprint(w->event, "%c%c%d %d\n", e->c1, e->c2, e->q0, e->q1);
+}
+
+static int
+nrunes(char *s, int nb)
+{
+	int i, n;
+	Rune r;
+
+	n = 0;
+	for(i=0; i<nb; n++)
+		i += chartorune(&r, s+i);
+	return n;
+}
+
+int
+winread(Window *w, uint q0, uint q1, char *data)
+{
+	int m, n, nr, nb;
+	char buf[256];
+
+	if(w->addr < 0)
+		w->addr = winopenfile(w, "addr");
+	if(w->data < 0)
+		w->data = winopenfile(w, "data");
+	m = q0;
+	nb = 0;
+	while(m < q1){
+		n = sprint(buf, "#%d", m);
+		if(write(w->addr, buf, n) != n)
+			error("error writing addr: %r");
+		n = read(w->data, buf, sizeof buf);
+		if(n < 0)
+			error("reading data: %r");
+		nr = nrunes(buf, n);
+		while(m+nr >q1){
+			do; while(n>0 && (buf[--n]&0xC0)==0x80);
+			--nr;
+		}
+		if(n == 0)
+			break;
+		memmove(data, buf, n);
+		nb += n;
+		data += n;
+		*data = 0;
+		m += nr;
+	}
+	return nb;
+}
+
+void
+windormant(Window *w)
+{
+	if(w->addr >= 0){
+		close(w->addr);
+		w->addr = -1;
+	}
+	if(w->body >= 0){
+		close(w->body);
+		w->body = -1;
+	}
+	if(w->data >= 0){
+		close(w->data);
+		w->data = -1;
+	}
+}
+
+int
+windel(Window *w, int sure)
+{
+	if(sure)
+		write(w->ctl, "delete\n", 7);
+	else if(write(w->ctl, "del\n", 4) != 4)
+		return 0;
+	/* event proc will die due to read error from event file */
+	windormant(w);
+	close(w->ctl);
+	w->ctl = -1;
+	close(w->event);
+	w->event = -1;
+	return 1;
+}
+
+void
+winclean(Window *w)
+{
+	ctlprint(w->ctl, "clean\n");
+}
+
+int
+winsetaddr(Window *w, char *addr, int errok)
+{
+	if(w->addr < 0)
+		w->addr = winopenfile(w, "addr");
+	if(write(w->addr, addr, strlen(addr)) < 0){
+		if(!errok)
+			error("error writing addr(%s): %r", addr);
+		return 0;
+	}
+	return 1;
+}
+
+int
+winselect(Window *w, char *addr, int errok)
+{
+	if(winsetaddr(w, addr, errok)){
+		ctlprint(w->ctl, "dot=addr\n");
+		return 1;
+	}
+	return 0;
+}
--- /dev/null
+++ b/acme/bin/unind
@@ -1,0 +1,3 @@
+#!/bin/rc
+
+sed 's/^	//' $*
--- /dev/null
+++ b/acme/bin/wnew
@@ -1,0 +1,5 @@
+#!/bin/rc -e
+
+id=`{mkwnew $*}
+cat >/mnt/acme/$id/body
+echo clean >/mnt/acme/$id/ctl
--- /dev/null
+++ b/acme/mail/guide
@@ -1,0 +1,4 @@
+Mail stored
+plumb /mail/box/$user/names
+mail -'x' someaddress
+mkbox /mail/box/$user/new_box
--- /dev/null
+++ b/acme/mail/mkbox
@@ -1,0 +1,11 @@
+#!/bin/rc
+
+for(i){
+	if(! test -e $i){
+		if(cp /dev/null $i){
+			chmod 600 $i
+			chmod +al $i
+		}
+	}
+	if not echo $i already exists
+}
--- /dev/null
+++ b/acme/mail/readme
@@ -1,0 +1,57 @@
+The Acme Mail program uses upas/fs to parse the mail box, and then
+presents a file-browser-like user interface to reading and sending
+messages.  The Mail window presents each numbered message like the
+contents of a directory presented one per line.  If a message has a
+Subject: line, that is shown indented on the following line.
+Multipart MIME-encoded messages are presented in the obvious
+hierarchical format.
+
+Mail uses upas/fs to access the mail box.  By default it reads "mbox",
+the standard user mail box.  If Mail is given an argument, it is
+passed to upas/fs as the name of the mail box (or upas/fs directory)
+to open.
+
+Although Mail works if the plumber is not running, it's designed to be
+run with plumbing enabled and many of its features work best if it is.
+
+The mailbox window has a few commands: Put writes back the mailbox;
+Mail creates a new window in which to compose a message; and Delmesg
+deletes messages by number.  The number may be given as argument or
+indicated by selecting the header line in the mailbox window.
+(Delmesg does not expand null selections, in the interest of safety.)
+
+Clicking the right button on a message number opens it; clicking on
+any of the subparts of a message opens that (and also opens the
+message itself).  Each message window has a few commands in the tag
+with obvious names: Reply, Delmsg, etc.  "Reply" replies to the single
+sender of the message, "Reply all" or "Replyall" replies to everyone
+in the From:, To:, and CC: lines.
+
+Message parts with recognized MIME types such as image/jpeg are sent
+to the plumber for further dispatch.  Acme Mail also listens to
+messages on the seemail and showmail plumbing ports, to report the
+arrival of new messages (highlighting the entry; right-click on the
+entry to open the message) and open them if you right-click on the
+face in the faces window.
+
+When composing a mail message or replying to a message, the first line
+of the text is a list of recipients of the message.  To:, and CC:, and BCC:
+lines are interpreted in the usual way. Two other header lines are
+special to Acme Mail:
+        Include: file places a copy of file in the message as an
+		inline MIME attachment.
+        Attach: file places a copy of file in the message as a regular
+		MIME attachment.
+
+Acme Mail uses these conventions when replying to messages,
+constructing headers for the default behavior.  You may edit these to
+change behavior.  Most important, when replying to a message Mail will
+always Include: the original message; delete that line if you don't
+want to include it.
+
+If the mailbox
+	/mail/box/$user/outgoing
+exists, Acme Mail will save your a copy of your outgoing messages
+there.  Attachments are described in the copy but not included.
+
+The -m mntpoint flag specifies a different mount point for /upas/fs.
--- /dev/null
+++ b/acme/mail/src/dat.h
@@ -1,0 +1,164 @@
+typedef struct Event Event;
+typedef struct Exec Exec;
+typedef struct Message Message;
+typedef struct Window Window;
+
+enum
+{
+	STACK		= 8192,
+	EVENTSIZE	= 256,
+	NEVENT		= 5,
+};
+
+struct Event
+{
+	int	c1;
+	int	c2;
+	int	q0;
+	int	q1;
+	int	flag;
+	int	nb;
+	int	nr;
+	char	b[EVENTSIZE*UTFmax+1];
+	Rune	r[EVENTSIZE+1];
+};
+
+struct Window
+{
+	/* file descriptors */
+	int		ctl;
+	int		event;
+	int		addr;
+	int		data;
+	Biobuf	*body;
+
+	/* event input */
+	char		buf[512];
+	char		*bufp;
+	int		nbuf;
+	Event	e[NEVENT];
+
+	int		id;
+	int		open;
+	Channel	*cevent;
+};
+
+struct Message
+{
+	Window	*w;
+	int		ctlfd;
+	char		*name;
+	char		*replyname;
+	uchar	opened;
+	uchar	dirty;
+	uchar	isreply;
+	uchar	deleted;
+	uchar	writebackdel;
+	uchar	tagposted;
+	uchar	recursed;
+	uchar	level;
+
+	/* header info */
+	char		*fromcolon;	/* from header file; all rest are from info file */
+	char		*from;
+	char		*to;
+	char		*cc;
+	char		*replyto;
+	char		*date;
+	char		*subject;
+	char		*type;
+	char		*disposition;
+	char		*filename;
+	char		*digest;
+
+	Message	*next;	/* next in this mailbox */
+	Message	*prev;	/* prev in this mailbox */
+	Message	*head;	/* first subpart */
+	Message	*tail;		/* last subpart */
+};
+
+enum
+{
+	NARGS		= 100,
+	NARGCHAR	= 8*1024,
+	EXECSTACK 	= STACK+(NARGS+1)*sizeof(char*)+NARGCHAR
+};
+
+struct Exec
+{
+	char		*prog;
+	char		**argv;
+	int		p[2];	/* p[1] is write to program; p[0] set to prog fd 0*/
+	int		q[2];	/* q[0] is read from program; q[1] set to prog fd 1 */
+	Channel	*sync;
+};
+
+extern	Window*	newwindow(void);
+extern	int		winopenfile(Window*, char*);
+extern	void		winopenbody(Window*, int);
+extern	void		winclosebody(Window*);
+extern	void		wintagwrite(Window*, char*, int);
+extern	void		winname(Window*, char*);
+extern	void		winwriteevent(Window*, Event*);
+extern	void		winread(Window*, uint, uint, char*);
+extern	int		windel(Window*, int);
+extern	void		wingetevent(Window*, Event*);
+extern	void		wineventproc(void*);
+extern	void		winwritebody(Window*, char*, int);
+extern	void		winclean(Window*);
+extern	int		winselect(Window*, char*, int);
+extern	char*	winselection(Window*);
+extern	int		winsetaddr(Window*, char*, int);
+extern	char*	winreadbody(Window*, int*);
+extern	void		windormant(Window*);
+extern	void		winsetdump(Window*, char*, char*);
+
+extern	void		readmbox(Message*, char*, char*);
+extern	void		rewritembox(Window*, Message*);
+
+extern	void		mkreply(Message*, char*, char*, Plumbattr*, char*);
+extern	void		delreply(Message*);
+
+extern	int		mesgadd(Message*, char*, Dir*, char*);
+extern	void		mesgmenu(Window*, Message*);
+extern	void		mesgmenunew(Window*, Message*);
+extern	int		mesgopen(Message*, char*, char*, Message*, int, char*);
+extern	void		mesgctl(void*);
+extern	void		mesgsend(Message*);
+extern	void		mesgdel(Message*, Message*);
+extern	void		mesgmenudel(Window*, Message*, Message*);
+extern	void		mesgmenumark(Window*, char*, char*);
+extern	void		mesgmenumarkdel(Window*, Message*, Message*, int);
+extern	Message*	mesglookup(Message*, char*, char*);
+extern	Message*	mesglookupfile(Message*, char*, char*);
+extern	void		mesgfreeparts(Message*);
+
+extern	char*	readfile(char*, char*, int*);
+extern	char*	readbody(char*, char*, int*);
+extern	void		ctlprint(int, char*, ...);
+extern	void*	emalloc(uint);
+extern	void*	erealloc(void*, uint);
+extern	char*	estrdup(char*);
+extern	char*	estrstrdup(char*, char*);
+extern	char*	egrow(char*, char*, char*);
+extern	char*	eappend(char*, char*, char*);
+extern	void		error(char*, ...);
+extern	int		tokenizec(char*, char**, int, char*);
+extern	void		execproc(void*);
+
+#pragma	varargck	argpos	error	1
+#pragma	varargck	argpos	ctlprint	2
+
+extern	Window	*wbox;
+extern	Message	mbox;
+extern	Message	replies;
+extern	char		*fsname;
+extern	int		plumbsendfd;
+extern	int		plumbseemailfd;
+extern	char		*home;
+extern	char		*outgoing;
+extern	char		*mailboxdir;
+extern	char		*user;
+extern	char		deleted[];
+extern	int		wctlfd;
+extern	int		shortmenu;
--- /dev/null
+++ b/acme/mail/src/html.c
@@ -1,0 +1,75 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include <ctype.h>
+#include <plumb.h>
+#include "dat.h"
+
+
+char*
+formathtml(char *body, int *np)
+{
+	int i, j, p[2], q[2];
+	Exec *e;
+	char buf[1024];
+	Channel *sync;
+
+	e = emalloc(sizeof(struct Exec));
+	if(pipe(p) < 0 || pipe(q) < 0)
+		error("can't create pipe: %r");
+
+	e->p[0] = p[0];
+	e->p[1] = p[1];
+	e->q[0] = q[0];
+	e->q[1] = q[1];
+	e->argv = emalloc(3*sizeof(char*));
+	e->argv[0] = estrdup("htmlfmt");
+	e->argv[1] = estrdup("-cutf-8");
+	e->argv[2] = nil;
+	e->prog = "/bin/htmlfmt";
+	sync = chancreate(sizeof(int), 0);
+	e->sync = sync;
+	proccreate(execproc, e, EXECSTACK);
+	recvul(sync);
+	close(p[0]);
+	close(q[1]);
+
+	if((i=write(p[1], body, *np)) != *np){
+		fprint(2, "Mail: warning: htmlfmt failed: wrote %d of %d: %r\n", i, *np);
+		close(p[1]);
+		close(q[0]);
+		return body;
+	}
+	close(p[1]);
+
+	free(body);
+	body = nil;
+	i = 0;
+	for(;;){
+		j = read(q[0], buf, sizeof buf);
+		if(j <= 0)
+			break;
+		body = realloc(body, i+j+1);
+		if(body == nil)
+			error("realloc failed: %r");
+		memmove(body+i, buf, j);
+		i += j;
+		body[i] = '\0';
+	}
+	close(q[0]);
+
+	*np = i;
+	return body;
+}
+
+char*
+readbody(char *type, char *dir, int *np)
+{
+	char *body;
+	
+	body = readfile(dir, "body", np);
+	if(body != nil && strcmp(type, "text/html") == 0)
+		return formathtml(body, np);
+	return body;
+}
--- /dev/null
+++ b/acme/mail/src/mail.c
@@ -1,0 +1,550 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include <plumb.h>
+#include <ctype.h>
+#include "dat.h"
+
+char	*maildir = "/mail/fs/";			/* mountpoint of mail file system */
+char	*mailtermdir = "/mnt/term/mail/fs/";	/* alternate mountpoint */
+char *mboxname = "mbox";			/* mailboxdir/mboxname is mail spool file */
+char	*mailboxdir = nil;				/* nil == /mail/box/$user */
+char *fsname;						/* filesystem for mailboxdir/mboxname is at maildir/fsname */
+char	*user;
+char	*outgoing;
+
+Window	*wbox;
+Message	mbox;
+Message	replies;
+char		*home;
+int		plumbsendfd;
+int		plumbseemailfd;
+int		plumbshowmailfd;
+int		plumbsendmailfd;
+Channel	*cplumb;
+Channel	*cplumbshow;
+Channel	*cplumbsend;
+int		wctlfd;
+void		mainctl(void*);
+void		plumbproc(void*);
+void		plumbshowproc(void*);
+void		plumbsendproc(void*);
+void		plumbthread(void);
+void		plumbshowthread(void*);
+void		plumbsendthread(void*);
+
+int			shortmenu;
+
+void
+usage(void)
+{
+	fprint(2, "usage: Mail [-sS] [-o outgoing] [mailboxname [directoryname]]\n");
+	threadexitsall("usage");
+}
+
+void
+removeupasfs(void)
+{
+	char buf[256];
+
+	if(strcmp(mboxname, "mbox") == 0)
+		return;
+	snprint(buf, sizeof buf, "close %s", mboxname);
+	write(mbox.ctlfd, buf, strlen(buf));
+}
+
+int
+ismaildir(char *s)
+{
+	char buf[256];
+	Dir *d;
+	int ret;
+
+	snprint(buf, sizeof buf, "%s%s", maildir, s);
+	d = dirstat(buf);
+	if(d == nil)
+		return 0;
+	ret = d->qid.type & QTDIR;
+	free(d);
+	return ret;
+}
+
+void
+threadmain(int argc, char *argv[])
+{
+	char *s, *name;
+	char err[ERRMAX], *cmd;
+	int i, newdir;
+	Fmt fmt;
+
+	doquote = needsrcquote;
+	quotefmtinstall();
+
+	/* open these early so we won't miss notification of new mail messages while we read mbox */
+	plumbsendfd = plumbopen("send", OWRITE|OCEXEC);
+	plumbseemailfd = plumbopen("seemail", OREAD|OCEXEC);
+	plumbshowmailfd = plumbopen("showmail", OREAD|OCEXEC);
+
+	shortmenu = 0;
+	ARGBEGIN{
+	case 's':
+		shortmenu = 1;
+		break;
+	case 'S':
+		shortmenu = 2;
+		break;
+	case 'o':
+		outgoing = EARGF(usage());
+		break;
+	case 'm':
+		smprint(maildir, "%s/", EARGF(usage()));
+		break;
+	default:
+		usage();
+	}ARGEND
+
+	name = "mbox";
+
+	/* bind the terminal /mail/fs directory over the local one */
+	if(access(maildir, 0)<0 && access(mailtermdir, 0)==0)
+		bind(mailtermdir, maildir, MAFTER);
+
+	newdir = 1;
+	if(argc > 0){
+		i = strlen(argv[0]);
+		if(argc>2 || i==0)
+			usage();
+		/* see if the name is that of an existing /mail/fs directory */
+		if(argc==1 && strchr(argv[0], '/')==0 && ismaildir(argv[0])){
+			name = argv[0];
+			mboxname = eappend(estrdup(maildir), "", name);
+			newdir = 0;
+		}else{
+			if(argv[0][i-1] == '/')
+				argv[0][i-1] = '\0';
+			s = strrchr(argv[0], '/');
+			if(s == nil)
+				mboxname = estrdup(argv[0]);
+			else{
+				*s++ = '\0';
+				if(*s == '\0')
+					usage();
+				mailboxdir = argv[0];
+				mboxname = estrdup(s);
+			}
+			if(argc > 1)
+				name = argv[1];
+			else
+				name = mboxname;
+		}
+	}
+
+	user = getenv("user");
+	if(user == nil)
+		user = "none";
+	if(mailboxdir == nil)
+		mailboxdir = estrstrdup("/mail/box/", user);
+	if(outgoing == nil)
+		outgoing = estrstrdup(mailboxdir, "/outgoing");
+
+	s = estrstrdup(maildir, "ctl");
+	mbox.ctlfd = open(s, ORDWR|OCEXEC);
+	if(mbox.ctlfd < 0)
+		error("can't open %s: %r", s);
+
+	fsname = estrdup(name);
+	if(newdir && argc > 0){
+		s = emalloc(5+strlen(mailboxdir)+strlen(mboxname)+strlen(name)+10+1);
+		for(i=0; i<10; i++){
+			sprint(s, "open %s/%s %s", mailboxdir, mboxname, fsname);
+			if(write(mbox.ctlfd, s, strlen(s)) >= 0)
+				break;
+			err[0] = '\0';
+			errstr(err, sizeof err);
+			if(strstr(err, "mbox name in use") == nil)
+				error("can't create directory %s for mail: %s", name, err);
+			free(fsname);
+			fsname = emalloc(strlen(name)+10);
+			sprint(fsname, "%s-%d", name, i);
+		}
+		if(i == 10)
+			error("can't open %s/%s: %r", mailboxdir, mboxname);
+		free(s);
+	}
+
+	s = estrstrdup(fsname, "/");
+	mbox.name = estrstrdup(maildir, s);
+	mbox.level= 0;
+	readmbox(&mbox, maildir, s);
+	home = getenv("home");
+	if(home == nil)
+		home = "/";
+
+	wbox = newwindow();
+	winname(wbox, mbox.name);
+	wintagwrite(wbox, "Put Mail Delmesg ", 3+1+4+1+7+1);
+	threadcreate(mainctl, wbox, STACK);
+
+	fmtstrinit(&fmt);
+	fmtprint(&fmt, "Mail");
+	if(shortmenu)
+		fmtprint(&fmt, " -%c", "sS"[shortmenu-1]);
+	if(outgoing)
+		fmtprint(&fmt, " -o %s", outgoing);
+	fmtprint(&fmt, " %s", name);
+	cmd = fmtstrflush(&fmt);
+	if(cmd == nil)
+		sysfatal("out of memory");
+	winsetdump(wbox, "/acme/mail", cmd);
+	mbox.w = wbox;
+
+	mesgmenu(wbox, &mbox);
+	winclean(wbox);
+
+	wctlfd = open("/dev/wctl", OWRITE|OCEXEC);	/* for acme window */
+	cplumb = chancreate(sizeof(Plumbmsg*), 0);
+	cplumbshow = chancreate(sizeof(Plumbmsg*), 0);
+	if(strcmp(name, "mbox") == 0){
+		/*
+		 * Avoid creating multiple windows to send mail by only accepting
+		 * sendmail plumb messages if we're reading the main mailbox.
+		 */
+		plumbsendmailfd = plumbopen("sendmail", OREAD|OCEXEC);
+		cplumbsend = chancreate(sizeof(Plumbmsg*), 0);
+		proccreate(plumbsendproc, nil, STACK);
+		threadcreate(plumbsendthread, nil, STACK);
+	}
+	/* start plumb reader as separate proc ... */
+	proccreate(plumbproc, nil, STACK);
+	proccreate(plumbshowproc, nil, STACK);
+	threadcreate(plumbshowthread, nil, STACK);
+	/* ... and use this thread to read the messages */
+	plumbthread();
+}
+
+void
+plumbproc(void*)
+{
+	Plumbmsg *m;
+
+	threadsetname("plumbproc");
+	for(;;){
+		m = plumbrecv(plumbseemailfd);
+		sendp(cplumb, m);
+		if(m == nil)
+			threadexits(nil);
+	}
+}
+
+void
+plumbshowproc(void*)
+{
+	Plumbmsg *m;
+
+	threadsetname("plumbshowproc");
+	for(;;){
+		m = plumbrecv(plumbshowmailfd);
+		sendp(cplumbshow, m);
+		if(m == nil)
+			threadexits(nil);
+	}
+}
+
+void
+plumbsendproc(void*)
+{
+	Plumbmsg *m;
+
+	threadsetname("plumbsendproc");
+	for(;;){
+		m = plumbrecv(plumbsendmailfd);
+		sendp(cplumbsend, m);
+		if(m == nil)
+			threadexits(nil);
+	}
+}
+
+void
+newmesg(char *name, char *digest)
+{
+	Dir *d;
+
+	if(strncmp(name, mbox.name, strlen(mbox.name)) != 0)
+		return;	/* message is about another mailbox */
+	if(mesglookupfile(&mbox, name, digest) != nil)
+		return;
+	d = dirstat(name);
+	if(d == nil)
+		return;
+	if(mesgadd(&mbox, mbox.name, d, digest))
+		mesgmenunew(wbox, &mbox);
+	free(d);
+}
+
+void
+showmesg(char *name, char *digest)
+{
+	char *n;
+
+	if(strncmp(name, mbox.name, strlen(mbox.name)) != 0)
+		return;	/* message is about another mailbox */
+	n = estrdup(name+strlen(mbox.name));
+	if(n[strlen(n)-1] != '/')
+		n = egrow(n, "/", nil);
+	mesgopen(&mbox, mbox.name, name+strlen(mbox.name), nil, 1, digest);
+	free(n);
+}
+
+void
+delmesg(char *name, char *digest, int dodel)
+{
+	Message *m;
+
+	m = mesglookupfile(&mbox, name, digest);
+	if(m != nil){
+		mesgmenumarkdel(wbox, &mbox, m, 0);
+		if(dodel)
+			m->writebackdel = 1;
+	}
+}
+
+void
+plumbthread(void)
+{
+	Plumbmsg *m;
+	Plumbattr *a;
+	char *type, *digest;
+
+	threadsetname("plumbthread");
+	while((m = recvp(cplumb)) != nil){
+		a = m->attr;
+		digest = plumblookup(a, "digest");
+		type = plumblookup(a, "mailtype");
+		if(type == nil)
+			fprint(2, "Mail: plumb message with no mailtype attribute\n");
+		else if(strcmp(type, "new") == 0)
+			newmesg(m->data, digest);
+		else if(strcmp(type, "delete") == 0)
+			delmesg(m->data, digest, 0);
+		else
+			fprint(2, "Mail: unknown plumb attribute %s\n", type);
+		plumbfree(m);
+	}
+	threadexits(nil);
+}
+
+void
+plumbshowthread(void*)
+{
+	Plumbmsg *m;
+
+	threadsetname("plumbshowthread");
+	while((m = recvp(cplumbshow)) != nil){
+		showmesg(m->data, plumblookup(m->attr, "digest"));
+		plumbfree(m);
+	}
+	threadexits(nil);
+}
+
+void
+plumbsendthread(void*)
+{
+	Plumbmsg *m;
+
+	threadsetname("plumbsendthread");
+	while((m = recvp(cplumbsend)) != nil){
+		mkreply(nil, "Mail", m->data, m->attr, nil);
+		plumbfree(m);
+	}
+	threadexits(nil);
+}
+
+int
+mboxcommand(Window *w, char *s)
+{
+	char *args[10], **targs;
+	Message *m, *next;
+	int ok, nargs, i, j;
+	char buf[128];
+
+	nargs = tokenize(s, args, nelem(args));
+	if(nargs == 0)
+		return 0;
+	if(strcmp(args[0], "Mail") == 0){
+		if(nargs == 1)
+			mkreply(nil, "Mail", "", nil, nil);
+		else
+			mkreply(nil, "Mail", args[1], nil, nil);
+		return 1;
+	}
+	if(strcmp(s, "Del") == 0){
+		if(mbox.dirty){
+			mbox.dirty = 0;
+			fprint(2, "mail: mailbox not written\n");
+			return 1;
+		}
+		ok = 1;
+		for(m=mbox.head; m!=nil; m=next){
+			next = m->next;
+			if(m->w){
+				if(windel(m->w, 0))
+					m->w = nil;
+				else
+					ok = 0;
+			}
+		}
+		for(m=replies.head; m!=nil; m=next){
+			next = m->next;
+			if(m->w){
+				if(windel(m->w, 0))
+					m->w = nil;
+				else
+					ok = 0;
+			}
+		}
+		if(ok){
+			windel(w, 1);
+			removeupasfs();
+			threadexitsall(nil);
+		}
+		return 1;
+	}
+	if(strcmp(s, "Put") == 0){
+		rewritembox(wbox, &mbox);
+		return 1;
+	}
+	if(strcmp(s, "Delmesg") == 0){
+		if(nargs > 1){
+			for(i=1; i<nargs; i++){
+				snprint(buf, sizeof buf, "%s%s", mbox.name, args[i]);
+				delmesg(buf, nil, 1);
+			}
+		}
+		s = winselection(w);
+		if(s == nil)
+			return 1;
+		nargs = 1;
+		for(i=0; s[i]; i++)
+			if(s[i] == '\n')
+				nargs++;
+		targs = emalloc(nargs*sizeof(char*));	/* could be too many for a local array */
+		nargs = getfields(s, targs, nargs, 1, "\n");
+		for(i=0; i<nargs; i++){
+			if(!isdigit(targs[i][0]))
+				continue;
+			j = atoi(targs[i]);	/* easy way to parse the number! */
+			if(j == 0)
+				continue;
+			snprint(buf, sizeof buf, "%s%d", mbox.name, j);
+			delmesg(buf, nil, 1);
+		}
+		free(s);
+		free(targs);
+		return 1;
+	}
+	return 0;
+}
+
+void
+mainctl(void *v)
+{
+	Window *w;
+	Event *e, *e2, *eq, *ea;
+	int na, nopen;
+	char *s, *t, *buf;
+
+	w = v;
+	proccreate(wineventproc, w, STACK);
+
+	for(;;){
+		e = recvp(w->cevent);
+		switch(e->c1){
+		default:
+		Unknown:
+			print("unknown message %c%c\n", e->c1, e->c2);
+			break;
+	
+		case 'E':	/* write to body; can't affect us */
+			break;
+	
+		case 'F':	/* generated by our actions; ignore */
+			break;
+	
+		case 'K':	/* type away; we don't care */
+			break;
+	
+		case 'M':
+			switch(e->c2){
+			case 'x':
+			case 'X':
+				ea = nil;
+				e2 = nil;
+				if(e->flag & 2)
+					e2 = recvp(w->cevent);
+				if(e->flag & 8){
+					ea = recvp(w->cevent);
+					na = ea->nb;
+					recvp(w->cevent);
+				}else
+					na = 0;
+				s = e->b;
+				/* if it's a known command, do it */
+				if((e->flag&2) && e->nb==0)
+					s = e2->b;
+				if(na){
+					t = emalloc(strlen(s)+1+na+1);
+					sprint(t, "%s %s", s, ea->b);
+					s = t;
+				}
+				/* if it's a long message, it can't be for us anyway */
+				if(!mboxcommand(w, s))	/* send it back */
+					winwriteevent(w, e);
+				if(na)
+					free(s);
+				break;
+	
+			case 'l':
+			case 'L':
+				buf = nil;
+				eq = e;
+				if(e->flag & 2){
+					e2 = recvp(w->cevent);
+					eq = e2;
+				}
+				s = eq->b;
+				if(eq->q1>eq->q0 && eq->nb==0){
+					buf = emalloc((eq->q1-eq->q0)*UTFmax+1);
+					winread(w, eq->q0, eq->q1, buf);
+					s = buf;
+				}
+				nopen = 0;
+				do{
+					/* skip 'deleted' string if present' */
+					if(strncmp(s, deleted, strlen(deleted)) == 0)
+						s += strlen(deleted);
+					/* skip mail box name if present */
+					if(strncmp(s, mbox.name, strlen(mbox.name)) == 0)
+						s += strlen(mbox.name);
+					nopen += mesgopen(&mbox, mbox.name, s, nil, 0, nil);
+					while(*s!='\0' && *s++!='\n')
+						;
+				}while(*s);
+				if(nopen == 0)	/* send it back */
+					winwriteevent(w, e);
+				free(buf);
+				break;
+	
+			case 'I':	/* modify away; we don't care */
+			case 'D':
+			case 'd':
+			case 'i':
+				break;
+	
+			default:
+				goto Unknown;
+			}
+		}
+	}
+}
+
--- /dev/null
+++ b/acme/mail/src/mesg.c
@@ -1,0 +1,1322 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include <ctype.h>
+#include <plumb.h>
+#include "dat.h"
+
+enum
+{
+	DIRCHUNK = 32*sizeof(Dir)
+};
+
+char	regexchars[] = "\\/[].+?()*^$";
+char	deleted[] = "(deleted)-";
+char	deletedrx[] = "\\(deleted\\)-";
+char	deletedrx01[] = "(\\(deleted\\)-)?";
+char	deletedaddr[] = "-#0;/^\\(deleted\\)-/";
+
+struct{
+	char	*type;
+	char	*port;
+	char *suffix;
+} ports[] = {
+	"text/",			"edit",		".txt",
+	/* text must be first for plumbport() */
+	"image/gif",			"image",	".gif",
+	"image/jpeg",			"image",	".jpg",
+	"image/jpeg",			"image",	".jpeg",
+	"image/png",			"image",	".png",
+	"image/tiff",			"image",	".tif",
+	"application/postscript",	"postscript",	".ps",
+	"application/pdf",		"postscript",	".pdf",
+	"application/msword",		"msword",	".doc",
+	"application/rtf",		"msword",	".rtf",
+	"audio/x-wav",			"wav",		".wav",
+	nil,	nil
+};
+
+char *goodtypes[] = {
+	"text",
+	"text/plain",
+	"message/rfc822",
+	"text/richtext",
+	"text/tab-separated-values",
+	"application/octet-stream",
+	nil,
+};
+
+struct{
+	char *type;
+	char	*ext;
+} exts[] = {
+	"image/gif",	".gif",
+	"image/jpeg",	".jpg",
+	nil, nil
+};
+
+char *okheaders[] =
+{
+	"From:",
+	"Date:",
+	"To:",
+	"CC:",
+	"Subject:",
+	nil
+};
+
+char *extraheaders[] =
+{
+	"Resent-From:",
+	"Resent-To:",
+	"Sort:",
+	nil,
+};
+
+char*
+line(char *data, char **pp)
+{
+	char *p, *q;
+
+	for(p=data; *p!='\0' && *p!='\n'; p++)
+		;
+	if(*p == '\n')
+		*pp = p+1;
+	else
+		*pp = p;
+	q = emalloc(p-data + 1);
+	memmove(q, data, p-data);
+	return q;
+}
+
+void
+scanheaders(Message *m, char *dir)
+{
+	char *s, *t, *u, *f;
+
+	s = f = readfile(dir, "header", nil);
+	if(s != nil)
+		while(*s){
+			t = line(s, &s);
+			if(strncmp(t, "From: ", 6) == 0){
+				m->fromcolon = estrdup(t+6);
+				/* remove all quotes; they're ugly and irregular */
+				for(u=m->fromcolon; *u; u++)
+					if(*u == '"')
+						memmove(u, u+1, strlen(u));
+			}
+			if(strncmp(t, "Subject: ", 9) == 0)
+				m->subject = estrdup(t+9);
+			free(t);
+		}
+	if(m->fromcolon == nil)
+		m->fromcolon = estrdup(m->from);
+	free(f);
+}
+
+int
+loadinfo(Message *m, char *dir)
+{
+	int n;
+	char *data, *p, *s;
+
+	data = readfile(dir, "info", &n);
+	if(data == nil)
+		return 0;
+	m->from = line(data, &p);
+	scanheaders(m, dir);	/* depends on m->from being set */
+	m->to = line(p, &p);
+	m->cc = line(p, &p);
+	m->replyto = line(p, &p);
+	m->date = line(p, &p);
+	s = line(p, &p);
+	if(m->subject == nil)
+		m->subject = s;
+	else
+		free(s);
+	m->type = line(p, &p);
+	m->disposition = line(p, &p);
+	m->filename = line(p, &p);
+	m->digest = line(p, &p);
+	free(data);
+	return 1;
+}
+
+int
+isnumeric(char *s)
+{
+	while(*s){
+		if(!isdigit(*s))
+			return 0;
+		s++;
+	}
+	return 1;
+}
+
+Dir*
+loaddir(char *name, int *np)
+{
+	int fd;
+	Dir *dp;
+
+	fd = open(name, OREAD);
+	if(fd < 0)
+		return nil;
+	*np = dirreadall(fd, &dp);
+	close(fd);
+	return dp;
+}
+
+void
+readmbox(Message *mbox, char *dir, char *subdir)
+{
+	char *name;
+	Dir *d, *dirp;
+	int i, n;
+
+	name = estrstrdup(dir, subdir);
+	dirp = loaddir(name, &n);
+	mbox->recursed = 1;
+	if(dirp)
+		for(i=0; i<n; i++){
+			d = &dirp[i];
+			if(isnumeric(d->name))
+				mesgadd(mbox, name, d, nil);
+		}
+	free(dirp);
+	free(name);
+}
+
+/* add message to box, in increasing numerical order */
+int
+mesgadd(Message *mbox, char *dir, Dir *d, char *digest)
+{
+	Message *m;
+	char *name;
+	int loaded;
+
+	m = emalloc(sizeof(Message));
+	m->name = estrstrdup(d->name, "/");
+	m->next = nil;
+	m->prev = mbox->tail;
+	m->level= mbox->level+1;
+	m->recursed = 0;
+	name = estrstrdup(dir, m->name);
+	loaded = loadinfo(m, name);
+	free(name);
+	/* if two upas/fs are running, we can get misled, so check digest before accepting message */
+	if(loaded==0 || (digest!=nil && m->digest!=nil && strcmp(digest, m->digest)!=0)){
+		mesgfreeparts(m);
+		free(m);
+		return 0;
+	}
+	if(mbox->tail != nil)
+		mbox->tail->next = m;
+	mbox->tail = m;
+	if(mbox->head == nil)
+		mbox->head = m;
+
+	if (m->level != 1){
+		m->recursed = 1;
+		readmbox(m, dir, m->name); 
+	}
+	return 1;
+}
+
+int
+thisyear(char *year)
+{
+	static char now[10];
+	char *s;
+
+	if(now[0] == '\0'){
+		s = ctime(time(nil));
+		strcpy(now, s+24);
+	}
+	return strncmp(year, now, 4) == 0;
+}
+
+char*
+stripdate(char *as)
+{
+	int n;
+	char *s, *fld[10];
+
+	as = estrdup(as);
+	s = estrdup(as);
+	n = tokenize(s, fld, 10);
+	if(n > 5){
+		sprint(as, "%.3s ", fld[0]);	/* day */
+		/* some dates have 19 Apr, some Apr 19 */
+		if(strlen(fld[1])<4 && isnumeric(fld[1]))
+			sprint(as+strlen(as), "%.3s %.3s ", fld[1], fld[2]);	/* date, month */
+		else
+			sprint(as+strlen(as), "%.3s %.3s ", fld[2], fld[1]);	/* date, month */
+		/* do we use time or year?  depends on whether year matches this one */
+		if(thisyear(fld[5])){
+			if(strchr(fld[3], ':') != nil)
+				sprint(as+strlen(as), "%.5s ", fld[3]);	/* time */
+			else if(strchr(fld[4], ':') != nil)
+				sprint(as+strlen(as), "%.5s ", fld[4]);	/* time */
+		}else
+			sprint(as+strlen(as), "%.4s ", fld[5]);	/* year */
+	}
+	free(s);
+	return as;
+}
+
+char*
+readfile(char *dir, char *name, int *np)
+{
+	char *file, *data;
+	int fd, len;
+	Dir *d;
+
+	if(np != nil)
+		*np = 0;
+	file = estrstrdup(dir, name);
+	fd = open(file, OREAD);
+	if(fd < 0)
+		return nil;
+	d = dirfstat(fd);
+	free(file);
+	len = 0;
+	if(d != nil)
+		len = d->length;
+	free(d);
+	data = emalloc(len+1);
+	read(fd, data, len);
+	close(fd);
+	if(np != nil)
+		*np = len;
+	return data;
+}
+
+char*
+info(Message *m, int ind, int ogf)
+{
+	char *i;
+	int j, len, lens;
+	char *p;
+	char fmt[80], s[80];
+
+	if (ogf)
+		p=m->to;
+	else
+		p=m->fromcolon;
+
+	if(ind==0 && shortmenu){
+		len = 30;
+		lens = 30;
+		if(shortmenu > 1){
+			len = 10;
+			lens = 25;
+		}
+		if(ind==0 && m->subject[0]=='\0'){
+			snprint(fmt, sizeof fmt, " %%-%d.%ds", len, len);
+			snprint(s, sizeof s, fmt, p);
+		}else{
+			snprint(fmt, sizeof fmt, " %%-%d.%ds  %%-%d.%ds", len, len, lens, lens);
+			snprint(s, sizeof s, fmt, p, m->subject);
+		}
+		i = estrdup(s);
+
+		return i;
+	} 
+
+	i = estrdup("");
+	i = eappend(i, "\t", p);
+	i = egrow(i, "\t", stripdate(m->date));
+	if(ind == 0){
+		if(strcmp(m->type, "text")!=0 && strncmp(m->type, "text/", 5)!=0 && 
+		   strncmp(m->type, "multipart/", 10)!=0)
+			i = egrow(i, "\t(", estrstrdup(m->type, ")"));
+	}else if(strncmp(m->type, "multipart/", 10) != 0)
+		i = egrow(i, "\t(", estrstrdup(m->type, ")"));
+	if(m->subject[0] != '\0'){
+		i = eappend(i, "\n", nil);
+		for(j=0; j<ind; j++)
+			i = eappend(i, "\t", nil);
+		i = eappend(i, "\t", m->subject);
+	}
+	return i;
+}
+
+void
+mesgmenu0(Window *w, Message *mbox, char *realdir, char *dir, int ind, Biobuf *fd, int onlyone, int dotail)
+{
+	int i;
+	Message *m;
+	char *name, *tmp;
+	int ogf=0;
+
+	if(strstr(realdir, "outgoing") != nil)
+		ogf=1;
+
+	/* show mail box in reverse order, pieces in forward order */
+	if(ind > 0)
+		m = mbox->head;
+	else
+		m = mbox->tail;
+	while(m != nil){
+		for(i=0; i<ind; i++)
+			Bprint(fd, "\t");
+		if(ind != 0)
+			Bprint(fd, "  ");
+		name = estrstrdup(dir, m->name);
+		tmp = info(m, ind, ogf);
+		Bprint(fd, "%s%s\n", name, tmp);
+		free(tmp);
+		if(dotail && m->tail)
+			mesgmenu0(w, m, realdir, name, ind+1, fd, 0, dotail);
+		free(name);
+		if(ind)
+			m = m->next;
+		else
+			m = m->prev;
+		if(onlyone)
+			m = nil;
+	}
+}
+
+void
+mesgmenu(Window *w, Message *mbox)
+{
+	winopenbody(w, OWRITE);
+	mesgmenu0(w, mbox, mbox->name, "", 0, w->body, 0, !shortmenu);
+	winclosebody(w);
+}
+
+/* one new message has arrived, as mbox->tail */
+void
+mesgmenunew(Window *w, Message *mbox)
+{
+	Biobuf *b;
+
+	winselect(w, "0", 0);
+	w->data = winopenfile(w, "data");
+	b = emalloc(sizeof(Biobuf));
+	Binit(b, w->data, OWRITE);
+	mesgmenu0(w, mbox, mbox->name, "", 0, b, 1, !shortmenu);
+	Bterm(b);
+	free(b);
+	if(!mbox->dirty)
+		winclean(w);
+	/* select tag line plus following indented lines, but not final newline (it's distinctive) */
+	winselect(w, "0/.*\\n((\t.*\\n)*\t.*)?/", 1);
+	close(w->addr);
+	close(w->data);
+	w->addr = -1;
+	w->data = -1;
+}
+
+char*
+name2regexp(char *prefix, char *s)
+{
+	char *buf, *p, *q;
+
+	buf = emalloc(strlen(prefix)+2*strlen(s)+50);	/* leave room to append more */
+	p = buf;
+	*p++ = '0';
+	*p++ = '/';
+	*p++ = '^';
+	strcpy(p, prefix);
+	p += strlen(prefix);
+	for(q=s; *q!='\0'; q++){
+		if(strchr(regexchars, *q) != nil)
+			*p++ = '\\';
+		*p++ = *q;
+	}
+	*p++ = '/';
+	*p = '\0';
+	return buf;
+}
+
+void
+mesgmenumarkdel(Window *w, Message *mbox, Message *m, int writeback)
+{
+	char *buf;
+
+
+	if(m->deleted)
+		return;
+	m->writebackdel = writeback;
+	if(w->data < 0)
+		w->data = winopenfile(w, "data");
+	buf = name2regexp("", m->name);
+	strcat(buf, "-#0");
+	if(winselect(w, buf, 1))
+		write(w->data, deleted, 10);
+	free(buf);
+	close(w->data);
+	close(w->addr);
+	w->addr = w->data = -1;
+	mbox->dirty = 1;
+	m->deleted = 1;
+}
+
+void
+mesgmenumarkundel(Window *w, Message*, Message *m)
+{
+	char *buf;
+
+	if(m->deleted == 0)
+		return;
+	if(w->data < 0)
+		w->data = winopenfile(w, "data");
+	buf = name2regexp(deletedrx, m->name);
+	if(winselect(w, buf, 1))
+		if(winsetaddr(w, deletedaddr, 1))
+			write(w->data, "", 0);
+	free(buf);
+	close(w->data);
+	close(w->addr);
+	w->addr = w->data = -1;
+	m->deleted = 0;
+}
+
+void
+mesgmenudel(Window *w, Message *mbox, Message *m)
+{
+	char *buf;
+
+	if(w->data < 0)
+		w->data = winopenfile(w, "data");
+	buf = name2regexp(deletedrx, m->name);
+	if(winsetaddr(w, buf, 1) && winsetaddr(w, ".,./.*\\n(\t.*\\n)*/", 1))
+		write(w->data, "", 0);
+	free(buf);
+	close(w->data);
+	close(w->addr);
+	w->addr = w->data = -1;
+	mbox->dirty = 1;
+	m->deleted = 1;
+}
+
+void
+mesgmenumark(Window *w, char *which, char *mark)
+{
+	char *buf;
+
+	if(w->data < 0)
+		w->data = winopenfile(w, "data");
+	buf = name2regexp(deletedrx01, which);
+	if(winsetaddr(w, buf, 1) && winsetaddr(w, "+0-#1", 1))	/* go to end of line */
+		write(w->data, mark, strlen(mark));
+	free(buf);
+	close(w->data);
+	close(w->addr);
+	w->addr = w->data = -1;
+	if(!mbox.dirty)
+		winclean(w);
+}
+
+void
+mesgfreeparts(Message *m)
+{
+	free(m->name);
+	free(m->replyname);
+	free(m->fromcolon);
+	free(m->from);
+	free(m->to);
+	free(m->cc);
+	free(m->replyto);
+	free(m->date);
+	free(m->subject);
+	free(m->type);
+	free(m->disposition);
+	free(m->filename);
+	free(m->digest);
+}
+
+void
+mesgdel(Message *mbox, Message *m)
+{
+	Message *n, *next;
+
+	if(m->opened)
+		error("internal error: deleted message still open in mesgdel");
+	/* delete subparts */
+	for(n=m->head; n!=nil; n=next){
+		next = n->next;
+		mesgdel(m, n);
+	}
+	/* remove this message from list */
+	if(m->next)
+		m->next->prev = m->prev;
+	else
+		mbox->tail = m->prev;
+	if(m->prev)
+		m->prev->next = m->next;
+	else
+		mbox->head = m->next;
+
+	mesgfreeparts(m);
+}
+
+int
+mesgsave(Message *m, char *s)
+{
+	int ofd, n, k, ret;
+	char *t, *raw, *unixheader, *all;
+
+	t = estrstrdup(mbox.name, m->name);
+	raw = readfile(t, "raw", &n);
+	unixheader = readfile(t, "unixheader", &k);
+	if(raw==nil || unixheader==nil){
+		fprint(2, "Mail: can't read %s: %r\n", t);
+		free(t);
+		return 0;
+	}
+	free(t);
+
+	all = emalloc(n+k+1);
+	memmove(all, unixheader, k);
+	memmove(all+k, raw, n);
+	memmove(all+k+n, "\n", 1);
+	n = k+n+1;
+	free(unixheader);
+	free(raw);
+	ret = 1;
+	s = estrdup(s);
+	if(s[0] != '/')
+		s = egrow(estrdup(mailboxdir), "/", s);
+	ofd = open(s, OWRITE);
+	if(ofd < 0){
+		fprint(2, "Mail: can't open %s: %r\n", s);
+		ret = 0;
+	}else if(seek(ofd, 0LL, 2)<0 || write(ofd, all, n)!=n){
+		fprint(2, "Mail: save failed: can't write %s: %r\n", s);
+		ret = 0;
+	}
+	free(all);
+	close(ofd);
+	free(s);
+	return ret;
+}
+
+int
+mesgcommand(Message *m, char *cmd)
+{
+	char *s;
+	char *args[10];
+	int ok, ret, nargs;
+
+	s = cmd;
+	ret = 1;
+	nargs = tokenize(s, args, nelem(args));
+	if(nargs == 0)
+		return 0;
+	if(strcmp(args[0], "Post") == 0){
+		mesgsend(m);
+		goto Return;
+	}
+	if(strncmp(args[0], "Save", 4) == 0){
+		if(m->isreply)
+			goto Return;
+		s = estrdup("\t[saved");
+		if(nargs==1 || strcmp(args[1], "")==0){
+			ok = mesgsave(m, "stored");
+		}else{
+			ok = mesgsave(m, args[1]);
+			s = eappend(s, " ", args[1]);
+		}
+		if(ok){
+			s = egrow(s, "]", nil);
+			mesgmenumark(mbox.w, m->name, s);
+		}
+		free(s);
+		goto Return;
+	}
+	if(strcmp(args[0], "Reply")==0){
+		if(nargs>=2 && strcmp(args[1], "all")==0)
+			mkreply(m, "Replyall", nil, nil, nil);
+		else
+			mkreply(m, "Reply", nil, nil, nil);
+		goto Return;
+	}
+	if(strcmp(args[0], "Q") == 0){
+		s = winselection(m->w);	/* will be freed by mkreply */
+		if(nargs>=3 && strcmp(args[1], "Reply")==0 && strcmp(args[2], "all")==0)
+			mkreply(m, "QReplyall", nil, nil, s);
+		else
+			mkreply(m, "QReply", nil, nil, s);
+		goto Return;
+	}
+	if(strcmp(args[0], "Del") == 0){
+		if(windel(m->w, 0)){
+			chanfree(m->w->cevent);
+			free(m->w);
+			m->w = nil;
+			if(m->isreply)
+				delreply(m);
+			else{
+				m->opened = 0;
+				m->tagposted = 0;
+			}
+			free(cmd);
+			threadexits(nil);
+		}
+		goto Return;
+	}
+	if(strcmp(args[0], "Delmesg") == 0){
+		if(!m->isreply){
+			mesgmenumarkdel(wbox, &mbox, m, 1);
+			free(cmd);	/* mesgcommand might not return */
+			mesgcommand(m, estrdup("Del"));
+			return 1;
+		}
+		goto Return;
+	}
+	if(strcmp(args[0], "UnDelmesg") == 0){
+		if(!m->isreply && m->deleted)
+			mesgmenumarkundel(wbox, &mbox, m);
+		goto Return;
+	}
+//	if(strcmp(args[0], "Headers") == 0){
+//		m->showheaders();
+//		return True;
+//	}
+
+	ret = 0;
+
+    Return:
+	free(cmd);
+	return ret;
+}
+
+void
+mesgtagpost(Message *m)
+{
+	if(m->tagposted)
+		return;
+	wintagwrite(m->w, " Post", 5);
+	m->tagposted = 1;
+}
+
+/* need to expand selection more than default word */
+#pragma varargck argpos eval 2
+
+long
+eval(Window *w, char *s, ...)
+{
+	char buf[64];
+	va_list arg;
+
+	va_start(arg, s);
+	vsnprint(buf, sizeof buf, s, arg);
+	va_end(arg);
+
+	if(winsetaddr(w, buf, 1)==0)
+		return -1;
+
+	if(pread(w->addr, buf, 24, 0) != 24)
+		return -1;
+	return strtol(buf, 0, 10);
+}
+
+int
+isemail(char *s)
+{
+	int nat;
+
+	nat = 0;
+	for(; *s; s++)
+		if(*s == '@')
+			nat++;
+		else if(!isalpha(*s) && !isdigit(*s) && !strchr("_.-+/", *s))
+			return 0;
+	return nat==1;
+}
+
+char addrdelim[] =  "/[ \t\\n<>()\\[\\]]/";
+char*
+expandaddr(Window *w, Event *e)
+{
+	char *s;
+	long q0, q1;
+
+	if(e->q0 != e->q1)	/* cannot happen */
+		return nil;
+
+	q0 = eval(w, "#%d-%s", e->q0, addrdelim);
+	if(q0 == -1)	/* bad char not found */
+		q0 = 0;
+	else			/* increment past bad char */
+		q0++;
+
+	q1 = eval(w, "#%d+%s", e->q0, addrdelim);
+	if(q1 < 0){
+		q1 = eval(w, "$");
+		if(q1 < 0)
+			return nil;
+	}
+	if(q0 >= q1)
+		return nil;
+	s = emalloc((q1-q0)*UTFmax+1);
+	winread(w, q0, q1, s);
+	return s;
+}
+
+int
+replytoaddr(Window *w, Message *m, Event *e, char *s)
+{
+	int did;
+	char *buf;
+	Plumbmsg *pm;
+
+	buf = nil;
+	did = 0;
+	if(e->flag & 2){
+		/* autoexpanded; use our own bigger expansion */
+		buf = expandaddr(w, e);
+		if(buf == nil)
+			return 0;
+		s = buf;
+	}
+	if(isemail(s)){
+		did = 1;
+		pm = emalloc(sizeof(Plumbmsg));
+		pm->src = estrdup("Mail");
+		pm->dst = estrdup("sendmail");
+		pm->data = estrdup(s);
+		pm->ndata = -1;
+		if(m->subject && m->subject[0]){
+			pm->attr = emalloc(sizeof(Plumbattr));
+			pm->attr->name = estrdup("Subject");
+			if(tolower(m->subject[0]) != 'r' || tolower(m->subject[1]) != 'e' || m->subject[2] != ':')
+				pm->attr->value = estrstrdup("Re: ", m->subject);
+			else
+				pm->attr->value = estrdup(m->subject);
+			pm->attr->next = nil;
+		}
+		if(plumbsend(plumbsendfd, pm) < 0)
+			fprint(2, "error writing plumb message: %r\n");
+		plumbfree(pm);
+	}
+	free(buf);
+	return did;
+}
+
+
+void
+mesgctl(void *v)
+{
+	Message *m;
+	Window *w;
+	Event *e, *eq, *e2, *ea;
+	int na, nopen, i, j;
+	char *os, *s, *t, *buf;
+
+	m = v;
+	w = m->w;
+	threadsetname("mesgctl");
+	proccreate(wineventproc, w, STACK);
+	for(;;){
+		e = recvp(w->cevent);
+		switch(e->c1){
+		default:
+		Unk:
+			print("unknown message %c%c\n", e->c1, e->c2);
+			break;
+
+		case 'E':	/* write to body; can't affect us */
+			break;
+
+		case 'F':	/* generated by our actions; ignore */
+			break;
+
+		case 'K':	/* type away; we don't care */
+		case 'M':
+			switch(e->c2){
+			case 'x':	/* mouse only */
+			case 'X':
+				ea = nil;
+				eq = e;
+				if(e->flag & 2){
+					e2 = recvp(w->cevent);
+					eq = e2;
+				}
+				if(e->flag & 8){
+					ea = recvp(w->cevent);
+					recvp(w->cevent);
+					na = ea->nb;
+				}else
+					na = 0;
+				if(eq->q1>eq->q0 && eq->nb==0){
+					s = emalloc((eq->q1-eq->q0)*UTFmax+1);
+					winread(w, eq->q0, eq->q1, s);
+				}else
+					s = estrdup(eq->b);
+				if(na){
+					t = emalloc(strlen(s)+1+na+1);
+					sprint(t, "%s %s", s, ea->b);
+					free(s);
+					s = t;
+				}
+				if(!mesgcommand(m, s))	/* send it back */
+					winwriteevent(w, e);
+				break;
+
+			case 'l':	/* mouse only */
+			case 'L':
+				buf = nil;
+				eq = e;
+				if(e->flag & 2){
+					e2 = recvp(w->cevent);
+					eq = e2;
+				}
+				s = eq->b;
+				if(eq->q1>eq->q0 && eq->nb==0){
+					buf = emalloc((eq->q1-eq->q0)*UTFmax+1);
+					winread(w, eq->q0, eq->q1, buf);
+					s = buf;
+				}
+				os = s;
+				nopen = 0;
+				do{
+					/* skip mail box name if present */
+					if(strncmp(s, mbox.name, strlen(mbox.name)) == 0)
+						s += strlen(mbox.name);
+					if(strstr(s, "body") != nil){
+						/* strip any known extensions */
+						for(i=0; exts[i].ext!=nil; i++){
+							j = strlen(exts[i].ext);
+							if(strlen(s)>j && strcmp(s+strlen(s)-j, exts[i].ext)==0){
+								s[strlen(s)-j] = '\0';
+								break;
+							}
+						}
+						if(strlen(s)>5 && strcmp(s+strlen(s)-5, "/body")==0)
+							s[strlen(s)-4] = '\0';	/* leave / in place */
+					}
+					nopen += mesgopen(&mbox, mbox.name, s, m, 0, nil);
+					while(*s!=0 && *s++!='\n')
+						;
+				}while(*s);
+				if(nopen == 0 && e->c1 == 'L')
+					nopen += replytoaddr(w, m, e, os);
+				if(nopen == 0)
+					winwriteevent(w, e);
+				free(buf);
+				break;
+
+			case 'I':	/* modify away; we don't care */
+			case 'D':
+				mesgtagpost(m);
+				/* fall through */
+			case 'd':
+			case 'i':
+				break;
+
+			default:
+				goto Unk;
+			}
+		}
+	}
+}
+
+void
+mesgline(Message *m, char *header, char *value)
+{
+	if(strlen(value) > 0)
+		Bprint(m->w->body, "%s: %s\n", header, value);
+}
+
+int
+isprintable(char *type)
+{
+	int i;
+
+	for(i=0; goodtypes[i]!=nil; i++)
+		if(strcmp(type, goodtypes[i])==0)
+			return 1;
+	return 0;
+}
+
+char*
+ext(char *type)
+{
+	int i;
+
+	for(i=0; exts[i].type!=nil; i++)
+		if(strcmp(type, exts[i].type)==0)
+			return exts[i].ext;
+	return "";
+}
+
+void
+mimedisplay(Message *m, char *name, char *rootdir, Window *w, int fileonly)
+{
+	char *dest;
+
+	if(strcmp(m->disposition, "file")==0 || strlen(m->filename)!=0){
+		if(strlen(m->filename) == 0){
+			dest = estrdup(m->name);
+			dest[strlen(dest)-1] = '\0';
+		}else
+			dest = estrdup(m->filename);
+		if(m->filename[0] != '/')
+			dest = egrow(estrdup(home), "/", dest);
+		Bprint(w->body, "\tcp %s%sbody%s %q\n", rootdir, name, ext(m->type), dest);
+		free(dest);
+	}else if(!fileonly)
+		Bprint(w->body, "\tfile is %s%sbody%s\n", rootdir, name, ext(m->type));
+}
+
+void
+printheader(char *dir, Biobuf *b, char **okheaders)
+{
+	char *s;
+	char *lines[100];
+	int i, j, n;
+
+	s = readfile(dir, "header", nil);
+	if(s == nil)
+		return;
+	n = getfields(s, lines, nelem(lines), 0, "\n");
+	for(i=0; i<n; i++)
+		for(j=0; okheaders[j]; j++)
+			if(cistrncmp(lines[i], okheaders[j], strlen(okheaders[j])) == 0)
+				Bprint(b, "%s\n", lines[i]);
+	free(s);
+}
+
+void
+mesgload(Message *m, char *rootdir, char *file, Window *w)
+{
+	char *s, *subdir, *name, *dir;
+	Message *mp, *thisone;
+	int n;
+
+	dir = estrstrdup(rootdir, file);
+
+	if(strcmp(m->type, "message/rfc822") != 0){	/* suppress headers of envelopes */
+		if(strlen(m->from) > 0){
+			Bprint(w->body, "From: %s\n", m->from);
+			mesgline(m, "Date", m->date);
+			mesgline(m, "To", m->to);
+			mesgline(m, "CC", m->cc);
+			mesgline(m, "Subject", m->subject);
+			printheader(dir, w->body, extraheaders);
+		}else{
+			printheader(dir, w->body, okheaders);
+			printheader(dir, w->body, extraheaders);
+		}
+		Bprint(w->body, "\n");
+	}
+
+	if(m->level == 1 && m->recursed == 0){
+		m->recursed = 1;
+		readmbox(m, rootdir, m->name);
+	}
+	if(m->head == nil){	/* single part message */
+		if(strcmp(m->type, "text")==0 || strncmp(m->type, "text/", 5)==0){
+			mimedisplay(m, m->name, rootdir, w, 1);
+			s = readbody(m->type, dir, &n);
+			winwritebody(w, s, n);
+			free(s);
+		}else
+			mimedisplay(m, m->name, rootdir, w, 0);
+	}else{
+		/* multi-part message, either multipart/* or message/rfc822 */
+		thisone = nil;
+		if(strcmp(m->type, "multipart/alternative") == 0){
+			thisone = m->head;	/* in case we can't find a good one */
+			for(mp=m->head; mp!=nil; mp=mp->next)
+				if(isprintable(mp->type)){
+					thisone = mp;
+					break;
+				}
+		}
+		for(mp=m->head; mp!=nil; mp=mp->next){
+			if(thisone!=nil && mp!=thisone)
+				continue;
+			subdir = estrstrdup(dir, mp->name);
+			name = estrstrdup(file, mp->name);
+			/* skip first element in name because it's already in window name */
+			if(mp != m->head)
+				Bprint(w->body, "\n===> %s (%s) [%s]\n", strchr(name, '/')+1, mp->type, mp->disposition);
+			if(strcmp(mp->type, "text")==0 || strncmp(mp->type, "text/", 5)==0){
+				mimedisplay(mp, name, rootdir, w, 1);
+				printheader(subdir, w->body, okheaders);
+				printheader(subdir, w->body, extraheaders);
+				winwritebody(w, "\n", 1);
+				s = readbody(mp->type, subdir, &n);
+				winwritebody(w, s, n);
+				free(s);
+			}else{
+				if(strncmp(mp->type, "multipart/", 10)==0 || strcmp(mp->type, "message/rfc822")==0){
+					mp->w = w;
+					mesgload(mp, rootdir, name, w);
+					mp->w = nil;
+				}else
+					mimedisplay(mp, name, rootdir, w, 0);
+			}
+			free(name);
+			free(subdir);
+		}
+	}
+	free(dir);
+}
+
+int
+tokenizec(char *str, char **args, int max, char *splitc)
+{
+	int na;
+	int intok = 0;
+
+	if(max <= 0)
+		return 0;	
+	for(na=0; *str != '\0';str++){
+		if(strchr(splitc, *str) == nil){
+			if(intok)
+				continue;
+			args[na++] = str;
+			intok = 1;
+		}else{
+			/* it's a separator/skip character */
+			*str = '\0';
+			if(intok){
+				intok = 0;
+				if(na >= max)
+					break;
+			}
+		}
+	}
+	return na;
+}
+
+Message*
+mesglookup(Message *mbox, char *name, char *digest)
+{
+	int n;
+	Message *m;
+	char *t;
+
+	if(digest){
+		/* can find exactly */
+		for(m=mbox->head; m!=nil; m=m->next)
+			if(strcmp(digest, m->digest) == 0)
+				break;
+		return m;
+	}
+
+	n = strlen(name);
+	if(n == 0)
+		return nil;
+	if(name[n-1] == '/')
+		t = estrdup(name);
+	else
+		t = estrstrdup(name, "/");
+	for(m=mbox->head; m!=nil; m=m->next)
+		if(strcmp(t, m->name) == 0)
+			break;
+	free(t);
+	return m;
+}
+
+/*
+ * Find plumb port, knowing type is text, given file name (by extension)
+ */
+int
+plumbportbysuffix(char *file)
+{
+	char *suf;
+	int i, nsuf, nfile;
+
+	nfile = strlen(file);
+	for(i=0; ports[i].type!=nil; i++){
+		suf = ports[i].suffix;
+		nsuf = strlen(suf);
+		if(nfile > nsuf)
+			if(cistrncmp(file+nfile-nsuf, suf, nsuf) == 0)
+				return i;
+	}
+	return 0;
+}
+
+/*
+ * Find plumb port using type and file name (by extension)
+ */
+int
+plumbport(char *type, char *file)
+{
+	int i;
+
+	for(i=0; ports[i].type!=nil; i++)
+		if(strncmp(type, ports[i].type, strlen(ports[i].type)) == 0)
+			return i;
+	/* see if it's a text type */
+	for(i=0; goodtypes[i]!=nil; i++)
+		if(strncmp(type, goodtypes[i], strlen(goodtypes[i])) == 0)
+			return plumbportbysuffix(file);
+	return -1;
+}
+
+void
+plumb(Message *m, char *dir)
+{
+	int i;
+	char *port;
+	Plumbmsg *pm;
+
+	if(strlen(m->type) == 0)
+		return;
+	i = plumbport(m->type, m->filename);
+	if(i < 0)
+		fprint(2, "can't find destination for message subpart\n");
+	else{
+		port = ports[i].port;
+		pm = emalloc(sizeof(Plumbmsg));
+		pm->src = estrdup("Mail");
+		if(port)
+			pm->dst = estrdup(port);
+		else
+			pm->dst = nil;
+		pm->wdir = nil;
+		pm->type = estrdup("text");
+		pm->ndata = -1;
+		pm->data = estrstrdup(dir, "body");
+		pm->data = eappend(pm->data, "", ports[i].suffix);
+		if(plumbsend(plumbsendfd, pm) < 0)
+			fprint(2, "error writing plumb message: %r\n");
+		plumbfree(pm);
+	}
+}
+
+int
+mesgopen(Message *mbox, char *dir, char *s, Message *mesg, int plumbed, char *digest)
+{
+	char *t, *u, *v;
+	Message *m;
+	char *direlem[10];
+	int i, ndirelem, reuse;
+
+	/* find white-space-delimited first word */
+	for(t=s; *t!='\0' && !isspace(*t); t++)
+		;
+	u = emalloc(t-s+1);
+	memmove(u, s, t-s);
+	/* separate it on slashes */
+	ndirelem = tokenizec(u, direlem, nelem(direlem), "/");
+	if(ndirelem <= 0){
+    Error:
+		free(u);
+		return 0;
+	}
+	if(plumbed){
+		write(wctlfd, "top", 3);
+		write(wctlfd, "current", 7);
+	}
+	/* open window for message */
+	m = mesglookup(mbox, direlem[0], digest);
+	if(m == nil)
+		goto Error;
+	if(mesg!=nil && m!=mesg)	/* string looked like subpart but isn't part of this message */
+		goto Error;
+	if(m->opened == 0){
+		if(m->w == nil){
+			reuse = 0;
+			m->w = newwindow();
+		}else{
+			reuse = 1;
+			/* re-use existing window */
+			if(winsetaddr(m->w, "0,$", 1)){
+				if(m->w->data < 0)
+					m->w->data = winopenfile(m->w, "data");
+				write(m->w->data, "", 0);
+			}
+		}
+		v = estrstrdup(mbox->name, m->name);
+		winname(m->w, v);
+		free(v);
+		if(!reuse){
+			if(m->deleted)
+				wintagwrite(m->w, "Q Reply all UnDelmesg Save ", 2+6+4+10+5);
+			else
+				wintagwrite(m->w, "Q Reply all Delmesg Save ", 2+6+4+8+5);
+		}
+		threadcreate(mesgctl, m, STACK);
+		winopenbody(m->w, OWRITE);
+		mesgload(m, dir, m->name, m->w);
+		winclosebody(m->w);
+		winclean(m->w);
+		m->opened = 1;
+		if(ndirelem == 1){
+			free(u);
+			return 1;
+		}
+	}
+	if(ndirelem == 1 && plumbport(m->type, m->filename) <= 0){
+		/* make sure dot is visible */
+		ctlprint(m->w->ctl, "show\n");
+		return 0;
+	}
+	/* walk to subpart */
+	dir = estrstrdup(dir, m->name);
+	for(i=1; i<ndirelem; i++){
+		m = mesglookup(m, direlem[i], digest);
+		if(m == nil)
+			break;
+		dir = egrow(dir, m->name, nil);
+	}
+	if(m != nil && plumbport(m->type, m->filename) > 0)
+		plumb(m, dir);
+	free(dir);
+	free(u);
+	return 1;
+}
+
+void
+rewritembox(Window *w, Message *mbox)
+{
+	Message *m, *next;
+	char *deletestr, *t;
+	int nopen;
+
+	deletestr = estrstrdup("delete ", fsname);
+
+	nopen = 0;
+	for(m=mbox->head; m!=nil; m=next){
+		next = m->next;
+		if(m->deleted == 0)
+			continue;
+		if(m->opened){
+			nopen++;
+			continue;
+		}
+		if(m->writebackdel){
+			/* messages deleted by plumb message are not removed again */
+			t = estrdup(m->name);
+			if(strlen(t) > 0)
+				t[strlen(t)-1] = '\0';
+			deletestr = egrow(deletestr, " ", t);
+		}
+		mesgmenudel(w, mbox, m);
+		mesgdel(mbox, m);
+	}
+	if(write(mbox->ctlfd, deletestr, strlen(deletestr)) < 0)
+		fprint(2, "Mail: warning: error removing mail message files: %r\n");
+	free(deletestr);
+	winselect(w, "0", 0);
+	if(nopen == 0)
+		winclean(w);
+	mbox->dirty = 0;
+}
+
+/* name is a full file name, but it might not belong to us */
+Message*
+mesglookupfile(Message *mbox, char *name, char *digest)
+{
+	int k, n;
+
+	k = strlen(name);
+	n = strlen(mbox->name);
+	if(k==0 || strncmp(name, mbox->name, n) != 0){
+//		fprint(2, "Mail: message %s not in this mailbox\n", name);
+		return nil;
+	}
+	return mesglookup(mbox, name+n, digest);
+}
--- /dev/null
+++ b/acme/mail/src/mkfile
@@ -1,0 +1,30 @@
+</$objtype/mkfile
+
+TARG=Mail
+OFILES=\
+		html.$O\
+		mail.$O\
+		mesg.$O\
+		reply.$O\
+		util.$O\
+		win.$O
+
+HFILES=dat.h
+LIB=
+
+BIN=/acme/bin/$objtype
+
+UPDATE=\
+	mkfile\
+	$HFILES\
+	${OFILES:%.$O=%.c}\
+
+</sys/src/cmd/mkone
+
+$O.out: $OFILES
+	$LD -o $target  $LDFLAGS $OFILES
+
+syms:V:
+	8c -a mail.c	>syms
+	8c -aa mesg.c reply.c util.c win.c 	>>syms
+
--- /dev/null
+++ b/acme/mail/src/reply.c
@@ -1,0 +1,567 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include <ctype.h>
+#include <plumb.h>
+#include "dat.h"
+
+static int	replyid;
+
+int
+quote(Message *m, Biobuf *b, char *dir, char *quotetext)
+{
+	char *body, *type;
+	int i, n, nlines;
+	char **lines;
+
+	if(quotetext){
+		body = quotetext;
+		n = strlen(body);
+		type = nil;
+	}else{
+		/* look for first textual component to quote */
+		type = readfile(dir, "type", &n);
+		if(type == nil){
+			print("no type in %s\n", dir);
+			return 0;
+		}
+		if(strncmp(type, "multipart/", 10)==0 || strncmp(type, "message/", 8)==0){
+			dir = estrstrdup(dir, "1/");
+			if(quote(m, b, dir, nil)){
+				free(type);
+				free(dir);
+				return 1;
+			}
+			free(dir);
+		}
+		if(strncmp(type, "text", 4) != 0){
+			free(type);
+			return 0;
+		}
+		body = readbody(m->type, dir, &n);
+		if(body == nil)
+			return 0;
+	}
+	nlines = 0;
+	for(i=0; i<n; i++)
+		if(body[i] == '\n')
+			nlines++;
+	nlines++;
+	lines = emalloc(nlines*sizeof(char*));
+	nlines = getfields(body, lines, nlines, 0, "\n");
+	/* delete leading and trailing blank lines */
+	i = 0;
+	while(i<nlines && lines[i][0]=='\0')
+		i++;
+	while(i<nlines && lines[nlines-1][0]=='\0')
+		nlines--;
+	while(i < nlines){
+		Bprint(b, ">%s%s\n", lines[i][0]=='>'? "" : " ", lines[i]);
+		i++;
+	}
+	free(lines);
+	free(body);	/* will free quotetext if non-nil */
+	free(type);
+	return 1;
+}
+
+void
+mkreply(Message *m, char *label, char *to, Plumbattr *attr, char *quotetext)
+{
+	Message *r;
+	char *dir, *t;
+	int quotereply;
+	Plumbattr *a;
+
+	quotereply = (label[0] == 'Q');
+	r = emalloc(sizeof(Message));
+	r->isreply = 1;
+	if(m != nil)
+		r->replyname = estrdup(m->name);
+	r->next = replies.head;
+	r->prev = nil;
+	if(replies.head != nil)
+		replies.head->prev = r;
+	replies.head = r;
+	if(replies.tail == nil)
+		replies.tail = r;
+	r->name = emalloc(strlen(mbox.name)+strlen(label)+10);
+	sprint(r->name, "%s%s%d", mbox.name, label, ++replyid);
+	r->w = newwindow();
+	winname(r->w, r->name);
+	ctlprint(r->w->ctl, "cleartag");
+	wintagwrite(r->w, "fmt Look Post Undo", 4+5+5+4);
+	r->tagposted = 1;
+	threadcreate(mesgctl, r, STACK);
+	winopenbody(r->w, OWRITE);
+	if(to!=nil && to[0]!='\0')
+		Bprint(r->w->body, "%s\n", to);
+	for(a=attr; a; a=a->next)
+		Bprint(r->w->body, "%s: %s\n", a->name, a->value);
+	dir = nil;
+	if(m != nil){
+		dir = estrstrdup(mbox.name, m->name);
+		if(to == nil && attr == nil){
+			/* Reply goes to replyto; Reply all goes to From and To and CC */
+			if(strstr(label, "all") == nil)
+				Bprint(r->w->body, "To: %s\n", m->replyto);
+			else{	/* Replyall */
+				if(strlen(m->from) > 0)
+					Bprint(r->w->body, "To: %s\n", m->from);
+				if(strlen(m->to) > 0)
+					Bprint(r->w->body, "To: %s\n", m->to);
+				if(strlen(m->cc) > 0)
+					Bprint(r->w->body, "CC: %s\n", m->cc);
+			}
+		}
+		if(strlen(m->subject) > 0){
+			t = "Subject: Re: ";
+			if(strlen(m->subject) >= 3)
+				if(tolower(m->subject[0])=='r' && tolower(m->subject[1])=='e' && m->subject[2]==':')
+					t = "Subject: ";
+			Bprint(r->w->body, "%s%s\n", t, m->subject);
+		}
+		if(!quotereply){
+			Bprint(r->w->body, "Include: %sraw\n", dir);
+			free(dir);
+		}
+	}
+	Bprint(r->w->body, "\n");
+	if(m == nil)
+		Bprint(r->w->body, "\n");
+	else if(quotereply){
+		quote(m, r->w->body, dir, quotetext);
+		free(dir);
+	}
+	winclosebody(r->w);
+	if(m==nil && (to==nil || to[0]=='\0'))
+		winselect(r->w, "0", 0);
+	else
+		winselect(r->w, "$", 0);
+	winclean(r->w);
+	windormant(r->w);
+}
+
+void
+delreply(Message *m)
+{
+	if(m->next == nil)
+		replies.tail = m->prev;
+	else
+		m->next->prev = m->prev;
+	if(m->prev == nil)
+		replies.head = m->next;
+	else
+		m->prev->next = m->next;
+	mesgfreeparts(m);
+	free(m);
+}
+
+/* copy argv to stack and free the incoming strings, so we don't leak argument vectors */
+void
+buildargv(char **inargv, char *argv[NARGS+1], char args[NARGCHAR])
+{
+	int i, n;
+	char *s, *a;
+
+	s = args;
+	for(i=0; i<NARGS; i++){
+		a = inargv[i];
+		if(a == nil)
+			break;
+		n = strlen(a)+1;
+		if((s-args)+n >= NARGCHAR)	/* too many characters */
+			break;
+		argv[i] = s;
+		memmove(s, a, n);
+		s += n;
+		free(a);
+	}
+	argv[i] = nil;
+}
+
+void
+execproc(void *v)
+{
+	struct Exec *e;
+	int p[2], q[2];
+	char *prog;
+	char *argv[NARGS+1], args[NARGCHAR];
+
+	e = v;
+	p[0] = e->p[0];
+	p[1] = e->p[1];
+	q[0] = e->q[0];
+	q[1] = e->q[1];
+	prog = e->prog;	/* known not to be malloc'ed */
+	rfork(RFFDG);
+	sendul(e->sync, 1);
+	buildargv(e->argv, argv, args);
+	free(e->argv);
+	chanfree(e->sync);
+	free(e);
+	dup(p[0], 0);
+	close(p[0]);
+	close(p[1]);
+	if(q[0]){
+		dup(q[1], 1);
+		close(q[0]);
+		close(q[1]);
+	}
+	procexec(nil, prog, argv);
+//fprint(2, "exec: %s", e->prog);
+//{int i;
+//for(i=0; argv[i]; i++) print(" '%s'", argv[i]);
+//print("\n");
+//}
+//argv[0] = "cat";
+//argv[1] = nil;
+//procexec(nil, "/bin/cat", argv);
+	fprint(2, "Mail: can't exec %s: %r\n", prog);
+	threadexits("can't exec");
+}
+
+enum{
+	ATTACH,
+	BCC,
+	CC,
+	FROM,
+	INCLUDE,
+	TO,
+};
+
+char *headers[] = {
+	"attach:",
+	"bcc:",
+	"cc:",
+	"from:",
+	"include:",
+	"to:",
+	nil,
+};
+
+int
+whichheader(char *h)
+{
+	int i;
+
+	for(i=0; headers[i]!=nil; i++)
+		if(cistrcmp(h, headers[i]) == 0)
+			return i;
+	return -1;
+}
+
+char *tolist[200];
+char	*cclist[200];
+char	*bcclist[200];
+int ncc, nbcc, nto;
+char	*attlist[200];
+char	included[200];
+
+int
+addressed(char *name)
+{
+	int i;
+
+	for(i=0; i<nto; i++)
+		if(strcmp(name, tolist[i]) == 0)
+			return 1;
+	for(i=0; i<ncc; i++)
+		if(strcmp(name, cclist[i]) == 0)
+			return 1;
+	for(i=0; i<nbcc; i++)
+		if(strcmp(name, bcclist[i]) == 0)
+			return 1;
+	return 0;
+}
+
+char*
+skipbl(char *s, char *e)
+{
+	while(s < e){
+		if(*s!=' ' && *s!='\t' && *s!=',')
+			break;
+		s++;
+	}
+	return s;
+}
+
+char*
+findbl(char *s, char *e)
+{
+	while(s < e){
+		if(*s==' ' || *s=='\t' || *s==',')
+			break;
+		s++;
+	}
+	return s;
+}
+
+/*
+ * comma-separate possibly blank-separated strings in line; e points before newline
+ */
+void
+commas(char *s, char *e)
+{
+	char *t;
+
+	/* may have initial blanks */
+	s = skipbl(s, e);
+	while(s < e){
+		s = findbl(s, e);
+		if(s == e)
+			break;
+		t = skipbl(s, e);
+		if(t == e)	/* no more words */
+			break;
+		/* patch comma */
+		*s++ = ',';
+		while(s < t)
+			*s++ = ' ';
+	}
+}
+
+int
+print2(int fd, int ofd, char *fmt, ...)
+{
+	int m, n;
+	char *s;
+	va_list arg;
+
+	va_start(arg, fmt);
+	s = vsmprint(fmt, arg);
+	va_end(arg);
+	if(s == nil)
+		return -1;
+	m = strlen(s);
+	n = write(fd, s, m);
+	if(ofd > 0)
+		write(ofd, s, m);
+	return n;
+}
+
+void
+write2(int fd, int ofd, char *buf, int n, int nofrom)
+{
+	char *from, *p;
+	int m;
+
+	write(fd, buf, n);
+
+	if(ofd <= 0)
+		return;
+
+	if(nofrom == 0){
+		write(ofd, buf, n);
+		return;
+	}
+
+	/* need to escape leading From lines to avoid corrupting 'outgoing' mailbox */
+	for(p=buf; *p; p+=m){
+		from = cistrstr(p, "from");
+		if(from == nil)
+			m = n;
+		else
+			m = from - p;
+		if(m > 0)
+			write(ofd, p, m);
+		if(from){
+			if(p==buf || from[-1]=='\n')
+				write(ofd, " ", 1);	/* escape with space if From is at start of line */
+			write(ofd, from, 4);
+			m += 4;
+		}
+		n -= m;
+	}
+}
+
+void
+mesgsend(Message *m)
+{
+	char *s, *body, *to;
+	int i, j, h, n, natt, p[2];
+	struct Exec *e;
+	Channel *sync;
+	int first, nfld, delit, ofd;
+	char *copy, *fld[100], *now;
+
+	body = winreadbody(m->w, &n);
+	/* assemble to: list from first line, to: line, and cc: line */
+	nto = 0;
+	natt = 0;
+	ncc = 0;
+	nbcc = 0;
+	first = 1;
+	to = body;
+	for(;;){
+		for(s=to; *s!='\n'; s++)
+			if(*s == '\0'){
+				free(body);
+				return;
+			}
+		if(s++ == to)	/* blank line */
+			break;
+		/* make copy of line to tokenize */
+		copy = emalloc(s-to);
+		memmove(copy, to, s-to);
+		copy[s-to-1] = '\0';
+		nfld = tokenizec(copy, fld, nelem(fld), ", \t");
+		if(nfld == 0){
+			free(copy);
+			break;
+		}
+		n -= s-to;
+		switch(h = whichheader(fld[0])){
+		case TO:
+		case FROM:
+			delit = 1;
+			commas(to+strlen(fld[0]), s-1);
+			for(i=1; i<nfld && nto<nelem(tolist); i++)
+				if(!addressed(fld[i]))
+					tolist[nto++] = estrdup(fld[i]);
+			break;
+		case BCC:
+			delit = 1;
+			commas(to+strlen(fld[0]), s-1);
+			for(i=1; i<nfld && nbcc<nelem(bcclist); i++)
+				if(!addressed(fld[i]))
+					bcclist[nbcc++] = estrdup(fld[i]);
+			break;
+		case CC:
+			delit = 1;
+			commas(to+strlen(fld[0]), s-1);
+			for(i=1; i<nfld && ncc<nelem(cclist); i++)
+				if(!addressed(fld[i]))
+					cclist[ncc++] = estrdup(fld[i]);
+			break;
+		case ATTACH:
+		case INCLUDE:
+			delit = 1;
+			for(i=1; i<nfld && natt<nelem(attlist); i++){
+				attlist[natt] = estrdup(fld[i]);
+				included[natt++] = (h == INCLUDE);
+			}
+			break;
+		default:
+			if(first){
+				delit = 1;
+				for(i=0; i<nfld && nto<nelem(tolist); i++)
+					tolist[nto++] = estrdup(fld[i]);
+			}else	/* ignore it */
+				delit = 0;
+			break;
+		}
+		if(delit){
+			/* delete line from body */
+			memmove(to, s, n+1);
+		}else
+			to = s;
+		free(copy);
+		first = 0;
+	}
+
+	ofd = open(outgoing, OWRITE|OCEXEC);	/* no error check necessary */
+	if(ofd > 0){
+		/* From dhog Fri Aug 24 22:13:00 EDT 2001 */
+		now = ctime(time(0));
+		fprint(ofd, "From %s %s", user, now);
+		fprint(ofd, "From: %s\n", user);
+		fprint(ofd, "Date: %s", now);
+		for(i=0; i<natt; i++)
+			if(included[i])
+				fprint(ofd, "Include: %s\n", attlist[i]);
+			else
+				fprint(ofd, "Attach: %s\n", attlist[i]);
+		/* needed because mail is by default Latin-1 */
+		fprint(ofd, "Content-Type: text/plain; charset=\"UTF-8\"\n");
+		fprint(ofd, "Content-Transfer-Encoding: 8bit\n");
+	}
+
+	e = emalloc(sizeof(struct Exec));
+	if(pipe(p) < 0)
+		error("can't create pipe: %r");
+	e->p[0] = p[0];
+	e->p[1] = p[1];
+	e->prog = "/bin/upas/marshal";
+	e->argv = emalloc((1+1+2+4*natt+1)*sizeof(char*));
+	e->argv[0] = estrdup("marshal");
+	e->argv[1] = estrdup("-8");
+	j = 2;
+	if(m->replyname){
+		e->argv[j++] = estrdup("-R");
+		e->argv[j++] = estrstrdup(mbox.name, m->replyname);
+	}
+	for(i=0; i<natt; i++){
+		if(included[i])
+			e->argv[j++] = estrdup("-A");
+		else
+			e->argv[j++] = estrdup("-a");
+		e->argv[j++] = estrdup(attlist[i]);
+	}
+	sync = chancreate(sizeof(int), 0);
+	e->sync = sync;
+	proccreate(execproc, e, EXECSTACK);
+	recvul(sync);
+	close(p[0]);
+
+	/* using marshal -8, so generate rfc822 headers */
+	if(nto > 0){
+		print2(p[1], ofd, "To: ");
+		for(i=0; i<nto-1; i++)
+			print2(p[1], ofd, "%s, ", tolist[i]);
+		print2(p[1], ofd, "%s\n", tolist[i]);
+	}
+	if(ncc > 0){
+		print2(p[1], ofd, "CC: ");
+		for(i=0; i<ncc-1; i++)
+			print2(p[1], ofd, "%s, ", cclist[i]);
+		print2(p[1], ofd, "%s\n", cclist[i]);
+	}
+	if(nbcc > 0){
+		print2(p[1], ofd, "BCC: ");
+		for(i=0; i<nbcc-1; i++)
+			print2(p[1], ofd, "%s, ", bcclist[i]);
+		print2(p[1], ofd, "%s\n", bcclist[i]);
+	}
+
+	i = strlen(body);
+	if(i > 0)
+		write2(p[1], ofd, body, i, 1);
+
+	/* guarantee a blank line, to ensure attachments are separated from body */
+	if(i==0 || body[i-1]!='\n')
+		write2(p[1], ofd, "\n\n", 2, 0);
+	else if(i>1 && body[i-2]!='\n')
+		write2(p[1], ofd, "\n", 1, 0);
+
+	/* these look like pseudo-attachments in the "outgoing" box */
+	if(ofd>0 && natt>0){
+		for(i=0; i<natt; i++)
+			if(included[i])
+				fprint(ofd, "=====> Include: %s\n", attlist[i]);
+			else
+				fprint(ofd, "=====> Attach: %s\n", attlist[i]);
+	}
+	if(ofd > 0)
+		write(ofd, "\n", 1);
+
+	for(i=0; i<natt; i++)
+		free(attlist[i]);
+	close(ofd);
+	close(p[1]);
+	free(body);
+
+	if(m->replyname != nil)
+		mesgmenumark(mbox.w, m->replyname, "\t[replied]");
+	if(m->name[0] == '/')
+		s = estrdup(m->name);
+	else
+		s = estrstrdup(mbox.name, m->name);
+	s = egrow(s, "-R", nil);
+	winname(m->w, s);
+	free(s);
+	winclean(m->w);
+	/* mark message unopened because it's no longer the original message */
+	m->opened = 0;
+}
--- /dev/null
+++ b/acme/mail/src/util.c
@@ -1,0 +1,105 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include <plumb.h>
+#include "dat.h"
+
+void*
+emalloc(uint n)
+{
+	void *p;
+
+	p = malloc(n);
+	if(p == nil)
+		error("can't malloc: %r");
+	memset(p, 0, n);
+	setmalloctag(p, getcallerpc(&n));
+	return p;
+}
+
+void*
+erealloc(void *p, uint n)
+{
+	p = realloc(p, n);
+	if(p == nil)
+		error("can't realloc: %r");
+	setmalloctag(p, getcallerpc(&n));
+	return p;
+}
+
+char*
+estrdup(char *s)
+{
+	char *t;
+
+	t = emalloc(strlen(s)+1);
+	strcpy(t, s);
+	return t;
+}
+
+char*
+estrstrdup(char *s, char *t)
+{
+	char *u;
+
+	u = emalloc(strlen(s)+strlen(t)+1);
+	strcpy(u, s);
+	strcat(u, t);
+	return u;
+}
+
+char*
+eappend(char *s, char *sep, char *t)
+{
+	char *u;
+
+	if(t == nil)
+		u = estrstrdup(s, sep);
+	else{
+		u = emalloc(strlen(s)+strlen(sep)+strlen(t)+1);
+		strcpy(u, s);
+		strcat(u, sep);
+		strcat(u, t);
+	}
+	free(s);
+	return u;
+}
+
+char*
+egrow(char *s, char *sep, char *t)
+{
+	s = eappend(s, sep, t);
+	free(t);
+	return s;
+}
+
+void
+error(char *fmt, ...)
+{
+	Fmt f;
+	char buf[64];
+	va_list arg;
+
+	fmtfdinit(&f, 2, buf, sizeof buf);
+	fmtprint(&f, "Mail: ");
+	va_start(arg, fmt);
+	fmtvprint(&f, fmt, arg);
+	va_end(arg);
+	fmtprint(&f, "\n");
+	fmtfdflush(&f);
+	threadexitsall(fmt);
+}
+
+void
+ctlprint(int fd, char *fmt, ...)
+{
+	int n;
+	va_list arg;
+
+	va_start(arg, fmt);
+	n = vfprint(fd, fmt, arg);
+	va_end(arg);
+	if(n <= 0)
+		error("control file write error: %r");
+}
--- /dev/null
+++ b/acme/mail/src/win.c
@@ -1,0 +1,341 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include <plumb.h>
+#include "dat.h"
+
+Window*
+newwindow(void)
+{
+	char buf[12];
+	Window *w;
+
+	w = emalloc(sizeof(Window));
+	w->ctl = open("/mnt/wsys/new/ctl", ORDWR|OCEXEC);
+	if(w->ctl<0 || read(w->ctl, buf, 12)!=12)
+		error("can't open window ctl file: %r");
+	ctlprint(w->ctl, "noscroll\n");
+	w->id = atoi(buf);
+	w->event = winopenfile(w, "event");
+	w->addr = -1;	/* will be opened when needed */
+	w->body = nil;
+	w->data = -1;
+	w->cevent = chancreate(sizeof(Event*), 0);
+	return w;
+}
+
+void
+winsetdump(Window *w, char *dir, char *cmd)
+{
+	if(dir != nil)
+		ctlprint(w->ctl, "dumpdir %s\n", dir);
+	if(cmd != nil)
+		ctlprint(w->ctl, "dump %s\n", cmd);
+}
+
+void
+wineventproc(void *v)
+{
+	Window *w;
+	int i;
+
+	w = v;
+	for(i=0; ; i++){
+		if(i >= NEVENT)
+			i = 0;
+		wingetevent(w, &w->e[i]);
+		sendp(w->cevent, &w->e[i]);
+	}
+}
+
+static int
+winopenfile1(Window *w, char *f, int m)
+{
+	char buf[64];
+	int fd;
+
+	sprint(buf, "/mnt/wsys/%d/%s", w->id, f);
+	fd = open(buf, m|OCEXEC);
+	if(fd < 0)
+		error("can't open window file %s: %r", f);
+	return fd;
+}
+
+int
+winopenfile(Window *w, char *f)
+{
+	return winopenfile1(w, f, ORDWR);
+}
+
+void
+wintagwrite(Window *w, char *s, int n)
+{
+	int fd;
+
+	fd = winopenfile(w, "tag");
+	if(write(fd, s, n) != n)
+		error("tag write: %r");
+	close(fd);
+}
+
+void
+winname(Window *w, char *s)
+{
+	ctlprint(w->ctl, "name %s\n", s);
+}
+
+void
+winopenbody(Window *w, int mode)
+{
+	char buf[256];
+
+	sprint(buf, "/mnt/wsys/%d/body", w->id);
+	w->body = Bopen(buf, mode|OCEXEC);
+	if(w->body == nil)
+		error("can't open window body file: %r");
+}
+
+void
+winclosebody(Window *w)
+{
+	if(w->body != nil){
+		Bterm(w->body);
+		w->body = nil;
+	}
+}
+
+void
+winwritebody(Window *w, char *s, int n)
+{
+	if(w->body == nil)
+		winopenbody(w, OWRITE);
+	if(Bwrite(w->body, s, n) != n)
+		error("write error to window: %r");
+}
+
+int
+wingetec(Window *w)
+{
+	if(w->nbuf == 0){
+		w->nbuf = read(w->event, w->buf, sizeof w->buf);
+		if(w->nbuf <= 0){
+			/* probably because window has exited, and only called by wineventproc, so just shut down */
+			threadexits(nil);
+		}
+		w->bufp = w->buf;
+	}
+	w->nbuf--;
+	return *w->bufp++;
+}
+
+int
+wingeten(Window *w)
+{
+	int n, c;
+
+	n = 0;
+	while('0'<=(c=wingetec(w)) && c<='9')
+		n = n*10+(c-'0');
+	if(c != ' ')
+		error("event number syntax");
+	return n;
+}
+
+int
+wingeter(Window *w, char *buf, int *nb)
+{
+	Rune r;
+	int n;
+
+	r = wingetec(w);
+	buf[0] = r;
+	n = 1;
+	if(r >= Runeself) {
+		while(!fullrune(buf, n))
+			buf[n++] = wingetec(w);
+		chartorune(&r, buf);
+	} 
+	*nb = n;
+	return r;
+}
+
+void
+wingetevent(Window *w, Event *e)
+{
+	int i, nb;
+
+	e->c1 = wingetec(w);
+	e->c2 = wingetec(w);
+	e->q0 = wingeten(w);
+	e->q1 = wingeten(w);
+	e->flag = wingeten(w);
+	e->nr = wingeten(w);
+	if(e->nr > EVENTSIZE)
+		error("event string too long");
+	e->nb = 0;
+	for(i=0; i<e->nr; i++){
+		e->r[i] = wingeter(w, e->b+e->nb, &nb);
+		e->nb += nb;
+	}
+	e->r[e->nr] = 0;
+	e->b[e->nb] = 0;
+	if(wingetec(w) != '\n')
+		error("event syntax error");
+}
+
+void
+winwriteevent(Window *w, Event *e)
+{
+	fprint(w->event, "%c%c%d %d\n", e->c1, e->c2, e->q0, e->q1);
+}
+
+void
+winread(Window *w, uint q0, uint q1, char *data)
+{
+	int m, n, nr;
+	char buf[256];
+
+	if(w->addr < 0)
+		w->addr = winopenfile(w, "addr");
+	if(w->data < 0)
+		w->data = winopenfile(w, "data");
+	m = q0;
+	while(m < q1){
+		n = sprint(buf, "#%d", m);
+		if(write(w->addr, buf, n) != n)
+			error("error writing addr: %r");
+		n = read(w->data, buf, sizeof buf);
+		if(n <= 0)
+			error("reading data: %r");
+		nr = utfnlen(buf, n);
+		while(m+nr >q1){
+			do; while(n>0 && (buf[--n]&0xC0)==0x80);
+			--nr;
+		}
+		if(n == 0)
+			break;
+		memmove(data, buf, n);
+		data += n;
+		*data = 0;
+		m += nr;
+	}
+}
+
+void
+windormant(Window *w)
+{
+	if(w->addr >= 0){
+		close(w->addr);
+		w->addr = -1;
+	}
+	if(w->body != nil){
+		Bterm(w->body);
+		w->body = nil;
+	}
+	if(w->data >= 0){
+		close(w->data);
+		w->data = -1;
+	}
+}
+
+
+int
+windel(Window *w, int sure)
+{
+	if(sure)
+		write(w->ctl, "delete\n", 7);
+	else if(write(w->ctl, "del\n", 4) != 4)
+		return 0;
+	/* event proc will die due to read error from event file */
+	windormant(w);
+	close(w->ctl);
+	w->ctl = -1;
+	close(w->event);
+	w->event = -1;
+	return 1;
+}
+
+void
+winclean(Window *w)
+{
+	if(w->body)
+		Bflush(w->body);
+	ctlprint(w->ctl, "clean\n");
+}
+
+int
+winsetaddr(Window *w, char *addr, int errok)
+{
+	if(w->addr < 0)
+		w->addr = winopenfile(w, "addr");
+	if(write(w->addr, addr, strlen(addr)) < 0){
+		if(!errok)
+			error("error writing addr(%s): %r", addr);
+		return 0;
+	}
+	return 1;
+}
+
+int
+winselect(Window *w, char *addr, int errok)
+{
+	if(winsetaddr(w, addr, errok)){
+		ctlprint(w->ctl, "dot=addr\n");
+		return 1;
+	}
+	return 0;
+}
+
+char*
+winreadbody(Window *w, int *np)	/* can't use readfile because acme doesn't report the length */
+{
+	char *s;
+	int m, na, n;
+
+	if(w->body != nil)
+		winclosebody(w);
+	winopenbody(w, OREAD);
+	s = nil;
+	na = 0;
+	n = 0;
+	for(;;){
+		if(na < n+512){
+			na += 1024;
+			s = realloc(s, na+1);
+		}
+		m = Bread(w->body, s+n, na-n);
+		if(m <= 0)
+			break;
+		n += m;
+	}
+	s[n] = 0;
+	winclosebody(w);
+	*np = n;
+	return s;
+}
+
+char*
+winselection(Window *w)
+{
+	int fd, m, n;
+	char *buf;
+	char tmp[256];
+
+	fd = winopenfile1(w, "rdsel", OREAD);
+	if(fd < 0)
+		error("can't open rdsel: %r");
+	n = 0;
+	buf = nil;
+	for(;;){
+		m = read(fd, tmp, sizeof tmp);
+		if(m <= 0)
+			break;
+		buf = erealloc(buf, n+m+1);
+		memmove(buf+n, tmp, m);
+		n += m;
+		buf[n] = '\0';
+	}
+	close(fd);
+	return buf;
+}
--- /dev/null
+++ b/acme/mkfile
@@ -1,0 +1,10 @@
+</$objtype/mkfile
+
+none:VQ:
+	echo mk all, install, clean, nuke, installall, update
+
+all install clean nuke installall update:V:
+	@{cd bin/source; mk $target}
+	@{cd mail/src; mk $target}
+	@{cd news/src; mk $target}
+	@{cd wiki/src; mk $target}
--- /dev/null
+++ b/acme/news/guide
@@ -1,0 +1,2 @@
+Local nntpfs
+News comp.os.plan9
--- /dev/null
+++ b/acme/news/src/mkfile
@@ -1,0 +1,18 @@
+</$objtype/mkfile
+
+TARG=News
+OFILES=\
+	news.$O\
+	util.$O\
+	win.$O\
+
+HFILES=
+
+BIN=/acme/bin/$objtype
+UPDATE=\
+	mkfile\
+	$HFILES\
+	${OFILES:%.$O=%.c}\
+	${TARG:%=/acme/bin/386/%}\
+
+</sys/src/cmd/mkone
--- /dev/null
+++ b/acme/news/src/news.c
@@ -1,0 +1,1007 @@
+/*
+ * Acme interface to nntpfs.
+ */
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include "win.h"
+#include <ctype.h>
+
+int canpost;
+int debug;
+int nshow = 20;
+
+int lo;	/* next message to look at in dir */
+int hi;	/* current hi message in dir */
+char *dir = "/mnt/news";
+char *group;
+char *from;
+
+typedef struct Article Article;
+struct Article {
+	Ref;
+	Article *prev;
+	Article *next;
+	Window *w;
+	int n;
+	int dead;
+	int dirtied;
+	int sayspost;
+	int headers;
+	int ispost;
+};
+
+Article *mlist;
+Window *root;
+
+int
+cistrncmp(char *a, char *b, int n)
+{
+	while(n-- > 0){
+		if(tolower(*a++) != tolower(*b++))
+			return -1;
+	}
+	return 0;
+}
+
+int
+cistrcmp(char *a, char *b)
+{
+	for(;;){
+		if(tolower(*a) != tolower(*b++))
+			return -1;
+		if(*a++ == 0)
+			break;
+	}
+	return 0;
+}
+
+char*
+skipwhite(char *p)
+{
+	while(isspace(*p))
+		p++;
+	return p;
+}
+
+int
+gethi(void)
+{
+	Dir *d;
+	int hi;
+
+	if((d = dirstat(dir)) == nil)
+		return -1;
+	hi = d->qid.vers;
+	free(d);
+	return hi;
+}
+
+char*
+fixfrom(char *s)
+{
+	char *r, *w;
+
+	s = estrdup(s);
+	/* remove quotes */
+	for(r=w=s; *r; r++)
+		if(*r != '"')
+			*w++ = *r;
+	*w = '\0';
+	return s;
+}
+
+char *day[] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", };
+char *mon[] = {
+	"Jan", "Feb", "Mar", "Apr",
+	"May", "Jun", "Jul", "Aug",
+	"Sep", "Oct", "Nov", "Dec",
+};
+
+char*
+fixdate(char *s)
+{
+	char *f[10], *m, *t, *wd, tmp[40];
+	int d, i, j, nf, hh, mm;
+
+	nf = tokenize(s, f, nelem(f));
+
+	wd = nil;
+	d = 0;
+	m = nil;
+	t = nil;
+	for(i=0; i<nf; i++){
+		for(j=0; j<7; j++)
+			if(cistrncmp(day[j], f[i], 3)==0)
+				wd = day[j];
+		for(j=0; j<12; j++)
+			if(cistrncmp(mon[j], f[i], 3)==0)
+				m = mon[j];
+		j = atoi(f[i]);
+		if(1 <= j && j <= 31 && d != 0)
+			d = j;
+		if(strchr(f[i], ':'))
+			t = f[i];
+	}
+
+	if(d==0 || wd==nil || m==nil || t==nil)
+		return nil;
+
+	hh = strtol(t, 0, 10);
+	mm = strtol(strchr(t, ':')+1, 0, 10);
+	sprint(tmp, "%s %d %s %d:%.2d", wd, d, m, hh, mm);
+	return estrdup(tmp);
+}
+
+void
+msgheadline(Biobuf *bin, int n, Biobuf *bout)
+{
+	char *p, *q;
+	char *date;
+	char *from;
+	char *subject;
+
+	date = nil;
+	from = nil;
+	subject = nil;
+	while(p = Brdline(bin, '\n')){
+		p[Blinelen(bin)-1] = '\0';
+		if((q = strchr(p, ':')) == nil)
+			continue;
+		*q++ = '\0';
+		if(cistrcmp(p, "from")==0)
+			from = fixfrom(skipwhite(q));
+		else if(cistrcmp(p, "subject")==0)
+			subject = estrdup(skipwhite(q));
+		else if(cistrcmp(p, "date")==0)
+			date = fixdate(skipwhite(q));
+	}
+
+	Bprint(bout, "%d/\t%s", n, from ? from : "");
+	if(date)
+		Bprint(bout, "\t%s", date);
+	if(subject)
+		Bprint(bout, "\n\t%s", subject);
+	Bprint(bout, "\n");
+
+	free(date);
+	free(from);
+	free(subject);
+}
+
+/*
+ * Write the headers for at most nshow messages to b,
+ * starting with hi and working down to lo.
+ * Return number of first message not scanned.
+ */
+int
+adddir(Biobuf *body, int hi, int lo, int nshow)
+{
+	char *p, *q, tmp[40];
+	int i, n;
+	Biobuf *b;
+
+	n = 0;
+	for(i=hi; i>=lo && n<nshow; i--){
+		sprint(tmp, "%d", i);
+		p = estrstrdup(dir, tmp);
+		if(access(p, OREAD) < 0){
+			free(p);
+			break;
+		}
+		q = estrstrdup(p, "/header");
+		free(p);
+		b = Bopen(q, OREAD);
+		free(q);
+		if(b == nil)
+			continue;
+		msgheadline(b, i, body);
+		Bterm(b);
+		n++;
+	}
+	return i;
+}
+
+/* 
+ * Show the first nshow messages in the window.
+ * This depends on nntpfs presenting contiguously
+ * numbered directories, and on the qid version being
+ * the topmost numbered directory.
+ */
+void
+dirwindow(Window *w)
+{
+	if((hi=gethi()) < 0)
+		return;
+
+	if(w->data < 0)
+		w->data = winopenfile(w, "data");
+
+	fprint(w->ctl, "dirty\n");
+	
+	winopenbody(w, OWRITE);
+	lo = adddir(w->body, hi, 0, nshow);
+	winclean(w);
+}
+
+/* return 1 if handled, 0 otherwise */
+static int
+iscmd(char *s, char *cmd)
+{
+	int len;
+
+	len = strlen(cmd);
+	return strncmp(s, cmd, len)==0 && (s[len]=='\0' || s[len]==' ' || s[len]=='\t' || s[len]=='\n');
+}
+
+static char*
+skip(char *s, char *cmd)
+{
+	s += strlen(cmd);
+	while(*s==' ' || *s=='\t' || *s=='\n')
+		s++;
+	return s;
+}
+
+void
+unlink(Article *m)
+{
+	if(mlist == m)
+		mlist = m->next;
+
+	if(m->next)
+		m->next->prev = m->prev;
+	if(m->prev)
+		m->prev->next = m->next;
+	m->next = m->prev = nil;
+}
+
+int mesgopen(char*);
+int fillmesgwindow(int, Article*);
+Article *newpost(void);
+void replywindow(Article*);
+void mesgpost(Article*);
+
+/*
+ * Depends on d.qid.vers being highest numbered message in dir.
+ */
+void
+acmetimer(Article *m, Window *w)
+{
+	Biobuf *b;
+	Dir *d;
+
+	assert(m==nil && w==root);
+
+	if((d = dirstat(dir))==nil | hi==d->qid.vers){
+		free(d);
+		return;
+	}
+
+	if(w->data < 0)
+		w->data = winopenfile(w, "data");
+	if(winsetaddr(w, "0", 0))
+		write(w->data, "", 0);
+
+	b = emalloc(sizeof(*b));
+	Binit(b, w->data, OWRITE);
+	adddir(b, d->qid.vers, hi+1, d->qid.vers);
+	hi = d->qid.vers;
+	Bterm(b);
+	free(b);
+	free(d);
+	winselect(w, "0,.", 0);
+}
+
+int
+acmeload(Article*, Window*, char *s)
+{
+	int nopen;
+
+//fprint(2, "load %s\n", s);
+
+	nopen = 0;
+	while(*s){
+		/* skip directory name */
+		if(strncmp(s, dir, strlen(dir))==0)
+			s += strlen(dir);
+		nopen += mesgopen(s);
+		if((s = strchr(s, '\n')) == nil)
+			break;
+		s = skip(s, "");
+	}
+	return nopen;
+}
+
+int
+acmecmd(Article *m, Window *w, char *s)
+{
+	int n;
+	Biobuf *b;
+
+//fprint(2, "cmd %s\n", s);
+
+	s = skip(s, "");
+
+	if(iscmd(s, "Del")){
+		if(m == nil){	/* don't close dir until messages close */
+			if(mlist != nil){
+				ctlprint(mlist->w->ctl, "show\n");
+				return 1;
+			}
+			if(windel(w, 0))
+				threadexitsall(nil);
+			return 1;
+		}else{
+			if(windel(w, 0))
+				m->dead = 1;
+			return 1;
+		}
+	}
+	if(m==nil && iscmd(s, "More")){
+		s = skip(s, "More");
+		if(n = atoi(s))
+			nshow = n;
+
+		if(w->data < 0)
+			w->data = winopenfile(w, "data");
+		winsetaddr(w, "$", 1);
+	
+		fprint(w->ctl, "dirty\n");
+
+		b = emalloc(sizeof(*b));
+		Binit(b, w->data, OWRITE);
+		lo = adddir(b, lo, 0, nshow);
+		Bterm(b);
+		free(b);		
+		winclean(w);
+		winsetaddr(w, ".,", 0);
+	}
+	if(m!=nil && !m->ispost && iscmd(s, "Headers")){
+		m->headers = !m->headers;
+		fillmesgwindow(-1, m);
+		return 1;
+	}
+	if(iscmd(s, "Newpost")){
+		m = newpost();
+		winopenbody(m->w, OWRITE);
+		Bprint(m->w->body, "%s\nsubject: \n\n", group);
+		winclean(m->w);
+		winselect(m->w, "$", 0);
+		return 1;
+	}
+	if(m!=nil && !m->ispost && iscmd(s, "Reply")){
+		replywindow(m);
+		return 1;
+	}
+//	if(m!=nil && iscmd(s, "Replymail")){
+//		fprint(2, "no replymail yet\n");
+//		return 1;
+//	}
+	if(iscmd(s, "Post")){
+		mesgpost(m);
+		return 1;
+	}
+	return 0;
+}
+
+void
+acmeevent(Article *m, Window *w, Event *e)
+{
+	Event *ea, *e2, *eq;
+	char *s, *t, *buf;
+	int na;
+	//int n;
+	//ulong q0, q1;
+
+	switch(e->c1){	/* origin of action */
+	default:
+	Unknown:
+		fprint(2, "unknown message %c%c\n", e->c1, e->c2);
+		break;
+
+	case 'T':	/* bogus timer event! */
+		acmetimer(m, w);
+		break;
+
+	case 'F':	/* generated by our actions; ignore */
+		break;
+
+	case 'E':	/* write to body or tag; can't affect us */
+		break;
+
+	case 'K':	/* type away; we don't care */
+		if(m && (e->c2 == 'I' || e->c2 == 'D')){
+			m->dirtied = 1;
+			if(!m->sayspost){
+				wintagwrite(w, "Post ", 5);
+				m->sayspost = 1;
+			}
+		}
+		break;
+
+	case 'M':	/* mouse event */
+		switch(e->c2){		/* type of action */
+		case 'x':	/* mouse: button 2 in tag */
+		case 'X':	/* mouse: button 2 in body */
+			ea = nil;
+			//e2 = nil;
+			s = e->b;
+			if(e->flag & 2){	/* null string with non-null expansion */
+				e2 = recvp(w->cevent);
+				if(e->nb==0)
+					s = e2->b;
+			}
+			if(e->flag & 8){	/* chorded argument */
+				ea = recvp(w->cevent);	/* argument */
+				na = ea->nb;
+				recvp(w->cevent);		/* ignore origin */
+			}else
+				na = 0;
+			
+			/* append chorded arguments */
+			if(na){
+				t = emalloc(strlen(s)+1+na+1);
+				sprint(t, "%s %s", s, ea->b);
+				s = t;
+			}
+			/* if it's a known command, do it */
+			/* if it's a long message, it can't be for us anyway */
+		//	DPRINT(2, "exec: %s\n", s);
+			if(!acmecmd(m, w, s))	/* send it back */
+				winwriteevent(w, e);
+			if(na)
+				free(s);
+			break;
+
+		case 'l':	/* mouse: button 3 in tag */
+		case 'L':	/* mouse: button 3 in body */
+			//buf = nil;
+			eq = e;
+			if(e->flag & 2){
+				e2 = recvp(w->cevent);
+				eq = e2;
+			}
+			s = eq->b;
+			if(eq->q1>eq->q0 && eq->nb==0){
+				buf = emalloc((eq->q1-eq->q0)*UTFmax+1);
+				winread(w, eq->q0, eq->q1, buf);
+				s = buf;
+			}
+			if(!acmeload(m, w, s))
+				winwriteevent(w, e);
+			break;
+
+		case 'i':	/* mouse: text inserted in tag */
+		case 'd':	/* mouse: text deleted from tag */
+			break;
+
+		case 'I':	/* mouse: text inserted in body */
+		case 'D':	/* mouse: text deleted from body */
+			if(m == nil)
+				break;
+
+			m->dirtied = 1;
+			if(!m->sayspost){
+				wintagwrite(w, "Post ", 5);
+				m->sayspost = 1;
+			}
+			break;
+
+		default:
+			goto Unknown;
+		}
+	}
+}
+
+void
+dirthread(void *v)
+{
+	Event *e;
+	Window *w;
+
+	w = v;
+	while(e = recvp(w->cevent))
+		acmeevent(nil, w, e);
+
+	threadexitsall(nil);
+}
+
+void
+mesgthread(void *v)
+{
+	Event *e;
+	Article *m;
+
+	m = v;
+	while(!m->dead && (e = recvp(m->w->cevent)))
+		acmeevent(m, m->w, e);
+
+//fprint(2, "msg %p exits\n", m);
+	unlink(m);
+	free(m->w);
+	free(m);
+	threadexits(nil);
+}
+
+/*
+Xref: news.research.att.com comp.os.plan9:7360
+Newsgroups: comp.os.plan9
+Path: news.research.att.com!batch0!uunet!ffx.uu.net!finch!news.mindspring.net!newsfeed.mathworks.com!fu-berlin.de!server1.netnews.ja.net!hgmp.mrc.ac.uk!pegasus.csx.cam.ac.uk!bath.ac.uk!ccsdhd
+From: Stephen Adam <saadam@bigpond.com>
+Subject: Future of Plan9
+Approved: plan9mod@bath.ac.uk
+X-Newsreader: Microsoft Outlook Express 5.00.2014.211
+X-Mimeole: Produced By Microsoft MimeOLE V5.00.2014.211
+Sender: ccsdhd@bath.ac.uk (Dennis Davis)
+Nntp-Posting-Date: Wed, 13 Dec 2000 21:28:45 EST
+NNTP-Posting-Host: 203.54.121.233
+Organization: Telstra BigPond Internet Services (http://www.bigpond.com)
+X-Date: Wed, 13 Dec 2000 20:43:37 +1000
+Lines: 12
+Message-ID: <xbIZ5.157945$e5.114349@newsfeeds.bigpond.com>
+References: <95pghu$3lf$1@news.fas.harvard.edu> <95ph36$3m9$1@news.fas.harvard.edu> <slrn980iic.u5q.mperrin@hcs.harvard.edu> <95png6$4ln$1@news.fas.harvard.edu> <95poqg$4rq$1@news.fas.harvard.edu> <slrn980vh8.2gb.myLastName@is07.fas.harvard.edu> <95q40h$66c$2@news.fas.harvard.edu> <95qjhu$8ke$1@news.fas.harvard.edu> <95riue$bu2$1@news.fas.harvard.edu> <95rnar$cbu$1@news.fas.harvard.edu>
+X-Msmail-Priority: Normal
+X-Trace: newsfeeds.bigpond.com 976703325 203.54.121.233 (Wed, 13 Dec 2000 21:28:45 EST)
+X-Priority: 3
+Date: Wed, 13 Dec 2000 10:49:50 GMT
+*/
+
+char *skipheader[] = 
+{
+	"x-",
+	"path:",
+	"xref:",
+	"approved:",
+	"sender:",
+	"nntp-",
+	"organization:",
+	"lines:",
+	"message-id:",
+	"references:",
+	"reply-to:",
+	"mime-",
+	"content-",
+};
+
+int
+fillmesgwindow(int fd, Article *m)
+{
+	Biobuf *b;
+	char *p, tmp[40];
+	int i, inhdr, copy, xfd;
+	Window *w;
+
+	xfd = -1;
+	if(fd == -1){
+		sprint(tmp, "%d/article", m->n);
+		p = estrstrdup(dir, tmp);
+		if((xfd = open(p, OREAD)) < 0){
+			free(p);	
+			return 0;
+		}
+		free(p);
+		fd = xfd;
+	}
+
+	w = m->w;
+	if(w->data < 0)
+		w->data = winopenfile(w, "data");
+	if(winsetaddr(w, ",", 0))
+		write(w->data, "", 0);
+
+	winopenbody(m->w, OWRITE);
+	b = emalloc(sizeof(*b));
+	Binit(b, fd, OREAD);
+
+	inhdr = 1;
+	copy = 1;
+	while(p = Brdline(b, '\n')){
+		if(Blinelen(b)==1)
+			inhdr = 0, copy=1;
+		if(inhdr && !isspace(p[0])){
+			copy = 1;
+			if(!m->headers){
+				if(cistrncmp(p, "from:", 5)==0){
+					p[Blinelen(b)-1] = '\0';
+					p = fixfrom(skip(p, "from:"));
+					Bprint(m->w->body, "From: %s\n", p);
+					free(p);
+					copy = 0;
+					continue;
+				}
+				for(i=0; i<nelem(skipheader); i++)
+					if(cistrncmp(p, skipheader[i], strlen(skipheader[i]))==0)
+						copy=0;
+			}
+		}
+		if(copy)
+			Bwrite(m->w->body, p, Blinelen(b));
+	}
+	Bterm(b);
+	free(b);
+	winclean(m->w);
+	if(xfd != -1)
+		close(xfd);
+	return 1;
+}
+
+Article*
+newpost(void)
+{
+	Article *m;
+	char *p, tmp[40];
+	static int nnew;
+
+	m = emalloc(sizeof *m);
+	sprint(tmp, "Post%d", ++nnew);
+	p = estrstrdup(dir, tmp);
+
+	m->w = newwindow();
+	proccreate(wineventproc, m->w, STACK);
+	winname(m->w, p);
+	wintagwrite(m->w, "Post ", 5);
+	m->sayspost = 1;
+	m->ispost = 1;
+	threadcreate(mesgthread, m, STACK);
+
+	if(mlist){
+		m->next = mlist;
+		mlist->prev = m;
+	}
+	mlist = m;
+	return m;
+}
+
+void
+replywindow(Article *m)
+{
+	Biobuf *b;
+	char *p, *ep, *q, tmp[40];
+	int fd, copy;
+	Article *reply;
+
+	sprint(tmp, "%d/article", m->n);
+	p = estrstrdup(dir, tmp);
+	if((fd = open(p, OREAD)) < 0){
+		free(p);	
+		return;
+	}
+	free(p);
+
+	reply = newpost();
+	winopenbody(reply->w, OWRITE);
+	b = emalloc(sizeof(*b));
+	Binit(b, fd, OREAD);
+	copy = 0;
+	while(p = Brdline(b, '\n')){
+		if(Blinelen(b)==1)
+			break;
+		ep = p+Blinelen(b);
+		if(!isspace(*p)){
+			copy = 0;
+			if(cistrncmp(p, "newsgroups:", 11)==0){
+				for(q=p+11; *q!='\n'; q++)
+					if(*q==',')
+						*q = ' ';
+				copy = 1;
+			}else if(cistrncmp(p, "subject:", 8)==0){
+				if(!strstr(p, " Re:") && !strstr(p, " RE:") && !strstr(p, " re:")){
+					p = skip(p, "subject:");
+					ep[-1] = '\0';
+					Bprint(reply->w->body, "Subject: Re: %s\n", p);
+				}else
+					copy = 1;
+			}else if(cistrncmp(p, "message-id:", 11)==0){
+				Bprint(reply->w->body, "References: ");
+				p += 11;
+				copy = 1;
+			}
+		}
+		if(copy)
+			Bwrite(reply->w->body, p, ep-p);
+	}
+	Bterm(b);
+	close(fd);
+	free(b);
+	Bprint(reply->w->body, "\n");
+	winclean(reply->w);
+	winselect(reply->w, "$", 0);
+}
+
+char*
+skipbl(char *s, char *e)
+{
+	while(s < e){
+		if(*s!=' ' && *s!='\t' && *s!=',')
+			break;
+		s++;
+	}
+	return s;
+}
+
+char*
+findbl(char *s, char *e)
+{
+	while(s < e){
+		if(*s==' ' || *s=='\t' || *s==',')
+			break;
+		s++;
+	}
+	return s;
+}
+
+/*
+ * comma-separate possibly blank-separated strings in line; e points before newline
+ */
+void
+commas(char *s, char *e)
+{
+	char *t;
+
+	/* may have initial blanks */
+	s = skipbl(s, e);
+	while(s < e){
+		s = findbl(s, e);
+		if(s == e)
+			break;
+		t = skipbl(s, e);
+		if(t == e)	/* no more words */
+			break;
+		/* patch comma */
+		*s++ = ',';
+		while(s < t)
+			*s++ = ' ';
+	}
+}
+void
+mesgpost(Article *m)
+{
+	Biobuf *b;
+	char *p, *ep;
+	int isfirst, ishdr, havegroup, havefrom;
+
+	p = estrstrdup(dir, "post");
+	if((b = Bopen(p, OWRITE)) == nil){
+		fprint(2, "cannot open %s: %r\n", p);
+		free(p);
+		return;
+	}
+	free(p);
+
+	winopenbody(m->w, OREAD);
+	ishdr = 1;
+	isfirst = 1;
+	havegroup = havefrom = 0;
+	while(p = Brdline(m->w->body, '\n')){
+		ep = p+Blinelen(m->w->body);
+		if(ishdr && p+1==ep){
+			if(!havegroup)
+				Bprint(b, "Newsgroups: %s\n", group);
+			if(!havefrom)
+				Bprint(b, "From: %s\n", from);
+			ishdr = 0;
+		}
+		if(ishdr){
+			ep[-1] = '\0';
+			if(isfirst && strchr(p, ':')==0){	/* group list */
+				commas(p, ep);
+				Bprint(b, "newsgroups: %s\n", p);
+				havegroup = 1;
+				isfirst = 0;
+				continue;
+			}
+			if(cistrncmp(p, "newsgroup:", 10)==0){
+				commas(skip(p, "newsgroup:"), ep);
+				Bprint(b, "newsgroups: %s\n", skip(p, "newsgroup:"));
+				havegroup = 1;
+				continue;
+			}
+			if(cistrncmp(p, "newsgroups:", 11)==0){
+				commas(skip(p, "newsgroups:"), ep);
+				Bprint(b, "newsgroups: %s\n", skip(p, "newsgroups:"));
+				havegroup = 1;
+				continue;
+			}
+			if(cistrncmp(p, "from:", 5)==0)
+				havefrom = 1;
+			ep[-1] = '\n';
+		}
+		Bwrite(b, p, ep-p);
+	}
+	winclosebody(m->w);
+	Bflush(b);
+	if(write(Bfildes(b), "", 0) == 0)
+		winclean(m->w);
+	else
+		fprint(2, "post: %r\n");
+	Bterm(b);
+}
+
+int
+mesgopen(char *s)
+{
+	char *p, tmp[40];
+	int fd, n;
+	Article *m;
+
+	n = atoi(s);
+	if(n==0)
+		return 0;
+
+	for(m=mlist; m; m=m->next){
+		if(m->n == n){
+			ctlprint(m->w->ctl, "show\n");
+			return 1;
+		}
+	}
+
+	sprint(tmp, "%d/article", n);
+	p = estrstrdup(dir, tmp);
+	if((fd = open(p, OREAD)) < 0){
+		free(p);	
+		return 0;
+	}
+
+	m = emalloc(sizeof(*m));
+	m->w = newwindow();
+	m->n = n;
+	proccreate(wineventproc, m->w, STACK);
+	p[strlen(p)-strlen("article")] = '\0';
+	winname(m->w, p);
+	if(canpost)
+		wintagwrite(m->w, "Reply ", 6);
+	wintagwrite(m->w, "Headers ", 8);
+
+	free(p);
+	if(mlist){
+		m->next = mlist;
+		mlist->prev = m;
+	}
+	mlist = m;
+	threadcreate(mesgthread, m, STACK);
+
+	fillmesgwindow(fd, m);
+	close(fd);
+	windormant(m->w);
+	return 1;
+}
+
+void
+usage(void)
+{
+	fprint(2, "usage: News [-d /mnt/news] comp.os.plan9\n");
+	exits("usage");
+}
+
+void
+timerproc(void *v)
+{
+	Event e;
+	Window *w;
+
+	memset(&e, 0, sizeof e);
+	e.c1 = 'T';
+	w = v;
+
+	for(;;){
+		sleep(60*1000);
+		sendp(w->cevent, &e);
+	}
+}
+
+char*
+findfrom(void)
+{
+	char *p, *u;
+	Biobuf *b;
+
+	u = getuser();
+	if(u==nil)
+		return "glenda";
+
+	p = estrstrstrdup("/usr/", u, "/lib/newsfrom");
+	b = Bopen(p, OREAD);
+	free(p);
+	if(b){
+		p = Brdline(b, '\n');
+		if(p){
+			p[Blinelen(b)-1] = '\0';
+			p = estrdup(p);
+			Bterm(b);
+			return p;
+		}
+		Bterm(b);
+	}
+
+	p = estrstrstrdup("/mail/box/", u, "/headers");
+	b = Bopen(p, OREAD);
+	free(p);
+	if(b){
+		while(p = Brdline(b, '\n')){
+			p[Blinelen(b)-1] = '\0';
+			if(cistrncmp(p, "from:", 5)==0){
+				p = estrdup(skip(p, "from:"));
+				Bterm(b);
+				return p;
+			}
+		}
+		Bterm(b);
+	}
+
+	return u;
+}
+
+void
+threadmain(int argc, char **argv)
+{
+	char *p, *q;
+	Dir *d;
+	Window *w;
+
+	ARGBEGIN{
+	case 'D':
+		debug++;
+		break;
+	case 'd':
+		dir = EARGF(usage());
+		break;
+	default:
+		usage();
+		break;
+	}ARGEND
+
+	if(argc != 1)
+		usage();
+
+	from = findfrom();
+
+	group = estrdup(argv[0]);	/* someone will be cute */
+	while(q=strchr(group, '/'))
+		*q = '.';
+
+	p = estrdup(argv[0]);
+	while(q=strchr(p, '.'))
+		*q = '/';
+	p = estrstrstrdup(dir, "/", p);
+	cleanname(p);
+
+	if((d = dirstat(p)) == nil){	/* maybe it is a new group */
+		if((d = dirstat(dir)) == nil){
+			fprint(2, "dirstat(%s) fails: %r\n", dir);
+			threadexitsall(nil);
+		}
+		if((d->mode&DMDIR)==0){
+			fprint(2, "%s not a directory\n", dir);
+			threadexitsall(nil);
+		}
+		free(d);
+		if((d = dirstat(p)) == nil){
+			fprint(2, "stat %s: %r\n", p);
+			threadexitsall(nil);
+		}
+	}
+	if((d->mode&DMDIR)==0){
+		fprint(2, "%s not a directory\n", dir);
+		threadexitsall(nil);
+	}
+	free(d);
+	dir = estrstrdup(p, "/");
+
+	q = estrstrdup(dir, "post");
+	canpost = access(q, AWRITE)==0;
+
+	w = newwindow();
+	root = w;
+	proccreate(wineventproc, w, STACK);
+	proccreate(timerproc, w, STACK);
+
+	winname(w, dir);
+	if(canpost)
+		wintagwrite(w, "Newpost ", 8);
+	wintagwrite(w, "More ", 5);
+	dirwindow(w);
+	threadcreate(dirthread, w, STACK);
+	threadexits(nil);
+}
--- /dev/null
+++ b/acme/news/src/util.c
@@ -1,0 +1,106 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include "win.h"
+
+void*
+emalloc(uint n)
+{
+	void *p;
+
+	p = malloc(n);
+	if(p == nil)
+		error("can't malloc: %r");
+	memset(p, 0, n);
+	return p;
+}
+
+char*
+estrdup(char *s)
+{
+	char *t;
+
+	t = emalloc(strlen(s)+1);
+	strcpy(t, s);
+	return t;
+}
+
+char*
+estrstrdup(char *s, char *t)
+{
+	char *u;
+
+	u = emalloc(strlen(s)+strlen(t)+1);
+	strcpy(u, s);
+	strcat(u, t);
+	return u;
+}
+
+char*
+estrstrstrdup(char *r, char *s, char *t)
+{
+	char *u;
+
+	u = emalloc(strlen(r)+strlen(s)+strlen(t)+1);
+	strcpy(u, r);
+	strcat(u, s);
+	strcat(u, t);
+	return u;
+}
+
+char*
+eappend(char *s, char *sep, char *t)
+{
+	char *u;
+
+	if(t == nil)
+		u = estrstrdup(s, sep);
+	else{
+		u = emalloc(strlen(s)+strlen(sep)+strlen(t)+1);
+		strcpy(u, s);
+		strcat(u, sep);
+		strcat(u, t);
+	}
+	free(s);
+	return u;
+}
+
+char*
+egrow(char *s, char *sep, char *t)
+{
+	s = eappend(s, sep, t);
+	free(t);
+	return s;
+}
+
+void
+error(char *fmt, ...)
+{
+	va_list arg;
+	char buf[256];
+	Fmt f;
+
+	fmtfdinit(&f, 2, buf, sizeof buf);
+	fmtprint(&f, "%s: ", argv0);
+	va_start(arg, fmt);
+	fmtprint(&f, fmt, arg);
+	va_end(arg);
+	fmtprint(&f, "\n");
+	fmtfdflush(&f);
+	threadexitsall(fmt);
+}
+
+void
+ctlprint(int fd, char *fmt, ...)
+{
+	int n;
+	va_list arg;
+	char buf[256];
+
+	va_start(arg, fmt);
+	n = vfprint(fd, fmt, arg);
+	va_end(arg);
+	if(n < 0)
+		error("control file write(%s) error: %r", buf);
+}
--- /dev/null
+++ b/acme/news/src/win.c
@@ -1,0 +1,324 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include "win.h"
+
+Window*
+newwindow(void)
+{
+	char buf[12];
+	Window *w;
+
+	w = emalloc(sizeof(Window));
+	w->ctl = open("/mnt/wsys/new/ctl", ORDWR|OCEXEC);
+	if(w->ctl<0 || read(w->ctl, buf, 12)!=12)
+		error("can't open window ctl file: %r");
+	ctlprint(w->ctl, "noscroll\n");
+	w->id = atoi(buf);
+	w->event = winopenfile(w, "event");
+	w->addr = -1;	/* will be opened when needed */
+	w->body = nil;
+	w->data = -1;
+	w->cevent = chancreate(sizeof(Event*), 0);
+	if(w->cevent == nil)
+		error("cevent is nil: %r");
+	return w;
+}
+
+void
+winsetdump(Window *w, char *dir, char *cmd)
+{
+	if(dir != nil)
+		ctlprint(w->ctl, "dumpdir %s\n", dir);
+	if(cmd != nil)
+		ctlprint(w->ctl, "dump %s\n", cmd);
+}
+
+void
+wineventproc(void *v)
+{
+	Window *w;
+	int i;
+
+	threadsetname("wineventproc");
+	w = v;
+	for(i=0; ; i++){
+		if(i >= NEVENT)
+			i = 0;
+		wingetevent(w, &w->e[i]);
+		sendp(w->cevent, &w->e[i]);
+	}
+}
+
+int
+winopenfile(Window *w, char *f)
+{
+	char buf[64];
+	int fd;
+
+	sprint(buf, "/mnt/wsys/%d/%s", w->id, f);
+	fd = open(buf, ORDWR|OCEXEC);
+	if(fd < 0)
+		error("can't open window file %s: %r", f);
+	return fd;
+}
+
+void
+wintagwrite(Window *w, char *s, int n)
+{
+	int fd;
+
+	fd = winopenfile(w, "tag");
+	if(write(fd, s, n) != n)
+		error("tag write: %r");
+	close(fd);
+}
+
+void
+winname(Window *w, char *s)
+{
+	ctlprint(w->ctl, "name %s\n", s);
+}
+
+void
+winopenbody(Window *w, int mode)
+{
+	char buf[256];
+
+	sprint(buf, "/mnt/wsys/%d/body", w->id);
+	w->body = Bopen(buf, mode|OCEXEC);
+	if(w->body == nil)
+		error("can't open window body file: %r");
+}
+
+void
+winclosebody(Window *w)
+{
+	if(w->body != nil){
+		Bterm(w->body);
+		w->body = nil;
+	}
+}
+
+void
+winwritebody(Window *w, char *s, int n)
+{
+	if(w->body == nil)
+		winopenbody(w, OWRITE);
+	if(Bwrite(w->body, s, n) != n)
+		error("write error to window: %r");
+}
+
+int
+wingetec(Window *w)
+{
+	if(w->nbuf == 0){
+		w->nbuf = read(w->event, w->buf, sizeof w->buf);
+		if(w->nbuf <= 0){
+			/* probably because window has exited, and only called by wineventproc, so just shut down */
+			threadexits(nil);
+		}
+		w->bufp = w->buf;
+	}
+	w->nbuf--;
+	return *w->bufp++;
+}
+
+int
+wingeten(Window *w)
+{
+	int n, c;
+
+	n = 0;
+	while('0'<=(c=wingetec(w)) && c<='9')
+		n = n*10+(c-'0');
+	if(c != ' ')
+		error("event number syntax");
+	return n;
+}
+
+int
+wingeter(Window *w, char *buf, int *nb)
+{
+	Rune r;
+	int n;
+
+	r = wingetec(w);
+	buf[0] = r;
+	n = 1;
+	if(r >= Runeself) {
+		while(!fullrune(buf, n))
+			buf[n++] = wingetec(w);
+		chartorune(&r, buf);
+	} 
+	*nb = n;
+	return r;
+}
+
+void
+wingetevent(Window *w, Event *e)
+{
+	int i, nb;
+
+	e->c1 = wingetec(w);
+	e->c2 = wingetec(w);
+	e->q0 = wingeten(w);
+	e->q1 = wingeten(w);
+	e->flag = wingeten(w);
+	e->nr = wingeten(w);
+	if(e->nr > EVENTSIZE)
+		error("event string too long");
+	e->nb = 0;
+	for(i=0; i<e->nr; i++){
+		e->r[i] = wingeter(w, e->b+e->nb, &nb);
+		e->nb += nb;
+	}
+	e->r[e->nr] = 0;
+	e->b[e->nb] = 0;
+	if(wingetec(w) != '\n')
+		error("event syntax error");
+}
+
+void
+winwriteevent(Window *w, Event *e)
+{
+	fprint(w->event, "%c%c%d %d\n", e->c1, e->c2, e->q0, e->q1);
+}
+
+static int
+nrunes(char *s, int nb)
+{
+	int i, n;
+	Rune r;
+
+	n = 0;
+	for(i=0; i<nb; n++)
+		i += chartorune(&r, s+i);
+	return n;
+}
+
+void
+winread(Window *w, uint q0, uint q1, char *data)
+{
+	int m, n, nr;
+	char buf[256];
+
+	if(w->addr < 0)
+		w->addr = winopenfile(w, "addr");
+	if(w->data < 0)
+		w->data = winopenfile(w, "data");
+	m = q0;
+	while(m < q1){
+		n = sprint(buf, "#%d", m);
+		if(write(w->addr, buf, n) != n)
+			error("error writing addr: %r");
+		n = read(w->data, buf, sizeof buf);
+		if(n <= 0)
+			error("reading data: %r");
+		nr = nrunes(buf, n);
+		while(m+nr >q1){
+			do; while(n>0 && (buf[--n]&0xC0)==0x80);
+			--nr;
+		}
+		if(n == 0)
+			break;
+		memmove(data, buf, n);
+		data += n;
+		*data = 0;
+		m += nr;
+	}
+}
+
+void
+windormant(Window *w)
+{
+	if(w->addr >= 0){
+		close(w->addr);
+		w->addr = -1;
+	}
+	if(w->body != nil){
+		Bterm(w->body);
+		w->body = nil;
+	}
+	if(w->data >= 0){
+		close(w->data);
+		w->data = -1;
+	}
+}
+
+
+int
+windel(Window *w, int sure)
+{
+	if(sure)
+		write(w->ctl, "delete\n", 7);
+	else if(write(w->ctl, "del\n", 4) != 4)
+		return 0;
+	/* event proc will die due to read error from event file */
+	windormant(w);
+	close(w->ctl);
+	w->ctl = -1;
+	close(w->event);
+	w->event = -1;
+	return 1;
+}
+
+void
+winclean(Window *w)
+{
+	if(w->body)
+		Bflush(w->body);
+	ctlprint(w->ctl, "clean\n");
+}
+
+int
+winsetaddr(Window *w, char *addr, int errok)
+{
+	if(w->addr < 0)
+		w->addr = winopenfile(w, "addr");
+	if(write(w->addr, addr, strlen(addr)) < 0){
+		if(!errok)
+			error("error writing addr(%s): %r", addr);
+		return 0;
+	}
+	return 1;
+}
+
+int
+winselect(Window *w, char *addr, int errok)
+{
+	if(winsetaddr(w, addr, errok)){
+		ctlprint(w->ctl, "dot=addr\n");
+		return 1;
+	}
+	return 0;
+}
+
+char*
+winreadbody(Window *w, int *np)	/* can't use readfile because acme doesn't report the length */
+{
+	char *s;
+	int m, na, n;
+
+	if(w->body != nil)
+		winclosebody(w);
+	winopenbody(w, OREAD);
+	s = nil;
+	na = 0;
+	n = 0;
+	for(;;){
+		if(na < n+512){
+			na += 1024;
+			s = realloc(s, na+1);
+		}
+		m = Bread(w->body, s+n, na-n);
+		if(m <= 0)
+			break;
+		n += m;
+	}
+	s[n] = 0;
+	winclosebody(w);
+	*np = n;
+	return s;
+}
--- /dev/null
+++ b/acme/news/src/win.h
@@ -1,0 +1,75 @@
+/* acme */
+typedef struct Event Event;
+typedef struct Window Window;
+
+enum
+{
+	STACK		= 8192,
+	EVENTSIZE	= 256,
+	NEVENT		= 5,
+};
+
+struct Event
+{
+	int	c1;
+	int	c2;
+	int	q0;
+	int	q1;
+	int	flag;
+	int	nb;
+	int	nr;
+	char	b[EVENTSIZE*UTFmax+1];
+	Rune	r[EVENTSIZE+1];
+};
+
+struct Window
+{
+	/* file descriptors */
+	int		ctl;
+	int		event;
+	int		addr;
+	int		data;
+	Biobuf	*body;
+
+	/* event input */
+	char		buf[512];
+	char		*bufp;
+	int		nbuf;
+	Event	e[NEVENT];
+
+	int		dirtied;
+	int		id;
+	int		open;
+	Channel	*cevent;	/* chan(Event*) */
+};
+
+extern	Window*	newwindow(void);
+extern	int		winopenfile(Window*, char*);
+extern	void		winopenbody(Window*, int);
+extern	void		winclosebody(Window*);
+extern	void		wintagwrite(Window*, char*, int);
+extern	void		winname(Window*, char*);
+extern	void		winwriteevent(Window*, Event*);
+extern	void		winread(Window*, uint, uint, char*);
+extern	int		windel(Window*, int);
+extern	void		wingetevent(Window*, Event*);
+extern	void		wineventproc(void*);
+extern	void		winwritebody(Window*, char*, int);
+extern	void		winclean(Window*);
+extern	int		winselect(Window*, char*, int);
+extern	int		winsetaddr(Window*, char*, int);
+extern	char*	winreadbody(Window*, int*);
+extern	void		windormant(Window*);
+extern	void		winsetdump(Window*, char*, char*);
+
+extern	char*	readfile(char*, char*, int*);
+extern	void		ctlprint(int, char*, ...);
+extern	void*	emalloc(uint);
+extern	char*	estrdup(char*);
+extern	char*	estrstrdup(char*, char*);
+extern	char*	estrstrstrdup(char*, char*, char*);
+extern	char*	egrow(char*, char*, char*);
+extern	char*	eappend(char*, char*, char*);
+extern	void		error(char*, ...);
+extern	int		tokenizec(char*, char**, int, char*);
+
--- /dev/null
+++ b/acme/wiki/guide
@@ -1,0 +1,3 @@
+Local 9fs wiki
+# Local wikifs /sys/lib/wiki
+Wiki /mnt/wiki
--- /dev/null
+++ b/acme/wiki/src/awiki.h
@@ -1,0 +1,114 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+
+/* acme */
+typedef struct Event Event;
+typedef struct Window Window;
+
+enum
+{
+	STACK		= 8192,
+	EVENTSIZE	= 256,
+	NEVENT		= 5,
+};
+
+struct Event
+{
+	int	c1;
+	int	c2;
+	int	q0;
+	int	q1;
+	int	flag;
+	int	nb;
+	int	nr;
+	char	b[EVENTSIZE*UTFmax+1];
+	Rune	r[EVENTSIZE+1];
+};
+
+struct Window
+{
+	/* file descriptors */
+	int		ctl;
+	int		event;
+	int		addr;
+	int		data;
+	Biobuf	*body;
+
+	/* event input */
+	char		buf[512];
+	char		*bufp;
+	int		nbuf;
+	Event	e[NEVENT];
+
+	int		warned;
+	int		id;
+	int		open;
+	Channel	*cevent;	/* chan(Event*) */
+};
+
+extern	Window*	newwindow(void);
+extern	int		winopenfile(Window*, char*);
+extern	void		winopenbody(Window*, int);
+extern	void		winclosebody(Window*);
+extern	void		wintagwrite(Window*, char*, int);
+extern	void		winname(Window*, char*);
+extern	void		winwriteevent(Window*, Event*);
+extern	void		winread(Window*, uint, uint, char*);
+extern	int		windel(Window*, int);
+extern	void		wingetevent(Window*, Event*);
+extern	void		wineventproc(void*);
+extern	void		winwritebody(Window*, char*, int);
+extern	void		winclean(Window*);
+extern	int		winisdirty(Window*);
+extern	int		winselect(Window*, char*, int);
+extern	int		winsetaddr(Window*, char*, int);
+extern	char*	winreadbody(Window*, int*);
+extern	void		windormant(Window*);
+extern	void		winsetdump(Window*, char*, char*);
+
+extern	char*	readfile(char*, char*, int*);
+extern	void		ctlprint(int, char*, ...);
+extern	void*	emalloc(uint);
+extern	char*	estrdup(char*);
+extern	char*	estrstrdup(char*, char*);
+extern	char*	egrow(char*, char*, char*);
+extern	char*	eappend(char*, char*, char*);
+extern	void		error(char*, ...);
+extern	int		tokenizec(char*, char**, int, char*);
+
+typedef struct Treq Treq;
+typedef struct Wiki Wiki;
+
+struct Treq {
+	char *title;
+	Channel *c;	/* chan(int) */
+};
+
+struct Wiki {
+	QLock;
+	int isnew;
+	int special;
+	char *arg;
+	char *addr;
+	int n;
+	int dead;
+	Window *win;
+	ulong time;
+	int linked;
+	Wiki *next;
+	Wiki *prev;
+};
+
+extern int debug;
+extern int mapfd;
+extern char *email;
+extern char *dir;
+
+void wikinew(char*);
+int wikiopen(char*, char*);
+int wikiput(Wiki*);
+void wikiget(Wiki*);
+int wikidiff(Wiki*);
+
--- /dev/null
+++ b/acme/wiki/src/main.c
@@ -1,0 +1,60 @@
+#include "awiki.h"
+
+int debug;
+int mapfd;
+char *email;
+char *dir;
+
+void
+usage(void)
+{
+	fprint(2, "usage: Wiki [-e email] [dir]\n");
+	exits("usage");
+}
+
+void
+threadmain(int argc, char **argv)
+{
+	char *s;
+	Dir *d;
+
+	rfork(RFNAMEG);
+	ARGBEGIN{
+	case 'D':
+		debug++;
+		break;
+	case 'e':
+		email = EARGF(usage());
+		break;
+	default:
+		usage();
+		break;
+	}ARGEND
+
+	if(argc > 1)
+		usage();
+	if(argc == 1)
+		dir = argv[0];
+	else
+		dir = "/mnt/wiki";
+
+	if(chdir(dir) < 0){
+		fprint(2, "chdir(%s) fails: %r\n", dir);
+		threadexitsall(nil);
+	}
+
+	if((mapfd = open("map", ORDWR)) < 0){
+		fprint(2, "open(map): %r\n");
+		threadexitsall(nil);
+	}
+
+	if((d = dirstat("1")) == nil){
+		fprint(2, "dirstat(%s/1) fails: %r\n", dir);
+		threadexitsall(nil);
+	}
+	s = emalloc(strlen(d->name)+2);
+	strcpy(s, d->name);
+	strcat(s, "/");
+	wikiopen(s, nil);
+	threadexits(nil);
+}
--- /dev/null
+++ b/acme/wiki/src/mkfile
@@ -1,0 +1,14 @@
+</$objtype/mkfile
+
+TARG=Wiki
+
+OFILES=\
+	main.$O\
+	util.$O\
+	wiki.$O\
+	win.$O\
+
+HFILES=awiki.h
+BIN=../../bin/$objtype
+
+</sys/src/cmd/mkone
--- /dev/null
+++ b/acme/wiki/src/util.c
@@ -1,0 +1,89 @@
+#include "awiki.h"
+
+void*
+emalloc(uint n)
+{
+	void *p;
+
+	p = malloc(n);
+	if(p == nil)
+		error("can't malloc: %r");
+	memset(p, 0, n);
+	return p;
+}
+
+char*
+estrdup(char *s)
+{
+	char *t;
+
+	t = emalloc(strlen(s)+1);
+	strcpy(t, s);
+	return t;
+}
+
+char*
+estrstrdup(char *s, char *t)
+{
+	char *u;
+
+	u = emalloc(strlen(s)+strlen(t)+1);
+	strcpy(u, s);
+	strcat(u, t);
+	return u;
+}
+
+char*
+eappend(char *s, char *sep, char *t)
+{
+	char *u;
+
+	if(t == nil)
+		u = estrstrdup(s, sep);
+	else{
+		u = emalloc(strlen(s)+strlen(sep)+strlen(t)+1);
+		strcpy(u, s);
+		strcat(u, sep);
+		strcat(u, t);
+	}
+	free(s);
+	return u;
+}
+
+char*
+egrow(char *s, char *sep, char *t)
+{
+	s = eappend(s, sep, t);
+	free(t);
+	return s;
+}
+
+void
+error(char *fmt, ...)
+{
+	int n;
+	va_list arg;
+	char buf[256];
+
+	fprint(2, "Wiki: ");
+	va_start(arg, fmt);
+	n = vseprint(buf, buf+sizeof buf, fmt, arg) - buf;
+	va_end(arg);
+	write(2, buf, n);
+	write(2, "\n", 1);
+	threadexitsall(fmt);
+}
+
+void
+ctlprint(int fd, char *fmt, ...)
+{
+	int n;
+	va_list arg;
+	char buf[256];
+
+	va_start(arg, fmt);
+	n = vseprint(buf, buf+sizeof buf, fmt, arg) - buf;
+	va_end(arg);
+	if(write(fd, buf, n) != n)
+		error("control file write(%s) error: %r", buf);
+}
--- /dev/null
+++ b/acme/wiki/src/wiki.c
@@ -1,0 +1,602 @@
+#include "awiki.h"
+
+Wiki *wlist;
+
+void
+link(Wiki *w)
+{
+	if(w->linked)
+		return;
+	w->linked = 1;
+	w->prev = nil;
+	w->next = wlist;
+	if(wlist)
+		wlist->prev = w;
+	wlist = w;
+}
+
+void
+unlink(Wiki *w)
+{
+	if(!w->linked)
+		return;
+	w->linked = 0;
+
+	if(w->next)
+		w->next->prev = w->prev;
+	if(w->prev)
+		w->prev->next = w->next;
+	else
+		wlist = w->next;
+
+	w->next = nil;
+	w->prev = nil;
+}
+
+void
+wikiname(Window *w, char *name)
+{
+	char *p, *q;
+
+	p = emalloc(strlen(dir)+1+strlen(name)+1+1);
+	strcpy(p, dir);
+	strcat(p, "/");
+	strcat(p, name);
+	for(q=p; *q; q++)
+		if(*q==' ')
+			*q = '_';
+	winname(w, p);
+	free(p);
+}
+
+int
+wikiput(Wiki *w)
+{
+	int fd, n;
+	char buf[1024], *p;
+	Biobuf *b;
+
+	if((fd = open("new", ORDWR)) < 0){
+		fprint(2, "Wiki: cannot open raw: %r\n");
+		return -1;
+	}
+
+	winopenbody(w->win, OREAD);
+	b = w->win->body;
+	if((p = Brdline(b, '\n'))==nil){
+	Short:
+		winclosebody(w->win);
+		fprint(2, "Wiki: no data\n");
+		close(fd);
+		return -1;
+	}
+	write(fd, p, Blinelen(b));
+
+	snprint(buf, sizeof buf, "D%lud\n", w->time);
+	if(email)
+		snprint(buf+strlen(buf), sizeof(buf)-strlen(buf), "A%s\n", email);
+
+	if(Bgetc(b) == '#'){
+		p = Brdline(b, '\n');
+		if(p == nil)
+			goto Short;
+		snprint(buf+strlen(buf), sizeof(buf)-strlen(buf), "C%s\n", p);
+	}
+	write(fd, buf, strlen(buf));
+	write(fd, "\n\n", 2);
+
+	while((n = Bread(b, buf, sizeof buf)) > 0)
+		write(fd, buf, n);
+	winclosebody(w->win);
+
+	werrstr("");
+	if((n=write(fd, "", 0)) != 0){
+		fprint(2, "Wiki commit %lud %d %d: %r\n", w->time, fd, n);
+		close(fd);
+		return -1;
+	}
+	seek(fd, 0, 0);
+	if((n = read(fd, buf, 300)) < 0){
+		fprint(2, "Wiki readback: %r\n");
+		close(fd);
+		return -1;
+	}
+	close(fd);
+	buf[n] = '\0';
+	sprint(buf, "%s/", buf);
+	free(w->arg);
+	w->arg = estrdup(buf);
+	w->isnew = 0;
+	wikiget(w);
+	wikiname(w->win, w->arg);
+	return n;
+}
+
+void
+wikiget(Wiki *w)
+{
+	char *p;
+	int fd, normal;
+	Biobuf *bin;
+
+	fprint(w->win->ctl, "dirty\n");
+
+	p = emalloc(strlen(w->arg)+8+1);
+	strcpy(p, w->arg);
+	normal = 1;
+	if(p[strlen(p)-1] == '/'){
+		normal = 0;
+		strcat(p, "current");
+	}else if(strlen(p)>8 && strcmp(p+strlen(p)-8, "/current")==0){
+		normal = 0;
+		w->arg[strlen(w->arg)-7] = '\0';
+	}
+
+	if((fd = open(p, OREAD)) < 0){
+		fprint(2, "Wiki: cannot read %s: %r\n", p);
+		winclean(w->win);
+		return;
+	}
+	free(p);
+
+	winopenbody(w->win, OWRITE);
+	bin = emalloc(sizeof(*bin));
+	Binit(bin, fd, OREAD);
+
+	p = nil;
+	if(!normal){
+		if((p = Brdline(bin, '\n')) == nil){
+			fprint(2, "Wiki: cannot read title: %r\n");
+			winclean(w->win);
+			close(fd);
+			free(bin);
+			return;
+		}
+		p[Blinelen(bin)-1] = '\0';
+	}
+	/* clear window */
+	if(w->win->data < 0)
+		w->win->data = winopenfile(w->win, "data");
+	if(winsetaddr(w->win, ",", 0))
+		write(w->win->data, "", 0);
+
+	if(!normal)
+		Bprint(w->win->body, "%s\n\n", p);
+
+	while(p = Brdline(bin, '\n')){
+		p[Blinelen(bin)-1] = '\0';
+		if(normal)
+			Bprint(w->win->body, "%s\n", p);
+		else{
+			if(p[0]=='D')
+				w->time = strtoul(p+1, 0, 10);
+			else if(p[0]=='#')
+				Bprint(w->win->body, "%s\n", p+1);
+		}
+	}
+	winclean(w->win);
+	free(bin);
+	close(fd);
+}
+
+static int
+iscmd(char *s, char *cmd)
+{
+	int len;
+
+	len = strlen(cmd);
+	return strncmp(s, cmd, len)==0 && (s[len]=='\0' || s[len]==' ' || s[len]=='\t' || s[len]=='\n');
+}
+
+static char*
+skip(char *s, char *cmd)
+{
+	s += strlen(cmd);
+	while(*s==' ' || *s=='\t' || *s=='\n')
+		s++;
+	return s;
+}
+
+int
+wikiload(Wiki *w, char *arg)
+{
+	char *p, *q, *path, *addr;
+	int rv;
+
+	p = nil;
+	if(arg[0] == '/')
+		path = arg;
+	else{
+		p = emalloc(strlen(w->arg)+1+strlen(arg)+1);
+		strcpy(p, w->arg);
+		if(q = strrchr(p, '/')){
+			++q;
+			*q = '\0';
+		}else
+			*p = '\0';
+		strcat(p, arg);
+		cleanname(p);
+		path = p;
+	}
+	if(addr=strchr(path, ':'))
+		*addr++ = '\0';
+
+	rv = wikiopen(path, addr)==0;
+	free(p);
+	if(rv)
+		return 1;
+	return wikiopen(arg, 0)==0;
+}
+
+/* return 1 if handled, 0 otherwise */
+int
+wikicmd(Wiki *w, char *s)
+{
+	char *p;
+	s = skip(s, "");
+
+	if(iscmd(s, "Del")){
+		if(windel(w->win, 0))
+			w->dead = 1;
+		return 1;
+	}
+	if(iscmd(s, "New")){
+		wikinew(skip(s, "New"));
+		return 1;
+	}
+	if(iscmd(s, "History"))
+		return wikiload(w, "history.txt");
+	if(iscmd(s, "Diff"))
+		return wikidiff(w);
+	if(iscmd(s, "Get")){
+		if(winisdirty(w->win) && !w->win->warned){
+			w->win->warned = 1;
+			fprint(2, "%s/%s modified\n", dir, w->arg);
+		}else{
+			w->win->warned = 0;
+			wikiget(w);
+		}
+		return 1;
+	}
+	if(iscmd(s, "Put")){
+		if((p=strchr(w->arg, '/')) && p[1]!='\0')
+			fprint(2, "%s/%s is read-only\n", dir, w->arg);
+		else
+			wikiput(w);
+		return 1;
+	}
+	return 0;
+}
+
+/* need to expand selection more than default word */
+static long
+eval(Window *w, char *s, ...)
+{
+	char buf[64];
+	va_list arg;
+
+	va_start(arg, s);
+	vsnprint(buf, sizeof buf, s, arg);
+	va_end(arg);
+
+	if(winsetaddr(w, buf, 1)==0)
+		return -1;
+
+	if(pread(w->addr, buf, 24, 0) != 24)
+		return -1;
+	return strtol(buf, 0, 10);
+}
+
+static int
+getdot(Window *w, long *q0, long *q1)
+{
+	char buf[24];
+
+	ctlprint(w->ctl, "addr=dot\n");
+	if(pread(w->addr, buf, 24, 0) != 24)
+		return -1;
+	*q0 = atoi(buf);
+	*q1 = atoi(buf+12);
+	return 0;
+}
+
+static Event*
+expand(Window *w, Event *e, Event *eacme)
+{
+	long q0, q1, x;
+
+	if(getdot(w, &q0, &q1)==0 && q0 <= e->q0 && e->q0 <= q1){
+		e->q0 = q0;
+		e->q1 = q1;
+		return e;
+	}
+
+	q0 = eval(w, "#%lud-/\\[/", e->q0);
+	if(q0 < 0)
+		return eacme;
+	if(eval(w, "#%lud+/\\]/", q0) < e->q0)	/* [ closes before us */
+		return eacme;
+	q1 = eval(w, "#%lud+/\\]/", e->q1);
+	if(q1 < 0)
+		return eacme;
+	if((x=eval(w, "#%lud-/\\[/", q1))==-1 || x > e->q1)	/* ] opens after us */
+		return eacme;
+	e->q0 = q0+1;
+	e->q1 = q1;
+	return e;
+}
+
+void
+acmeevent(Wiki *wiki, Event *e)
+{
+	Event *ea, *e2, *eq;
+	Window *w;
+	char *s, *t, *buf;
+	int na;
+
+	w = wiki->win;
+	switch(e->c1){	/* origin of action */
+	default:
+	Unknown:
+		fprint(2, "unknown message %c%c\n", e->c1, e->c2);
+		break;
+
+	case 'F':	/* generated by our actions; ignore */
+		break;
+
+	case 'E':	/* write to body or tag; can't affect us */
+		break;
+
+	case 'K':	/* type away; we don't care */
+		if(e->c2 == 'I' || e->c2 == 'D')
+			w->warned = 0;
+		break;
+
+	case 'M':	/* mouse event */
+		switch(e->c2){		/* type of action */
+		case 'x':	/* mouse: button 2 in tag */
+		case 'X':	/* mouse: button 2 in body */
+			ea = nil;
+			//e2 = nil;
+			s = e->b;
+			if(e->flag & 2){	/* null string with non-null expansion */
+				e2 = recvp(w->cevent);
+				if(e->nb==0)
+					s = e2->b;
+			}
+			if(e->flag & 8){	/* chorded argument */
+				ea = recvp(w->cevent);	/* argument */
+				na = ea->nb;
+				recvp(w->cevent);		/* ignore origin */
+			}else
+				na = 0;
+			
+			/* append chorded arguments */
+			if(na){
+				t = emalloc(strlen(s)+1+na+1);
+				sprint(t, "%s %s", s, ea->b);
+				s = t;
+			}
+			/* if it's a known command, do it */
+			/* if it's a long message, it can't be for us anyway */
+		//	DPRINT(2, "exec: %s\n", s);
+			if(!wikicmd(wiki, s))	/* send it back */
+				winwriteevent(w, e);
+			if(na)
+				free(s);
+			break;
+
+		case 'l':	/* mouse: button 3 in tag */
+		case 'L':	/* mouse: button 3 in body */
+			//buf = nil;
+			eq = e;
+			if(e->flag & 2){	/* we do our own expansion for loads */
+				e2 = recvp(w->cevent);
+				eq = expand(w, eq, e2);
+			}
+			s = eq->b;
+			if(eq->q1>eq->q0 && eq->nb==0){
+				buf = emalloc((eq->q1-eq->q0)*UTFmax+1);
+				winread(w, eq->q0, eq->q1, buf);
+				s = buf;
+			}
+			if(!wikiload(wiki, s))
+				winwriteevent(w, e);
+			break;
+
+		case 'i':	/* mouse: text inserted in tag */
+		case 'd':	/* mouse: text deleted from tag */
+			break;
+
+		case 'I':	/* mouse: text inserted in body */
+		case 'D':	/* mouse: text deleted from body */
+			w->warned = 0;
+			break;
+
+		default:
+			goto Unknown;
+		}
+	}
+}
+
+void
+wikithread(void *v)
+{
+	char tmp[40];
+	Event *e;
+	Wiki *w;
+
+	w = v;
+
+	if(w->isnew){
+		sprint(tmp, "+new+%d", w->isnew);
+		wikiname(w->win, tmp);
+		if(w->arg){
+			winopenbody(w->win, OWRITE);
+			Bprint(w->win->body, "%s\n\n", w->arg);
+		}
+		winclean(w->win);
+	}else if(!w->special){
+		wikiget(w);
+		wikiname(w->win, w->arg);
+		if(w->addr)
+			winselect(w->win, w->addr, 1);
+	}
+	fprint(w->win->ctl, "menu\n");
+	wintagwrite(w->win, "Get History Diff New", 4+8+4+4);
+	winclean(w->win);
+		
+	while(!w->dead && (e = recvp(w->win->cevent)))
+		acmeevent(w, e);
+
+	windormant(w->win);
+	unlink(w);
+	free(w->win);
+	free(w->arg);
+	free(w);
+	threadexits(nil);
+}
+
+int
+wikiopen(char *arg, char *addr)
+{
+	Dir *d;
+	char *p;
+	Wiki *w;
+
+/*
+	if(arg==nil){
+		if(write(mapfd, title, strlen(title)) < 0
+		|| seek(mapfd, 0, 0) < 0 || (n=read(mapfd, tmp, sizeof(tmp)-2)) < 0){
+			fprint(2, "Wiki: no page '%s' found: %r\n", title);
+			return -1;
+		}
+		if(tmp[n-1] == '\n')
+			tmp[--n] = '\0';
+		tmp[n++] = '/';
+		tmp[n] = '\0';
+		arg = tmp;
+	}
+*/
+
+	/* replace embedded '\n' in links by ' ' */
+	for(p=arg; *p; p++)
+		if(*p=='\n')
+			*p = ' ';
+
+	if(strncmp(arg, dir, strlen(dir))==0 && arg[strlen(dir)]=='/' && arg[strlen(dir)+1])
+		arg += strlen(dir)+1;
+	else if(arg[0] == '/')
+		return -1;
+
+	if((d = dirstat(arg)) == nil)
+		return -1;
+
+	if((d->mode&DMDIR) && arg[strlen(arg)-1] != '/'){
+		p = emalloc(strlen(arg)+2);
+		strcpy(p, arg);
+		strcat(p, "/");
+		arg = p;
+	}else if(!(d->mode&DMDIR) && arg[strlen(arg)-1]=='/'){
+		arg = estrdup(arg);
+		arg[strlen(arg)-1] = '\0';
+	}else
+		arg = estrdup(arg);
+	free(d);
+
+	/* rewrite /current into / */
+	if(strlen(arg) > 8 && strcmp(arg+strlen(arg)-8, "/current")==0)
+		arg[strlen(arg)-8+1] = '\0';
+
+	/* look for window already open */
+	for(w=wlist; w; w=w->next){
+		if(strcmp(w->arg, arg)==0){
+			ctlprint(w->win->ctl, "show\n");
+			return 0;
+		}
+	}
+
+	w = emalloc(sizeof *w);
+	w->arg = arg;
+	w->addr = addr;
+	w->win = newwindow();
+	link(w);
+
+	proccreate(wineventproc, w->win, STACK);
+	threadcreate(wikithread, w, STACK);
+	return 0;
+}
+
+void
+wikinew(char *arg)
+{
+	static int n;
+	Wiki *w;
+
+	w = emalloc(sizeof *w);
+	if(arg)
+		arg = estrdup(arg);
+	w->arg = arg;
+	w->win = newwindow();
+	w->isnew = ++n;
+	proccreate(wineventproc, w->win, STACK);
+	threadcreate(wikithread, w, STACK);
+}
+
+typedef struct Diffarg Diffarg;
+struct Diffarg {
+	Wiki *w;
+	char *dir;
+};
+
+void
+execdiff(void *v)
+{
+	char buf[64];
+	Diffarg *a;
+
+	a = v;
+
+	rfork(RFFDG);
+	close(0);
+	open("/dev/null", OREAD);
+	sprint(buf, "/mnt/wsys/%d/body", a->w->win->id);
+	close(1);
+	open(buf, OWRITE);
+	close(2);
+	open(buf, OWRITE);
+	sprint(buf, "/mnt/wsys/%d", a->w->win->id);
+	bind(buf, "/dev", MBEFORE);
+	
+	procexecl(nil, "/acme/wiki/wiki.diff", "wiki.diff", a->dir, nil);
+}
+
+int
+wikidiff(Wiki *w)
+{
+	Diffarg *d;
+	char *p, *q, *r;
+	Wiki *nw;
+
+	p = emalloc(strlen(w->arg)+10);
+	strcpy(p, w->arg);
+	if(q = strchr(p, '/'))
+		*q = '\0';
+	r = estrdup(p);
+	strcat(p, "/+Diff");
+
+	nw = emalloc(sizeof *w);
+	nw->arg = p;
+	nw->win = newwindow();
+	nw->special = 1;
+
+	d = emalloc(sizeof(*d));
+	d->w = nw;
+	d->dir = r;
+	wikiname(nw->win, p);
+	proccreate(wineventproc, nw->win, STACK);
+	proccreate(execdiff, d, STACK);
+	threadcreate(wikithread, nw, STACK);
+	return 1;
+}
+
--- /dev/null
+++ b/acme/wiki/src/win.c
@@ -1,0 +1,341 @@
+#include "awiki.h"
+
+Window*
+newwindow(void)
+{
+	char buf[12];
+	Window *w;
+
+	w = emalloc(sizeof(Window));
+	w->ctl = open("/mnt/wsys/new/ctl", ORDWR|OCEXEC);
+	if(w->ctl<0 || read(w->ctl, buf, 12)!=12)
+		error("can't open window ctl file: %r");
+	ctlprint(w->ctl, "noscroll\n");
+	w->id = atoi(buf);
+	w->event = winopenfile(w, "event");
+	w->addr = -1;	/* will be opened when needed */
+	w->body = nil;
+	w->data = -1;
+	w->cevent = chancreate(sizeof(Event*), 0);
+	if(w->cevent == nil)
+		error("cevent is nil: %r");
+	return w;
+}
+
+void
+winsetdump(Window *w, char *dir, char *cmd)
+{
+	if(dir != nil)
+		ctlprint(w->ctl, "dumpdir %s\n", dir);
+	if(cmd != nil)
+		ctlprint(w->ctl, "dump %s\n", cmd);
+}
+
+void
+wineventproc(void *v)
+{
+	Window *w;
+	int i;
+
+	threadsetname("wineventproc");
+	w = v;
+	for(i=0; ; i++){
+		if(i >= NEVENT)
+			i = 0;
+		wingetevent(w, &w->e[i]);
+		sendp(w->cevent, &w->e[i]);
+	}
+}
+
+int
+winopenfile(Window *w, char *f)
+{
+	char buf[64];
+	int fd;
+
+	sprint(buf, "/mnt/wsys/%d/%s", w->id, f);
+	fd = open(buf, ORDWR|OCEXEC);
+	if(fd < 0)
+		error("can't open window file %s: %r", f);
+	return fd;
+}
+
+void
+wintagwrite(Window *w, char *s, int n)
+{
+	int fd;
+
+	fd = winopenfile(w, "tag");
+	if(write(fd, s, n) != n)
+		error("tag write: %r");
+	close(fd);
+}
+
+void
+winname(Window *w, char *s)
+{
+	ctlprint(w->ctl, "name %s\n", s);
+}
+
+void
+winopenbody(Window *w, int mode)
+{
+	char buf[256];
+
+	sprint(buf, "/mnt/wsys/%d/body", w->id);
+	w->body = Bopen(buf, mode|OCEXEC);
+	if(w->body == nil)
+		error("can't open window body file: %r");
+}
+
+void
+winclosebody(Window *w)
+{
+	if(w->body != nil){
+		Bterm(w->body);
+		w->body = nil;
+	}
+}
+
+void
+winwritebody(Window *w, char *s, int n)
+{
+	if(w->body == nil)
+		winopenbody(w, OWRITE);
+	if(Bwrite(w->body, s, n) != n)
+		error("write error to window: %r");
+}
+
+int
+wingetec(Window *w)
+{
+	if(w->nbuf == 0){
+		w->nbuf = read(w->event, w->buf, sizeof w->buf);
+		if(w->nbuf <= 0){
+			/* probably because window has exited, and only called by wineventproc, so just shut down */
+			threadexits(nil);
+		}
+		w->bufp = w->buf;
+	}
+	w->nbuf--;
+	return *w->bufp++;
+}
+
+int
+wingeten(Window *w)
+{
+	int n, c;
+
+	n = 0;
+	while('0'<=(c=wingetec(w)) && c<='9')
+		n = n*10+(c-'0');
+	if(c != ' ')
+		error("event number syntax");
+	return n;
+}
+
+int
+wingeter(Window *w, char *buf, int *nb)
+{
+	Rune r;
+	int n;
+
+	r = wingetec(w);
+	buf[0] = r;
+	n = 1;
+	if(r >= Runeself) {
+		while(!fullrune(buf, n))
+			buf[n++] = wingetec(w);
+		chartorune(&r, buf);
+	} 
+	*nb = n;
+	return r;
+}
+
+void
+wingetevent(Window *w, Event *e)
+{
+	int i, nb;
+
+	e->c1 = wingetec(w);
+	e->c2 = wingetec(w);
+	e->q0 = wingeten(w);
+	e->q1 = wingeten(w);
+	e->flag = wingeten(w);
+	e->nr = wingeten(w);
+	if(e->nr > EVENTSIZE)
+		error("event string too long");
+	e->nb = 0;
+	for(i=0; i<e->nr; i++){
+		e->r[i] = wingeter(w, e->b+e->nb, &nb);
+		e->nb += nb;
+	}
+	e->r[e->nr] = 0;
+	e->b[e->nb] = 0;
+	if(wingetec(w) != '\n')
+		error("event syntax error");
+}
+
+void
+winwriteevent(Window *w, Event *e)
+{
+	fprint(w->event, "%c%c%d %d\n", e->c1, e->c2, e->q0, e->q1);
+}
+
+static int
+nrunes(char *s, int nb)
+{
+	int i, n;
+	Rune r;
+
+	n = 0;
+	for(i=0; i<nb; n++)
+		i += chartorune(&r, s+i);
+	return n;
+}
+
+void
+winread(Window *w, uint q0, uint q1, char *data)
+{
+	int m, n, nr;
+	char buf[256];
+
+	if(w->addr < 0)
+		w->addr = winopenfile(w, "addr");
+	if(w->data < 0)
+		w->data = winopenfile(w, "data");
+	m = q0;
+	while(m < q1){
+		n = sprint(buf, "#%d", m);
+		if(write(w->addr, buf, n) != n)
+			error("error writing addr: %r");
+		n = read(w->data, buf, sizeof buf);
+		if(n <= 0)
+			error("reading data: %r");
+		nr = nrunes(buf, n);
+		while(m+nr >q1){
+			do; while(n>0 && (buf[--n]&0xC0)==0x80);
+			--nr;
+		}
+		if(n == 0)
+			break;
+		memmove(data, buf, n);
+		data += n;
+		*data = 0;
+		m += nr;
+	}
+}
+
+void
+windormant(Window *w)
+{
+	if(w->addr >= 0){
+		close(w->addr);
+		w->addr = -1;
+	}
+	if(w->body != nil){
+		Bterm(w->body);
+		w->body = nil;
+	}
+	if(w->data >= 0){
+		close(w->data);
+		w->data = -1;
+	}
+}
+
+
+int
+windel(Window *w, int sure)
+{
+	if(sure)
+		write(w->ctl, "delete\n", 7);
+	else if(write(w->ctl, "del\n", 4) != 4)
+		return 0;
+	/* event proc will die due to read error from event file */
+	windormant(w);
+	close(w->ctl);
+	w->ctl = -1;
+	close(w->event);
+	w->event = -1;
+	return 1;
+}
+
+void
+winclean(Window *w)
+{
+	if(w->body)
+		Bflush(w->body);
+	ctlprint(w->ctl, "clean\n");
+}
+
+int
+winisdirty(Window *w)
+{
+	char m;
+
+	if (seek(w->ctl, 4*(11+1) + 10, 0) < 0)
+		error("control file seek error: %r");
+		
+	if(read(w->ctl, &m, 1)  != 1)
+		error("control file read error: %r");
+
+	if (m == '0')
+		return 0;
+	else if (m == '1')
+		return 1;
+	else
+		error("can't parse ismodified field: %c", m);
+	return 1; // better safe than sorry
+
+}
+
+int
+winsetaddr(Window *w, char *addr, int errok)
+{
+	if(w->addr < 0)
+		w->addr = winopenfile(w, "addr");
+	if(write(w->addr, addr, strlen(addr)) < 0){
+		if(!errok)
+			error("error writing addr(%s): %r", addr);
+		return 0;
+	}
+	return 1;
+}
+
+int
+winselect(Window *w, char *addr, int errok)
+{
+	if(winsetaddr(w, addr, errok)){
+		ctlprint(w->ctl, "dot=addr\n");
+		return 1;
+	}
+	return 0;
+}
+
+char*
+winreadbody(Window *w, int *np)	/* can't use readfile because acme doesn't report the length */
+{
+	char *s;
+	int m, na, n;
+
+	if(w->body != nil)
+		winclosebody(w);
+	winopenbody(w, OREAD);
+	s = nil;
+	na = 0;
+	n = 0;
+	for(;;){
+		if(na < n+512){
+			na += 1024;
+			s = realloc(s, na+1);
+		}
+		m = Bread(w->body, s+n, na-n);
+		if(m <= 0)
+			break;
+		n += m;
+	}
+	s[n] = 0;
+	winclosebody(w);
+	*np = n;
+	return s;
+}
--- /dev/null
+++ b/acme/wiki/wiki.diff
@@ -1,0 +1,27 @@
+#!/bin/rc
+
+rfork n
+cd $1
+*=(`{ls -drp [0-9]*})
+
+while(! ~ $#* 0 1){
+	diff -n $2/index.txt $1/index.txt | awk  -F'[\/ :]' '
+	$1 ~/^[0-9]+$/ {  
+		getA = "cat "$5"/current | sed -n -e ''1d; /^A/s/^A//p; /^#/q''"
+		getA | getline A; close getA
+		$1 = t2d($1)
+		$5 = t2d($5)
+		print "\n" A ":\n" $1":"$3" "$4" "$5":"$7
+		next
+	} 
+	{ print }
+
+	function t2d(t) {
+		c = "date "t; c|getline l; close c
+		split(l, a, "[ :]+")
+		return  a[1]" "a[2]" "a[3]" "a[4]":"a[5]" "a[8]"("t")"
+	}'
+	shift
+}  
+
+echo clean >/dev/ctl >[2]/dev/null
--