shithub: nc

Download patch

ref: e8246c63bfaf30f8c06a18ec371179191137ab73
author: phil9 <[email protected]>
date: Mon Dec 26 00:11:53 EST 2022

initial import

	this should have happened way earlier :x

--- /dev/null
+++ b/LICENSE
@@ -1,0 +1,21 @@
+MIT License
+
+Copyright (c) 2022 phil9 <[email protected]>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
--- /dev/null
+++ b/README.md
@@ -1,0 +1,33 @@
+nc
+===
+nein commander is a two-pane file manager for plan9 inspired by the likes of midnight commander.
+nc is mainly keyboard-driven, see the internal help for a list of available shortcuts.
+
+Installation:
+-------------
+```sh
+% git/clone <repository_url>
+% cd nc
+% mk
+% mk install
+```
+
+Missing features:
+-----------------
+nc is in early alpha stage and all features are not yet implemented:
+- dirview: text can overflow columns
+- most file ops are not implemented (copy, move, ...)
+- no sorting
+- ...
+
+Author:
+-------
+phil9
+
+License:
+--------
+MIT
+
+Bugs:
+-----
+Look and you shall find.
--- /dev/null
+++ b/a.h
@@ -1,0 +1,174 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <thread.h>
+#include <plumb.h>
+#include <bio.h>
+
+
+typedef struct Dirview Dirview;
+typedef struct Dirpanel Dirpanel;
+typedef struct Dirmodel Dirmodel;
+typedef struct Text Text;
+typedef struct Actionbar Actionbar;
+typedef struct Binding Binding;
+
+typedef void(*Action)(void);
+
+struct Dirview
+{
+	Rectangle	r;
+	Image*		b;
+	Channel*	c;
+	Rectangle	leftr;
+	Dirpanel*	leftp;
+	Rectangle	rightr;
+	Dirpanel*	rightp;
+};
+
+struct Dirpanel
+{
+	Rectangle	r;
+	Image*		b;
+	Channel*	c;
+	Dirmodel*	model;
+	int			focused;
+	int			nlines;
+	int			offset;
+	int			cursor;
+	Rectangle	intr;
+	Rectangle	titler;
+	Rectangle	filesr;
+	int			colw[3];
+};
+
+struct Dirmodel
+{
+	Channel*	c;
+	char*		path;
+	int			isroot;
+	Dir*		dirs;
+	long		ndirs;
+	uchar*		sel;
+	char*		filter;
+	Dir*		fdirs;
+	long		fndirs;
+};
+
+enum
+{
+	Maxlines = 65535,
+};
+
+struct Text
+{
+	Image*		b;
+	Channel*	c;
+	Rectangle	r;
+	Rectangle	intr;
+	Rectangle	titler;
+	Rectangle	textr;
+	int			vlines;
+	int			offset;
+	char*		title;
+	char*		data;
+	usize		ndata;
+	usize		lines[Maxlines];
+	int			nlines;
+	int			s0;
+	int			s1;
+};
+
+struct Actionbar
+{
+	Rectangle	r;
+	Image*		b;
+	char*		labels[10];
+	Action		actions[10];
+};
+
+struct Binding
+{
+	Rune	k;
+	Action	f;
+};
+
+Dirview*	mkdirview(char*);
+void		dirviewsetrect(Dirview*, Rectangle);
+void		dirviewredraw(Dirview*);
+void		dirviewemouse(Dirview*, Mouse);
+Dirpanel*	dirviewcurrentpanel(Dirview*);
+Dirpanel*	dirviewotherpanel(Dirview*);
+
+Dirmodel*	mkdirmodel(char*);
+Dir			dirmodelgetdir(Dirmodel*, int);
+long		dirmodelcount(Dirmodel*);
+void		dirmodelreload(Dirmodel*);
+void		dirmodelcd(Dirmodel*, char*);
+void		dirmodelfilter(Dirmodel*, char*);
+
+Dirpanel*	mkdirpanel(Dirmodel*);
+void		dirpanelsetrect(Dirpanel*, Rectangle);
+void		dirpanelredraw(Dirpanel*);
+void		dirpanelredrawnotify(Dirpanel*);
+void		dirpanelemouse(Dirpanel*, Mouse);
+void		dirpanelresetcursor(Dirpanel*);
+int			dirpanelselectedindex(Dirpanel*);
+
+Text*		mktext(void);
+void		textsetrect(Text*, Rectangle);
+void		textredraw(Text*);
+void		textemouse(Text*, Mouse);
+void		textscroll(Text*, int);
+void		textset(Text*, char*, char*, usize);
+
+Actionbar*	mkactionbar(void);
+void		actionbarsetrect(Actionbar*, Rectangle);
+void		actionbarredraw(Actionbar*);
+void		actionbaremouse(Actionbar*, Mouse);
+void		actionbarclear(Actionbar*);
+void		actionbarset(Actionbar*, int, char*, Action);
+
+void		setmode(int);
+void		setupdirviewbindings(void);
+void		setupviewerbindings(void);
+
+int			match(char*, char*);
+
+Rectangle	boundsrect(Rectangle);
+Image*		ealloccolor(ulong);
+void*		emalloc(ulong);
+void*		erealloc(void*, ulong);
+char*		slurp(char*);
+char*		homedir(void);
+char*		abspath(char*, char*);
+int			mkdir(char*, char*);
+
+enum
+{
+	Mdir,
+	Mhelp,
+	Mview,
+};
+
+enum
+{
+	Cbg,
+	Cfg,
+	Clfg,
+	Ctitle,
+	Cborder,
+	Csel,
+	Ncols
+};
+extern Image*		cols[Ncols];
+extern Mousectl*	mc;
+extern Keyboardctl*	kc;
+extern int			mode;
+extern Dirview*		dview;
+extern Text*		text;
+extern Actionbar*	abar;
+extern Binding*		bindings;
+extern char*		help;
--- /dev/null
+++ b/actionbar.c
@@ -1,0 +1,80 @@
+#include "a.h"
+
+Image *bg;
+
+void
+actionbarsetrect(Actionbar *abar, Rectangle r)
+{
+	abar->r = r;
+	freeimage(abar->b);
+	abar->b = nil;
+}
+
+void
+actionbarredraw(Actionbar *abar)
+{
+	Rectangle r, wr;
+	Point p;
+	int i, w;
+	char buf[16];
+	
+	r = Rect(0, 0, Dx(abar->r), Dy(abar->r));
+	if(abar->b == nil)
+		abar->b = allocimage(display, r, screen->chan, 0, DNofill);
+	draw(abar->b, r, cols[Cbg], nil, ZP);
+	w = Dx(r)/10;
+	p = Pt(0, 2);
+	for(i = 0; i < 10; i++){
+		p.x = i*w;
+		snprint(buf, sizeof buf, "%2d", i+1);
+		p = string(abar->b, p, cols[Cfg], ZP, font, buf);
+		wr = Rect(p.x, 1, (i+1)*w - 1, r.max.y - 1);
+		draw(abar->b, wr, cols[Ctitle], nil, ZP);
+		p.x += 2;
+		if(abar->labels[i] != nil)
+			string(abar->b, p, cols[Cfg], ZP, font, abar->labels[i]);
+	}
+}
+
+void
+actionbaremouse(Actionbar *abar, Mouse m)
+{
+	int w, i;
+
+	if(!(ptinrect(m.xy, abar->r) && m.buttons == 4))
+		return;
+	w = Dx(abar->r) / 10;
+	i = (m.xy.x - screen->r.min.x) / w;
+	if(abar->actions[i] != nil)
+		abar->actions[i]();
+}
+
+Actionbar*
+mkactionbar(void)
+{
+	Actionbar *abar;
+
+	abar = emalloc(sizeof *abar);
+	abar->b = nil;
+	return abar;
+}
+
+void
+actionbarclear(Actionbar *abar)
+{
+	int i;
+
+	for(i = 0; i < 10; i++){
+		abar->labels[i] = nil;
+		abar->actions[i] = nil;
+	}
+}
+
+void
+actionbarset(Actionbar *abar, int index, char *label, Action action)
+{
+	if(index < 1 || index > 10)
+		return;
+	abar->labels[index-1] = label;
+	abar->actions[index-1] = action;
+}
--- /dev/null
+++ b/dirmodel.c
@@ -1,0 +1,137 @@
+#include "a.h"
+
+static int
+dircmpname(Dir *a, Dir *b)
+{
+	if(a->qid.type == b->qid.type)
+		return strcmp(a->name, b->name);
+	if(a->qid.type&QTDIR)
+		return -1;
+	return 1;
+} 
+
+static void
+loadpath(Dirmodel *m)
+{
+	char buf[1024];
+	int fd;
+	Dir *t, *d;
+	
+	m->isroot = strcmp(m->path, "/") == 0;
+	fd = open(m->path, OREAD);
+	/* FIXME: error handling */
+	m->ndirs = dirreadall(fd, &m->dirs);
+	if(m->ndirs > 0)
+		qsort(m->dirs, m->ndirs, sizeof *m->dirs, (int(*)(void*,void*))dircmpname);
+	close(fd);
+	if(!m->isroot){
+		t = emalloc((m->ndirs + 1) * sizeof(Dir));
+		memmove(&t[1], m->dirs, m->ndirs*sizeof(Dir));
+		m->dirs = t;
+		m->ndirs++;
+		snprint(buf, sizeof buf, "%s/..", m->path);
+		d = dirstat(buf);
+		memmove(&m->dirs[0], &d[0], sizeof(Dir));
+		m->dirs[0].name = "..";
+	}
+	m->sel = emalloc(m->ndirs * sizeof(uchar));
+	memset(m->sel, 0, m->ndirs * sizeof(uchar));
+}
+
+void
+dirmodelreload(Dirmodel *m)
+{
+	free(m->dirs);
+	free(m->sel);
+	loadpath(m);
+	sendul(m->c, 1);
+}
+
+void
+dirmodelcd(Dirmodel *m, char *p)
+{
+	char newpath[1024] = {0};
+
+	if(p[0] == '/')
+		snprint(newpath, sizeof newpath, "%s", p);
+	else
+		snprint(newpath, sizeof newpath, "%s/%s", m->path, p);
+	if(access(newpath, 0)<0)
+		sysfatal("directory does not exist: %r");
+	free(m->path);
+	m->path = abspath(m->path, newpath);
+	free(m->filter);
+	m->filter = nil;
+	free(m->fdirs);
+	m->fndirs = 0;
+	dirmodelreload(m);
+}	
+
+Dirmodel*
+mkdirmodel(char *path)
+{
+	Dirmodel *dm;
+	
+	dm = emalloc(sizeof *dm);
+	dm->c = chancreate(sizeof(ulong), 1);
+	dm->path = strdup(path);
+	dm->filter = nil;
+	dm->fdirs = nil;
+	loadpath(dm);
+	return dm;
+}
+
+Dir
+dirmodelgetdir(Dirmodel *m, int i)
+{
+	if(m->filter != nil)
+		return m->fdirs[i];
+	return m->dirs[i];
+}
+
+long
+dirmodelcount(Dirmodel *m)
+{
+	if(m->filter != nil)
+		return m->fndirs;
+	return m->ndirs;
+}
+
+void
+dirmodelfilter(Dirmodel *m, char *p)
+{
+	char buf[1024];
+	int fd, i;
+	Dir *d, *u;
+	long n;
+
+	if(p == nil){
+		free(m->filter);
+		m->filter = nil;
+		free(m->fdirs);
+		m->fndirs = 0;
+		sendul(m->c, 1);
+		return;
+	}
+	fd = open(m->path, OREAD);
+	/* FIXME: error handling */
+	n = dirreadall(fd, &d);
+	if(n > 0)
+		qsort(d, n, sizeof *d, (int(*)(void*,void*))dircmpname);
+	close(fd);
+	m->fdirs = emalloc((n+1) * sizeof(Dir));
+	if(!m->isroot){
+		snprint(buf, sizeof buf, "%s/..", m->path);
+		u = dirstat(buf);
+		memmove(&m->fdirs[0], &u[0], sizeof(Dir));
+		m->fdirs[0].name = "..";
+		m->fndirs++;
+	}
+	for(i = 0; i < n; i++){
+		if((d[i].qid.type&QTDIR) || match(d[i].name, p))
+			memmove(&m->fdirs[m->fndirs++], &d[i], sizeof(Dir));
+	}
+	memset(m->sel, 0, m->ndirs * sizeof(uchar));
+	m->filter = strdup(p);
+	sendul(m->c, 1);
+}
--- /dev/null
+++ b/dirpanel.c
@@ -1,0 +1,148 @@
+#include "a.h"
+
+void
+datestr(char *buf, ulong bufsz, long dt)
+{
+	char *months[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", 
+			   "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
+	Tm *tm;
+
+	tm = localtime(dt);
+	snprint(buf, bufsz, "%s %02d %02d:%02d", months[tm->mon], tm->mday, tm->hour, tm->min);
+}
+
+Dirpanel*
+mkdirpanel(Dirmodel *dm)
+{
+	Dirpanel *dp;
+	
+	dp = emalloc(sizeof *dp);
+	dp->c = chancreate(sizeof(ulong), 1);
+	dp->b = nil;
+	dp->model = dm;
+	dp->focused = 0;
+	dp->offset = 0;
+	dp->cursor = 0;
+	return dp;
+}
+
+void
+dirpanelsetrect(Dirpanel *p, Rectangle r)
+{
+	p->r = r;
+	freeimage(p->b);
+	p->b = nil;
+	p->intr = insetrect(boundsrect(r), 2);
+	p->titler = p->intr;
+	p->titler.max.y = p->titler.min.y + 2 + font->height + 2;
+	p->filesr = p->intr;
+	p->filesr.min.y = p->titler.max.y;
+	p->nlines = Dy(p->filesr) / (font->height + 2);
+}
+
+void
+drawline(Dirpanel *p, int index) 
+{
+	Rectangle r;
+	Image *b, *f;
+	Point pr, pt;
+	char buf[32];
+	Dir d;
+
+	d = dirmodelgetdir(p->model, p->offset + index);
+	r = p->filesr;
+	r.min.x += 2;
+	r.min.y += index * (1 + font->height + 1);
+	r.max.x -= 2;
+	r.max.y = r.min.y + 1 + font->height + 1;
+	b = index == p->cursor ? cols[Csel] : cols[Cbg];
+	f = cols[Cfg];
+	if(p->model->sel[p->offset + index])
+		f = cols[Ctitle];
+	else if(!p->focused)
+		f = cols[Clfg];
+	draw(p->b, r, b, nil, ZP);
+	pt = addpt(r.min, Pt(4, 1));
+	pt = string(p->b, pt, f, ZP, font, (d.qid.type&QTDIR) ? "/" : " ");
+	string(p->b, pt, f, ZP, font, d.name);
+	pt.x = p->filesr.min.x + p->colw[0] + 4;
+	snprint(buf, sizeof buf, "%*lld", 6, d.length);
+	string(p->b, pt, f, ZP, font, buf);
+	pt.x = p->filesr.max.x - p->colw[2] + 4;
+	datestr(buf, sizeof buf, d.mtime);
+	string(p->b, pt, f, ZP, font, buf);
+	pr = addpt(r.min, Pt(p->colw[0] - 2, 0));
+	pt = addpt(r.min, Pt(p->colw[0] - 2, Dy(r) + 1));
+	line(p->b, pr, pt, 0, 0, 0, cols[Cborder], ZP);
+	pr = addpt(pr, Pt(p->colw[1], 0));
+	pt = addpt(pt, Pt(p->colw[1], 0));
+	line(p->b, pr, pt, 0, 0, 0, cols[Cborder], ZP);
+}
+
+void
+dirpanelredraw(Dirpanel *p)
+{
+	Rectangle r, clipr, ir;
+	Point pr, pt;
+	Image *b;
+	int i;
+
+	p->colw[2] = 4 + stringwidth(font, "XXX 99 99:99") + 4;
+	p->colw[1] = 4 + stringwidth(font, "000000") + 4;
+	p->colw[0] = Dx(p->filesr) - (p->colw[1] + p->colw[2]);
+	r = boundsrect(p->r);
+	if(p->b == nil)
+		p->b = allocimage(display, r, screen->chan, 0, DNofill);
+	draw(p->b, r, cols[Cbg], nil, ZP);
+	b = p->focused ? cols[Ctitle] : cols[Cborder];
+	ir = insetrect(r, 2);
+	border(p->b, ir, 2, b, ZP);
+	pt = string(p->b, addpt(ir.min, Pt(4, 2)), cols[Cfg], ZP, font, p->model->path);
+	if(p->model->filter != nil){
+		pt = string(p->b, pt, cols[Clfg], ZP, font, " [");
+		pt = string(p->b, pt, cols[Cfg], ZP, font, p->model->filter);
+		pt = string(p->b, pt, cols[Clfg], ZP, font, "]");
+	}
+	pr = Pt(0, ir.min.y + 2 + font->height + 2);
+	line(p->b, addpt(pr, Pt(ir.min.x, 0)), addpt(pr, Pt(ir.max.x, 0)), 0, 0, 1, b, ZP);
+	pt = addpt(p->filesr.min, Pt(4, 1));
+	clipr = p->b->clipr;
+	replclipr(p->b, 0, p->filesr);
+	for(i = 0; ; i++){
+		if(i >= p->nlines || (p->offset + i) >= dirmodelcount(p->model))
+			break;
+		drawline(p, i);
+	}
+	replclipr(p->b, 0, clipr);
+	pr = addpt(p->filesr.min, Pt(p->colw[0], 1));
+	pt = addpt(p->filesr.min, Pt(p->colw[0], Dy(p->filesr) - 1));
+	line(p->b, pr, pt, 0, 0, 0, cols[Cborder], ZP);
+	pr = addpt(pr, Pt(p->colw[1], 0));
+	pt = addpt(pt, Pt(p->colw[1], 0));
+	line(p->b, pr, pt, 0, 0, 0, cols[Cborder], ZP);
+}
+
+void
+dirpanelredrawnotify(Dirpanel *p)
+{
+	dirpanelredraw(p);
+	sendul(p->c, 1);
+}
+
+void
+dirpanelemouse(Dirpanel*, Mouse)
+{
+}
+
+void
+dirpanelresetcursor(Dirpanel *p)
+{
+	p->cursor = 0;
+	p->offset = 0;
+}
+
+int
+dirpanelselectedindex(Dirpanel *p)
+{
+	return p->offset + p->cursor;
+}
--- /dev/null
+++ b/dirview.c
@@ -1,0 +1,90 @@
+#include "a.h"
+
+void
+dirviewsetrect(Dirview *d, Rectangle r)
+{
+	Rectangle lr, rr;
+
+	d->r = r;
+	freeimage(d->b);
+	d->b = nil;
+	lr = r;
+	lr.max.x = r.min.x + Dx(r)/2;
+	rr = r;
+	rr.min.x = lr.max.x;
+	dirpanelsetrect(d->leftp, lr);
+	dirpanelsetrect(d->rightp, rr);
+}
+
+void
+dirviewredraw(Dirview *d)
+{
+	Rectangle r, lr, rr;
+	
+	r = Rect(0, 0, Dx(d->r), Dy(d->r));
+	lr = r;
+	lr.max.x = r.min.x + Dx(r)/2;
+	d->leftr = lr;
+	rr = r;
+	rr.min.x = lr.max.x + 1;
+	d->rightr = rr;
+	if(d->b == nil)
+		d->b = allocimage(display, r, screen->chan, 0, DNofill);
+	dirpanelredraw(d->leftp);
+	dirpanelredraw(d->rightp);
+	draw(d->b, r, cols[Cbg], nil, ZP);
+	draw(d->b, lr, d->leftp->b, nil, ZP);
+	draw(d->b, rr, d->rightp->b, nil, ZP);
+}
+
+void
+switchfocus(Dirview *v)
+{
+	v->leftp->focused = !v->leftp->focused;
+	v->rightp->focused = !v->rightp->focused;
+	dirviewredraw(v);
+	sendul(v->c, 1);
+}
+
+void
+dirviewemouse(Dirview *v, Mouse m)
+{
+	if(m.buttons != 1)
+		return;
+	if((ptinrect(m.xy, v->leftp->r) && !v->leftp->focused) 
+	|| (ptinrect(m.xy, v->rightp->r) && !v->rightp->focused))
+		switchfocus(v);
+}
+
+Dirview*
+mkdirview(char *path)
+{
+	Dirview *dv;
+	Dirmodel *m;
+	
+	dv = emalloc(sizeof *dv);
+	dv->c = chancreate(sizeof(ulong), 1);
+	dv->b = nil;
+	m = mkdirmodel(path);
+	dv->leftp = mkdirpanel(m);
+	dv->leftp->focused = 1;
+	m = mkdirmodel(path);
+	dv->rightp = mkdirpanel(m);
+	return dv;
+}
+
+Dirpanel*
+dirviewcurrentpanel(Dirview *v)
+{
+	if(v->leftp->focused)
+		return v->leftp;
+	return v->rightp;
+}
+
+Dirpanel*
+dirviewotherpanel(Dirview *v)
+{
+	if(v->leftp->focused)
+		return v->rightp;
+	return v->leftp;
+}
--- /dev/null
+++ b/dirviewcmd.c
@@ -1,0 +1,373 @@
+#include "a.h"
+
+static void
+cmdhelp(void)
+{
+	textset(text, "Help", help, strlen(help));
+	setmode(Mhelp);
+}
+
+static void
+cmdview(void)
+{
+	Dirpanel *p;
+	Dir d;
+	char *s, *t;
+
+	p = dirviewcurrentpanel(dview);
+	d = dirmodelgetdir(p->model, dirpanelselectedindex(p));
+	if(d.qid.type&QTDIR){
+		p->cursor = 0;
+		p->offset = 0;
+		dirmodelcd(p->model, d.name);
+	}else{
+		t = smprint("%s/%s", p->model->path, d.name);
+		s = slurp(t);
+		textset(text, t, s, strlen(s));
+		setmode(Mview);
+	}
+}
+
+static void
+cmdplumb(void)
+{
+	Dirpanel *p;
+	Dir d;
+	int fd;
+	char buf[1024];
+	
+	p = dirviewcurrentpanel(dview);
+	d = dirmodelgetdir(p->model, dirpanelselectedindex(p));
+	fd = plumbopen("send", OWRITE|OCEXEC);
+	snprint(buf, sizeof buf, "%s/%s", p->model->path, d.name);
+	if(d.qid.type&QTDIR)
+		plumbsendtext(fd, "nc", nil, nil, buf);
+	else
+		plumbsendtext(fd, "nc", nil, p->model->path, buf);
+	close(fd);
+}
+
+static void
+cmdcopy(void)
+{
+	fprint(2, "TODO: copy\n");
+}
+
+static void
+cmdrenmov(void)
+{
+	Dirpanel *p;
+	Dir d, null;
+	char opath[1024] = {0}, buf[255] = {0};
+	int n;
+
+	p = dirviewcurrentpanel(dview);
+	if(strcmp(p->model->path, dirviewotherpanel(dview)->model->path) == 0){
+		d = dirmodelgetdir(p->model, dirpanelselectedindex(p));
+		snprint(buf, sizeof buf, d.name);
+		if((n = enter("rename:", buf, sizeof buf, mc, kc, nil)) <= 0)
+			return;
+		if(strncmp(buf, d.name, n) == 0)
+			return;
+		snprint(opath, sizeof opath, "%s/%s", p->model->path, d.name);
+		nulldir(&null);
+		null.name = buf;
+		if(dirwstat(opath, &null) < 0)
+			fprint(2, "rename failed: %r\n");
+		else{
+			dirmodelreload(p->model);
+			dirmodelreload(dirviewotherpanel(dview)->model);
+		}			
+		return;
+	}
+};
+
+static void
+cmdmkdir(void)
+{
+	Dirpanel *p;
+	char buf[1024] = {0};
+
+	p = dirviewcurrentpanel(dview);
+	if(enter("new dir:", buf, sizeof buf, mc, kc, nil) <= 0)
+		return;
+	if(mkdir(p->model->path, buf) < 0){
+		fprint(2, "mkdir: %r\n");
+		return;
+	}
+	dirmodelreload(p->model);
+}
+
+static void
+cmddelete(void)
+{
+	fprint(2, "TODO: delete\n");
+}
+
+static void
+cmdquit(void)
+{
+	threadexitsall(nil);
+}
+
+static void
+cmdswitchfocus(void)
+{
+	dview->leftp->focused = !dview->leftp->focused;
+	dview->rightp->focused = !dview->rightp->focused;
+	dirviewredraw(dview);
+	sendul(dview->c, 1);
+}
+
+static void
+cmdreload(void)
+{
+	Dirpanel *p;
+	
+	p = dirviewcurrentpanel(dview);
+	dirmodelreload(p->model);
+}
+
+static void
+cmdcd(void)
+{
+	Dirpanel *p;
+	char buf[1024] = {0};
+
+	p = dirviewcurrentpanel(dview);
+	if(enter("cd:", buf, sizeof buf, mc, kc, nil) <= 0)
+		return;
+	dirpanelresetcursor(p);
+	dirmodelcd(p->model, buf);
+}
+
+static void
+cmdselectgroup(void)
+{
+	Dirpanel *p;
+	Dir d;
+	int i;
+	char buf[256] = {0};
+
+	p = dirviewcurrentpanel(dview);
+	if(enter("select:", buf, sizeof(buf), mc, kc, nil) <= 0)
+		return;
+	for(i = !p->model->isroot; i < dirmodelcount(p->model); i++){
+		d = dirmodelgetdir(p->model, i);
+		p->model->sel[i] = match(d.name, buf);
+	}
+	dirpanelredrawnotify(p);
+}
+
+static void
+cmdunselectgroup(void)
+{
+	Dirpanel *p;
+	Dir d;
+	int i, r;
+	char buf[256] = {0};
+
+	p = dirviewcurrentpanel(dview);
+	if(enter("unselect:", buf, sizeof(buf), mc, kc, nil) <= 0)
+		return;
+	r = 0;
+	for(i = !p->model->isroot; i < dirmodelcount(p->model); i++){
+		d = dirmodelgetdir(p->model, i);
+		if(match(d.name, buf) && p->model->sel[i]){
+			p->model->sel[i] = 0;
+			r = 1;
+		}
+	}
+	if(r)
+		dirpanelredrawnotify(p);
+}
+
+static void
+cmdinvertselection(void)
+{
+	Dirpanel *p;
+	int i;
+
+	p = dirviewcurrentpanel(dview);
+	for(i = !p->model->isroot; i < dirmodelcount(p->model); i++)
+		p->model->sel[i] = !p->model->sel[i];
+	dirpanelredrawnotify(p);
+}
+
+static void
+cmdselect(void)
+{
+	Dirpanel *p;
+	Dir d;
+	int i;
+
+	p = dirviewcurrentpanel(dview);
+	i = p->offset + p->cursor;
+	d = dirmodelgetdir(p->model, i);
+	if(i == 0 && !p->model->isroot)
+		return;
+	p->model->sel[i] = !p->model->sel[i];
+	if(p->cursor == p->nlines - 1 && (p->offset + p->nlines >= dirmodelcount(p->model)))
+		goto Draw;
+	if(p->offset + p->cursor + 1 >= dirmodelcount(p->model))
+		goto Draw;
+	if(p->cursor == p->nlines - 1){
+		p->offset += p->nlines;
+		p->cursor = 0;
+	}else{
+		p->cursor += 1;
+	}
+Draw:
+	dirpanelredrawnotify(p);
+}
+
+static void
+cmdfilter(void)
+{
+	Dirpanel *p;
+	char buf[256] = {0};
+
+	p = dirviewcurrentpanel(dview);
+	if(enter("filter:", buf, sizeof buf, mc, kc, nil) <= 0){
+		if(p->model->filter != nil){
+			dirpanelresetcursor(p);
+			dirmodelfilter(p->model, nil);
+		}
+	}else{
+		dirpanelresetcursor(p);
+		dirmodelfilter(p->model, buf);
+	}
+}
+
+static void
+cmdup(void)
+{
+	Dirpanel *p;
+
+	p = dirviewcurrentpanel(dview);
+	if(p->cursor == 0 && p->offset == 0)
+		return;
+	if(p->cursor == 0){
+		p->offset -= p->nlines;
+		p->cursor = p->nlines -1;
+	}else{
+		p->cursor -= 1;
+	}
+	dirpanelredrawnotify(p);
+}
+
+static void
+cmddown(void)
+{
+	Dirpanel *p;
+
+	p = dirviewcurrentpanel(dview);
+	if(p->cursor == p->nlines - 1 && (p->offset + p->nlines >= dirmodelcount(p->model)))
+		return;
+	if(p->offset + p->cursor + 1 >= dirmodelcount(p->model))
+		return;
+	if(p->cursor == p->nlines - 1){
+		p->offset += p->nlines;
+		p->cursor = 0;
+	}else{
+		p->cursor += 1;
+	}
+	dirpanelredrawnotify(p);
+}
+
+static void
+cmdhome(void)
+{
+	Dirpanel *p;
+
+	p = dirviewcurrentpanel(dview);
+	dirpanelresetcursor(p);
+	dirpanelredrawnotify(p);
+}
+
+static void
+cmdend(void)
+{
+	Dirpanel *p;
+
+	p = dirviewcurrentpanel(dview);
+	p->offset = p->nlines * (dirmodelcount(p->model) / p->nlines);
+	p->cursor = dirmodelcount(p->model) - p->offset - 1;
+	dirpanelredrawnotify(p);
+}
+
+static void
+cmdpageup(void)
+{
+	Dirpanel *p;
+
+	p = dirviewcurrentpanel(dview);
+	if(p->offset == 0 && p->cursor == 0)
+		return;
+	if(p->offset == 0)
+		p->cursor = 0;
+	else
+		p->offset -= p->nlines;
+	dirpanelredrawnotify(p);
+}
+
+static void
+cmdpagedown(void)
+{
+	Dirpanel *p;
+	int end;
+
+	p = dirviewcurrentpanel(dview);
+	end = dirmodelcount(p->model) - p->offset - 1;
+	if(p->offset + p->nlines >= dirmodelcount(p->model) && p->cursor == end)
+		return;
+	if(p->offset + p->nlines < dirmodelcount(p->model))
+		p->offset += p->nlines;
+	else
+		p->cursor = end;
+	if(p->cursor > end)
+		p->cursor = end;
+	dirpanelredrawnotify(p);
+}
+
+Binding	dirviewbindings[] = {
+	{ KF|1,		cmdhelp },
+	{ KF|3,		cmdview },
+	{ KF|4,		cmdplumb },
+	{ KF|5,		cmdcopy },
+	{ KF|6,		cmdrenmov },
+	{ KF|7,		cmdmkdir },
+	{ KF|8,		cmddelete },
+	{ KF|10,	cmdquit },
+	{ '\t',		cmdswitchfocus },
+	{ 'r',		cmdreload },
+	{ 'c',		cmdcd },
+	{ '\n',		cmdview },
+	{ '+',		cmdselectgroup },
+	{ '-',		cmdunselectgroup },
+	{ '*',		cmdinvertselection },
+	{ Kins,		cmdselect },
+	{ 'f',		cmdfilter },
+	{ Kup,		cmdup },
+	{ Kdown,	cmddown },
+	{ Khome,	cmdhome },
+	{ Kend,		cmdend },
+	{ Kpgup,	cmdpageup },
+	{ Kpgdown,	cmdpagedown },
+	nil
+};
+
+void
+setupdirviewbindings(void)
+{
+	bindings = dirviewbindings;
+	actionbarclear(abar);
+	actionbarset(abar, 1, "Help",   cmdhelp);
+	actionbarset(abar, 3, "View",   cmdview);
+	actionbarset(abar, 4, "Plumb",  cmdplumb);
+	actionbarset(abar, 5, "Copy",   cmdcopy);
+	actionbarset(abar, 6, "RenMov", cmdrenmov);
+	actionbarset(abar, 7, "Mkdir",  cmdmkdir);
+	actionbarset(abar, 8, "Delete", cmddelete);
+	actionbarset(abar, 10, "Quit",  cmdquit);
+}
--- /dev/null
+++ b/glob.c
@@ -1,0 +1,157 @@
+#include "a.h"
+
+#define	GLOB	((char)0x01)
+/*
+ * Is c the first character of a utf sequence?
+ */
+#define	onebyte(c)	(((c)&0x80)==0x00)
+#define twobyte(c)	(((c)&0xe0)==0xc0)
+#define threebyte(c)	(((c)&0xf0)==0xe0)
+#define fourbyte(c)	(((c)&0xf8)==0xf0)
+#define xbyte(c)	(((c)&0xc0)==0x80)
+
+/*
+ * Return a pointer to the next utf code in the string,
+ * not jumping past nuls in broken utf codes!
+ */
+static char*
+nextutf(char *p)
+{
+	int i, n, c = *p;
+
+	if(onebyte(c))
+		return p+1;
+	if(twobyte(c))
+		n = 2;
+	else if(threebyte(c))
+		n = 3;
+	else
+		n = 4;
+	for(i = 1; i < n; i++)
+		if(!xbyte(p[i]))
+			break;
+	return p+i;
+}
+
+/*
+ * Convert the utf code at *p to a unicode value
+ */
+static int
+unicode(char *p)
+{
+	int c = *p;
+
+	if(onebyte(c))
+		return c&0xFF;
+	if(twobyte(c)){
+		if(xbyte(p[1]))
+			return ((c&0x1F)<<6) | (p[1]&0x3F);
+	} else if(threebyte(c)){
+		if(xbyte(p[1]) && xbyte(p[2]))
+			return ((c&0x0F)<<12) | ((p[1]&0x3F)<<6) | (p[2]&0x3F);
+	} else if(fourbyte(c)){
+		if(xbyte(p[1]) && xbyte(p[2]) && xbyte(p[3]))
+			return ((c&0x07)<<18) | ((p[1]&0x3F)<<12) | ((p[2]&0x3F)<<6) | (p[3]&0x3F);
+	}
+	return -1;
+}
+
+/*
+ * Do p and q point at equal utf codes
+ */
+static int
+equtf(char *p, char *q)
+{
+	if(*p!=*q)
+ 		return 0;
+	return unicode(p) == unicode(q);
+}
+
+int
+domatch(char *s, char *p, int stop)
+{
+	int compl, hit, lo, hi, t, c;
+
+	for(; *p!=stop && *p!='\0'; s = nextutf(s), p = nextutf(p)){
+		if(*p!=GLOB){
+			if(!equtf(p, s)) return 0;
+		}
+		else switch(*++p){
+		case GLOB:
+			if(*s!=GLOB)
+				return 0;
+			break;
+		case '*':
+			for(;;){
+				if(domatch(s, nextutf(p), stop)) return 1;
+				if(!*s)
+					break;
+				s = nextutf(s);
+			}
+			return 0;
+		case '?':
+			if(*s=='\0')
+				return 0;
+			break;
+		case '[':
+			if(*s=='\0')
+				return 0;
+			c = unicode(s);
+			p++;
+			compl=*p=='~';
+			if(compl)
+				p++;
+			hit = 0;
+			while(*p!=']'){
+				if(*p=='\0')
+					return 0;		/* syntax error */
+				lo = unicode(p);
+				p = nextutf(p);
+				if(*p!='-')
+					hi = lo;
+				else{
+					p++;
+					if(*p=='\0')
+						return 0;	/* syntax error */
+					hi = unicode(p);
+					p = nextutf(p);
+					if(hi<lo){ t = lo; lo = hi; hi = t; }
+				}
+				if(lo<=c && c<=hi)
+					hit = 1;
+			}
+			if(compl)
+				hit=!hit;
+			if(!hit)
+				return 0;
+			break;
+		}
+	}
+	return *s=='\0';
+}
+
+/*
+ * Does the string s match the pattern p
+ * . and .. are only matched by patterns starting with .
+ * * matches any sequence of characters
+ * ? matches any single character
+ * [...] matches the enclosed list of characters
+ */
+
+int
+match(char *s, char *p)
+{
+	char pat[512] = {0};
+	int i, j;
+
+	if(s[0]=='.' && (s[1]=='\0' || s[1]=='.' && s[2]=='\0') && p[0]!='.')
+		return 0;
+	for(i = 0, j = 0; p[i] != '\0'; i++, j++){
+		if(i == 512) sysfatal("OVERFLOW IN GLOB PATTERN");
+		if(p[i] == '*' || p[i] == '[' || p[i] == '?')
+			pat[j++] = GLOB;
+		pat[j] = p[i];
+	}
+	return domatch(s, pat, '/');
+}
+
--- /dev/null
+++ b/main.c
@@ -1,0 +1,195 @@
+#include "a.h"
+
+Image		*cols[Ncols];
+Mousectl	*mc;
+Keyboardctl	*kc;
+int			mode;
+Dirview		*dview;
+Text		*text;
+Actionbar	*abar;
+Binding		*bindings;
+char*		help = 
+	"nein commander\n"
+	"A file manager for 9front\n";
+
+void
+colsinit(void)
+{
+	cols[Cbg] = display->white;
+	cols[Cfg] = display->black;
+	cols[Clfg] = ealloccolor(0x666666FF);
+	cols[Ctitle] = ealloccolor(DGreygreen);
+	cols[Cborder] = ealloccolor(0xAAAAAAFF);
+	cols[Csel] = ealloccolor(0xCCCCCCFF);
+}
+
+void
+redraw(void)
+{
+	draw(screen, screen->r, cols[Cbg], nil, ZP);
+	if(mode == Mdir){
+		dirviewredraw(dview);
+		draw(screen, dview->r, dview->b, nil, ZP);
+	}else if(mode == Mview || mode == Mhelp){
+		textredraw(text);
+		draw(screen, text->r, text->b, nil, ZP);
+	}
+	actionbarredraw(abar);
+	draw(screen, abar->r, abar->b, nil, ZP);
+	flushimage(display, 1);
+}
+
+void
+resize(void)
+{
+	Rectangle dr, ar;
+	int ah;
+	
+	ah = 2+font->height+2;
+	dr = screen->r;
+	dr.max.y -= ah;
+	dirviewsetrect(dview, dr);
+	textsetrect(text, dr);
+	ar = screen->r;
+	ar.min.y = dr.max.y;
+	actionbarsetrect(abar, ar);
+}
+
+void
+emouse(Mouse m)
+{
+	if(mode == Mdir)
+		dirviewemouse(dview, m);
+	else if(mode == Mview || mode == Mhelp)
+		textemouse(text, m);
+	actionbaremouse(abar, m);
+}
+
+void
+ekbd(Rune k)
+{
+	int i;
+	
+	for(i = 0; bindings[i].k != 0; i++){
+		if(bindings[i].k == k && bindings[i].f != nil){
+			bindings[i].f();
+			return;
+		}
+	}
+}
+
+void
+setmode(int m)
+{
+	mode = m;
+	switch(mode){
+	case Mdir:
+		setupdirviewbindings();
+		break;
+	case Mhelp:
+	case Mview:
+		setupviewerbindings();
+		break;
+	}
+	redraw();
+}
+
+enum
+{
+	Emouse,
+	Eresize,
+	Ekbd,
+	Edirview,
+	Eleftmodel,
+	Eleftpanel,
+	Erightmodel,
+	Erightpanel,
+	Etext,
+};
+
+void
+threadmain(int argc, char **argv)
+{
+	char *home;
+	Mouse m;
+	Rune k;
+	ulong l;
+	Alt alts[] = 
+	{
+		{ nil, &m,   CHANRCV },
+		{ nil, nil,  CHANRCV },
+		{ nil, &k,   CHANRCV },
+		{ nil, &l,   CHANRCV },
+		{ nil, &l,   CHANRCV },
+		{ nil, &l,   CHANRCV },
+		{ nil, &l,   CHANRCV },
+		{ nil, &l,   CHANRCV },
+		{ nil, &l,   CHANRCV },
+		{ nil, nil,  CHANEND },
+	};
+
+	ARGBEGIN{
+	}ARGEND
+	
+	if(initdraw(nil, nil, argv0) < 0)
+		sysfatal("initdraw: %r");
+	if((mc = initmouse(nil, screen)) == nil)
+		sysfatal("initmouse: %r");
+	if((kc = initkeyboard(nil)) == nil)
+		sysfatal("initkdb: %r");
+	display->locking = 0;
+	home = homedir();
+	dview = mkdirview(home);
+	text = mktext();
+	abar = mkactionbar();
+	colsinit();
+	resize();
+	setmode(Mdir);
+	alts[Emouse].c = mc->c;
+	alts[Eresize].c = mc->resizec;
+	alts[Ekbd].c = kc->c;
+	alts[Edirview].c = dview->c;
+	alts[Eleftmodel].c = dview->leftp->model->c;
+	alts[Eleftpanel].c = dview->leftp->c;
+	alts[Erightmodel].c = dview->rightp->model->c;
+	alts[Erightpanel].c = dview->rightp->c;
+	alts[Etext].c = text->c;
+	for(;;){
+		switch(alt(alts)){
+		case Emouse:
+			emouse(m);
+			break;
+		case Eresize:
+			if(getwindow(display, Refnone) < 0)
+				sysfatal("getwindow: %r");
+			resize();
+			redraw();
+			break;
+		case Ekbd:
+			ekbd(k);
+			break;
+		case Edirview:
+			draw(screen, dview->r, dview->b, nil, ZP);
+			flushimage(display, 1);
+			break;
+		case Eleftmodel:
+			dirpanelredraw(dview->leftp);
+		case Eleftpanel:
+			draw(dview->b, dview->leftr, dview->leftp->b, nil, ZP);
+			draw(screen, dview->r, dview->b, nil, ZP);
+			flushimage(display, 1);
+			break;		
+		case Erightmodel:
+			dirpanelredraw(dview->rightp);
+		case Erightpanel:
+			draw(dview->b, dview->rightr, dview->rightp->b, nil, ZP);
+			draw(screen, dview->r, dview->b, nil, ZP);
+			flushimage(display, 1);
+			break;
+		case Etext:
+			draw(screen, text->r, text->b, nil, ZP);
+			flushimage(display, 1);
+			break;
+		}
+	}
+}
--- /dev/null
+++ b/mkfile
@@ -1,0 +1,18 @@
+</$objtype/mkfile
+
+BIN=/$objtype/bin
+TARG=nc
+HFILES=a.h
+OFILES=\
+	main.$O			\
+	actionbar.$O	\
+	dirview.$O		\
+	dirviewcmd.$O	\
+	dirmodel.$O		\
+	dirpanel.$O		\
+	viewercmd.$O	\
+	text.$O			\
+	glob.$O			\
+	utils.$O
+
+</sys/src/cmd/mkone
--- /dev/null
+++ b/text.c
@@ -1,0 +1,317 @@
+#include "a.h"
+
+enum
+{
+	Scrollwidth = 12,
+	Padding = 4,
+};
+
+enum
+{
+	Msnarf,
+	Mplumb,
+};
+char *menu2str[] = {
+	"snarf",
+	"plumb",
+	nil,
+};
+Menu menu2 = { menu2str };
+
+static void
+computelines(Text *t)
+{
+	int i, x, w, l, c;
+	Rune r;
+
+	t->lines[0] = 0;
+	t->nlines = 1;
+	w = Dx(t->textr);
+	x = 0;
+	for(i = 0; i < t->ndata; ){
+		c = chartorune(&r, &t->data[i]);
+		if(r == '\n'){
+			if(i + c == t->ndata)
+				break;
+			t->lines[t->nlines++] = i + c;
+			x = 0;
+		}else{
+			l = 0;
+			if(r == '\t'){
+				x += stringwidth(font, "    ");
+			}else{
+				l = runestringnwidth(font, &r, 1);
+				x += l;
+			}
+			if(x > w){
+				t->lines[t->nlines++] = i;
+				x = l;
+			}
+		}
+		i += c;
+	}
+}
+
+static int
+indexat(Text *t, Point p)
+{
+	int line, i, s, e, x, c, l;
+	Rune r;
+	Rectangle textr;
+
+	textr = rectaddpt(t->textr, t->r.min);
+	if(!ptinrect(p, textr))
+		return -1;
+	line = t->offset + ((p.y - textr.min.y) / font->height);
+	s = t->lines[line];
+	if(line+1 >= t->nlines)
+		e = t->ndata;
+	else
+		e = t->lines[line+1] - 2;
+	x = textr.min.x;
+	for(i = s; i < e; ){
+		c = chartorune(&r, &t->data[i]);
+		if(r == '\t')
+			l = stringwidth(font, "    ");
+		else
+			l = runestringnwidth(font, &r, 1);
+		if(x <= p.x && p.x <= x+l)
+			break;
+		i += c;
+		x += l;
+	}
+	return i;
+}
+
+Text*
+mktext(void)
+{
+	Text *t;
+	
+	t = emalloc(sizeof *t);
+	t->c = chancreate(sizeof(ulong), 1);
+	t->b = nil;
+	t->s0 = -1;
+	t->s1 = -1;
+	t->offset = 0;
+	return t;
+}
+
+void
+textset(Text *t, char *title, char *data, usize ndata)
+{
+	t->s0 = -1;
+	t->s1 = -1;
+	t->offset = 0;
+	t->title = title;
+	t->data = data;
+	t->ndata = ndata;
+	computelines(t);
+}
+
+void
+textsetrect(Text *t, Rectangle r)
+{
+	t->r = r;
+	freeimage(t->b);
+	t->b = nil;
+	t->intr = insetrect(boundsrect(r), 2);
+	t->titler = t->intr;
+	t->titler.max.y = t->titler.min.y + 2 + font->height + 2;
+	t->textr = 	t->intr;
+	t->textr.min.x += 2;
+	t->textr.min.y = t->titler.max.y;
+	t->textr = insetrect(t->textr, 4);
+	t->vlines = Dy(t->textr) / font->height;
+	if(t->nlines > 0)
+		computelines(t);
+}
+
+static int
+selected(Text *t, int index)
+{
+	int s0, s1;
+
+	if(t->s0 < 0 || t->s1 < 0)
+		return 0;
+	s0 = t->s0 < t->s1 ? t->s0 : t->s1;
+	s1 = t->s0 > t->s1 ? t->s0 : t->s1;
+	return s0 <= index && index <= s1;
+}
+
+static void
+drawline(Text *t, int index)
+{
+	int i, s, e;
+	Point p;
+	Rune r;
+	Image *fg, *bg;
+
+	s = t->lines[t->offset+index];
+	if(t->offset+index+1 >= t->nlines)
+		e = t->ndata;
+	else
+		e = t->lines[t->offset+index+1];
+	p = addpt(t->textr.min, Pt(0, index*font->height));
+	for(i = s; i < e; ){
+		fg = cols[Cfg];
+		bg = selected(t, i) ? cols[Csel]  : cols[Cbg];
+		i += chartorune(&r, &t->data[i]);
+		if(r == '\n')
+			if(s + 1 == e) /* empty line */
+				r = L' ';
+			else
+				continue;
+		if(r == '\t')
+			p = stringbg(t->b, p, fg, ZP, font, "    ", bg, ZP);
+		else
+			p = runestringnbg(t->b, p, fg, ZP, font, &r, 1, bg, ZP);
+	}
+}
+
+void
+textredraw(Text *t)
+{
+	int i;
+	Rectangle r;
+
+	r = boundsrect(t->r);
+	if(t->b == nil)
+		t->b = allocimage(display, r, screen->chan, 0, DNofill);
+	draw(t->b, r, cols[Cbg], nil, ZP);
+	border(t->b, t->intr, 2, cols[Ctitle], ZP);
+	string(t->b, addpt(t->intr.min, Pt(4, 2)), cols[Cfg], ZP, font, t->title);
+	line(t->b, Pt(t->intr.min.x, t->titler.max.y + 1), Pt(t->intr.max.x, t->titler.max.y + 1), 0, 0, 0, cols[Ctitle], ZP);
+	for(i = 0; i < t->vlines; i++){
+		if(t->offset+i >= t->nlines)
+			break;
+		drawline(t, i);
+	}
+}
+
+void
+textscroll(Text *t, int lines)
+{
+	if(t->nlines <= t->vlines)
+		return;
+	if(lines < 0 && t->offset == 0)
+		return;
+	if(lines > 0 && t->offset + t->vlines >= t->nlines)
+		return;
+	t->offset += lines;
+	if(t->offset < 0)
+		t->offset = 0;
+	if(t->offset + t->nlines%t->vlines >= t->nlines)
+		t->offset = t->nlines - t->nlines%t->vlines;
+	textredraw(t);
+	sendul(t->c, 1);
+}
+
+static void
+snarfsel(Text *t)
+{
+	int fd, s0, s1;
+
+	if(t->s0 < 0 || t->s1 < 0)
+		return;
+	fd = open("/dev/snarf", OWRITE);
+	if(fd < 0)
+		return;
+	s0 = t->s0 < t->s1 ? t->s0 : t->s1;
+	s1 = t->s0 > t->s1 ? t->s0 : t->s1;
+	write(fd, &t->data[s0], s1 - s0 + 1);
+	close(fd);
+}
+
+static void
+plumbsel(Text *t)
+{
+	int fd, s0, s1;
+	char *s;
+
+	if(t->s0 < 0 || t->s1 < 0)
+		return;
+	fd = plumbopen("send", OWRITE);
+	if(fd < 0)
+		return;
+	s0 = t->s0 < t->s1 ? t->s0 : t->s1;
+	s1 = t->s0 > t->s1 ? t->s0 : t->s1;
+	s = smprint("%.*s", s1 - s0 + 1, &t->data[s0]);
+	plumbsendtext(fd, argv0, nil, nil, s);
+	free(s);
+	close(fd);
+}
+
+static void
+menu2hit(Text *t, Mousectl *mc)
+{
+	int n;
+
+	n = menuhit(2, mc, &menu2, nil);
+	switch(n){
+		case Msnarf:
+			snarfsel(t);
+			break;
+		case Mplumb:
+			plumbsel(t);
+			break;
+	}
+}
+
+void
+textemouse(Text *t, Mouse)
+{
+	static selecting = 0;
+	Point p;
+	int n;
+	Rectangle textr;
+
+	textr = rectaddpt(t->textr, t->r.min);
+	if(ptinrect(mc->xy, textr)){
+		if(mc->buttons == 0)
+			selecting = 0;
+		if(mc->buttons == 1){
+			if(!selecting){
+				selecting = 1;
+				t->s0 = t->s1 = -1;
+				n = indexat(t, mc->xy);
+				if(n < 0)
+					return;
+				t->s0 = n;
+				t->s1 = -1;
+				textredraw(t);
+				nbsendul(t->c, 1);
+			}else{
+				n = indexat(t, mc->xy);
+				if(n < 0)
+					return;
+				t->s1 = n;
+			}
+			for(readmouse(mc); mc->buttons == 1; readmouse(mc)){
+				p = mc->xy;
+				if(p.y <= textr.min.y){
+					textscroll(t, -1);
+					p.y = textr.min.y + 1;
+				}else if(p.y >= textr.max.y){
+					textscroll(t, 1);
+					p.y = textr.max.y - 1;
+				}
+				n = indexat(t, p);
+				if(n < 0)
+					break;
+				t->s1 = n;
+				textredraw(t);
+				nbsendul(t->c, 1);
+			}
+		}else if(mc->buttons == 2){
+			menu2hit(t, mc);
+		}else if(mc->buttons == 8){
+			n = mousescrollsize(t->vlines);
+			textscroll(t, -n);
+		}else if(mc->buttons == 16){
+			n = mousescrollsize(t->vlines);
+			textscroll(t, n);
+		}
+	}
+}
+
--- /dev/null
+++ b/utils.c
@@ -1,0 +1,123 @@
+#include "a.h"
+
+Rectangle
+boundsrect(Rectangle r)
+{
+	return Rect(0, 0, Dx(r), Dy(r));
+}
+
+Image*
+ealloccolor(ulong col)
+{
+	Image *b;
+	
+	b = allocimage(display, Rect(0, 0, 1, 1), screen->chan, 1, col);
+	if(b == nil)
+		sysfatal("allocimage: %r");
+	return b;
+}
+
+void*
+emalloc(ulong n)
+{
+	void *p;
+	
+	p = malloc(n);
+	if(p == nil)
+		sysfatal("malloc: %r");
+	return p;
+}
+
+void*
+erealloc(void *p, ulong n)
+{
+	void *t;
+	
+	t = realloc(p, n);
+	if(t == nil)
+		sysfatal("realloc: %r");
+	return t;
+}
+
+char*
+slurp(char *path)
+{
+	int fd;
+	long r, n, s;
+	char *buf;
+
+	n = 0;
+	s = 8192;
+	buf = malloc(s);
+	if(buf == nil)
+		return nil;
+	fd = open(path, OREAD);
+	if(fd < 0)
+		return nil;
+	for(;;){
+		r = read(fd, buf + n, s - n);
+		if(r < 0)
+			return nil;
+		if(r == 0)
+			break;
+		n += r;
+		if(n == s){
+			s *= 1.5;
+			buf = realloc(buf, s);
+			if(buf == nil)
+				return nil;
+		}
+	}
+	buf[n] = 0;
+	close(fd);
+	return buf;
+}
+
+char*
+homedir(void)
+{
+	Biobuf *bp;
+	char *s;
+	
+	bp = Bopen("/env/home", OREAD);
+	s = Brdstr(bp, 0, 0);
+	Bterm(bp);
+	if(s == nil)
+		s = strdup("/tmp");
+	return s;
+}
+
+char*
+abspath(char *wd, char *p)
+{
+	char *s;
+
+	if(p[0]=='/')
+		s = strdup(p);
+	else
+		s = smprint("%s/%s", wd, p);
+	cleanname(s);
+	return s;
+}
+
+int
+mkdir(char *wd, char *name)
+{
+	char *p;
+	int fd;
+	
+	p = abspath(wd, name);
+	if(access(p, 0) >= 0){
+		werrstr("directory already exists");
+		free(p);
+		return -1;
+	}
+	fd = create(p, OREAD, DMDIR|0755);
+	if(fd < 0){
+		free(p);
+		return -1;
+	}
+	free(p);
+	close(fd);
+	return 0;
+}
--- /dev/null
+++ b/viewercmd.c
@@ -1,0 +1,66 @@
+#include "a.h"
+
+static void
+cmdquit(void)
+{
+	if(mode == Mview){
+		free(text->title);
+		free(text->data);
+	}
+	setmode(Mdir);
+}
+
+static void
+cmdup(void)
+{
+	textscroll(text, -1);
+}
+
+static void
+cmddown(void)
+{
+	textscroll(text, 1);
+}
+
+static void
+cmdhome(void)
+{
+	textscroll(text, -text->nlines);
+}
+
+static void
+cmdend(void)
+{
+	textscroll(text, text->nlines);
+}
+
+static void
+cmdpageup(void)
+{
+	textscroll(text, -text->vlines);
+}
+
+static void
+cmdpagedown(void)
+{
+	textscroll(text, text->vlines);
+}
+
+Binding	viewerbindings[] = {
+	{ KF|10,	cmdquit },
+	{ Kup,		cmdup },
+	{ Kdown,	cmddown },
+	{ Khome,	cmdhome },
+	{ Kend,		cmdend },
+	{ Kpgup,	cmdpageup },
+	{ Kpgdown,	cmdpagedown },
+	nil
+};
+
+void
+setupviewerbindings(void)
+{
+	bindings = viewerbindings;
+	actionbarclear(abar);
+	actionbarset(abar, 10, "Quit", cmdquit);
+}