ref: 2183f9eaeca9fb5d57b637c15707e663caf33575
parent: 71292834bbafcf6bc409cfd7691f9e6650df025b
author: henesy <devnull@localhost>
date: Thu Oct 10 18:48:01 EDT 2019
add iobuf(2) from powerman and ttffs(4) from mjl
--- a/appl/cmd/mkfile
+++ b/appl/cmd/mkfile
@@ -162,6 +162,7 @@
tr.dis\
trfs.dis\
tsort.dis\
+ ttffs.dis\
unicode.dis\
units.dis\
uniq.dis\
--- /dev/null
+++ b/appl/cmd/ttffs.b
@@ -1,0 +1,691 @@
+implement Ttffs;
+
+# serve (over styx) ttf's as inferno/plan 9 (sub)fonts in arbitrary sizes.
+# fonts and subfonts are not listed in the directory, but can be walked to.
+# the font and subfont files are generated on the fly.
+# subfonts contain at most 128 glyphs.
+# at first read of a font, it is parsed and its glyph ranges determined.
+#
+# for each font file (fontpath/*.ttf) the following files are served:
+# <name>.<style>.<size>.font
+# <name>.<style>.<size>.<index>
+#
+# the second form are subfonts, index starts at 1. index 1 always has the single char 0.
+
+include "sys.m";
+ sys: Sys;
+ sprint: import sys;
+include "draw.m";
+ draw: Draw;
+ Display, Rect, Point, Image: import draw;
+include "arg.m";
+include "bufio.m";
+ bufio: Bufio;
+ Iobuf: import bufio;
+include "string.m";
+ str: String;
+include "styx.m";
+ styx: Styx;
+ Tmsg, Rmsg: import styx;
+include "styxservers.m";
+ styxservers: Styxservers;
+ Styxserver, Fid, Navigator, Navop: import styxservers;
+ nametree: Nametree;
+ Tree: import nametree;
+ Enotfound: import styxservers;
+include "freetype.m";
+ ft: Freetype;
+ Face, Glyph: import ft;
+include "readdir.m";
+ readdir: Readdir;
+include "tables.m";
+ tables: Tables;
+ Table, Strhash: import tables;
+
+Ttffs: module {
+ init: fn(nil: ref Draw->Context, args: list of string);
+};
+
+dflag: int;
+fontpath := "/fonts/ttf";
+mtpt: con "/mnt/ft";
+
+disp: ref Display;
+srv: ref Styxserver;
+styles := array[] of {"r", "i", "b", "ib"};
+
+Qroot, Qindex: con iota;
+idgen := 2;
+
+Font: adt {
+ sane,
+ family: string;
+ ranges: array of ref (int, int); # sorted, start-end inclusive
+ styles: cyclic array of ref Style; # indexed by Face.style
+};
+
+Style: adt {
+ f: ref Font;
+ dir: ref Sys->Dir;
+ path: string;
+ fc: ref Face;
+ sizes: cyclic ref Table[cyclic ref Size]; # size
+};
+
+Size: adt {
+ id: int; # qid.path. subfonts are id+Size.range
+ st: cyclic ref Style;
+ range: int; # index for Font.ranges. 0 is .font, 1 is first in range
+ size: int;
+ dat: array of byte;
+ nuse: int;
+};
+
+fonts: ref Strhash[ref Font]; # family
+sanefonts: ref Strhash[ref Font]; # sane name
+sizes: ref Table[ref Size]; # qid.path
+
+
+init(ctxt: ref Draw->Context, args: list of string)
+{
+ sys = load Sys Sys->PATH;
+ if(ctxt == nil || (disp = ctxt.display) == nil)
+ fail("no display");
+ draw = load Draw Draw->PATH;
+ arg := load Arg Arg->PATH;
+ bufio = load Bufio Bufio->PATH;
+ str = load String String->PATH;
+ ft = load Freetype Freetype->PATH;
+ styx = load Styx Styx->PATH;
+ styx->init();
+ styxservers = load Styxservers Styxservers->PATH;
+ styxservers->init(styx);
+ nametree = load Nametree Nametree->PATH;
+ nametree->init();
+ readdir = load Readdir Readdir->PATH;
+ tables = load Tables Tables->PATH;
+
+ sys->pctl(Sys->NEWPGRP, nil);
+
+ Mflag := 0;
+
+ arg->init(args);
+ arg->setusage(arg->progname()+" [-dM] [-p fontpath]");
+ while((c := arg->opt()) != 0)
+ case c {
+ 'd' => dflag++;
+ 'p' => fontpath = arg->arg();
+ 'M' => Mflag++;
+ * => arg->usage();
+ }
+ args = arg->argv();
+ if(args != nil)
+ arg->usage();
+
+ fonts = fonts.new(11, nil);
+ sanefonts = sanefonts.new(11, nil);
+ sizes = sizes.new(11, nil);
+
+ fds := array[2] of ref Sys->FD;
+ fd := sys->fildes(0);
+ if(!Mflag) {
+ if(sys->pipe(fds) < 0)
+ fail(sprint("pipe: %r"));
+ fd = fds[0];
+ }
+
+ navc := chan of ref Navop;
+ spawn navigator(navc);
+ msgc: chan of ref Tmsg;
+ (msgc, srv) = Styxserver.new(fd, Navigator.new(navc), big Qroot);
+
+ if(Mflag)
+ return main(msgc);
+
+ spawn main(msgc);
+ if(sys->mount(fds[1], nil, mtpt, sys->MAFTER, nil) < 0)
+ fail(sprint("mount: %r"));
+ return;
+}
+
+xwarn(s: string): array of ref (int, int)
+{
+ warn(s);
+ return nil;
+}
+
+# read cached glyph ranges available in the font
+readranges(path: string, mtime: int): array of ref (int, int)
+{
+ fd := sys->open(path, Sys->OREAD);
+ if(fd == nil)
+ return xwarn(sprint("open %q: %r", path));
+ (ok, d) := sys->fstat(fd);
+ if(ok != 0)
+ return xwarn(sprint("fstat %q: %r", path));
+ if(d.mtime <= mtime)
+ return xwarn(sprint("%q: older than ttf, ignoring", path));
+ if(sys->readn(fd, buf := array[int d.length] of byte, len buf) != len buf)
+ return xwarn(sprint("readn %q: %r", path));
+ s := string buf;
+ r: list of ref (int, int);
+ for(l := sys->tokenize(s, "\n").t1; l != nil; l = tl l) {
+ x := sys->tokenize(hd l, " ").t1;
+ if(len x != 2)
+ return xwarn(sprint("%q: bad glyph range line: %q", path, hd l));
+ (a, rem0) := str->toint(hd x, 10);
+ (b, rem1) := str->toint(hd tl x, 10);
+ if(rem0 != nil || rem1 != nil || b < a || b > 64*1024 || r != nil && a <= (hd r).t1)
+ return xwarn(sprint("%q: invalid glyph range: %q", path, hd l));
+ r = ref (a, b)::r;
+ }
+ return l2a(rev(r));
+}
+
+genranges(f: ref Face): array of ref (int, int)
+{
+ r := list of {ref (0, 0)};
+ max := 64*1024;
+ i := 1;
+ while(i < max) {
+ for(; i < max && !f.haschar(i); i++)
+ {}
+ s := i;
+ for(; i < max && f.haschar(i); i++)
+ {}
+ e := i;
+ while(s < e) {
+ n := e-s;
+ if(n > 128)
+ n = 128;
+ if(dflag > 1) say(sprint("range %d-%d", s, s+n-1));
+ r = ref (s, s+n-1)::r;
+ s += n;
+ }
+ }
+ return l2a(rev(r));
+}
+
+indexdat: array of byte;
+indexmtime: int;
+mkindex(): array of byte
+{
+say("mkindex0");
+ (ok, dir) := sys->stat(fontpath);
+ if(ok != 0) {
+ warn(sprint("stat %q: %r", fontpath));
+ return nil;
+ }
+ if(indexdat != nil && indexmtime == dir.mtime)
+ return indexdat;
+
+say("mkindex1");
+ nfonts := fonts.new(11, nil);
+ nsanefonts := sanefonts.new(11, nil);
+ nsizes := sizes.new(11, nil);
+
+ (a, n) := readdir->init(fontpath, Readdir->NONE);
+ if(n < 0)
+ return nil;
+ for(i := 0; i < len a; i++) {
+ if(!suffix(".ttf", a[i].name) && !suffix(".otf", a[i].name))
+ continue;
+ name := a[i].name;
+ name = name[:len name-len ".ttf"];
+
+ path := sprint("%s/%s", fontpath, a[i].name);
+ fc := ft->newface(path, 0);
+ if(fc == nil) {
+ warn(sprint("newface %q: %r", path));
+ continue;
+ }
+
+if(dflag) say(sprint("have face, nfaces=%d index=%d style=%d height=%d ascent=%d familyname=%q stylename=%q",
+ fc.nfaces, fc.index, fc.style, fc.height, fc.ascent, fc.familyname, fc.stylename));
+
+ rpath := sprint("%s/ranges.%s", fontpath, name);
+ ranges := readranges(rpath, a[i].mtime);
+ if(ranges == nil) {
+ ranges = genranges(fc);
+ s := "";
+ for(j := 0; j < len ranges; j++)
+ s += sprint("%d %d\n", ranges[j].t0, ranges[j].t1);
+ fd := sys->create(rpath, Sys->OWRITE, 8r666);
+ if(fd == nil || sys->write(fd, buf := array of byte s, len buf) != len buf)
+ warn(sprint("create or write %q: %r", rpath));
+ }
+
+ f := nfonts.find(fc.familyname);
+ if(f == nil) {
+ sane := sanitize(fc.familyname);
+ while(nsanefonts.find(sane) != nil)
+ sane += "0";
+ f = ref Font(sane, fc.familyname, ranges, array[len styles] of ref Style);
+ nfonts.add(f.family, f);
+ nsanefonts.add(f.sane, f);
+ } else if(f.styles[fc.style] != nil) {
+ warn(sprint("duplicate style %#q", styles[fc.style]));
+ continue;
+ }
+ st := ref Style(f, ref dir, path, fc, nil);
+ st.sizes = st.sizes.new(11, nil);
+ f.styles[st.fc.style] = st;
+ for(l := tabitems(st.sizes); l != nil; l = tl l) {
+ (nil, size) := *hd l;
+ nsizes.add(size.id, size);
+ }
+ }
+ s := "";
+ for(l := strtabitems(nfonts); l != nil; l = tl l) {
+ f := (hd l).t1;
+ st := "";
+ for(i = 0; i < len styles; i++)
+ if(f.styles[i] != nil)
+ st += ","+styles[i];
+ s += sprint("%q %q\n", f.sane, sprint(".%s.%s.2-", f.family, st[1:]));
+ }
+
+ # ensure we don't mkindex immediately after writing ranges files
+ (ok, dir) = sys->stat(fontpath);
+ if(ok != 0) {
+ warn(sprint("stat: %q: %r", fontpath));
+ return nil;
+ }
+
+ fonts = nfonts;
+ sanefonts = nsanefonts;
+ sizes = nsizes;
+ indexdat = array of byte s;
+ indexmtime = dir.mtime;
+ return indexdat;
+}
+
+sanitize(s: string): string
+{
+ s = str->tolower(s);
+ r: string;
+ for(i := 0; i < len s; i++)
+ case c := s[i] {
+ ' ' or '\t' or '-' =>
+ if(r != nil && r[len r-1] != '-')
+ r[len r] = '-';
+ '.' => {}
+ * => r[len r] = c;
+ }
+ return r;
+}
+
+mkname(s: ref Size): string
+{
+ st := s.st;
+ fc := st.fc;
+ f := st.f;
+ if(s.range == 0)
+ return sprint("%s.%s.%d.font", f.sane, styles[fc.style], s.size);
+ return sprint("%s.%s.%d.%d", f.sane, styles[fc.style], s.size, s.range);
+}
+
+mkdat(f: ref Size): array of byte
+{
+ if(f.dat == nil) {
+ if(f.range == 0)
+ f.dat = mkfont(f);
+ else
+ f.dat = mksubfont(f);
+ }
+ return f.dat;
+}
+
+mkfont(sz: ref Size): array of byte
+{
+ f := sz.st.f;
+ fc := sz.st.fc;
+ fc.setcharsize(sz.size<<6, 0, 0);
+ s := sprint("%d %d\n", fc.height, fc.ascent);
+ for(i := 0; i < len f.ranges; i++) {
+ (a, b) := *f.ranges[i];
+ s += sprint("0x%04x\t0x%04x\t%q\n", a, b, sprint("%s.%s.%d.%d", f.sane, styles[fc.style], sz.size, i+1));
+ }
+ return array of byte s;
+}
+
+mksubfont(sz: ref Size): array of byte
+{
+ (s, l) := *sz.st.f.ranges[sz.range-1];
+ fc := sz.st.fc;
+ fc.setcharsize(sz.size<<6, 0, 0);
+
+ imgs := array[l+1-s] of ref Image;
+ n := l+1-s;
+ width := 0;
+ left := array[len imgs+1] of {* => 0};
+ advance := array[len imgs+1] of {* => 0};
+ for(i := 0; i < n; i++) {
+ c := s+i;
+ g := fc.loadglyph(c);
+ if(g == nil)
+ fail(sprint("no glyph for %c (%#x)", c, c));
+if(dflag) say(sprint("glyph %#x, width=%d height=%d top=%d left=%d advance=%d,%d", c, g.width, g.height, g.top, g.left, g.advance.x>>6, g.advance.y>>6));
+ r := Rect((0,0), (g.width, fc.height));
+ img := imgs[i] = disp.newimage(r, Draw->GREY8, 0, Draw->Black);
+ gr: Rect;
+ gr.min = (0,fc.ascent-g.top);
+ gr.max = gr.min.add((g.width, g.height));
+ img.writepixels(gr, g.bitmap);
+
+ width += g.width;
+ left[i] = g.left;
+ advance[i] = g.advance.x>>6;
+ }
+
+ oimghdr := 0;
+ obuf := oimghdr + 5*12;
+ osubfhdr := obuf + fc.height*width;
+ ochars := osubfhdr + 3*12;
+ oend := ochars + (len imgs+1)*6;
+ buf := array[oend] of byte;
+
+ fontr := Rect((0,0), (width,fc.height));
+ fontimg := disp.newimage(fontr, Draw->GREY8, 0, Draw->Black);
+ buf[oimghdr:] = sys->aprint("%11s %11d %11d %11d %11d ", "k8", 0, 0, fontr.max.x, fontr.max.y);
+ x := 0;
+ buf[osubfhdr:] = sys->aprint("%11d %11d %11d ", len imgs, fc.height, fc.ascent);
+ o := ochars;
+ for(i = 0; i < len imgs+1; i++) {
+ if(i < len imgs)
+ img := imgs[i];
+ buf[o++] = byte (x>>0);
+ buf[o++] = byte (x>>8);
+ buf[o++] = byte 0; # top
+ buf[o++] = byte fc.height; # bottom
+ buf[o++] = byte left[i]; # left
+ if(img == nil) {
+ buf[o++] = byte 0; # width
+ break;
+ }
+ buf[o++] = byte advance[i]; # width
+ r := fontr;
+ r.min.x = x;
+ fontimg.draw(r, disp.white, img, Point(0,0));
+ x += img.r.dx();
+ }
+ if(o != len buf)
+ raise "bad pack";
+ r := fontimg.readpixels(fontimg.r, buf[obuf:]);
+ if(r != osubfhdr-obuf)
+ fail(sprint("readpixels, got %d, expected %d: %r", r, osubfhdr-obuf));
+ return buf;
+}
+
+main(msgc: chan of ref Tmsg)
+{
+ sys->pctl(Sys->FORKNS, nil);
+more:
+ for(;;) {
+ mm := <-msgc;
+ if(mm == nil)
+ break more;
+ pick m := mm {
+ Readerror =>
+ warn("read: "+m.error);
+ break more;
+ * =>
+ handle(mm);
+ pick x := mm {
+ Clunk or
+ Remove =>
+ cacheclean();
+ }
+ }
+ }
+ killgrp(pid());
+}
+
+cacheclean()
+{
+ for(k := tabitems(sizes); k != nil; k = tl k)
+ (hd k).t1.nuse = 0;
+ for(l := srv.allfids(); l != nil; l = tl l) {
+ fid := hd l;
+ f := sizes.find(int fid.path);
+ if(fid.isopen)
+ f.nuse++;
+ }
+ for(k = tabitems(sizes); k != nil; k = tl k) {
+ sz := (hd k).t1;
+ if(sz.nuse == 0 && sz.dat != nil) {
+ if(dflag) say(sprint("freeing %s.%s.%d.%d", sz.st.f.sane, styles[sz.st.fc.style], sz.size, sz.range));
+ sz.dat = nil;
+ }
+ }
+}
+
+navigator(navc: chan of ref Navop)
+{
+ for(;;)
+ navigate(<-navc);
+}
+
+navreply(op: ref Navop, d: ref Sys->Dir, err: string)
+{
+ op.reply <-= (d, err);
+}
+
+navigate(op: ref Navop)
+{
+ pick x := op {
+ Stat =>
+ case int x.path {
+ Qroot =>
+ navreply(x, ref dirroot(), nil);
+ Qindex =>
+ navreply(x, ref dirindex(), nil);
+ * =>
+ mkindex(); # ensure up to date index
+ f := sizes.find(int x.path);
+ if(f == nil)
+ return navreply(x, nil, sprint("missing Size for qid.path %bd/%#bx", x.path, x.path));
+ d := ref dir(mkname(f), int x.path, 8r444, len mkdat(f), 0);
+ navreply(x, d, nil);
+ }
+ Walk =>
+ if(x.name == "..")
+ return navreply(x, ref dirroot(), nil);
+ if(x.path != big Qroot)
+ return navreply(x, nil, Enotfound);
+
+ if(x.name == "index")
+ return navreply(x, ref dirindex(), nil);
+
+ mkindex(); # ensure up to date index
+ name, style, size, suf: string;
+ s := x.name;
+ (s, suf) = str->splitstrr(s, ".");
+ if(s != nil)
+ (s, size) = str->splitstrr(s[:len s-1], ".");
+ if(s != nil)
+ (name, style) = str->splitstrr(s[:len s-1], ".");
+ if(name == nil)
+ return navreply(x, nil, Enotfound);
+ name = name[:len name-1];
+if(dflag) say(sprint("walk, name %q, style %q, size %q, suf %q", name, style, size, suf));
+
+ # format is good
+ f := sanefonts.find(name);
+ if(f == nil)
+ return navreply(x, nil, "no such font");
+ sti := find(styles, style);
+ if(sti < 0 || f.styles[sti] == nil)
+ return navreply(x, nil, "no such style");
+ (szs, rem) := str->toint(size, 10);
+ if(rem != nil)
+ return navreply(x, nil, Enotfound);
+ if(szs <= 1)
+ return navreply(x, nil, "no such size");
+
+ r := 0;
+ if(suf != "font") {
+ (r, rem) = str->toint(suf, 10);
+ if(rem != nil || r <= 0 || r > len f.ranges)
+ return navreply(x, nil, "no such range");
+ }
+
+ st := f.styles[sti];
+ xsz := st.sizes.find(szs);
+ if(xsz == nil) {
+ xsz = ref Size(idgen++, st, 0, szs, nil, 0);
+ sizes.add(xsz.id, xsz);
+ for(i := 0; i < len f.ranges; i++) {
+ ssz := ref Size(idgen++, st, 1+i, szs, nil, 0);
+ sizes.add(ssz.id, ssz);
+ }
+ st.sizes.add(xsz.size, xsz);
+ }
+ sz := sizes.find(xsz.id+r);
+ navreply(x, ref dir(x.name, sz.id, 8r444, len mkdat(sz), 0), nil);
+
+ Readdir =>
+ dirs := array[] of {ref dirindex()};
+ s := x.offset;
+ if(s > len dirs)
+ s = len dirs;
+ e := x.offset+x.count;
+ if(e > len dirs)
+ e = len dirs;
+ while(s < e)
+ navreply(x, dirs[s++], nil);
+ navreply(x, nil, nil);
+ }
+}
+
+handle(mm: ref Tmsg)
+{
+ pick m := mm {
+ Read =>
+ ff := srv.getfid(m.fid);
+ if(ff == nil || ff.path == big Qroot || !ff.isopen)
+ break;
+
+ if(ff.path == big Qindex)
+ dat := mkindex();
+ else {
+ f := sizes.find(int ff.path);
+ if(f == nil) {
+ srv.reply(ref Rmsg.Error(m.tag, "size not found?"));
+ return;
+ }
+ dat = mkdat(f);
+ }
+ srv.reply(styxservers->readbytes(m, dat));
+ return;
+ }
+ srv.default(mm);
+}
+
+dirroot(): Sys->Dir
+{
+ return dir(".", Qroot, 8r555|Sys->DMDIR, 0, 0);
+}
+
+dirindex(): Sys->Dir
+{
+ mtime := 0;
+ (ok, d) := sys->stat(fontpath);
+ if(ok == 0)
+ mtime = d.mtime;
+ return dir("index", Qindex, 8r444, 0, mtime);
+}
+
+dir(name: string, path, mode, length, mtime: int): Sys->Dir
+{
+ d := sys->zerodir;
+ d.name = name;
+ d.uid = d.gid = "ttffs";
+ d.qid.path = big path;
+ d.qid.qtype = Sys->QTFILE;
+ if(mode&Sys->DMDIR)
+ d.qid.qtype = Sys->QTDIR;
+ d.mtime = d.atime = mtime;
+ d.mode = mode;
+ d.length = big length;
+ return d;
+}
+
+strtabitems[T](t: ref Strhash[T]): list of ref (string, T)
+{
+ r: list of ref (string, T);
+ for(i := 0; i < len t.items; i++)
+ for(l := t.items[i]; l != nil; l = tl l)
+ r = ref hd l::r;
+ return r;
+}
+
+tabitems[T](t: ref Table[T]): list of ref (int, T)
+{
+ r: list of ref (int, T);
+ for(i := 0; i < len t.items; i++)
+ for(l := t.items[i]; l != nil; l = tl l)
+ r = ref hd l::r;
+ return r;
+}
+
+find(a: array of string, s: string): int
+{
+ for(i := 0; i < len a; i++)
+ if(a[i] == s)
+ return i;
+ return -1;
+}
+
+suffix(suf, s: string): int
+{
+ return len s >= len suf && suf == s[len s-len suf:];
+}
+
+pid(): int
+{
+ return sys->pctl(0, nil);
+}
+
+killgrp(pid: int)
+{
+ sys->fprint(sys->open(sprint("/prog/%d/ctl", pid), Sys->OWRITE), "killgrp");
+}
+
+rev[T](l: list of T): list of T
+{
+ r: list of T;
+ for(; l != nil; l = tl l)
+ r = hd l::r;
+ return r;
+}
+
+l2a[T](l: list of T): array of T
+{
+ a := array[len l] of T;
+ i := 0;
+ for(; l != nil; l = tl l)
+ a[i++] = hd l;
+ return a;
+}
+
+fd2: ref Sys->FD;
+warn(s: string)
+{
+ if(fd2 == nil)
+ fd2 = sys->fildes(2);
+ sys->fprint(fd2, "%s\n", s);
+}
+
+say(s: string)
+{
+ if(dflag)
+ warn(s);
+}
+
+fail(s: string)
+{
+ warn(s);
+ killgrp(pid());
+ raise "fail:"+s;
+}
--- /dev/null
+++ b/appl/lib/iobuf.b
@@ -1,0 +1,335 @@
+implement IOBuf;
+
+include "sys.m";
+ sys: Sys;
+ sprint: import sys;
+include "iobuf.m";
+
+LF: array of byte;
+
+init()
+{
+ sys = load Sys Sys->PATH;
+
+ LF = array[] of { byte '\n' };
+}
+
+ReadBuf.new(fd: ref Sys->FD, bufsize: int): ref ReadBuf
+{
+ r := ref ReadBuf;
+ r.buf = array[bufsize] of byte;
+ r.s = 0;
+ r.e = 0;
+ r.setsep("\n", 1);
+ r.fd = fd;
+ r.reader = sysread;
+ r.is_eof = 0;
+ return r;
+}
+
+ReadBuf.newc(queuesize, bufsize: int): ref ReadBuf
+{
+ r := ReadBuf.new(nil, bufsize);
+ r.queue = chan[queuesize] of array of byte;
+ r.pending = chan[1] of (array of byte, Sys->Rwrite);
+ r.is_pending = chan[1] of int;
+ r.reader = chanread;
+ return r;
+}
+
+ReadBuf.setsep(r: self ref ReadBuf, sep: string, strip: int)
+{
+ if(sep == nil)
+ raise "iobuf:empty separator";
+ r.sep = array of byte sep;
+ r.strip = strip;
+}
+
+ReadBuf.reads(r: self ref ReadBuf): array of byte
+{
+ if(len r.sep != 1)
+ raise "iobuf:multibyte separator not implemented yet";
+ c := r.sep[0];
+
+ for(;;){
+ if(r.is_eof)
+ if(r.s == r.e)
+ return nil;
+ else{
+ s := r.s;
+ r.s = r.e;
+ return r.buf[s:r.e];
+ }
+
+ for(i := r.s; i < r.e; i++)
+ if(r.buf[i] == c){
+ s := r.s;
+ r.s = i+1;
+ return r.buf[s:i + 1 * !r.strip];
+ }
+
+ if(r.s != 0){
+ r.buf[0:] = r.buf[r.s:r.e];
+ r.e -= r.s;
+ r.s = 0;
+ }
+ if(r.e == len r.buf)
+ raise "iobuf:no separator found in full buffer";
+
+ if(r.reader(r) == 0)
+ r.is_eof = 1;
+
+ }
+}
+
+sysread(r: ref ReadBuf): int
+{
+ n := sys->read(r.fd, r.buf[r.e:], len r.buf - r.e);
+ if(n < 0)
+ raise sprint("iobuf:%r");
+ r.e += n;
+ return n;
+}
+
+bufread(r: ref ReadBuf, buf: array of byte): int
+{
+ n := len buf;
+ if(len r.buf - r.e < n)
+ n = len r.buf - r.e;
+ r.buf[r.e:] = buf[0:n];
+ r.e += n;
+ if(len buf > n)
+ r.leftover = buf[n:];
+ else
+ r.leftover = nil;
+ return n;
+}
+
+chanread(r: ref ReadBuf): int
+{
+ if(r.leftover != nil)
+ return bufread(r, r.leftover);
+
+ alt{
+ buf := <-r.queue =>
+ if(buf == nil)
+ return 0;
+ else
+ return bufread(r, buf);
+ (buf, wc) := <-r.pending =>
+ n := len buf;
+ alt{
+ buf2 := <-r.queue =>
+ r.queue <-= buf;
+ buf = buf2;
+ * =>
+ ;
+ }
+ <-r.is_pending;
+ if(wc != nil)
+ wc <-= (n, nil);
+ if(buf == nil)
+ return 0;
+ else
+ return bufread(r, buf);
+ }
+}
+
+ReadBuf.readn(r: self ref ReadBuf, n: int): array of byte
+{
+ if(r.is_eof)
+ return nil;
+
+ if(r.e - r.s >= n){
+ s := r.s;
+ r.s += n;
+ return r.buf[s:r.s];
+ }
+
+ oldbuf : array of byte;
+
+ if(len r.buf >= n){
+ if(len r.buf - r.s < n){
+ r.buf[0:] = r.buf[r.s:r.e];
+ r.e -= r.s;
+ r.s = 0;
+ }
+ }
+ else{
+ oldbuf = r.buf;
+ r.buf = array[n] of byte;
+ r.buf[0:] = oldbuf[r.s:r.e];
+ r.e -= r.s;
+ r.s = 0;
+ }
+
+ while(r.e - r.s < n)
+ if(r.reader(r) == 0){
+ r.is_eof = 1;
+ n = r.e - r.s;
+ }
+
+ if(oldbuf == nil){
+ s := r.s;
+ r.s += n;
+ return r.buf[s:r.s];
+ }
+ else{
+ tmp := r.buf;
+ r.buf = oldbuf;
+ r.s = r.e = 0;
+ return tmp[:n];
+ }
+}
+
+ReadBuf.fill(r: self ref ReadBuf, data: array of byte, wc: Sys->Rwrite)
+{
+ alt{
+ r.is_pending <-= 1 =>
+ <-r.is_pending;
+ alt{
+ r.queue <-= data =>
+ if(wc != nil)
+ wc <-= (len data, nil);
+ * =>
+ r.is_pending <-= 1;
+ r.pending <-= (data, wc);
+ }
+ * =>
+ if(wc != nil)
+ wc <-= (0, "concurrent writes not supported");
+ }
+}
+
+#
+
+WriteBuf.new(fd: ref Sys->FD, bufsize: int): ref WriteBuf
+{
+ w := ref WriteBuf;
+ w.buf = array[bufsize] of byte;
+ w.s = 0;
+ w.e = 0;
+ w.fd = fd;
+ w.writer = syswrite;
+ return w;
+}
+
+WriteBuf.newc(bufsize: int): ref WriteBuf
+{
+ w := WriteBuf.new(nil, bufsize);
+ w.pending = chan[1] of (int, Sys->Rread);
+ w.writer = chanwrite;
+ return w;
+}
+
+WriteBuf.write(w: self ref WriteBuf, buf: array of byte)
+{
+ n := 0;
+
+ if(w.e != 0){
+ n = len w.buf - w.e;
+ if(n > len buf)
+ n = len buf;
+ w.buf[w.e:] = buf[:n];
+ w.e += n;
+ if(len w.buf == w.e)
+ w.flush();
+ }
+
+ if(len buf > n){
+ n2 := int((len buf - n) / len w.buf) * len w.buf;
+ if(n2 > 0){
+ tmp := w.buf;
+ w.buf = buf[n:n + n2];
+ w.s = 0;
+ w.e = n2;
+ w.flush();
+ w.buf = tmp;
+ n += n2;
+ }
+ w.buf[0:] = buf[n:];
+ w.e = len buf - n;
+ }
+
+ if(w.fd == nil && w.s != w.e)
+ optchanwrite(w);
+}
+
+WriteBuf.writeln(w: self ref WriteBuf, buf: array of byte)
+{
+ w.write(buf);
+ w.write(LF);
+}
+
+syswrite(w: ref WriteBuf)
+{
+ n := sys->write(w.fd, w.buf[w.s:w.e], w.e - w.s);
+ if(n != w.e - w.s)
+ raise sprint("iobuf:%r");
+ w.s = 0;
+ w.e = 0;
+}
+
+chanwrite(w: ref WriteBuf)
+{
+ (n, rc) := <-w.pending;
+ if(rc == nil)
+ raise "iobuf:broken pipe";
+ if(n > w.e - w.s)
+ n = w.e - w.s;
+ buf := array[n] of byte;
+ buf[0:] = w.buf[w.s:w.s + n];
+ rc <-= (buf, nil);
+ w.s += n;
+}
+
+optchanwrite(w: ref WriteBuf)
+{
+ alt{
+ (n, rc) := <-w.pending =>
+ if(rc == nil)
+ raise "iobuf:broken pipe";
+ if(n > w.e - w.s)
+ n = w.e - w.s;
+ buf := array[n] of byte;
+ buf[0:] = w.buf[w.s:w.s + n];
+ rc <-= (buf, nil);
+ w.s += n;
+ * =>
+ ;
+ }
+}
+
+WriteBuf.flush(w: self ref WriteBuf)
+{
+ while(w.s != w.e)
+ w.writer(w);
+ w.s = w.e = 0;
+}
+
+WriteBuf.eof(w: self ref WriteBuf)
+{
+ w.flush();
+ if(w.fd != nil)
+ return;
+ for(;;){
+ (nil, rc) := <-w.pending;
+ if(rc == nil)
+ break;
+ rc <-= (nil, nil);
+ }
+}
+
+WriteBuf.request(w: self ref WriteBuf, n: int, rc: Sys->Rread)
+{
+ if(rc == nil)
+ alt{
+ <-w.pending => ;
+ * => ;
+ }
+ alt{
+ w.pending <-= (n, rc) =>;
+ * => rc <-= (nil, "concurrent reads not supported");
+ }
+}
+
--- a/appl/lib/mkfile
+++ b/appl/lib/mkfile
@@ -56,6 +56,7 @@
html.dis\
imageremap.dis\
inflate.dis\
+ iobuf.dis\
ip.dis\
ipattr.dis\
ir.dis\
--- /dev/null
+++ b/man/2/iobuf
@@ -1,0 +1,292 @@
+.TH IOBUF 2
+.SH NAME
+iobuf: ReadBuf, WriteBuf \- read/write buffers
+.SH SYNOPSIS
+.EX
+include "iobuf.m";
+ iobuf: IOBuf;
+ ReadBuf, WriteBuf: import iobuf;
+iobuf = load IOBuf IOBuf->PATH;
+
+init: fn();
+
+ReadBuf: adt{
+ new: fn(fd: ref Sys->FD, bufsize: int): ref ReadBuf;
+ newc: fn(queuesize, bufsize: int): ref ReadBuf;
+ setsep: fn(r: self ref ReadBuf, sep: string, strip: int);
+ reads: fn(r: self ref ReadBuf): array of byte;
+ readn: fn(r: self ref ReadBuf, n: int): array of byte;
+ fill: fn(r: self ref ReadBuf, data: array of byte, wc: Sys->Rwrite);
+}
+
+WriteBuf: adt{
+ new: fn(fd: ref Sys->FD, bufsize: int): ref WriteBuf;
+ newc: fn(bufsize: int): ref WriteBuf;
+ write: fn(w: self ref WriteBuf, buf: array of byte);
+ flush: fn(w: self ref WriteBuf);
+ eof: fn(w: self ref WriteBuf);
+ request: fn(w: self ref WriteBuf, n: int, rc: Sys->Rread);
+}
+
+.EE
+.SH DESCRIPTION
+.PP
+This module provide simpler and faster alternative to
+.IR bufio (2).
+On reading text file it's about 30-40 times faster than bufio,
+on writing text file it's about 3-4 times faster than bufio.
+.PP
+.B init
+must be called before invoking any other operation of the module.
+.SS ReadBuf
+.PP
+ReadBuf is used when we receive stream of bytes (from fd or
+file2chan for ex.) while we need to read by full records (either
+separated by some delimiter or having known size).
+.PP
+Reading from ReadBuf is blocking operation.
+.PP
+.B setsep
+convert
+.I sep
+from string to array of byte, and
+.B reads
+will use
+that array to search for separator.
+If separator will be Unicode char which may be encoded with different
+sequences of bytes,
+.B reads
+may fail to find it.
+.PP
+.B reads
+return record separated by
+.IR sep ,
+optionally with separator
+stripped from end of record.
+Last record may not end with separator, so ReadBuf can't distinguish
+between incomplete record because of unexpected EOF and full last record
+without separator.
+Will return nil on EOF.
+Will raise on I/O error.
+Will raise if neither
+.I sep
+nor EOF will be found in
+.I bufsize
+bytes.
+.PP
+.B readn
+return record with
+.I n
+bytes size, or less on EOF.
+It's possible to have
+.I n
+greater than
+.IR bufsize .
+Will return array with less than
+.I n
+bytes on EOF.
+Will return nil on EOF.
+Will raise on I/O error.
+.PP
+Arrays returned by
+.B reads
+and
+.B readn
+usually will be slices of
+ReadBuf's internal buffer, which may be overwritten on next
+.B reads
+or
+.B readn
+calls, so these calls may change contents of
+previously returned arrays.
+.PP
+When ReadBuf used with chan instead of fd,
+.I queuesize
+define
+maximum amount of packets (not bytes!) received from chan, which
+wasn't fetched yet by
+.B reads
+or
+.BR readn .
+This is needed to optimize latency.
+.PP
+When ReadBuf used with chan, while one process may block in
+.B reads
+or
+.BR readn ,
+another may receive data from chan, and
+should call
+.B fill
+to put this data into ReadBuf.
+.PP
+.B fill
+will either immediately send reply to
+.I wc
+if it was able
+to add data to ReadBuf, or save pending
+.I data
+and
+.I wc
+in
+ReadBuf (reply to
+.I wc
+will be sent later from process
+calling
+.B reads
+or
+.BR readn ).
+Call to
+.B fill
+never blocks.
+Will send error "concurrent writes not supported" to
+.I wc
+and drop
+.I data
+if will be called again before reply to
+previous
+.I wc
+will be sent (i.e. when previous
+.I wc
+is in
+pending state because of full incoming queue).
+.PP
+Resume:
+.RS
+.IP •
+Process reading from ReadBuf doesn't need to know about
+data source (fd or chan).
+.IP •
+Process reading from ReadBuf may intermix reads() and
+readn(), may change record separator at any time.
+.IP •
+Process reading from ReadBuf receive nil on EOF or got
+exception on I/O error.
+.IP •
+Process receiving data from chan (usually, file2chan) for
+ReadBuf just call fill() and don't bother about errors or
+replying to
+.IR wc .
+.RE
+.PP
+Limitations:
+.RS
+.IP •
+Unicode separator may not be detected in some cases.
+.IP •
+Offset/seek doesn't supported (so offset received with
+file2chan request will be ignored).
+.IP •
+No getb(), getc(), ungetb(), ungetc() - but they can be
+added later.
+.IP •
+Only one process may call reads() or readn() and only one
+(another) process may call fill().
+.RE
+.SS WriteBuf
+.PP
+WriteBuf is used when we sending stream of bytes (to fd or
+file2chan for ex.) while we want to write data by (possibly
+small) records.
+.PP
+Writing to WriteBuf is blocking operation.
+.PP
+.B write
+is adding data from
+.I buf
+to WriteBuf. Size of
+.I buf
+may
+be greater than
+.IR bufsize .
+It may call
+.BR flush .
+Will raise on I/O error.
+.PP
+.B flush
+ensure all buffered data in WriteBuf is actually written.
+Will raise on I/O error.
+.PP
+When WriteBuf used with chan, while one process may block in
+.B write
+or
+.BR flush ,
+another may receive read request from chan,
+and should call
+.B request
+to let
+.B write
+or
+.B flush
+send data from
+WriteBuf to chan when they'll be ready.
+.PP
+.B eof
+calls
+.BR flush ,
+but what it does next depends on WriteBuf type.
+When WriteBuf used with fd, it do nothing more and just returns.
+When WriteBuf used with chan, it'll wait for next
+.B request
+and will reply
+on all with EOF
+.BI ( "" nil "" , "" nil "" )
+, and will returns only after got
+.B request
+with
+.B nil
+.IR rc .
+.PP
+.B request
+notified WriteBuf about data requested by chan, to let
+.B write
+or
+.B flush
+to send data from
+.I buf
+to chan.
+Call to
+.B request
+never blocks.
+Will send error "concurrent reads not supported" to
+.I rc
+if will be called again before reply to previous
+.I rc
+will
+be sent.
+.PP
+Resume:
+.RS
+.IP •
+Process writing to WriteBuf doesn't need to know about
+data destination (fd or chan).
+.IP •
+Process writing to WriteBuf got exception on I/O error.
+.IP •
+Process receiving read requests from chan (usually, file2chan)
+from WriteBuf just call request() and don't bother about errors
+or replying to
+.IR rc .
+.RE
+.PP
+Limitations:
+.RS
+.IP •
+Offset/seek doesn't supported (so offset received with
+file2chan request will be ignored).
+.IP •
+Only one process may call write() or flush() and only one
+(another) process may call request().
+.RE
+.SH EXAMPLES
+.EX
+
+
+.EE
+.SH SOURCE
+.PP
+.B /opt/powerman/iobuf/appl/lib/iobuf.b
+.br
+.SH SEE ALSO
+.PP
+.IR bufio (2)
+.SH BUGS
--- /dev/null
+++ b/man/4/ttffs
@@ -1,0 +1,101 @@
+.TH TTFFS 4
+.SH NAME
+ttffs \- serve ttf fonts as Inferno (sub)fonts
+.SH SYNOPSIS
+.B ttffs
+[
+.B -dM
+] [
+.B -p
+.I fontpath
+]
+.SH DESCRIPTION
+.I Ttffs
+serves
+.I TrueType
+(ttf) font files
+as Inferno (sub)fonts in arbitrary sizes, over 9P2000.
+Directory /fonts/ttf is read at startup (or
+.I fontpath
+set with
+.BR -p ),
+and all ttf files added to ttffs' index.
+New ttf files are automatically indexed when the
+.I index
+file (described below) is opened.
+Each ttf file has an associated
+.I ranges
+file listing the glyphs available in the ttf file.
+If a ttf file does not yet have a ranges file, it is created by ttffs automatically.
+.I Ttffs
+mounts itself on
+.I /mnt/ft
+by default.
+With
+.BR -M ,
+9P2000 is served on file descriptor 0 instead.
+Option
+.B -d
+enables debug printing.
+.SS Files
+The following files are served:
+.TP
+index
+A read-only file returning the available fonts, one line for each font name.
+A line consists of two quoted strings, separated by a space.
+The first is a cleaned-up version of the name (as found in the ttf file) of the font.
+The second field is a font specification as understood by
+.IR fontsrv (4).
+The font name in the specification is the original name found in the ttf file.
+Multiple ttf files may result in a single line, e.g. in the case of a single font face available in multiple styles (regular, italic, bold).
+.TP
+.IR fontname . style . size .font
+When a file of this form is walked to, the ttf file associated with that font specification is opened, parsed and cached for subsequent use.
+.I Fontname
+is a cleaned-up version of the font name contained in the
+.I ttf
+file.
+.I Style
+must be one of
+.IR r ,
+.IR i ,
+.I b
+or
+.IR ib .
+.I Size
+(in pixels) must be a number greater than 1.
+.TP
+.IR fontname . style . size . index
+Subfonts that are referenced by the
+.IR .font -files.
+.I Index
+is a number indicating the glyph range of the subfont.
+.SH EXAMPLES
+.EX
+# ensure /fonts/ttf/DejaVuSansMono.ttf and /mnt/ft exist
+ttffs
+wm/sh -f /mnt/ft/dejavu-sans-mono.r.10.font
+.EE
+.SH SOURCE
+.B /appl/cmd/ttffs.b
+.SH FILES
+.B /mnt/ft/index
+.br
+.B /mnt/ft/*.font
+.br
+.B /fonts/ttf/*.ttf
+.br
+.B /fonts/ttf/ranges.*
+.SH SEE ALSO
+.IR draw-font (2),
+.IR fontsrv (4),
+.IR fonts (6).
+.SH BUGS
+No kerning or subpixel rendering is supported.
+Only 8-bit greyscale anti-aliased fonts are served.
+.PP
+The name
+.I ttffs
+ is not appropriate, more formats than just ttf could be served: freetype supports more than just ttf.
+.PP
+Generating ranges.* files is slow.
--- /dev/null
+++ b/module/iobuf.m
@@ -1,0 +1,45 @@
+IOBuf: module
+{
+ PATH: con "/dis/lib/iobuf.dis";
+
+ init: fn();
+
+ ReadBuf: adt{
+ new: fn(fd: ref Sys->FD, bufsize: int): ref ReadBuf;
+ newc: fn(queuesize, bufsize: int): ref ReadBuf;
+ setsep: fn(r: self ref ReadBuf, sep: string, strip: int);
+ reads: fn(r: self ref ReadBuf): array of byte;
+ readn: fn(r: self ref ReadBuf, n: int): array of byte;
+ fill: fn(r: self ref ReadBuf, data: array of byte, wc: Sys->Rwrite);
+ # Internal:
+ buf: array of byte;
+ s: int;
+ e: int;
+ sep: array of byte;
+ strip: int;
+ reader: ref fn(r: ref ReadBuf): int;
+ is_eof: int;
+ fd: ref Sys->FD;
+ leftover: array of byte;
+ queue: chan of array of byte;
+ pending: chan of (array of byte, Sys->Rwrite);
+ is_pending: chan of int;
+ };
+
+ WriteBuf: adt{
+ new: fn(fd: ref Sys->FD, bufsize: int): ref WriteBuf;
+ newc: fn(bufsize: int): ref WriteBuf;
+ write: fn(w: self ref WriteBuf, buf: array of byte);
+ writeln: fn(w: self ref WriteBuf, buf: array of byte);
+ flush: fn(w: self ref WriteBuf);
+ eof: fn(w: self ref WriteBuf);
+ request: fn(w: self ref WriteBuf, n: int, rc: Sys->Rread);
+ # Internal:
+ buf: array of byte;
+ s: int;
+ e: int;
+ writer: ref fn(w: ref WriteBuf);
+ fd: ref Sys->FD;
+ pending: chan of (int, Sys->Rread);
+ };
+};