shithub: jot

Download patch

ref: 9914a8da911d9fdfefd6b090870bbd385cacae69
author: aap <aap@papnet.eu>
date: Mon Jul 22 18:11:44 EDT 2024

first commit

--- /dev/null
+++ b/inc.h
@@ -1,0 +1,107 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <keyboard.h>
+#include <mouse.h>
+//#include <cursor.h>
+#include <frame.h>
+//#include <fcall.h>
+//#include <9p.h>
+//#include <complete.h>
+#include <plumb.h>
+
+typedef uchar bool;
+enum {
+	FALSE = 0,
+	TRUE = 1,
+
+	BIG = 3,
+};
+
+#define ALT(c, v, t) (Alt){ c, v, t, nil, nil, 0 }
+
+#define CTRL(c) ((c)&0x1F)
+
+
+extern Rune *snarf;
+extern int nsnarf;
+extern int snarfversion;
+extern int snarffd;
+enum { MAXSNARF = 100*1024 };
+void putsnarf(void);
+void getsnarf(void);
+void setsnarf(char *s, int ns);
+
+typedef struct Text Text;
+struct Text
+{
+	Frame;
+	Rectangle scrollr, lastsr;
+	Image *i;
+	Rune *r;
+	uint nr;
+	uint maxr;
+	uint org;	/* start of Frame's text */
+	uint q0, q1;	/* selection */
+	uint qh;	/* host point, output here */
+
+	/* not entirely happy with this in here */
+	bool rawmode;
+	Rune *raw;
+	int nraw;
+
+	int posx;
+};
+
+void xinit(Text *x, Rectangle textr, Rectangle scrollr, Font *ft, Image *b, Image **cols);
+void xsetrects(Text *x, Rectangle textr, Rectangle scrollr);
+void xclear(Text *x);
+void xredraw(Text *x);
+void xfullredraw(Text *x);
+uint xinsert(Text *x, Rune *r, int n, uint q0);
+void xfill(Text *x);
+void xdelete(Text *x, uint q0, uint q1);
+void xsetselect(Text *x, uint q0, uint q1);
+void xselect(Text *x, Mousectl *mc);
+void xscrdraw(Text *x);
+void xscroll(Text *x, Mousectl *mc, int but);
+void xscrolln(Text *x, int n);
+void xtickupdn(Text *x, int d);
+void xshow(Text *x, uint q0);
+void xplacetick(Text *x, uint q);
+void xtype(Text *x, Rune r);
+int xninput(Text *x);
+void xaddraw(Text *x, Rune *r, int nr);
+void xlook(Text *x);
+void xsnarf(Text *x);
+void xcut(Text *x);
+void xpaste(Text *x);
+void xsend(Text *x);
+int xplumb(Text *w, char *src, char *dir, int maxsize);
+void freescrtemps(void);
+
+#define	runemalloc(n)		malloc((n)*sizeof(Rune))
+#define	runerealloc(a, n)	realloc(a, (n)*sizeof(Rune))
+#define	runemove(a, b, n)	memmove(a, b, (n)*sizeof(Rune))
+#define min(a, b)	((a) < (b) ? (a) : (b))
+#define max(a, b)	((a) > (b) ? (a) : (b))
+
+void panic(char *s);
+void *emalloc(ulong size);
+void *erealloc(void *p, ulong size);
+char *estrdup(char *s);
+
+
+typedef struct Timer Timer;
+struct Timer
+{
+	int		dt;
+	int		cancel;
+	Channel	*c;	/* chan(int) */
+	Timer	*next;
+};
+void timerinit(void);
+Timer *timerstart(int dt);
+void timerstop(Timer *t);
+void timercancel(Timer *t);
--- /dev/null
+++ b/main.c
@@ -1,0 +1,448 @@
+#include "inc.h"
+#include <cursor.h>
+
+Keyboardctl *kbctl;
+Keyboardctl *fwdkc, kbdctl2;
+Mousectl *mctl;
+int shiftdown;	// TODO: needed?
+
+Text text;
+Image *colors[NCOL];
+char filename[1024];
+char *startdir;
+
+Cursor quest = {
+	{-7,-7},
+	{0x0f, 0xf0, 0x1f, 0xf8, 0x3f, 0xfc, 0x7f, 0xfe, 
+	 0x7c, 0x7e, 0x78, 0x7e, 0x00, 0xfc, 0x01, 0xf8, 
+	 0x03, 0xf0, 0x07, 0xe0, 0x07, 0xc0, 0x07, 0xc0, 
+	 0x07, 0xc0, 0x07, 0xc0, 0x07, 0xc0, 0x07, 0xc0, },
+	{0x00, 0x00, 0x0f, 0xf0, 0x1f, 0xf8, 0x3c, 0x3c, 
+	 0x38, 0x1c, 0x00, 0x3c, 0x00, 0x78, 0x00, 0xf0, 
+	 0x01, 0xe0, 0x03, 0xc0, 0x03, 0x80, 0x03, 0x80, 
+	 0x00, 0x00, 0x03, 0x80, 0x03, 0x80, 0x00, 0x00, }
+};
+
+
+
+void
+panic(char *s)
+{
+	fprint(2, "error: %s: %r\n", s);
+	threadexitsall("error");
+}
+
+void*
+emalloc(ulong size)
+{
+	void *p;
+
+	p = malloc(size);
+	if(p == nil)
+		panic("malloc failed");
+	memset(p, 0, size);
+	return p;
+}
+
+void*
+erealloc(void *p, ulong size)
+{
+	p = realloc(p, size);
+	if(p == nil)
+		panic("realloc failed");
+	return p;
+}
+
+char*
+estrdup(char *s)
+{
+	char *p;
+
+	p = malloc(strlen(s)+1);
+	if(p == nil)
+		panic("strdup failed");
+	strcpy(p, s);
+	return p;
+}
+
+
+
+
+
+/*
+ * /dev/snarf updates when the file is closed, so we must open our own
+ * fd here rather than use snarffd
+ */
+void
+putsnarf(void)
+{
+	int fd, i, n;
+
+	if(snarffd<0 || nsnarf==0)
+		return;
+	fd = open("/dev/snarf", OWRITE|OCEXEC);
+	if(fd < 0)
+		return;
+	/* snarf buffer could be huge, so fprint will truncate; do it in blocks */
+	for(i=0; i<nsnarf; i+=n){
+		n = nsnarf-i;
+		if(n >= 256)
+			n = 256;
+		if(fprint(fd, "%.*S", n, snarf+i) < 0)
+			break;
+	}
+	close(fd);
+}
+
+void
+setsnarf(char *s, int ns)
+{
+	free(snarf);
+	snarf = runesmprint("%.*s", ns, s);
+	nsnarf = runestrlen(snarf);
+	snarfversion++;
+}
+
+void
+getsnarf(void)
+{
+	int i, n;
+	char *s, *sn;
+
+	if(snarffd < 0)
+		return;
+	sn = nil;
+	i = 0;
+	seek(snarffd, 0, 0);
+	for(;;){
+		if(i > MAXSNARF)
+			break;
+		if((s = realloc(sn, i+1024+1)) == nil)
+			break;
+		sn = s;
+		if((n = read(snarffd, sn+i, 1024)) <= 0)
+			break;
+		i += n;
+	}
+	if(i == 0)
+		return;
+	sn[i] = 0;
+	setsnarf(sn, i);
+	free(sn);
+}
+
+
+
+
+void
+readfile(char *path)
+{
+	int fd, n, ns;
+	char *s, buf[1024];
+	Rune *rs;
+
+	fd = open(path, OREAD);
+	if(fd < 0)
+		return;
+	s = nil;
+	ns = 0;
+	while(n = read(fd, buf, sizeof(buf)), n > 0) {
+		s = realloc(s, ns+n);
+		memcpy(s+ns, buf, n);
+		ns += n;
+	}
+	close(fd);
+
+
+	rs = runesmprint("%.*s", ns, s);
+	free(s);
+	xdelete(&text, 0, text.nr);
+	xinsert(&text, rs, runestrlen(rs), 0);
+	free(rs);
+}
+
+int
+writefile(char *path)
+{
+	int fd;
+	char *s;
+
+	fd = create(path, OWRITE|OTRUNC, 0666);
+	if(fd < 0)
+		return 1;
+
+	s = smprint("%.*S", text.nr, text.r);
+	write(fd, s, strlen(s));
+	close(fd);
+	free(s);
+	return 0;
+}
+
+
+void
+confused(void)
+{
+	setcursor(mctl, &quest);
+	sleep(300);
+	setcursor(mctl, nil);
+}
+
+void
+editmenu(Text *x, Mousectl *mc)
+{
+	enum {
+		Cut,
+		Paste,
+		Snarf,
+		Plumb,
+		Look
+	};
+	static char *menu2str[] = {
+		"cut",
+		"paste",
+		"snarf",
+		"plumb",
+		"look",
+		nil
+	};
+	static Menu menu2 = { menu2str };
+
+	switch(menuhit(2, mc, &menu2, nil)){
+	case Cut:
+		xsnarf(x);
+		xcut(x);
+		break;
+	case Paste:
+		xpaste(x);
+		break;
+	case Snarf:
+		xsnarf(x);
+		break;
+	case Plumb:
+		if(xplumb(x, "jot", startdir, 31*1024))
+			confused();
+		break;
+	case Look:
+		xlook(x);
+		break;
+	}
+}
+
+void
+filemenu(Text *x, Mousectl *mc)
+{
+	USED(x);
+	enum {
+		Write,
+		Exit
+	};
+	static char *str[] = {
+		"write",
+		"exit",
+		nil
+	};
+	static Menu menu = { str };
+
+	switch(menuhit(3, mc, &menu, nil)){
+	case Write:
+		if(filename[0] == '\0'){
+			fwdkc = &kbdctl2;
+			enter("file", filename, sizeof(filename), mc, fwdkc, nil);
+			fwdkc = nil;
+		}
+		if(writefile(filename)){
+			memset(filename, 0, sizeof(filename));
+			confused();
+		}
+		break;
+	case Exit:
+		threadexitsall(nil);
+	}
+}
+
+void
+mousectl(Text *x, Mousectl *mc)
+{
+	int but;
+
+	for(but = 1; but < 6; but++)
+		if(mc->buttons == 1<<(but-1))
+			goto found;
+	return;
+found:
+
+/*	if(shiftdown && but > 3)
+		wkeyctl(w, but == 4 ? Kscrolloneup : Kscrollonedown);
+	else*/ if(ptinrect(mc->xy, x->scrollr) || but > 3)
+		xscroll(x, mc, but);
+	else if(but == 1)
+		xselect(x, mc);
+	else if(but == 2)
+		editmenu(x, mc);
+	else if(but == 3)
+		filemenu(x, mc);
+}
+
+void
+keyctl(Text *x, Rune r)
+{
+	int nlines, n;
+
+	nlines = x->maxlines;	/* need signed */
+	switch(r){
+
+	/* Scrolling */
+	case Kscrollonedown:
+		n = mousescrollsize(x->maxlines);
+		xscrolln(x, max(n, 1));
+		break;
+	case Kdown:
+		xscrolln(x, shiftdown ? 1 : nlines/3);
+		break;
+	case Kpgdown:
+		xscrolln(x, nlines*2/3);
+		break;
+	case Kscrolloneup:
+		n = mousescrollsize(x->maxlines);
+		xscrolln(x, -max(n, 1));
+		break;
+	case Kup:
+		xscrolln(x, -(shiftdown ? 1 : nlines/3));
+		break;
+	case Kpgup:
+		xscrolln(x, -nlines*2/3);
+		break;
+
+	case Khome:
+		xshow(x, 0);
+		break;
+	case Kend:
+		xshow(x, x->nr);
+		break;
+
+	/* Cursor movement */
+	case Kleft:
+		if(x->q0 > 0)
+			xplacetick(x, x->q0-1);
+		break;
+	case Kright:
+		if(x->q1 < x->nr)
+			xplacetick(x, x->q1+1);
+		break;
+	case CTRL('A'):
+		while(x->q0 > 0 && x->r[x->q0-1] != '\n' &&
+		      x->q0 != x->qh)
+			x->q0--;
+		xplacetick(x, x->q0);
+		break;
+	case CTRL('E'):
+		while(x->q0 < x->nr && x->r[x->q0] != '\n')
+			x->q0++;
+		xplacetick(x, x->q0);
+		break;
+	case CTRL('B'):
+		xplacetick(x, x->qh);
+		break;
+
+	case Kesc:
+		xsnarf(x);
+		xcut(x);
+		break;
+	case Kdel:
+		xtype(x, CTRL('H'));
+		break;
+
+	default:
+		xtype(x, r);
+		break;
+	}
+}
+
+void
+setsize(Text *x)
+{
+	Rectangle scrollr, textr;
+
+	draw(screen, screen->r, colors[BACK], nil, ZP);
+	scrollr = textr = insetrect(screen->r, 1);
+	scrollr.max.x = scrollr.min.x + 12;
+	textr.min.x = scrollr.max.x + 4;
+	xinit(x, textr, scrollr, font, screen, colors);
+}
+
+void
+mthread(void*)
+{
+	while(readmouse(mctl) != -1){
+		mousectl(&text, mctl);
+	}
+}
+
+void
+kbthread(void*)
+{
+	Rune r;
+
+	for(;;){
+		r = recvul(kbctl->c);
+		if(fwdkc)
+			send(fwdkc->c, &r);
+		else
+			keyctl(&text, r);
+		flushimage(display, 1);
+	}
+}
+
+void
+resthread(void*)
+{
+	for(;;){
+		recvul(mctl->resizec);
+		if(getwindow(display, Refnone) < 0)
+			sysfatal("resize failed: %r");
+
+		setsize(&text);
+	}
+}
+
+void
+threadmain(int argc, char *argv[])
+{
+	char buf[1024];
+//	newwindow(nil);
+
+	if(initdraw(nil, nil, "jot") < 0)
+		sysfatal("initdraw: %r");
+
+	kbctl = initkeyboard("/dev/cons");
+	if(kbctl == nil)
+		sysfatal("initkeyboard: %r");
+	kbdctl2.c = chancreate(sizeof(Rune), 20);
+
+	mctl = initmouse("/dev/mouse", screen);
+	if(mctl == nil)
+		sysfatal("initmouse: %r");
+	snarffd = open("/dev/snarf", OREAD|OCEXEC);
+	if(getwd(buf, sizeof(buf)) == nil)
+		startdir = estrdup(".");
+	else
+		startdir = estrdup(buf);
+
+	colors[BACK] = allocimagemix(display, DPaleyellow, DWhite);
+	colors[HIGH] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, DDarkyellow);
+	colors[BORD] = allocimage(display, Rect(0,0,2,2), screen->chan, 1, DYellowgreen);
+	colors[TEXT] = display->black;
+	colors[HTEXT] = display->black;
+
+	setsize(&text);
+
+	timerinit();
+	threadcreate(mthread, nil, mainstacksize);
+	threadcreate(kbthread, nil, mainstacksize);
+	threadcreate(resthread, nil, mainstacksize);
+
+	if(argc > 1){
+		strncpy(filename, argv[1], sizeof(filename));
+		readfile(filename);
+	}
+}
--- /dev/null
+++ b/mkfile
@@ -1,0 +1,13 @@
+< /$objtype/mkfile
+
+TARG=jot
+OFILES=\
+	main.$O \
+	text.$O \
+	time.$O
+
+HFILES=inc.h
+
+BIN=$home/bin/$objtype
+
+< /sys/src/cmd/mkone
--- /dev/null
+++ b/text.c
@@ -1,0 +1,964 @@
+#include "inc.h"
+
+enum
+{
+	HiWater	= 640000,	/* max size of history */
+	LoWater	= 400000,	/* min size of history after max'ed */
+	MinWater	= 20000,	/* room to leave available when reallocating */
+};
+
+void
+xinit(Text *x, Rectangle textr, Rectangle scrollr, Font *ft, Image *b, Image **cols)
+{
+	frinit(x, textr, ft, b, cols);
+	x->i = b;
+	x->scrollr = scrollr;
+	x->lastsr = ZR;
+	xfill(x);
+	xsetselect(x, x->q0, x->q1);
+	xscrdraw(x);
+}
+
+void
+xsetrects(Text *x, Rectangle textr, Rectangle scrollr)
+{
+	frsetrects(x, textr, x->b);
+	x->scrollr = scrollr;
+}
+
+void
+xclear(Text *x)
+{
+	free(x->r);
+	x->r = nil;
+	x->nr = 0;
+	free(x->raw);
+	x->r = nil;
+	x->nraw = 0;
+	frclear(x, TRUE);
+};
+
+void
+xredraw(Text *x)
+{
+	frredraw(x);
+	xscrdraw(x);
+}
+
+void
+xfullredraw(Text *x)
+{
+	xfill(x);
+	x->ticked = 0;
+	if(x->p0 > 0)
+		frdrawsel(x, frptofchar(x, 0), 0, x->p0, 0);
+	if(x->p1 < x->nchars)
+		frdrawsel(x, frptofchar(x, x->p1), x->p1, x->nchars, 0);
+	frdrawsel(x, frptofchar(x, x->p0), x->p0, x->p1, 1);
+	x->lastsr = ZR;
+	xscrdraw(x);
+}
+
+uint
+xinsert(Text *w, Rune *r, int n, uint q0)
+{
+	uint m;
+
+	if(n == 0)
+		return q0;
+	if(w->nr+n>HiWater && q0>=w->org && q0>=w->qh){
+		m = min(HiWater-LoWater, min(w->org, w->qh));
+		w->org -= m;
+		w->qh -= m;
+		if(w->q0 > m)
+			w->q0 -= m;
+		else
+			w->q0 = 0;
+		if(w->q1 > m)
+			w->q1 -= m;
+		else
+			w->q1 = 0;
+		w->nr -= m;
+		runemove(w->r, w->r+m, w->nr);
+		q0 -= m;
+	}
+	if(w->nr+n > w->maxr){
+		/*
+		 * Minimize realloc breakage:
+		 *	Allocate at least MinWater
+		 * 	Double allocation size each time
+		 *	But don't go much above HiWater
+		 */
+		m = max(min(2*(w->nr+n), HiWater), w->nr+n)+MinWater;
+		if(m > HiWater)
+			m = max(HiWater+MinWater, w->nr+n);
+		if(m > w->maxr){
+			w->r = runerealloc(w->r, m);
+			w->maxr = m;
+		}
+	}
+	runemove(w->r+q0+n, w->r+q0, w->nr-q0);
+	runemove(w->r+q0, r, n);
+	w->nr += n;
+	/* if output touches, advance selection, not qh; works best for keyboard and output */
+	if(q0 <= w->q1)
+		w->q1 += n;
+	if(q0 <= w->q0)
+		w->q0 += n;
+	if(q0 < w->qh)
+		w->qh += n;
+	if(q0 < w->org)
+		w->org += n;
+	else if(q0 <= w->org+w->nchars)
+		frinsert(w, r, r+n, q0-w->org);
+	return q0;
+}
+
+void
+xfill(Text *w)
+{
+	Rune *rp;
+	int i, n, m, nl;
+
+	while(w->lastlinefull == FALSE){
+		n = w->nr-(w->org+w->nchars);
+		if(n == 0)
+			break;
+		if(n > 2000)	/* educated guess at reasonable amount */
+			n = 2000;
+		rp = w->r+(w->org+w->nchars);
+
+		/*
+		 * it's expensive to frinsert more than we need, so
+		 * count newlines.
+		 */
+		nl = w->maxlines-w->nlines;
+		m = 0;
+		for(i=0; i<n; ){
+			if(rp[i++] == '\n'){
+				m++;
+				if(m >= nl)
+					break;
+			}
+		}
+		frinsert(w, rp, rp+i, w->nchars);
+	}
+}
+
+void
+xdelete(Text *w, uint q0, uint q1)
+{
+	uint n, p0, p1;
+
+	n = q1-q0;
+	if(n == 0)
+		return;
+	runemove(w->r+q0, w->r+q1, w->nr-q1);
+	w->nr -= n;
+	if(q0 < w->q0)
+		w->q0 -= min(n, w->q0-q0);
+	if(q0 < w->q1)
+		w->q1 -= min(n, w->q1-q0);
+	if(q1 < w->qh)
+		w->qh -= n;
+	else if(q0 < w->qh)
+		w->qh = q0;
+	if(q1 <= w->org)
+		w->org -= n;
+	else if(q0 < w->org+w->nchars){
+		p1 = q1 - w->org;
+		if(p1 > w->nchars)
+			p1 = w->nchars;
+		if(q0 < w->org){
+			w->org = q0;
+			p0 = 0;
+		}else
+			p0 = q0 - w->org;
+		frdelete(w, p0, p1);
+		xfill(w);
+	}
+}
+
+void
+xsetselect(Text *w, uint q0, uint q1)
+{
+	int p0, p1;
+
+	w->posx = -1;
+	/* w->p0 and w->p1 are always right; w->q0 and w->q1 may be off */
+	w->q0 = q0;
+	w->q1 = q1;
+	/* compute desired p0,p1 from q0,q1 */
+	p0 = q0-w->org;
+	p1 = q1-w->org;
+	if(p0 < 0)
+		p0 = 0;
+	if(p1 < 0)
+		p1 = 0;
+	if(p0 > w->nchars)
+		p0 = w->nchars;
+	if(p1 > w->nchars)
+		p1 = w->nchars;
+	if(p0==w->p0 && p1==w->p1)
+		return;
+	/* screen disagrees with desired selection */
+	if(w->p1<=p0 || p1<=w->p0 || p0==p1 || w->p1==w->p0){
+		/* no overlap or too easy to bother trying */
+		frdrawsel(w, frptofchar(w, w->p0), w->p0, w->p1, 0);
+		frdrawsel(w, frptofchar(w, p0), p0, p1, 1);
+		goto Return;
+	}
+	/* overlap; avoid unnecessary painting */
+	if(p0 < w->p0){
+		/* extend selection backwards */
+		frdrawsel(w, frptofchar(w, p0), p0, w->p0, 1);
+	}else if(p0 > w->p0){
+		/* trim first part of selection */
+		frdrawsel(w, frptofchar(w, w->p0), w->p0, p0, 0);
+	}
+	if(p1 > w->p1){
+		/* extend selection forwards */
+		frdrawsel(w, frptofchar(w, w->p1), w->p1, p1, 1);
+	}else if(p1 < w->p1){
+		/* trim last part of selection */
+		frdrawsel(w, frptofchar(w, p1), p1, w->p1, 0);
+	}
+
+    Return:
+	w->p0 = p0;
+	w->p1 = p1;
+}
+
+static void
+xsetorigin(Text *w, uint org, int exact)
+{
+	int i, a, fixup;
+	Rune *r;
+	uint n;
+
+	if(org>0 && !exact){
+		/* org is an estimate of the char posn; find a newline */
+		/* don't try harder than 256 chars */
+		for(i=0; i<256 && org<w->nr; i++){
+			if(w->r[org] == '\n'){
+				org++;
+				break;
+			}
+			org++;
+		}
+	}
+	a = org-w->org;
+	fixup = 0;
+	if(a>=0 && a<w->nchars){
+		frdelete(w, 0, a);
+		fixup = 1;	/* frdelete can leave end of last line in wrong selection mode; it doesn't know what follows */
+	}else if(a<0 && -a<w->nchars){
+		n = w->org - org;
+		r = w->r+org;
+		frinsert(w, r, r+n, 0);
+	}else
+		frdelete(w, 0, w->nchars);
+	w->org = org;
+	xfill(w);
+	xscrdraw(w);
+	xsetselect(w, w->q0, w->q1);
+	if(fixup && w->p1 > w->p0)
+		frdrawsel(w, frptofchar(w, w->p1-1), w->p1-1, w->p1, 1);
+}
+
+
+/*
+ * Scrolling
+ */
+
+static Image *scrtmp;
+
+static Image*
+scrtemps(void)
+{
+	int h;
+
+	if(scrtmp == nil){
+		h = BIG*Dy(screen->r);
+		scrtmp = allocimage(display, Rect(0, 0, 32, h), screen->chan, 0, DNofill);
+	}
+	return scrtmp;
+}
+
+void
+freescrtemps(void)
+{
+	if(scrtmp){
+		freeimage(scrtmp);
+		scrtmp = nil;
+	}
+}
+
+static Rectangle
+scrpos(Rectangle r, uint p0, uint p1, uint tot)
+{
+	Rectangle q;
+	int h;
+
+	q = r;
+	h = q.max.y-q.min.y;
+	if(tot == 0)
+		return q;
+	if(tot > 1024*1024){
+		tot>>=10;
+		p0>>=10;
+		p1>>=10;
+	}
+	if(p0 > 0)
+		q.min.y += h*p0/tot;
+	if(p1 < tot)
+		q.max.y -= h*(tot-p1)/tot;
+	if(q.max.y < q.min.y+2){
+		if(q.min.y+2 <= r.max.y)
+			q.max.y = q.min.y+2;
+		else
+			q.min.y = q.max.y-2;
+	}
+	return q;
+}
+
+void
+xscrdraw(Text *w)
+{
+	Rectangle r, r1, r2;
+	Image *b;
+
+	b = scrtemps();
+	if(b == nil || w->i == nil)
+		return;
+	r = w->scrollr;
+	r1 = r;
+	r1.min.x = 0;
+	r1.max.x = Dx(r);
+	r2 = scrpos(r1, w->org, w->org+w->nchars, w->nr);
+	if(!eqrect(r2, w->lastsr)){
+		w->lastsr = r2;
+		/* move r1, r2 to (0,0) to avoid clipping */
+		r2 = rectsubpt(r2, r1.min);
+		r1 = rectsubpt(r1, r1.min);
+		draw(b, r1, w->cols[BORD], nil, ZP);
+		draw(b, r2, w->cols[BACK], nil, ZP);
+		r2.min.x = r2.max.x-1;
+		draw(b, r2, w->cols[BORD], nil, ZP);
+		draw(w->i, r, b, nil, Pt(0, r1.min.y));
+	}
+}
+
+static uint
+xbacknl(Text *w, uint p, uint n)
+{
+	int i, j;
+
+	/* look for start of this line if n==0 */
+	if(n==0 && p>0 && w->r[p-1]!='\n')
+		n = 1;
+	i = n;
+	while(i-->0 && p>0){
+		--p;	/* it's at a newline now; back over it */
+		if(p == 0)
+			break;
+		/* at 128 chars, call it a line anyway */
+		for(j=128; --j>0 && p>0; p--)
+			if(w->r[p-1]=='\n')
+				break;
+	}
+	return p;
+}
+
+static void
+xscrsleep(Mousectl *mc, uint dt)
+{
+	Timer	*timer;
+	int y, b;
+	static Alt alts[3];
+
+	if(display->bufp > display->buf)
+		flushimage(display, 1);
+	timer = timerstart(dt);
+	y = mc->xy.y;
+	b = mc->buttons;
+	alts[0] = ALT(timer->c, nil, CHANRCV);
+	alts[1] = ALT(mc->c, &mc->Mouse, CHANRCV);
+	alts[2].op = CHANEND;
+	for(;;)
+		switch(alt(alts)){
+		case 0:
+			timerstop(timer);
+			return;
+		case 1:
+			if(abs(mc->xy.y-y)>2 || mc->buttons!=b){
+				timercancel(timer);
+				return;
+			}
+			break;
+		}
+}
+
+void
+xscroll(Text *w, Mousectl *mc, int but)
+{
+	uint p0, oldp0;
+	Rectangle s;
+	int y, my, h, first;
+
+	s = insetrect(w->scrollr, 1);
+	h = s.max.y-s.min.y;
+	oldp0 = ~0;
+	first = TRUE;
+	do{
+		my = mc->xy.y;
+		if(my < s.min.y)
+			my = s.min.y;
+		if(my >= s.max.y)
+			my = s.max.y;
+		if(but == 2){
+			y = my;
+			if(y > s.max.y-2)
+				y = s.max.y-2;
+			if(w->nr > 1024*1024)
+				p0 = ((w->nr>>10)*(y-s.min.y)/h)<<10;
+			else
+				p0 = w->nr*(y-s.min.y)/h;
+			if(oldp0 != p0)
+				xsetorigin(w, p0, FALSE);
+			oldp0 = p0;
+			readmouse(mc);
+			continue;
+		}
+		if(but == 1 || but == 4){
+			y = max(1, (my-s.min.y)/w->font->height);
+			p0 = xbacknl(w, w->org, y);
+		}else{
+			y = max(my, s.min.y+w->font->height);
+			p0 = w->org+frcharofpt(w, Pt(s.max.x, y));
+		}
+		if(oldp0 != p0)
+			xsetorigin(w, p0, TRUE);
+		oldp0 = p0;
+		/* debounce */
+		if(first){
+			if(display->bufp > display->buf)
+				flushimage(display, 1);
+			if(but > 3)
+				return;
+			sleep(200);
+			nbrecv(mc->c, &mc->Mouse);
+			first = FALSE;
+		}
+		xscrsleep(mc, 100);
+	}while(mc->buttons & (1<<(but-1)));
+	while(mc->buttons)
+		readmouse(mc);
+}
+
+void
+xscrolln(Text *x, int n)
+{
+	uint q0;
+
+	if(n < 0)
+		q0 = xbacknl(x, x->org, -n);
+	else
+		q0 = x->org+frcharofpt(x, Pt(x->Frame.r.min.x, x->Frame.r.min.y+n*x->font->height));
+	xsetorigin(x, q0, TRUE);
+}
+
+/* move tick up or down while staying at the same x position */
+void
+xtickupdn(Text *x, int d)
+{
+	Point p;
+	int py;
+	uint q0;
+
+	xshow(x, x->q0);
+	p = frptofchar(x, x->q0-x->org);
+	if(x->posx >= 0)
+		p.x = x->posx;
+	py = p.y;
+	p.y += d*x->font->height;
+	if(p.y < x->Frame.r.min.y ||
+	   p.y > x->Frame.r.max.y-x->font->height){
+		xscrolln(x, d);
+		p.y = py;
+	}
+	q0 = x->org+frcharofpt(x, p);
+	xsetselect(x, q0, q0);
+	x->posx = p.x;
+}
+
+static Text	*selecttext;
+static Mousectl *selectmc;
+static uint	selectq;
+
+static void
+xframescroll(Text *x, int dl)
+{
+	uint endq;
+
+	if(dl == 0){
+		xscrsleep(selectmc, 100);
+		return;
+	}
+	if(dl < 0){
+		endq = x->org+x->p0;
+	}else{
+		if(x->org+x->nchars == x->nr)
+			return;
+		endq = x->org+x->p1;
+	}
+	xscrolln(x, dl);
+	xsetselect(x, min(selectq, endq), max(selectq, endq));
+}
+
+static void
+framescroll(Frame *f, int dl)
+{
+	if(f != &selecttext->Frame)
+		panic("frameselect not right frame");
+	xframescroll(selecttext, dl);
+}
+
+/*
+ * Selection and deletion helpers
+ */
+
+int
+iswordrune(Rune r)
+{
+	return isalpharune(r) || isdigitrune(r);
+}
+
+static int
+xbswidth(Text *w, Rune c)
+{
+	uint q, stop;
+	Rune r;
+	int wd, inword;
+
+	/* there is known to be at least one character to erase */
+	if(c == Kbs)	/* ^H: erase character */
+		return 1;
+	q = w->q0;
+	stop = 0;
+	if(q > w->qh)
+		stop = w->qh;
+	inword = FALSE;
+	while(q > stop){
+		r = w->r[q-1];
+		if(r == '\n'){		/* eat at most one more character */
+			if(q == w->q0)	/* eat the newline */
+				--q;
+			break; 
+		}
+		/* ^W: erase word.
+		 * delete a bunch of non-word characters
+		 * followed by word characters */
+		if(c == CTRL('W')){
+			wd = iswordrune(r);
+			if(wd && !inword)
+				inword = TRUE;
+			else if(!wd && inword)
+				break;
+		}
+		--q;
+	}
+	return w->q0-q;
+}
+
+static Rune left1[] =  { L'{', L'[', L'(', L'<', L'«', 0 };
+static Rune right1[] = { L'}', L']', L')', L'>', L'»', 0 };
+static Rune left2[] =  { L'\n', 0 };
+static Rune left3[] =  { L'\'', L'"', L'`', 0 };
+
+static Rune *left[] = {
+	left1,
+	left2,
+	left3,
+	nil
+};
+static Rune *right[] = {
+	right1,
+	left2,
+	left3,
+	nil
+};
+
+static int
+xclickmatch(Text *x, int cl, int cr, int dir, uint *q)
+{
+	Rune c;
+	int nest;
+
+	nest = 1;
+	for(;;){
+		if(dir > 0){
+			if(*q == x->nr)
+				break;
+			c = x->r[*q];
+			(*q)++;
+		}else{
+			if(*q == 0)
+				break;
+			(*q)--;
+			c = x->r[*q];
+		}
+		if(c == cr){
+			if(--nest==0)
+				return 1;
+		}else if(c == cl)
+			nest++;
+	}
+	return cl=='\n' && nest==1;
+}
+
+static int
+inmode(Rune r, int mode)
+{
+	return (mode == 1) ? iswordrune(r) : r && !isspacerune(r);
+}
+
+static void
+xstretchsel(Text *x, uint pt, uint *q0, uint *q1, int mode)
+{
+	int c, i;
+	Rune *r, *l, *p;
+	uint q;
+
+	*q0 = pt;
+	*q1 = pt;
+	for(i=0; left[i]!=nil; i++){
+		q = *q0;
+		l = left[i];
+		r = right[i];
+		/* try matching character to left, looking right */
+		if(q == 0)
+			c = '\n';
+		else
+			c = x->r[q-1];
+		p = runestrchr(l, c);
+		if(p != nil){
+			if(xclickmatch(x, c, r[p-l], 1, &q))
+				*q1 = q-(c!='\n');
+			return;
+		}
+		/* try matching character to right, looking left */
+		if(q == x->nr)
+			c = '\n';
+		else
+			c = x->r[q];
+		p = runestrchr(r, c);
+		if(p != nil){
+			if(xclickmatch(x, c, l[p-r], -1, &q)){
+				*q1 = *q0+(*q0<x->nr && c=='\n');
+				*q0 = q;
+				if(c!='\n' || q!=0 || x->r[0]=='\n')
+					(*q0)++;
+			}
+			return;
+		}
+	}
+	/* try filling out word to right */
+	while(*q1<x->nr && inmode(x->r[*q1], mode))
+		(*q1)++;
+	/* try filling out word to left */
+	while(*q0>0 && inmode(x->r[*q0-1], mode))
+		(*q0)--;
+}
+
+static Mouse	lastclick;
+static Text	*clickfrm;
+static uint	clickcount;
+
+/* should be called with button 1 down */
+void
+xselect(Text *x, Mousectl *mc)
+{
+	uint q0, q1;
+	int dx, dy, dt, b;
+
+	/* reset click state if mouse is too different from last time */
+	dx = abs(mc->xy.x - lastclick.xy.x);
+	dy = abs(mc->xy.y - lastclick.xy.y);
+	dt = mc->msec - lastclick.msec;
+	if(x != clickfrm || dx > 3 || dy > 3 || dt >= 500)
+		clickcount = 0;
+
+	/* first button down can be a dragging selection or a click.
+	 * subsequent buttons downs can only be clicks.
+	 * both cases can be ended by chording. */
+	selectq = x->org+frcharofpt(x, mc->xy);
+	if(clickcount == 0){
+		/* what a kludge - can this be improved? */
+		selecttext = x;
+		selectmc = mc;
+		x->scroll = framescroll;
+		frselect(x, mc);
+		/* this is correct if the whole selection is visible */
+		q0 = x->org + x->p0;
+		q1 = x->org + x->p1;
+		/* otherwise replace one end with selectq */
+		if(selectq < x->org)
+			q0 = selectq;
+		if(selectq > x->org+x->nchars)
+			q1 = selectq;
+		xsetselect(x, q0, q1);
+
+		/* figure out whether it was a click */
+		if(q0 == q1 && mc->buttons == 0){
+			clickcount = 1;
+			clickfrm = x;
+		}
+	}else{
+		clickcount++;
+		xstretchsel(x, selectq, &q0, &q1, min(clickcount-1, 2));
+		xsetselect(x, q0, q1);
+		if(clickcount >= 3)
+			clickcount = 0;
+		b = mc->buttons;
+		while(mc->buttons == b)
+			readmouse(mc);
+	}
+	lastclick = mc->Mouse;		/* a bit unsure if this is correct */
+
+	/* chording */
+	while(mc->buttons){
+		clickcount = 0;
+		b = mc->buttons;
+		if(b & 6){
+			if(b & 2){
+				xsnarf(x);
+				xcut(x);
+			}else{
+				xpaste(x);
+			}
+		}
+		while(mc->buttons == b)
+			readmouse(mc);
+	}
+}
+
+void
+xshow(Text *w, uint q0)
+{
+	int qe;
+	int nl;
+	uint q;
+
+	qe = w->org+w->nchars;
+	if(w->org<=q0 && (q0<qe || (q0==qe && qe==w->nr)))
+		xscrdraw(w);
+	else{
+		nl = 4*w->maxlines/5;
+		q = xbacknl(w, q0, nl);
+		/* avoid going backwards if trying to go forwards - long lines! */
+		if(!(q0>w->org && q<w->org))
+			xsetorigin(w, q, TRUE);
+		while(q0 > w->org+w->nchars)
+			xsetorigin(w, w->org+1, FALSE);
+	}
+}
+
+void
+xplacetick(Text *x, uint q)
+{
+	xsetselect(x, q, q);
+	xshow(x, q);
+}
+
+void
+xtype(Text *x, Rune r)
+{
+	uint q0, q1;
+	int nb;
+
+	xsnarf(x);
+	xcut(x);
+	switch(r){
+	case CTRL('H'):	/* erase character */
+	case CTRL('W'):	/* erase word */
+	case CTRL('U'):	/* erase line */
+		if(x->q0==0 || x->q0==x->qh)
+			return;
+		nb = xbswidth(x, r);
+		q1 = x->q0;
+		q0 = q1-nb;
+		if(q0 < x->org){
+			q0 = x->org;
+			nb = q1-q0;
+		}
+		if(nb > 0){
+			xdelete(x, q0, q0+nb);
+			xsetselect(x, q0, q0);
+		}
+		break;
+	default:
+		xinsert(x, &r, 1, x->q0);
+		xshow(x, x->q0);
+		break;
+	}
+}
+
+int
+xninput(Text *x)
+{
+	uint q;
+	Rune r;
+
+	for(q = x->qh; q < x->nr; q++){
+		r = x->r[q];
+		if(r == '\n')
+			return q - x->qh + 1;
+		if(r == CTRL('D'))
+			return q - x->qh;
+	}
+	return -1;
+}
+
+void
+xaddraw(Text *x, Rune *r, int nr)
+{
+	x->raw = runerealloc(x->raw, x->nraw+nr);
+	runemove(x->raw+x->nraw, r, nr);
+	x->nraw += nr;
+}
+
+/* TODO: maybe pass what we're looking for in a string */
+void
+xlook(Text *x)
+{
+	int i, n, e;
+
+	i = x->q1;
+	n = i - x->q0;
+	e = x->nr - n;
+	if(n <= 0 || e < n)
+		return;
+
+	if(i > e)
+		i = 0;
+
+	while(runestrncmp(x->r+x->q0, x->r+i, n) != 0){
+		if(i < e)
+			i++;
+		else
+			i = 0;
+	}
+
+	xsetselect(x, i, i+n);
+	xshow(x, i);
+}
+
+Rune *snarf;
+int nsnarf;
+int snarfversion;
+int snarffd;
+
+void
+xsnarf(Text *x)
+{
+	if(x->q1 == x->q0)
+		return;
+	nsnarf = x->q1-x->q0;
+	snarf = runerealloc(snarf, nsnarf);
+	snarfversion++;
+	runemove(snarf, x->r+x->q0, nsnarf);
+	putsnarf();
+}
+
+void
+xcut(Text *x)
+{
+	if(x->q1 == x->q0)
+		return;
+	xdelete(x, x->q0, x->q1);
+	xsetselect(x, x->q0, x->q0);
+	xscrdraw(x);
+}
+
+void
+xpaste(Text *x)
+{
+	uint q0;
+
+	getsnarf();
+	if(nsnarf == 0)
+		return;
+	xcut(x);
+	q0 = x->q0;
+	if(x->rawmode && q0==x->nr){
+		xaddraw(x, snarf, nsnarf);
+		xsetselect(x, q0, q0);
+	}else{
+		q0 = xinsert(x, snarf, nsnarf, x->q0);
+		xsetselect(x, q0, q0+nsnarf);
+	}
+	xscrdraw(x);
+}
+
+void
+xsend(Text *x)
+{
+	getsnarf();
+	xsnarf(x);
+	if(nsnarf == 0)
+		return;
+	if(x->rawmode){
+		xaddraw(x, snarf, nsnarf);
+		if(snarf[nsnarf-1]!='\n' && snarf[nsnarf-1]!=CTRL('D'))
+			xaddraw(x, L"\n", 1);
+	}else{
+		xinsert(x, snarf, nsnarf, x->nr);
+		if(snarf[nsnarf-1]!='\n' && snarf[nsnarf-1]!=CTRL('D'))
+			xinsert(x, L"\n", 1, x->nr);
+	}
+	xplacetick(x, x->nr);
+}
+
+int
+xplumb(Text *w, char *src, char *dir, int maxsize)
+{
+	Plumbmsg *m;
+	static int fd = -2;
+	char buf[32];
+	uint p0, p1;
+
+	if(fd == -2)
+		fd = plumbopen("send", OWRITE|OCEXEC);
+	if(fd < 0)
+		return 0;
+	m = emalloc(sizeof(Plumbmsg));
+	m->src = estrdup(src);
+	m->dst = nil;
+	m->wdir = estrdup(dir);
+	m->type = estrdup("text");
+	p0 = w->q0;
+	p1 = w->q1;
+	if(w->q1 > w->q0)
+		m->attr = nil;
+	else{
+		while(p0>0 && w->r[p0-1]!=' ' && w->r[p0-1]!='\t' && w->r[p0-1]!='\n')
+			p0--;
+		while(p1<w->nr && w->r[p1]!=' ' && w->r[p1]!='\t' && w->r[p1]!='\n')
+			p1++;
+		snprint(buf, sizeof(buf), "click=%d", w->q0-p0);
+		m->attr = plumbunpackattr(buf);
+	}
+	if(p1-p0 > maxsize){
+		plumbfree(m);
+		return 0;	/* too large for 9P */
+	}
+	m->data = smprint("%.*S", p1-p0, w->r+p0);
+	m->ndata = strlen(m->data);
+	if(plumbsend(fd, m) < 0){
+		plumbfree(m);
+		return 1;
+	}
+	plumbfree(m);
+	return 0;
+}
--- /dev/null
+++ b/time.c
@@ -1,0 +1,114 @@
+#include "inc.h"
+
+/* taken from rio */
+
+static Channel*	ctimer;	/* chan(Timer*)[100] */
+static Timer *timer;
+
+static uint
+msec(void)
+{
+	return nsec()/1000000;
+}
+
+void
+timerstop(Timer *t)
+{
+	t->next = timer;
+	timer = t;
+}
+
+void
+timercancel(Timer *t)
+{
+	t->cancel = TRUE;
+}
+
+static void
+timerproc(void*)
+{
+	int i, nt, na, dt, del;
+	Timer **t, *x;
+	uint old, new;
+
+	rfork(RFFDG);
+	threadsetname("TIMERPROC");
+	t = nil;
+	na = 0;
+	nt = 0;
+	old = msec();
+	for(;;){
+		sleep(1);	/* will sleep minimum incr */
+		new = msec();
+		dt = new-old;
+		old = new;
+		if(dt < 0)	/* timer wrapped; go around, losing a tick */
+			continue;
+		for(i=0; i<nt; i++){
+			x = t[i];
+			x->dt -= dt;
+			del = 0;
+			if(x->cancel){
+				timerstop(x);
+				del = 1;
+			}else if(x->dt <= 0){
+				/*
+				 * avoid possible deadlock if client is
+				 * now sending on ctimer
+				 */
+				if(nbsendul(x->c, 0) > 0)
+					del = 1;
+			}
+			if(del){
+				memmove(&t[i], &t[i+1], (nt-i-1)*sizeof t[0]);
+				--nt;
+				--i;
+			}
+		}
+		if(nt == 0){
+			x = recvp(ctimer);
+	gotit:
+			if(nt == na){
+				na += 10;
+				t = realloc(t, na*sizeof(Timer*));
+				if(t == nil)
+					abort();
+			}
+			t[nt++] = x;
+			old = msec();
+		}
+		if(nbrecv(ctimer, &x) > 0)
+			goto gotit;
+	}
+}
+
+void
+timerinit(void)
+{
+	ctimer = chancreate(sizeof(Timer*), 100);
+	proccreate(timerproc, nil, mainstacksize);
+}
+
+/*
+ * timeralloc() and timerfree() don't lock, so can only be
+ * called from the main proc.
+ */
+
+Timer*
+timerstart(int dt)
+{
+	Timer *t;
+
+	t = timer;
+	if(t)
+		timer = timer->next;
+	else{
+		t = emalloc(sizeof(Timer));
+		t->c = chancreate(sizeof(int), 0);
+	}
+	t->next = nil;
+	t->dt = dt;
+	t->cancel = FALSE;
+	sendp(ctimer, t);
+	return t;
+}