ref: e8246c63bfaf30f8c06a18ec371179191137ab73
author: phil9 <telephil9@gmail.com>
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 <telephil9@gmail.com>
+
+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);
+}