code: purgatorio

Download patch

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);
+	};
+};