ref: 70c799c0e786e3c55633003c26c889ea9921fb54
author: phil9 <telephil9@gmail.com>
date: Thu Feb 3 13:05:04 EST 2022
initial import
--- /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,31 @@
+# mongrel
+An opinionated mail reader for plan9 inspired by mutt.
+mongrel only provides reading functionality, writing is done using nedmail.
+
+![mongrel](mongrel.png)
+
+If available, mongrel will use your current [theme](https://ftrv.se/14).
+
+## Quick start
+```sh
+% mk install
+% mongrel -m mbox
+```
+
+## Usage
+mongrel has two components:
+- the index which shows the list of messages
+- the pager which shows an individual mail
+
+Navigation in the index is done using the mouse, the arrow keys, page up/down, home and end. A left click on a given message will display its content. Right-clicking or pressing `enter` will display the message content (in this case the pager will open if not displayed already).
+Scrolling in the pager can be achieved with either the mouse or with a combination of pressing `alt` and the arrow keys or page up/down.
+The pager displays any attachments the message may have below the message headers. Right-clicking an individual attachment will send it to the plumber.
+`q` will hide the pager if it is open or quit mongrel if in the index.
+`Del` exit mongrel.
+
+## License
+MIT
+
+## Bugs
+This is work in progress and I already know of quite many of them.
+
--- /dev/null
+++ b/a.h
@@ -1,0 +1,97 @@
+typedef struct Mailbox Mailbox;
+typedef struct Message Message;
+typedef struct Mlist Mlist;
+typedef struct Mailevent Mailevent;
+
+struct Mailbox
+{
+ Lock;
+ char *name;
+ char *path;
+ int count;
+ int unseen;
+ Mlist* list;
+};
+
+struct Message
+{
+ int id;
+ char *path;
+ char *info;
+ char *from;
+ char *to;
+ char *cc;
+ char *sender;
+ char *subject;
+ char *date;
+ long time;
+ int flags;
+ char *type;
+ char *filename;
+ char *body;
+ Mlist *parts;
+};
+
+struct Mlist
+{
+ Message** elts;
+ usize nelts;
+ usize size;
+};
+
+enum
+{
+ Fanswered = 1<<0,
+ Fdeleted = 1<<1,
+ Fdraft = 1<<2,
+ Fflagged = 1<<3,
+ Frecent = 1<<4,
+ Fseen = 1<<5,
+ Fstored = 1<<6,
+};
+
+struct Mailevent
+{
+ int type;
+ char *path;
+};
+
+enum
+{
+ Enew,
+ Edelete,
+ Emodify,
+};
+
+Mailbox* loadmbox(char *name);
+void mesgloadbody(Message*);
+int mesgmarkseen(Mailbox*, Message*);
+void mboxadd(Mailbox *mbox, char *path);
+int mboxmod(Mailbox *mbox, char *path);
+void mboxdel(Mailbox *mbox, char *path);
+void mesgdel(Mailbox *mbox, Message *m);
+void seemailproc(void *v);
+
+Mlist* mkmlist(usize cap);
+int mladd(Mlist*, Message*);
+int mlins(Mlist*, usize, Message*);
+Message* mldel(Mlist*, usize);
+
+/* index */
+void indexinit(Channel*, Channel*, Theme*);
+Rectangle indexresize(Rectangle, int);
+void indexdraw(void);
+void indexmouse(Mouse);
+void indexkey(Rune);
+void indexresetsel(void);
+void indexswitch(Mailbox*);
+
+/* pager */
+void pagerinit(Mousectl*, Theme*);
+void pagerresize(Rectangle);
+void pagerdraw(void);
+void pagermouse(Mouse);
+void pagerkey(Rune);
+void pagershow(Message*);
+
+
--- /dev/null
+++ b/index.c
@@ -1,0 +1,337 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <thread.h>
+#include "theme.h"
+#include "a.h"
+
+enum
+{
+ Padding = 4,
+ Scrollwidth = 12,
+ Scrollgap = 2,
+ Scrollminh = 5,
+ Collapsedlines = 10,
+};
+
+enum
+{
+ BACK,
+ TEXT,
+ HIGH,
+ SCRL,
+ NCOLS,
+};
+
+enum
+{
+ Mreply,
+ Mreplyall,
+ Mforward,
+ Mdelete,
+};
+
+char *menustr[] =
+{
+ "reply",
+ "reply all",
+ "forward",
+ "delete",
+ nil
+};
+
+Menu menu =
+{
+ menustr
+};
+
+Channel *showc;
+Channel *selc;
+Mailbox *mbox;
+static Image *cols[NCOLS];
+static Rectangle viewr;
+static Rectangle listr;
+static Rectangle scrollr;
+static int nlines;
+static int offset;
+static int sel;
+static int lineh;
+
+void
+indexresetsel(void)
+{
+ sel = 0;
+ offset = 0;
+}
+
+void
+indexswitch(Mailbox *mb)
+{
+ indexresetsel();
+ mbox = mb;
+}
+
+
+Message*
+messageat(int index)
+{
+ index = mbox->count - index - 1;
+ return mbox->list->elts[index];
+}
+
+void
+drawmessage(Message *m, Point p, int selected)
+{
+ const Rune *ellipsis = L"…";
+ Image *fg, *bg;
+ char *s, buf[9];
+ char n, r;
+ Tm t;
+ Rune rn;
+ int i, w;
+ Point pe;
+
+ bg = cols[HIGH];
+ fg = cols[TEXT];
+ if(selected)
+ draw(screen, Rect(p.x, p.y-Padding/2, p.x+Dx(viewr), p.y+lineh-Padding/2), bg, nil, ZP);
+ n = m->flags&Fseen?' ':'N';
+ r = m->flags&Fanswered ? 'R':' ';
+ snprint(buf, sizeof buf, "[%c%c] ", n, r);
+ p = string(screen, p, fg, ZP, font, buf);
+ tmtime(&t, m->time, nil);
+ snprint(buf, sizeof buf, "%τ", tmfmt(&t, "DD/MM/YY"));
+ p = string(screen, p, fg, ZP, font, buf);
+ p = string(screen, p, fg, ZP, font, " ");
+ s = m->sender;
+ pe = addpt(p, Pt(20*stringwidth(font, " "), 0));
+ for(i = 0; i < 20; i++){
+ if(*s == '@')
+ s = "";
+ if(*s && i == 19){
+ p = runestringn(screen, p, fg, ZP, font, ellipsis, 1);
+ break;
+ }else if(*s){
+ s += chartorune(&rn, s);
+ p = runestringn(screen, p, fg, ZP, font, &rn, 1);
+ }else
+ p = stringn(screen, p, fg, ZP, font, " ", 1);
+ }
+ p = string(screen, pe, fg, ZP, font, " ");
+ s = m->subject;
+ while(s && *s){
+ s += chartorune(&rn, s);
+ w = runestringnwidth(font, &rn, 1);
+ if(p.x + w + 2*Padding > viewr.max.x){
+ runestringn(screen, p, fg, ZP, font, ellipsis, 1);
+ break;
+ }
+ p = runestringn(screen, p, fg, ZP, font, &rn, 1);
+ }
+}
+
+void
+indexdraw(void)
+{
+ Rectangle scrposr;
+ Point p;
+ int i, h, y;
+
+ draw(screen, viewr, cols[BACK], nil, ZP);
+ draw(screen, scrollr, cols[SCRL], nil, ZP);
+ h = ((double)nlines/mbox->count) * Dy(scrollr);
+ y = ((double)offset/mbox->count) * Dy(scrollr);
+ if(h < Scrollminh)
+ h = Scrollminh;
+ scrposr = Rpt(addpt(scrollr.min, Pt(0,y)), addpt(scrollr.min, Pt(Dx(scrollr)-1, y+h)));
+ draw(screen, scrposr, cols[BACK], nil, ZP);
+ p = addpt(listr.min, Pt(Padding, Padding));
+ for(i = offset; i < offset + nlines; i++){
+ if(i >= mbox->list->nelts)
+ break;
+ drawmessage(messageat(i), p, i == sel);
+ p.y += lineh;
+ }
+}
+
+void
+indexdrawsync(void)
+{
+ indexdraw();
+ flushimage(display, 1);
+}
+
+Rectangle
+indexresize(Rectangle r, int collapsed)
+{
+ lineh = font->height + Padding;
+ viewr = r;
+ if(collapsed)
+ viewr.max.y = viewr.min.y + Collapsedlines * lineh + Padding;
+ scrollr = viewr;
+ scrollr.max.x = scrollr.min.x + Scrollwidth + Scrollgap;
+ scrollr = insetrect(scrollr, 1);
+ listr = viewr;
+ listr.min.x += Scrollwidth + Scrollgap;
+ listr.max.x -= Padding;
+ nlines = Dy(viewr) / lineh;
+ return viewr;
+}
+
+void
+indexinit(Channel *c0, Channel *c1, Theme *theme)
+{
+ Rectangle r;
+
+ sel = 0;
+ offset = 0;
+ showc = c0;
+ selc = c1;
+ if(theme != nil){
+ cols[BACK] = theme->back;
+ cols[TEXT] = theme->text;
+ cols[HIGH] = theme->border;
+ cols[SCRL] = theme->border;
+ }else{
+ r = Rect(0, 0, 1, 1);
+ cols[BACK] = display->white;
+ cols[TEXT] = display->black;
+ cols[HIGH] = allocimage(display, r, screen->chan, 1, 0xCCCCCCFF);
+ cols[SCRL] = allocimage(display, r, screen->chan, 1, 0x999999FF);
+ /*
+ cols[BACK] = allocimage(display, r, screen->chan, 1, 0x282828FF);
+ cols[TEXT] = allocimage(display, r, screen->chan, 1, 0xA89984FF);
+ cols[HIGH] = allocimage(display, r, screen->chan, 1, 0x3C3836FF);
+ cols[SCRL] = allocimage(display, r, screen->chan, 1, 0x504945FF);
+ */
+ }
+}
+
+void
+scroll(int Δ, int ssel)
+{
+ int nelts;
+
+ nelts = mbox->list->nelts;
+ if(nelts <= nlines)
+ return;
+ if(Δ < 0 && offset == 0)
+ return;
+ if(Δ > 0 && offset + nlines >= nelts)
+ return;
+ offset += Δ;
+ if(offset < 0)
+ offset = 0;
+ if(offset + nelts%nlines >= nelts)
+ offset = nelts - nelts%nlines;
+ if(ssel){
+ if(Δ > 0)
+ sel = 0;
+ else
+ sel = nlines - 1;
+ }
+ indexdrawsync();
+}
+
+void
+changesel(int Δ)
+{
+ if(Δ < 0 && sel == 0)
+ return;
+ if(Δ > 0 && sel == mbox->count - 1)
+ return;
+ sel += Δ;
+ indexdrawsync();
+}
+
+void
+setsel(Point p)
+{
+ int n;
+
+ n = (p.y-listr.min.y)/lineh;
+ sel = n+offset;
+ indexdrawsync();
+}
+
+void
+indexmouse(Mouse m)
+{
+ int sl;
+
+ if(!ptinrect(m.xy, viewr))
+ return;
+ if(m.buttons & 1){
+ setsel(m.xy);
+ sendp(selc, messageat(sel));
+ }else if(m.buttons & 2){
+ /* TODO: menu */
+ }else if(m.buttons & 4){
+ setsel(m.xy);
+ sendp(showc, messageat(sel));
+ }else if(m.buttons & 8){
+ sl = mousescrollsize(nlines);
+ scroll(-sl, 0);
+ }else if(m.buttons & 16){
+ sl = mousescrollsize(nlines);
+ scroll(sl, 0);
+ }
+}
+
+void
+indexkey(Rune k)
+{
+ switch(k){
+ case Kup:
+ if(sel == offset)
+ scroll(-nlines, 1);
+ else if(sel > offset)
+ changesel(-1);
+ sendp(selc, messageat(sel));
+ break;
+ case Kdown:
+ if(sel < (mbox->count - 1)){
+ if(sel == offset + nlines - 1){
+ sel = offset + nlines;
+ scroll(nlines, 0);
+ }else
+ changesel(1);
+ sendp(selc, messageat(sel));
+ }
+ break;
+ case '\n':
+ sendp(showc, messageat(sel));
+ break;
+ case Kpgup:
+ if(sel > 0){
+ sel -= nlines;
+ if(sel < 0)
+ sel = 0;
+ scroll(-nlines, 0);
+ sendp(selc, messageat(sel));
+ }
+ break;
+ case Kpgdown:
+ if(sel < (mbox->count - 1)){
+ sel += nlines;
+ if(sel >= mbox->count)
+ sel = mbox->count - 1;
+ scroll(nlines, 0);
+ sendp(selc, messageat(sel));
+ }
+ break;
+ case Khome:
+ sel = 0;
+ scroll(-mbox->count, 0);
+ sendp(selc, messageat(sel));
+ break;
+ case Kend:
+ sel = mbox->count - 1;
+ scroll(mbox->count, 0);
+ sendp(selc, messageat(sel));
+ break;
+ }
+}
+
--- /dev/null
+++ b/kbd.c
@@ -1,0 +1,113 @@
+#include <u.h>
+#include <libc.h>
+#include <thread.h>
+#include <keyboard.h>
+#include "kbd.h"
+
+void
+kbdproc(void *v)
+{
+ Kbdctl *kc;
+ char buf[128], buf2[128], *s;
+ long n;
+ Rune r;
+ Key k;
+ int mods = 0;
+
+ kc = v;
+ threadsetname("kbdproc");
+ buf[0] = 0;
+ buf2[0] = 0;
+ buf2[1] = 0;
+ while(kc->fd >= 0){
+ if(buf[0] != 0){
+ n = strlen(buf)+1;
+ memmove(buf, buf+n, sizeof(buf)-n);
+ }
+ if (buf[0] == 0) {
+ n = read(kc->fd, buf, sizeof(buf)-1);
+ if (n <= 0){
+ yield();
+ if(kc->fd < 0)
+ threadexits(nil);
+ break;
+ }
+ buf[n-1] = 0;
+ buf[n] = 0;
+ }
+ switch(buf[0]){
+ case 'c':
+ if(chartorune(&r, buf+1) > 0 && r != Runeerror){
+ k = (Key){ r, mods };
+ nbsend(kc->c, &k);
+ }
+ default:
+ continue;
+ case 'k':
+ s = buf+1;
+ while(*s){
+ s += chartorune(&r, s);
+ if(utfrune(buf2+1, r) == nil){
+ if(r == Kctl)
+ mods |= Mctrl;
+ else if(r == Kalt)
+ mods |= Malt;
+ else if(r == Kshift)
+ mods |= Mshift;
+ else{
+ k = (Key){r, mods};
+ nbsend(kc->c, &k);
+ }
+ }
+ }
+ break;
+ case 'K':
+ s = buf2+1;
+ while(*s){
+ s += chartorune(&r, s);
+ if(utfrune(buf+1, r) == nil) {
+ if(r == Kctl)
+ mods ^= Mctrl;
+ else if(r == Kalt)
+ mods ^= Malt;
+ else if(r == Kshift)
+ mods ^= Mshift;
+ }
+ }
+ break;
+ }
+ strcpy(buf2, buf);
+ }
+}
+
+Kbdctl*
+initkbd(void)
+{
+ Kbdctl *kc;
+
+ kc = malloc(sizeof *kc);
+ if(kc == nil)
+ return nil;
+ kc->fd = open("/dev/kbd", OREAD);
+ if(kc->fd < 0){
+ free(kc);
+ return nil;
+ }
+ kc->c = chancreate(sizeof(Key), 0);
+ if(kc->c == nil){
+ close(kc->fd);
+ free(kc);
+ return nil;
+ }
+ kc->pid = proccreate(kbdproc, kc, 8192);
+ return kc;
+}
+
+void
+closekbd(Kbdctl *kc)
+{
+ close(kc->fd);
+ kc->fd = -1;
+ threadint(kc->pid);
+}
+
--- /dev/null
+++ b/kbd.h
@@ -1,0 +1,25 @@
+typedef struct Kbdctl Kbdctl;
+typedef struct Key Key;
+
+struct Kbdctl
+{
+ int fd;
+ int pid;
+ Channel *c;
+};
+
+struct Key
+{
+ Rune k;
+ ushort mods;
+};
+
+enum
+{
+ Mctrl = 1<<0,
+ Malt = 1<<1,
+ Mshift = 1<<2,
+};
+
+Kbdctl *initkbd(void);
+void closekbd(Kbdctl*);
--- /dev/null
+++ b/main.c
@@ -1,0 +1,318 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <draw.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <thread.h>
+#include <plumb.h>
+#include "theme.h"
+#include "a.h"
+#include "kbd.h"
+
+enum
+{
+ Padding = 4,
+};
+
+enum
+{
+ Emouse,
+ Eresize,
+ Ekeyboard,
+ Eseemail,
+ Eshowmesg,
+ Eselmesg,
+};
+
+enum
+{
+ BACK,
+ TEXT,
+ BORD,
+ NCOLS,
+};
+
+Mousectl *mctl;
+Kbdctl *kctl;
+Channel *showc;
+Channel *selc;
+Channel *eventc;
+Mailbox *mboxes[16];
+int nmboxes;
+Mailbox *mbox;
+static Image *cols[NCOLS];
+Rectangle headr;
+Rectangle indexr;
+Rectangle pagerr;
+int collapsed = 0;
+
+void
+drawheader(void)
+{
+ char buf[255] = {0};
+ Point p;
+
+ draw(screen, headr, cols[BACK], nil, ZP);
+ p = headr.min;
+ p.x += Padding;
+ p.y += Padding / 2;
+ if(mbox->unseen > 0)
+ snprint(buf, sizeof buf, "» %s [total:%d - new:%d]", mbox->name, mbox->count, mbox->unseen);
+ else
+ snprint(buf, sizeof buf, "» %s [total:%d]", mbox->name, mbox->count);
+ string(screen, p, cols[TEXT], ZP, font, buf);
+ line(screen, Pt(headr.min.x, headr.max.y), headr.max, 0, 0, 0, cols[BORD], ZP);
+}
+
+void
+redraw(void)
+{
+ draw(screen, screen->r, cols[BACK], nil, ZP);
+ drawheader();
+ indexdraw();
+ if(collapsed){
+ line(screen, Pt(indexr.min.x, indexr.max.y), indexr.max, 0, 0, 0, cols[BORD], ZP);
+ pagerdraw();
+ }
+ flushimage(display, 1);
+}
+
+void
+resize(void)
+{
+ headr = screen->r;
+ headr.max.y = headr.min.y+Padding+font->height;
+ indexr = screen->r;
+ indexr.min.y = headr.max.y + 1;
+ indexr = indexresize(indexr, collapsed);
+ if(collapsed){
+ pagerr = screen->r;
+ pagerr.min.y = indexr.max.y + 1;
+ pagerresize(pagerr);
+ }
+ redraw();
+}
+
+void
+mouse(Mouse m)
+{
+ indexmouse(m);
+ pagermouse(m);
+}
+
+void
+key(Key k)
+{
+ if(k.k == Kdel)
+ threadexitsall(nil);
+ else if(k.k == 'q'){
+ if(collapsed){
+ collapsed = 0;
+ resize();
+ }else
+ threadexitsall(nil);
+ }else if(k.mods == Malt){
+ if(collapsed)
+ pagerkey(k.k);
+ }else
+ indexkey(k.k);
+}
+
+void
+seemail(Mailevent e)
+{
+ Mailbox *mb;
+ char *s;
+ int i;
+
+ for(mb = nil, i = 0; i < nmboxes; i++){
+ if(strncmp(mboxes[i]->path, e.path, strlen(mboxes[i]->path))==0){
+ mb = mboxes[i];
+ break;
+ }
+ }
+ if(mb==nil)
+ return;
+ s = e.path;
+ switch(e.type){
+ case Enew:
+ mboxadd(mb, s);
+ if(mb==mbox){
+ indexresetsel();
+ redraw();
+ }
+ break;
+ case Edelete:
+ mboxdel(mb, s);
+ if(mb==mbox){
+ indexresetsel();
+ redraw();
+ }
+ break;
+ case Emodify:
+ if(mboxmod(mb, s) && mb==mbox)
+ redraw();
+ break;
+ }
+ free(s);
+}
+
+void
+init(Channel *selc)
+{
+ Theme *theme;
+ Rectangle r;
+
+ theme = loadtheme();
+ if(theme != nil){
+ cols[BACK] = theme->back;
+ cols[TEXT] = theme->text;
+ cols[BORD] = theme->title;
+ }else{
+ r = Rect(0, 0, 1, 1);
+ cols[BACK] = allocimage(display, r, screen->chan, 1, 0xFFFFFFFF);
+ cols[TEXT] = allocimage(display, r, screen->chan, 1, 0x000000FF);
+ cols[BORD] = allocimage(display, r, screen->chan, 1, DGreygreen);
+ /*
+ cols[BACK] = allocimage(display, r, screen->chan, 1, 0x282828FF);
+ cols[TEXT] = allocimage(display, r, screen->chan, 1, 0xA89984FF);
+ cols[BORD] = allocimage(display, r, screen->chan, 1, 0x98971AFF);
+ */
+ }
+ indexinit(showc, selc, theme);
+ pagerinit(mctl, theme);
+}
+
+void
+switchmbox(int n)
+{
+ if(mbox==mboxes[n])
+ return;
+ mbox = mboxes[n];
+ indexswitch(mbox);
+}
+
+void
+plumbmsg(Message *m)
+{
+ int fd;
+
+ fd = plumbopen("send", OWRITE|OCEXEC);
+ if(fd<0)
+ return;
+ plumbsendtext(fd, "mongrel", nil, nil, m->path);
+ close(fd);
+}
+
+void
+showmsg(Message *m)
+{
+ if(collapsed == 0){
+ collapsed = 1;
+ resize();
+ }
+ pagershow(m);
+ if(mesgmarkseen(mbox, m))
+ redraw();
+}
+
+void
+selchanged(Message *m)
+{
+ if(collapsed == 0)
+ return;
+ showmsg(m);
+}
+
+
+void
+usage(void)
+{
+ fprint(2, "usage: %s [-m maildir]\n", argv0);
+ exits("usage");
+}
+
+void
+threadmain(int argc, char *argv[])
+{
+ Mouse m;
+ Key k;
+ Mailevent e;
+ Message *msg;
+ Alt alts[] =
+ {
+ { nil, &m, CHANRCV },
+ { nil, nil, CHANRCV },
+ { nil, &k, CHANRCV },
+ { nil, &e, CHANRCV },
+ { nil, &msg, CHANRCV },
+ { nil, &msg, CHANRCV },
+ { nil, nil, CHANEND },
+ };
+ char *s;
+
+ nmboxes = 0;
+ ARGBEGIN{
+ case 'm':
+ s = EARGF(usage());
+ mboxes[nmboxes++] = loadmbox(s);
+ break;
+ default:
+ fprint(2, "unknown flag '%c'\n", ARGC());
+ usage();
+ }ARGEND
+ if(nmboxes==0){
+ fprint(2, "no maildir specified\n");
+ usage();
+ }
+ tmfmtinstall();
+ if(initdraw(nil, nil, "mongrel")<0)
+ sysfatal("initdraw: %r");
+ display->locking = 0;
+ if((mctl = initmouse(nil, screen)) == nil)
+ sysfatal("initmouse: %r");
+ if((kctl = initkbd()) == nil)
+ sysfatal("initkbd: %r");
+ if((eventc = chancreate(sizeof(Mailevent), 0))==nil)
+ sysfatal("chancreate: %r");
+ if((showc = chancreate(sizeof(Message*), 1))==nil)
+ sysfatal("chancreate: %r");
+ if((selc = chancreate(sizeof(Message*), 1))==nil)
+ sysfatal("chancreate: %r");
+ alts[0].c = mctl->c;
+ alts[1].c = mctl->resizec;
+ alts[2].c = kctl->c;
+ alts[3].c = eventc;
+ alts[4].c = showc;
+ alts[5].c = selc;
+ proccreate(seemailproc, eventc, 8192);
+ init(selc);
+ switchmbox(0);
+ resize();
+ for(;;){
+ switch(alt(alts)){
+ case Emouse:
+ mouse(m);
+ break;
+ case Eresize:
+ if(getwindow(display, Refnone)<0)
+ sysfatal("cannot reattach: %r");
+ resize();
+ break;
+ case Ekeyboard:
+ key(k);
+ break;
+ case Eseemail:
+ seemail(e);
+ break;
+ case Eshowmesg:
+ showmsg(msg);
+ break;
+ case Eselmesg:
+ //plumbmsg(msg);
+ selchanged(msg);
+ break;
+ }
+ }
+}
--- /dev/null
+++ b/mbox.c
@@ -1,0 +1,344 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <plumb.h>
+#include <thread.h>
+#include <draw.h>
+#include <mouse.h>
+#include "theme.h"
+#include "a.h"
+
+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*
+readpart(char *path, char *part)
+{
+ Biobuf *bp;
+ char *f, *s;
+
+ f = smprint("%s/%s", path, part);
+ if(f==nil)
+ sysfatal("smprint: %r");
+ bp = Bopen(f, OREAD);
+ if(bp==nil)
+ sysfatal("bopen %s: %r", f);
+ s = Brdstr(bp, '\n', 1);
+ Bterm(bp);
+ return s;
+}
+
+int
+parseflags(char *s)
+{
+ int f;
+
+ f = 0;
+ if(s[0]!='-')
+ f |= Fanswered;
+ if(s[5]!='-')
+ f |= Fseen;
+ return f;
+}
+
+int
+readflags(char *path)
+{
+ char *f;
+ int i;
+
+ f = readpart(path, "flags");
+ i = parseflags(f);
+ free(f);
+ return i;
+}
+
+#define Datefmt "?WWW, ?MMM ?DD hh:mm:ss ?Z YYYY"
+
+/* most code stolen from nedmail */
+Message*
+loadmessage(char *path)
+{
+ Message *m;
+ char p[256], *f[20+1];
+ int n;
+ Tm tm;
+
+ snprint(p, sizeof p, "%s/info", path);
+ m = mallocz(sizeof *m, 1);
+ if(m==nil)
+ sysfatal("mallocz: %r");
+ m->path = strdup(path);
+ m->info = slurp(p);
+ if(m->info == nil)
+ sysfatal("read info: %r");
+ n = getfields(m->info, f, nelem(f), 0, "\n");
+ if(n < 17)
+ sysfatal("invalid info file %s: only %d fields", path, n);
+ m->from = f[0];
+ m->to = f[1];
+ m->cc = f[2];
+ m->date = f[4];
+ m->subject = f[5];
+ m->type = f[6];
+ m->filename = f[8];
+ if(n > 17)
+ m->flags = parseflags(f[17]);
+ else
+ m->flags = readflags(path);
+ if(n >= 20 && f[19] != nil && strlen(f[19]) > 0)
+ m->sender = f[19];
+ else
+ m->sender = strdup(m->from);
+ m->time = time(nil);
+ if(tmparse(&tm, Datefmt, m->date, nil, nil) != nil)
+ m->time = tmnorm(&tm);
+ return m;
+}
+
+void
+mesgloadbody(Message *m)
+{
+ char path[255];
+ int i;
+ Dir *d;
+ Message *p;
+
+ if(m->body != nil || m->parts != nil)
+ return;
+ snprint(path, sizeof path, "%s/body", m->path);
+ m->body = slurp(path);
+ if(m->type && strncmp(m->type, "multipart/", 10) == 0){
+ m->parts = mkmlist(8);
+ for(i = 1; ; i++){
+ snprint(path, sizeof path, "%s/%d", m->path, i);
+ d = dirstat(path);
+ if(d == nil)
+ break;
+ if((d->qid.type & QTDIR) != QTDIR){
+ free(d);
+ break;
+ }
+ free(d);
+ p = loadmessage(path);
+ mesgloadbody(p);
+ mladd(m->parts, p);
+ }
+ }
+}
+
+int
+mesgmarkseen(Mailbox *mbox, Message *m)
+{
+ char path[255];
+ int fd;
+
+ if(m->flags & Fseen)
+ return 0;
+ snprint(path, sizeof path, "%s/flags", m->path);
+ fd = open(path, OWRITE);
+ if(fd < 0)
+ return 0;
+ fprint(fd, "+s");
+ close(fd);
+ m->flags |= Fseen;
+ mbox->unseen -= 1;
+ return 1;
+}
+
+int
+dircmp(Dir *a, Dir *b)
+{
+ return atoi(a->name) - atoi(b->name);
+}
+
+Mailbox*
+loadmbox(char *name)
+{
+ Mailbox *mb;
+ Dir *d;
+ int n, fd, i;
+ char buf[256];
+ Message *m;
+
+ mb = mallocz(sizeof(Mailbox), 1);
+ if(mb==nil)
+ sysfatal("malloc: %r");
+ mb->name = strdup(name);
+ mb->path = smprint("/mail/fs/%s", name);
+ fd = open(mb->path, OREAD);
+ if(fd<0)
+ sysfatal("open: %r");
+ n = dirreadall(fd, &d);
+ close(fd);
+ qsort(d, n, sizeof *d, (int(*)(void*,void*))dircmp);
+ mb->list = mkmlist(n*1.5);
+ for(i = 1; i < n; i++){
+ snprint(buf, sizeof buf, "%s/%s", mb->path, d[i].name);
+ if((d[i].qid.type & QTDIR)==0)
+ continue;
+ m = loadmessage(buf);
+ m->id = atoi(d[i].name);
+ mladd(mb->list, m);
+ if((m->flags & Fseen) == 0)
+ ++mb->unseen;
+ ++mb->count;
+ }
+ free(d);
+ return mb;
+}
+
+void
+mboxadd(Mailbox *mbox, char *path)
+{
+ Message *m;
+ int i, id;
+
+ id = atoi(path+strlen(mbox->path)+1);
+ for(i = 0; i < mbox->list->nelts; i++){
+ m = mbox->list->elts[i];
+ if(m->id == id)
+ return;
+ }
+ m = loadmessage(path);
+ m->id = id;
+ if((m->flags & Fseen) == 0)
+ ++mbox->unseen;
+ mladd(mbox->list, m);
+ ++mbox->count;
+}
+
+int
+mboxmod(Mailbox *mbox, char *path)
+{
+ Message *m;
+ int i, f;
+
+ m = nil;
+ for(i = 0; i < mbox->list->nelts; i++){
+ m = mbox->list->elts[i];
+ if(strcmp(path, m->path)==0)
+ break;
+ }
+ if(m==nil){
+ fprint(2, "could not find mail '%s'\n", path);
+ return 0;
+ }
+ f = readflags(path);
+ if(m->flags != f){
+ if((m->flags & Fseen) != 0 && (f & Fseen) == 0)
+ ++mbox->unseen;
+ else if((m->flags & Fseen) == 0 && (f & Fseen) != 0)
+ --mbox->unseen;
+ m->flags = f;
+ return 1;
+ }
+ return 0;
+}
+
+void
+mboxdel(Mailbox *mbox, char *path)
+{
+ Message *m;
+ int i;
+
+ m = nil;
+ for(i = 0; i < mbox->list->nelts; i++){
+ m = mbox->list->elts[i];
+ if(strcmp(path, m->path)==0){
+ mldel(mbox->list, i);
+ free(m->path);
+ free(m->info);
+ free(m->sender);
+ free(m);
+ --mbox->count;
+ return;
+ }
+ }
+ if(m==nil){
+ fprint(2, "could not find mail '%s'\n", path);
+ return;
+ }
+}
+
+void
+mesgdel(Mailbox *mbox, Message *m)
+{
+ int fd;
+
+ fd = open("/mail/fs/ctl", OWRITE);
+ if(fd<0)
+ sysfatal("open: %r");
+ fprint(fd, "delete %s %s", mbox->name, m->path+strlen(mbox->path)+1);
+ close(fd);
+}
+
+void
+seemailproc(void *v)
+{
+ Channel *c;
+ Mailevent e;
+ Plumbmsg *m;
+ int fd;
+ char *s;
+
+ c = v;
+ threadsetname("seemailproc");
+ fd = plumbopen("seemail", OREAD);
+ if(fd<0)
+ sysfatal("cannot open seemail: %r");
+ for(;;){
+ m = plumbrecv(fd);
+ if(m==nil)
+ sysfatal("seemail plumbrecv: %r");
+ s = plumblookup(m->attr, "filetype");
+ if(s != nil && strcmp(s, "vwhois") == 0){
+ plumbfree(m);
+ continue;
+ }
+ s = plumblookup(m->attr, "mailtype");
+ if(strcmp(s, "modify")==0)
+ e.type = Emodify;
+ else if(strcmp(s, "delete")==0)
+ e.type = Edelete;
+ else if(strcmp(s, "new")==0)
+ e.type = Enew;
+ else
+ fprint(2, "received unknown message type: %s\n", s);
+ e.path = strdup(m->data);
+ send(c, &e);
+ plumbfree(m);
+ }
+}
--- /dev/null
+++ b/mkfile
@@ -1,0 +1,8 @@
+</$objtype/mkfile
+
+BIN=/$objtype/bin
+TARG=mongrel
+OFILES=main.$O index.$O pager.$O mbox.$O utils.$O kbd.$O text.$O theme.$O
+HFILES=a.h w.h kbd.h theme.h
+
+</sys/src/cmd/mkone
binary files /dev/null b/mongrel.png differ
--- /dev/null
+++ b/pager.c
@@ -1,0 +1,277 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <thread.h>
+#include <plumb.h>
+#include "theme.h"
+#include "a.h"
+#include "w.h"
+
+enum
+{
+ Padding = 4,
+};
+
+static Mousectl *mc;
+static Text text;
+static Rectangle viewr;
+static Rectangle headr;
+static Rectangle partsr;
+static Rectangle textr;
+static Image *cols[NCOLS];
+static Image *headercol;
+static Message *mesg;
+static Tzone *tz;
+static int showcc;
+static Message *parts[16];
+static int nparts;
+
+char*
+findtextpart(Message *m)
+{
+ Message *p;
+ int i;
+
+ if(strncmp(m->type, "multipart/", 10) == 0){
+ for(i = 0; i < m->parts->nelts; i++){
+ p = m->parts->elts[i];
+ if(strcmp(p->type, "text/plain") == 0)
+ return p->body;
+ else if(strncmp(p->type, "multipart/", 10) == 0)
+ return findtextpart(p);
+ }
+ }
+ return m->body;
+}
+
+void
+collectparts(Message *m)
+{
+ Message *p;
+ int i;
+
+ if(m->parts == nil)
+ return;
+ for(i = 0; i < m->parts->nelts; i++){
+ p = m->parts->elts[i];
+ if(strncmp(p->type, "multipart/", 10) == 0)
+ collectparts(p);
+ else{
+ if(strcmp(p->type, "text/plain") == 0)
+ continue;
+ parts[nparts++] = p;
+ }
+ }
+}
+
+void
+pagershow(Message *m)
+{
+ char *body;
+ int needresize;
+ int oldnparts;
+
+ oldnparts = nparts;
+ needresize = 0;
+ mesg = m;
+ if(showcc){
+ if(mesg->cc == nil || mesg->cc[0] == 0){
+ showcc = 0;
+ needresize = 1;
+ }
+ }else{
+ if(mesg->cc != nil && mesg->cc[0] != 0){
+ showcc = 1;
+ needresize = 1;
+ }
+ }
+ mesgloadbody(mesg);
+ nparts = 0;
+ collectparts(mesg);
+ if(nparts != oldnparts)
+ needresize = 1;
+ if(needresize)
+ pagerresize(viewr);
+ body = findtextpart(mesg);
+ textset(&text, body, strlen(body));
+ pagerdraw();
+}
+
+static
+Point
+drawheader(Point p, char *h, char *s)
+{
+ Rune r;
+
+ p = string(screen, p, cols[BORD], ZP, font, h);
+ while(s && *s){
+ s += chartorune(&r, s);
+ p = runestringn(screen, p, cols[TEXT], ZP, font, &r, 1);
+ }
+ return p;
+}
+
+Point
+drawparts(Point p)
+{
+ Point q;
+ int i;
+
+ for(i = 0; i < nparts; i++){
+// q = string(screen, p, cols[TEXT], ZP, font, "=> ");
+ q = p;
+ if(parts[i]->filename != nil && parts[i]->filename[0] != 0)
+ q = string(screen, q, cols[TEXT], ZP, font, parts[i]->filename);
+ else
+ q = string(screen, q, cols[TEXT], ZP, font, "unnamed");
+ q = string(screen, q, cols[BORD], ZP, font, " [");
+ q = string(screen, q, cols[BORD], ZP, font, parts[i]->type);
+ string(screen, q, cols[BORD], ZP, font, "]");
+ p.y += font->height + Padding;
+ }
+ return p;
+}
+
+void
+pagerdraw(void)
+{
+ Point p, q;
+ char buf[32], *s;
+ Rune r;
+ Tm t;
+ int w;
+
+ draw(screen, viewr, cols[BACK], nil, ZP);
+ if(mesg != nil){
+ p = addpt(headr.min, Pt(Padding, Padding));
+ tmtime(&t, mesg->time, tz);
+ snprint(buf, sizeof buf, "%τ", tmfmt(&t, "DD/MM/YYYY hh:mm"));
+ w = stringwidth(font, buf);
+ string(screen, addpt(p, Pt(Dx(headr) - w - 2*Padding, 0)), cols[TEXT], ZP, font, buf);
+ q = drawheader(p, " From ", mesg->sender);
+ if(strcmp(mesg->sender, mesg->from) != 0){
+ q = string(screen, q, cols[TEXT], ZP, font, " <");
+ s = mesg->from;
+ while(s && *s){
+ s += chartorune(&r, s);
+ q = runestringn(screen, q, cols[TEXT], ZP, font, &r, 1);
+ }
+ string(screen, q, cols[TEXT], ZP, font, ">");
+ }
+ p.y += font->height + Padding;
+ drawheader(p, " To ", mesg->to);
+ if(showcc){
+ p.y += font->height + Padding;
+ drawheader(p, " Cc ", mesg->cc);
+ }
+ p.y += font->height + Padding;
+ drawheader(p, "Subject ", mesg->subject);
+ }
+ line(screen, addpt(headr.min, Pt(0, Dy(headr))), headr.max, 0, 0, 0, headercol, ZP);
+ if(nparts > 0){
+ p = addpt(partsr.min, Pt(Padding, Padding));
+ drawparts(p);
+ line(screen, addpt(partsr.min, Pt(0, Dy(partsr))), partsr.max, 0, 0, 0, headercol, ZP);
+ }
+ textdraw(&text);
+}
+
+void
+pagerresize(Rectangle r)
+{
+ int n;
+
+ n = showcc ? 4 : 3;
+ viewr = r;
+ headr = viewr;
+ headr.max.y = headr.min.y + Padding + n*(font->height + Padding);
+ if(nparts > 0){
+ partsr = viewr;
+ partsr.min.y = headr.max.y + 1;
+ partsr.max.y = partsr.min.y + nparts*(font->height + Padding) + Padding;
+ }
+ textr = viewr;
+ if(nparts > 0)
+ textr.min.y = partsr.max.y + 1;
+ else
+ textr.min.y = headr.max.y + 1;
+ textresize(&text, textr);
+}
+
+void
+partclick(Point p)
+{
+ Message *m;
+ char *dst, buf[1024] = {0};
+ int fd, n;
+
+ fd = plumbopen("send", OWRITE);
+ if(fd < 0)
+ return;
+ n = (p.y - partsr.min.y) / (font->height+Padding);
+ if(n < 0 || n >= nparts){
+ close(fd);
+ return;
+ }
+ m = parts[n];
+ dst = nil;
+ if(strcmp(m->type, "text/html") == 0){
+ snprint(buf, sizeof buf, "file://%s/body.html", m->path);
+ dst = "web";
+ }else if(strncmp(m->type, "image/", 6) == 0){
+ snprint(buf, sizeof buf, "%s/body.%s", m->path, m->type+6);
+ dst = "image";
+ }else{
+ snprint(buf, sizeof buf, "%s/body", m->path);
+ }
+ plumbsendtext(fd, "mongrel", dst, nil, buf);
+ close(fd);
+}
+
+void
+pagermouse(Mouse m)
+{
+ if(!ptinrect(m.xy, viewr))
+ return;
+ if(nparts > 0 && ptinrect(m.xy, partsr) && m.buttons == 4)
+ partclick(m.xy);
+ else if(ptinrect(m.xy, textr))
+ textmouse(&text, mc);
+}
+
+void
+pagerkey(Rune k)
+{
+ textkeyboard(&text, k);
+}
+
+void
+pagerinit(Mousectl *mctl, Theme *theme)
+{
+ Rectangle r;
+
+ mc = mctl;
+ tz = tzload("local");
+ showcc = 0;
+ nparts = 0;
+ if(theme != nil){
+ cols[BACK] = theme->back;
+ cols[BORD] = theme->border;
+ cols[TEXT] = theme->text;
+ cols[HTEXT] = theme->htext;
+ cols[HIGH] = theme->high;
+ headercol = theme->title;
+ }else{
+ r = Rect(0, 0, 1, 1);
+ cols[BACK] = display->white;
+ cols[BORD] = allocimage(display, r, screen->chan, 1, 0x999999FF);
+ cols[TEXT] = allocimage(display, r, screen->chan, 1, 0x000000FF);
+ cols[HTEXT] = allocimage(display, r, screen->chan, 1, 0x000000FF);
+ cols[HIGH] = allocimage(display, r, screen->chan, 1, 0xCCCCCCFF);
+ headercol = allocimage(display, r, screen->chan, 1, DGreygreen);
+ }
+ textinit(&text, screen, screen->r, font, cols);
+}
+
--- /dev/null
+++ b/text.c
@@ -1,0 +1,347 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <thread.h>
+#include <plumb.h>
+#include "w.h"
+
+enum
+{
+ Scrollwidth = 12,
+ Padding = 4,
+};
+
+enum
+{
+ Msnarf,
+ Mplumb,
+};
+char *menu2str[] = {
+ "snarf",
+ "plumb",
+ nil,
+};
+Menu menu2 = { menu2str };
+
+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(t->font, " ");
+ }else{
+ l = runestringnwidth(t->font, &r, 1);
+ x += l;
+ }
+ if(x > w){
+ t->lines[t->nlines++] = i;
+ x = l;
+ }
+ }
+ i += c;
+ }
+}
+
+int
+indexat(Text *t, Point p)
+{
+ int line, i, s, e, x, c, l;
+ Rune r;
+
+ if(!ptinrect(p, t->textr))
+ return -1;
+ line = t->offset + ((p.y - t->textr.min.y) / font->height);
+ s = t->lines[line];
+ if(line+1 >= t->nlines)
+ e = t->ndata;
+ else
+ e = t->lines[line+1];
+ x = t->textr.min.x;
+ for(i = s; i <= e; ){
+ c = chartorune(&r, &t->data[i]);
+ if(r == '\t')
+ l = stringwidth(t->font, " ");
+ else
+ l = runestringnwidth(t->font, &r, 1);
+ if(x <= p.x && p.x <= x+l)
+ break;
+ i += c;
+ x += l;
+ }
+ return i;
+}
+
+void
+textinit(Text *t, Image *b, Rectangle r, Font *f, Image *cols[NCOLS])
+{
+ memset(t, 0, sizeof *t);
+ t->b = b;
+ t->font = f;
+ t->s0 = -1;
+ t->s1 = -1;
+ t->offset = 0;
+ textresize(t, r);
+ memmove(t->cols, cols, sizeof t->cols);
+}
+
+void
+textset(Text *t, char *data, usize ndata)
+{
+ t->data = data;
+ t->ndata = ndata;
+ computelines(t);
+}
+
+void
+textresize(Text *t, Rectangle r)
+{
+ t->r = r;
+ t->vlines = Dy(t->r) / t->font->height;
+ t->scrollr = rectaddpt(Rect(0, 0, Scrollwidth, Dy(r)), r.min);
+ t->textr = r;
+ t->textr.min.x = t->scrollr.max.x + Padding;
+ if(t->nlines > 0)
+ computelines(t);
+}
+
+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;
+}
+
+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 = selected(t, i) ? t->cols[HTEXT] : t->cols[TEXT];
+ bg = selected(t, i) ? t->cols[HIGH] : t->cols[BACK];
+ 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, t->font, " ", bg, ZP);
+ else
+ p = runestringnbg(t->b, p, fg, ZP, t->font, &r, 1, bg, ZP);
+ }
+}
+
+void
+textdraw(Text *t)
+{
+ int i, h, y;
+ Rectangle sr;
+
+ draw(t->b, t->r, t->cols[BACK], nil, ZP);
+ draw(t->b, t->scrollr, t->cols[BORD], nil, ZP);
+ border(t->b, t->scrollr, 0, t->cols[TEXT], ZP);
+ if(t->nlines > 0){
+ h = ((double)t->vlines / t->nlines) * Dy(t->scrollr);
+ y = ((double)t->offset / t->nlines) * Dy(t->scrollr);
+ sr = Rect(t->scrollr.min.x + 1, t->scrollr.min.y + y + 1, t->scrollr.max.x - 1, t->scrollr.min.y + y + h - 1);
+ }else
+ sr = insetrect(t->scrollr, -1);
+ draw(t->b, sr, t->cols[BACK], nil, ZP);
+ for(i = 0; i < t->vlines; i++){
+ if(t->offset+i >= t->nlines)
+ break;
+ drawline(t, i);
+ }
+ flushimage(display, 1);
+}
+
+static
+void
+scroll(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;
+ textdraw(t);
+}
+
+void
+textkeyboard(Text *t, Rune k)
+{
+ switch(k){
+ case Kup:
+ scroll(t, -1);
+ break;
+ case Kdown:
+ scroll(t, 1);
+ break;
+ case Kpgup:
+ scroll(t, -t->vlines);
+ break;
+ case Kpgdown:
+ scroll(t, t->vlines);
+ break;
+ case Khome:
+ scroll(t, -t->nlines);
+ break;
+ case Kend:
+ scroll(t, t->nlines);
+ break;
+ }
+}
+
+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);
+}
+
+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);
+}
+
+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
+textmouse(Text *t, Mousectl *mc)
+{
+ static selecting = 0;
+ Point p;
+ int n;
+
+ if(ptinrect(mc->xy, t->scrollr)){
+ if(mc->buttons == 1){
+ n = (mc->xy.y - t->scrollr.min.y) / font->height;
+ scroll(t, -n);
+ }else if(mc->buttons == 2){
+ t->offset = (mc->xy.y - t->scrollr.min.y) * t->nlines / Dy(t->scrollr);
+ textdraw(t);
+ }else if(mc->buttons == 4){
+ n = (mc->xy.y - t->scrollr.min.y) / font->height;
+ scroll(t, n);
+ }
+ }else if(ptinrect(mc->xy, t->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;
+ textdraw(t);
+ }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 <= t->textr.min.y){
+ scroll(t, -1);
+ p.y = t->textr.min.y + 1;
+ }else if(p.y >= t->textr.max.y){
+ scroll(t, 1);
+ p.y = t->textr.max.y - 1;
+ }
+ n = indexat(t, p);
+ if(n < 0)
+ break;
+ t->s1 = n;
+ textdraw(t);
+ }
+ }else if(mc->buttons == 2){
+ menu2hit(t, mc);
+ }else if(mc->buttons == 8){
+ n = mousescrollsize(t->vlines);
+ scroll(t, -n);
+ }else if(mc->buttons == 16){
+ n = mousescrollsize(t->vlines);
+ scroll(t, n);
+ }
+ }
+}
+
--- /dev/null
+++ b/theme.c
@@ -1,0 +1,84 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <draw.h>
+#include "theme.h"
+
+Image*
+ereadcol(char *s)
+{
+ Image *i;
+ char *e;
+ ulong c;
+
+ c = strtoul(s, &e, 16);
+ if(e == nil || e == s)
+ return nil;
+ c = (c << 8) | 0xff;
+ i = allocimage(display, Rect(0, 0, 1, 1), screen->chan, 1, c);
+ if(i == nil)
+ sysfatal("allocimage: %r");
+ return i;
+}
+
+Theme*
+loadtheme(void)
+{
+ Theme *theme;
+ Biobuf *bp;
+ char *s;
+
+ if(access("/dev/theme", AREAD) < 0)
+ return 0;
+ bp = Bopen("/dev/theme", OREAD);
+ if(bp == nil)
+ return 0;
+ theme = malloc(sizeof *theme);
+ if(theme == nil){
+ Bterm(bp);
+ return nil;
+ }
+ for(;;){
+ s = Brdstr(bp, '\n', 1);
+ if(s == nil)
+ break;
+ if(strncmp(s, "back", 4) == 0)
+ theme->back = ereadcol(s+5);
+ else if(strncmp(s, "high", 4) == 0)
+ theme->high = ereadcol(s+5);
+ else if(strncmp(s, "border", 6) == 0)
+ theme->border = ereadcol(s+7);
+ else if(strncmp(s, "text", 4) == 0)
+ theme->text = ereadcol(s+5);
+ else if(strncmp(s, "htext", 5) == 0)
+ theme->htext = ereadcol(s+6);
+ else if(strncmp(s, "title", 5) == 0)
+ theme->title = ereadcol(s+6);
+ else if(strncmp(s, "ltitle", 6) == 0)
+ theme->ltitle = ereadcol(s+7);
+ else if(strncmp(s, "hold", 4) == 0)
+ theme->hold = ereadcol(s+5);
+ else if(strncmp(s, "lhold", 5) == 0)
+ theme->lhold = ereadcol(s+6);
+ else if(strncmp(s, "palehold", 8) == 0)
+ theme->palehold = ereadcol(s+9);
+ else if(strncmp(s, "paletext", 8) == 0)
+ theme->paletext = ereadcol(s+9);
+ else if(strncmp(s, "size", 4) == 0)
+ theme->size = ereadcol(s+5);
+ else if(strncmp(s, "menuback", 8) == 0)
+ theme->menuback = ereadcol(s+9);
+ else if(strncmp(s, "menuhigh", 8) == 0)
+ theme->menuhigh = ereadcol(s+9);
+ else if(strncmp(s, "menubord", 8) == 0)
+ theme->menubord = ereadcol(s+9);
+ else if(strncmp(s, "menutext", 8) == 0)
+ theme->menutext = ereadcol(s+9);
+ else if(strncmp(s, "menuhtext", 5) == 0)
+ theme->menuhtext = ereadcol(s+6);
+ free(s);
+ }
+ Bterm(bp);
+ return theme;
+}
+
--- /dev/null
+++ b/theme.h
@@ -1,0 +1,25 @@
+typedef struct Theme Theme;
+
+struct Theme
+{
+ Image *back;
+ Image *high;
+ Image *border;
+ Image *text;
+ Image *htext;
+ Image *title;
+ Image *ltitle;
+ Image *hold;
+ Image *lhold;
+ Image *palehold;
+ Image *paletext;
+ Image *size;
+ Image *menubar;
+ Image *menuback;
+ Image *menuhigh;
+ Image *menubord;
+ Image *menutext;
+ Image *menuhtext;
+};
+
+Theme* loadtheme(void);
--- /dev/null
+++ b/utils.c
@@ -1,0 +1,83 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <mouse.h>
+#include "theme.h"
+#include "a.h"
+
+void*
+emalloc(ulong size)
+{
+ void *p;
+
+ p = malloc(size);
+ if(p == nil)
+ sysfatal("malloc: %r");
+ return p;
+}
+
+void*
+erealloc(void *p, ulong size)
+{
+ p = realloc(p, size);
+ if(p == nil)
+ sysfatal("realloc: %r");
+ return p;
+}
+
+void
+mlmaybegrow(Mlist *ml)
+{
+ if(ml->nelts < ml->size)
+ return;
+ ml->size *= 1.5;
+ ml->elts = erealloc(ml->elts, ml->size * sizeof(Message*));
+}
+
+Mlist*
+mkmlist(usize cap)
+{
+ Mlist *ml;
+
+ ml = emalloc(sizeof *ml);
+ ml->size = cap;
+ ml->nelts = 0;
+ ml->elts = emalloc(cap * sizeof(Message*));
+ return ml;
+}
+
+int
+mladd(Mlist *ml, Message *m)
+{
+ mlmaybegrow(ml);
+ ml->elts[ml->nelts] = m;
+ ml->nelts += 1;
+ return 0;
+}
+
+int
+mlinsert(Mlist *ml, usize index, Message *m)
+{
+ if(index > ml->nelts || index >= ml->size)
+ return -1;
+ mlmaybegrow(ml);
+ memmove(&ml->elts[index+1], &ml->elts[index], (ml->nelts - index + 1)*sizeof(Message*));
+ ml->elts[index] = m;
+ ml->nelts += 1;
+ return 0;
+}
+
+Message*
+mldel(Mlist *ml, usize index)
+{
+ Message *m;
+
+ if(index >= ml->nelts || index >= ml->size)
+ return nil;
+ m = ml->elts[index];
+ memmove(&ml->elts[index], &ml->elts[index+1], (ml->nelts -index)*sizeof(Message*));
+ ml->nelts -= 1;
+ return m;
+}
+
--- /dev/null
+++ b/w.h
@@ -1,0 +1,42 @@
+typedef struct Text Text;
+
+enum
+{
+ BACK,
+ BORD,
+ TEXT,
+ HTEXT,
+ HIGH,
+ NCOLS,
+};
+
+enum
+{
+ Maxlines = 65535,
+};
+
+struct Text
+{
+ Image *b;
+ Rectangle r;
+ Rectangle scrollr;
+ Rectangle textr;
+ Font *font;
+ Image *cols[NCOLS];
+ int vlines;
+ int offset;
+ char *data;
+ usize ndata;
+ usize lines[Maxlines];
+ int nlines;
+ int s0;
+ int s1;
+};
+
+void textinit(Text *t, Image *b, Rectangle r, Font *f, Image *cols[NCOLS]);
+void textset(Text *t, char *data, usize ndata);
+void textresize(Text *t, Rectangle r);
+void textkeyboard(Text *t, Rune k);
+void textmouse(Text *t, Mousectl *m);
+void textdraw(Text *t);
+