shithub: irc.myr

Download patch

ref: c62a976e77e3fa7f12a2f105c4fc1adcf40fb194
parent: 1e548b7fd5c731682ad49d01bb834037e987c9cf
author: Ori Bernstein <ori@eigenstate.org>
date: Thu Jan 4 05:43:13 EST 2018

Split client into multiple files.

--- a/bld.proj
+++ b/bld.proj
@@ -1,2 +1,8 @@
-bin irc.myr = main.myr ;;
+bin irc.myr =
+	types.myr
+	main.myr
+	irc.myr
+	draw.myr
+;;
+
 man = irc.myr.1 ;;
--- /dev/null
+++ b/draw.myr
@@ -1,0 +1,272 @@
+use std
+use date
+use iter
+use termdraw
+
+use "types"
+use "irc"
+
+pkg =
+	const redraw	: (irc : irc# -> void)
+;;
+
+const redraw = {irc
+	var c, x, y, w, h
+
+	if irc.term.width < 10 || irc.term.height < 5
+		-> void
+	;;
+
+	x = 0
+	y = 0
+	w = irc.term.width
+	h = irc.term.height
+	c = curchan(irc)
+	if irc.chandirty
+		drawbanner(irc, 0, 0, w, 1, c)
+		drawtext(irc, 0, 1, w, h - 2, c)
+		drawlist(irc, 0, h - 2, w, h - 1, c)
+		irc.chandirty = false
+	;;
+	if irc.cmddirty
+		drawinput(irc, 0, h - 1, w, h, c)
+		irc.cmddirty = false
+	;;
+	termdraw.flush(irc.term)
+}
+
+const drawbanner = {irc, x0, y0, x1, y1, c
+	var t
+
+	t = irc.term
+	termdraw.setbg(t, termdraw.Blue)
+	termdraw.clear(t, x0, y0, x1, y1)
+	termdraw.move(t, x0, y0)
+	termdraw.put(t, "{}", c.topic)
+}
+
+const drawtext = {irc, x0, y0, x1, y1, c
+	var height, margin, width, off
+	var t, dx, dy
+	var count
+	var x, y
+
+	t = irc.term
+	termdraw.setbg(t, termdraw.Default)
+	termdraw.clear(t, x0, y0, x1, y1)
+	termdraw.move(t, x0, y0)
+
+	dx = x1 - x0
+	dy = y1 - y0
+	count = 0
+	height = 0
+	width = dx
+	off = 0
+	/* nothing worth drawing... */
+	if dx <= c.gutter
+		-> void
+	;;
+	for (tm, ent) : iter.byreverse(c.hist[:c.hist.len - c.scroll])
+		margin = 0
+		match ent
+		| `Msg (m, ln):
+			margin = strwidth(x0, c.gutter, " | ") + c.gutter
+			width = strwidth(x0, margin, ln)
+		| `Act (m, ln):
+			margin = strwidth(x0, 0, "  *")
+			width = strwidth(x0, margin, m)
+			width += strwidth(x0, margin, " ")
+			width += strwidth(x0, margin, ln)
+		| `Join user:
+			margin = strwidth(x0, 0, "#joined: ")
+			width = strwidth(x0, margin, user)
+		| `Part user:
+			margin = strwidth(x0, 0, "#parted: ")
+			width = strwidth(x0, margin, user)
+		| `Status msg:
+			margin = strwidth(x0, 0, "! ")
+			width = strwidth(x0, margin, msg)
+		;;
+		/* no point in drawing if all we draw is gutter */
+		if dx - margin <=  0
+			-> void
+		;;
+		height++
+		if width > 0
+			height += width / (dx - margin)
+		;;
+		count++
+		if height == dy
+			break
+		elif height > dy
+			off = (dx - margin)*(height - dy)
+			break
+		;;
+	;;
+
+	x = x0
+	y = y0
+	for (tm, h) : c.hist[c.hist.len - c.scroll - count:]
+		match h
+		| `Msg (m, ln):
+			termdraw.setattr(t, termdraw.Bold)
+			x = std.clamp(x0 + c.gutter - strwidth(x0, 0, m), 0, dx)
+			termdraw.move(t, x0 + c.gutter, y)
+			(x, y) = draw(t, m, x, y, x1, y1)
+			(x, y) = draw(t, " | ", x, y, x1, y1)
+			termdraw.setattr(t, lineattr(irc, ln[off:]))
+			(x, y) = draw(t, ln[off:], x, y, x1, y1)
+			termdraw.setattr(t, termdraw.Normal)
+		| `Act (m, ln):
+			termdraw.setattr(t, termdraw.Bold)
+			(x, y) = draw(t, "  *", x0, y, x1, y1)
+			(x, y) = draw(t, m, x, y, x1, y1)
+			termdraw.setattr(t, lineattr(irc, ln[off:]))
+			(x, y) = draw(t, " ", x, y, x1, y1)
+			(x, y) = draw(t, ln[off:], x, y, x1, y1)
+			termdraw.setattr(t, termdraw.Normal)
+		| `Join user:
+			(x, y) = draw(t, "#joined ", x0, y, x1, y1)
+			(x, y) = draw(t, user[off:], x, y, x1, y1)
+		| `Part user:
+			(x, y) = draw(t, "#parted ", x0, y, x1, y1)
+			(x, y) = draw(t, user[off:], x, y, x1, y1)
+		| `Status msg:
+			(x, y) = draw(t, "! ", x0, y, x1, y1)
+			(x, y) = draw(t, msg[off:], x, y, x1, y1)
+		;;
+		y++
+		off = 0
+		if y >= y1
+			break
+		;;
+	;;
+}
+
+const strwidth = {x0, margin, str
+	var x
+
+	x = x0 + margin
+	for c : std.bychar(str)
+		match c
+		| '\t':	x = (x / 8 + 1) * 8
+		| _:	x += (std.cellwidth(c) : int)
+		;;
+	;;
+	-> x - (x0 + margin)
+}
+
+const draw = {t, msg, x0, y0, x1, y1
+	var x, y
+
+	x = x0
+	y = y0
+	for l : std.bychar(msg)
+		match l
+		| '\t':	
+			x = (x / 8 + 1)*8
+		| '\n':	
+			termdraw.put(t, "\\n")
+			x += 2
+		| chr:
+			if x < x1
+				termdraw.move(t, x, y)
+				termdraw.putc(t, chr)
+				x++
+			;;
+		;;
+		if x >= x1
+			x = x0
+			y++
+		;;
+	;;
+	-> (x, y)
+}
+
+const drawlist = {irc, x0, y0, x1, y1, c
+	var t
+
+	t = irc.term
+	termdraw.setbg(t, termdraw.Blue)
+	termdraw.clear(t, x0, y0, x1, y1)
+	termdraw.move(t, x0, y0)
+	termdraw.putc(t, '|')
+	termdraw.put(t, "[{}]", irc.self.name)
+	for s : irc.srv
+		for ch : s.chan
+			if ch.flagged
+				termdraw.setattr(t, termdraw.Bold)
+				termdraw.put(t, "[@{}]", ch.name)
+				termdraw.setattr(t, termdraw.Normal)
+			elif ch.stale
+				termdraw.put(t, "[*{}]", ch.name)
+			else
+				termdraw.put(t, "[{}]", ch.name)
+			;;
+		;;
+		termdraw.setattr(t, termdraw.Normal)
+		termdraw.putc(t, '|')
+	;;
+}
+
+const drawinput = {irc, x0, y0, x1, y1, c
+	var t, chan, ln, cx, dx
+
+	dx = x1 - x0
+	t = irc.term
+	chan = std.fmt("[{}] ", c.name)
+	ln = inputstr(irc, chan, dx)
+	cx = x0 + (std.strcellwidth(chan) + irc.off : int)
+	if cx > dx
+		cx = dx
+	;;
+	termdraw.setbg(t, termdraw.Default)
+	termdraw.move(t, x0, y0)
+	termdraw.clear(t, x0, y0, x1, y1)
+	termdraw.put(t, "{}{}", chan, ln)
+	termdraw.cursorpos(t, cx, y0)
+	std.slfree(chan)
+	std.slfree(ln)
+}
+
+const lineattr = {irc, ln
+	match cursrv(irc)
+	| `std.None:	-> termdraw.Normal
+	| `std.Some srv:
+		match std.strfind(ln, srv.nick)
+		| `std.Some _:	-> termdraw.Italic
+		| `std.None:	-> termdraw.Normal
+		;;
+	;;
+}
+
+const inputstr = {irc, chan, dx
+	var w, s, o
+	var start, end
+
+	s = irc.cmd
+	o = (irc.off : int)
+	dx -= strwidth(0, 0, chan)
+	w = 0
+	start = 0
+	end = 0
+	for var i = 0; i < s.len; i++
+		w += std.cellwidth(s[i])
+		if w >= dx
+			break
+		;;
+		end++
+	;;
+	while end < s.len && irc.off >= (w : std.size)
+		start++
+		while std.cellwidth(s[end]) == 0
+			start++
+		;;
+		w += std.cellwidth(s[end])
+		end++
+		while end < s.len && std.cellwidth(s[end]) == 0
+			end++
+		;;
+	;;
+	-> getstr(s[start:end])
+}
--- /dev/null
+++ b/irc.myr
@@ -1,0 +1,830 @@
+use std
+use sys
+use bio
+use date
+use iter
+use termdraw
+use fileutil	/* for homedir() */
+
+use "types"
+
+pkg =
+	const io	: (irc : irc#, fd : std.fd -> void)
+	const curchan	: (irc : irc# -> chan#)
+	const cursrv	: (irc : irc# -> std.option(server#))
+	const send	: (irc : irc#, srv : server#, fmt : byte[:], args : ... -> void)
+	const status	: (irc : irc#, chan : chan#, fmt : byte[:], args : ... -> void)
+	const chancycle	: (irc : irc#, delta : std.size -> void)
+	const handshake	: (irc : irc#, srv : server# -> bool)
+	const message	: (irc : irc#, ln : byte[:] -> void)
+	const cmd	: (irc : irc#, ln : byte[:] -> void)
+	const getstr	: (chars : char[:] -> byte[:])
+	const puthist	: (irc : irc#, chan : chan#, ent : (std.time, hist) -> void)
+;;
+
+const io = {irc, fd
+	var src, cmd, args
+	var srv, ln
+
+	srv = fd2srv(irc, fd)
+
+	match std.read(srv.fd, srv.buf[srv.nbuf:])
+	| `std.Ok 0:	closed(irc, srv, "eof")
+	| `std.Ok n:	srv.nbuf += n
+	| `std.Err e:	closed(irc, srv, "error reading")
+	;;
+
+	while true
+		match nextln(irc, srv)
+		| `std.Some l:	ln = l
+		| `std.None:	-> void
+		;;
+		(src, cmd, args) = parse(ln)
+		uppercase(cmd)
+		match cmd
+		| "001":	srvmsg(irc, args)
+		| "002":	srvmsg(irc, args)
+		| "003":	srvmsg(irc, args)
+		| "004":	srvmsg(irc, args)
+		| "005":	srvmsg(irc, args)
+		| "250":	srvmsg(irc, args)
+		| "251":	srvmsg(irc, args)
+		| "254":	srvmsg(irc, args)
+		| "255":	srvmsg(irc, args)
+		| "265":	srvmsg(irc, args)
+		| "266":	srvmsg(irc, args)
+		| "375":	srvmsg(irc, args)
+		| "372":	srvmsg(irc, args)
+		| "376":	srvmsg(irc, args)
+		| "NOTICE":	srvmsg(irc, args)
+		| "332":	topic(irc, srv, args)
+		| "353":	addusers(irc, srv, args)
+		| "QUIT":	deluser(irc, srv, src)
+		| "PART":	delchanuser(irc, srv, src, args)
+		| "366":	shownames(irc, srv, args, 1)
+		| "PRIVMSG":	recievemsg(irc, srv, src, args)
+		| "PING":	send(irc, srv, "PONG :{}\r\n", args[0])
+		| "JOIN":	joined(irc, srv, src, args)
+		| "NICK":	renamed(irc, srv, src, args)
+		| c:		status(irc, irc.self, "unknown server command {}", ln)
+		;;
+		std.slfree(args)
+		std.slfree(ln)
+	;;
+}
+
+const renamed = {irc, srv, src, args
+	var a, b
+	if args.len > 0
+		for c : srv.chan
+			a = displayname(src)
+			b = displayname(args[0])
+			match std.lsearch(c.users, a, std.strcmp)
+			| `std.None:	/* skip */
+			| `std.Some i:
+				std.slfree(c.users[i])
+				c.users[i] = std.sldup(b)
+				status(irc, c, "changed nick: {} => {}", a, b)
+			;;
+		;;
+	;;
+}
+
+const joined = {irc, srv, src, args
+	var c, name
+
+	if args.len == 1
+		c = name2chan(irc, srv, args[0])
+		name = displayname(src)
+		c.gutter = std.max(c.gutter, std.cellwidth(name.len))
+		match std.lsearch(c.users, name, std.strcmp)
+		| `std.None:	std.slpush(&c.users, std.sldup(name))
+		| `std.Some _:	/* ignore */
+		;;
+		puthist(irc, c, (std.now(), `Join std.sldup(name)))
+		irc.chandirty = true
+		irc.cmddirty = true
+	;;
+}
+
+const topic = {irc, srv, args
+	var c
+
+	if args.len >= 3
+		c = name2chan(irc, srv, args[1])
+		std.slfree(c.topic)
+		c.topic = std.sldup(args[2])
+	;;
+}
+
+const recievemsg = {irc, srv, src, args
+	var c
+
+	if std.sleq(args[0], srv.nick)
+		c = name2chan(irc, srv, displayname(src))
+	else
+		c = name2chan(irc, srv, args[0])
+	;;
+	if args.len > 1 && isctcp(args[1]) 
+		std.put("got CTCP {e}\n", args[1])
+		ctcpmsg(irc, srv, c, src, args[1][1:args[1].len-1])
+	else
+		chanmsg(irc, srv, c, src, args[1])
+	;;
+}
+
+const isctcp = {m
+	-> std.hasprefix(m, "\x01") && std.hassuffix(m, "\x01")
+}
+
+const addusers = {irc, srv, args
+	var c
+
+	if args.len != 4
+		-> void
+	;;
+	c = name2chan(irc, srv, args[2])
+	for n : std.bysplit(args[3], " ")
+		n = std.strstrip(n)
+		match std.lsearch(c.users, n, std.strcmp)
+		| `std.None:    std.slpush(&c.users, std.sldup(n))
+		| `std.Some _:  /* ignore */
+		;;
+		c.gutter = std.max(c.gutter, n.len)
+	;;
+}
+
+const deluser = {irc, srv, user
+	for c : srv.chan
+		user = displayname(user)
+		match std.lsearch(c.users, user, std.strcmp)
+		| `std.None:    
+			continue
+		| `std.Some i:  
+			puthist(irc, c, (std.now(), `Part std.sldup(user)))
+			std.slfree(c.users[i])
+			std.sldel(&c.users, i)
+		;;
+	;;
+}
+
+const delchanuser = {irc, srv, user, args
+	var c
+
+	if args.len == 0
+		-> void
+	;;
+	c = name2chan(irc, srv, args[0])
+	user = displayname(user)
+	match std.lsearch(c.users, user, std.strcmp)
+	| `std.None:    /* ignore */
+	| `std.Some i:
+		std.slfree(c.users[i])
+		std.sldel(&c.users, i)
+		puthist(irc, c, (std.now(), `Part std.sldup(user)))
+	;;
+}
+
+const fd2srv = {irc, fd
+	for s : irc.srv
+		if s.fd == fd
+			-> s
+		;;
+	;;
+	std.fatal("missing srv for fd {}", fd)
+}
+
+const name2chan = {irc, srv, chan
+	var new
+
+	for c : srv.chan
+		if std.sleq(c.name, chan)
+			-> c
+		;;
+	;;
+
+	new = mkchan(irc, srv.ds, chan, "")
+	std.slpush(&srv.chan, new)
+	if srv.focus == -1
+		srv.focus = srv.chan.len - 1
+	;;
+	status(irc, new, "joined on {}", date.now("local"))
+	-> new
+}
+
+const mkchan = {irc, srvname, name, topic
+	var logpath, logfd
+
+	logpath = std.pathjoin([irc.logdir, srvname, name][:])
+	std.mkpath(std.dirname(logpath))
+	match std.openmode(logpath, std.Ordwr | std.Ocreat | std.Oappend, 0o666)
+	| `std.Ok fd:	logfd = fd
+	| `std.Err _:	logfd = -1
+	;;
+	-> std.mk([
+		.log = logfd,
+		.hist = [][:], /* FIXME: read back from log */
+		.name = std.sldup(name),
+		.topic = std.sldup(topic),
+		.gutter = 8
+	])
+}
+
+const mksrv = {irc, fd, ds -> server#
+	-> std.mk([
+		.fd=fd,
+		.ds=std.sldup(ds),
+		.chan=[][:],
+		.focus=-1,
+		.user=std.sldup(irc.user),
+		.nick=std.sldup(irc.nick),
+	])
+}
+
+const nextln = {irc, srv
+	var r
+
+	match std.strfind(srv.buf[:srv.nbuf], "\r\n")
+	| `std.Some i:
+		r = std.sldup(srv.buf[:i])
+		std.slcp(srv.buf[:srv.nbuf - i - 2], srv.buf[i + 2:srv.nbuf])
+		srv.nbuf = srv.nbuf - i - 2
+		-> `std.Some r
+	| `std.None:
+		-> `std.None
+	;;
+}
+
+const closed = {irc, srv, msg
+	if srv.death == 0
+		srv.death = std.now()
+		status(irc, irc.self, "{} closed: {}", srv.ds, msg)
+	;;
+}
+
+
+const displayname = {src
+	match std.strfind(src, "!")
+	| `std.Some i:	-> src[:i]
+	| `std.None:	-> src
+	;;
+}
+
+const freechan = {chan
+	std.slfree(chan.name)
+	std.slfree(chan.topic)
+	std.free(chan)
+}
+
+const getstr = {chars
+	var sb = std.mksb()
+	for c : chars
+		std.sbputc(sb, c)
+	;;
+	-> std.sbfin(sb)
+}
+
+const cmd = {irc, text
+	var sp
+
+	sp = std.strtok(text)
+	if sp.len == 0
+		-> void
+	;;
+	match sp[0]
+	| "/connect":	connect(irc, sp[1:])
+	| "/join":	join(irc, sp[1:])
+	| "/leave":	leave(irc, sp[1:])
+	| "/chan":	chanswitch(irc, sp[1:])
+	| "/win":	chanswitch(irc, sp[1:])
+	| "/quit":	quit(irc, sp[1:])
+	| "/help":	help(irc, sp[1:])
+	| "/nick":	changenick(irc, sp[1:])
+	| "/user":	changeuser(irc, sp[1:])
+	| "/srv":	changeserver(irc, sp[1:])
+	| "/names":	listnames(irc, sp[1:])
+	| "/msg":	privmsg(irc, sp[1:])
+	| "/me":	action(irc, sp[1:])
+	| "/":		message(irc, std.strfstrip(text[1:]))
+	| c:		status(irc, irc.self, "unknown command: /{}", text)
+	;;
+	std.slfree(sp)
+}
+
+
+const changenick = {irc, args
+	if args.len != 1
+		status(irc, irc.self, "/nick: invalid args {j= }", args)
+		-> void
+	;;
+
+	match cursrv(irc)
+	| `std.None:
+		std.slfree(irc.nick)
+		irc.nick = std.sldup(args[0])
+		status(irc, irc.self, "default nick changed: {}", args[0])
+	| `std.Some srv:
+		std.slfree(irc.nick)
+		srv.nick = std.sldup(args[0])
+		send(irc, srv, "NICK {}\r\n", srv.nick)
+		status(irc, irc.self, "nick changed for {}: {}", srv.ds, args[0])
+	;;
+}
+
+const changeuser = {irc, args
+	if args.len != 1
+		status(irc, irc.self, "/nick: invalid args {j= }", args)
+		-> void
+	;;
+
+	match cursrv(irc)
+	| `std.None:
+		std.slfree(irc.user)
+		irc.user = std.sldup(args[0])
+	| `std.Some srv:
+		std.slfree(irc.user)
+		srv.user = std.sldup(args[0])
+		send(irc, srv, "NICK {}\r\n", srv.user)
+	;;
+}
+
+const changeserver = {irc, args
+	if args.len != 1
+		status(irc, irc.self, "/nick: invalid args {j= }", args)
+		-> void
+	;;
+	match findsrv(irc, args[0])
+	| `std.Some s:	irc.focus = s.id
+	| `std.None:	status(irc, irc.self, "no server '{}", args[0])
+	;;
+}
+
+const connect = {irc, args
+	var ds, srv
+
+	ds = ""
+	match args.len
+	| 1:	ds = std.netaddr(args[0], "tcp", "ircd")
+	| 0:	status(irc, irc.self, "join: missing server")
+	| _:	status(irc, irc.self, "join: invalid arguments '{j= }'", args)
+	;;
+	if ds.len == 0
+		-> void
+	;;
+
+	for s : irc.srv
+		if std.sleq(s.ds, ds)
+			status(irc, irc.self, "already connected to {j= }", args)
+			-> void
+		;;
+	;;
+
+	status(irc, irc.self, "dialing {}", ds)
+	match std.dial(ds)
+	| `std.Ok fd:
+		srv = mksrv(irc, fd, ds)
+		if handshake(irc, srv)
+			srv.id = irc.srv.len
+			std.slpush(&irc.srv, srv)
+			if irc.focus == -1
+				irc.focus = irc.srv.len - 1
+				srv.death = 0
+			;;
+			status(irc, irc.self, "connected to {}", ds)
+		else
+			status(irc, irc.self, "could not handshake with {j= }", args)
+			std.close(srv.fd)
+			std.free(srv)
+		;;
+	| `std.Err e:
+		status(irc, irc.self, "failed to connect: {}", e)
+	;;
+}
+
+const listnames = {irc, args
+	match cursrv(irc)
+	| `std.Some s:	shownames(irc, s, args, 0)
+	| `std.None:	/* nothing */
+	;;
+}
+
+const shownames = {irc, srv, args, idx
+	var c
+
+	if args.len > idx &&  irc.focus >= 0
+		c = name2chan(irc, srv, args[idx])
+	else
+		c = curchan(irc)
+	;;
+	if c != irc.self
+		status(irc, c, "{j= }", c.users)
+	;;
+}
+
+const handshake = {irc, srv
+	send(irc, srv, "NICK {}\r\n", srv.nick)
+	send(irc, srv, "USER {} 8 * :{}\r\n", srv.user, srv.user)
+	-> true
+}
+
+const join = {irc, args
+	if args.len == 2
+		match findsrv(irc, args[1])
+		| `std.Some s:	send(irc, s, "JOIN {}\r\n", args[0])
+		| `std.None:	status(irc, irc.self, "no server '{}", args[1])
+		;;
+	elif args.len == 1
+		match cursrv(irc)
+		| `std.Some s:	send(irc, s, "JOIN {}\r\n", args[0])
+		| `std.None:	status(irc, irc.self, "no server focused")
+		;;
+	else
+		-> status(irc, irc.self, "invalid arguments: {j= }", args)
+	;;
+}
+
+const leave = {irc, args
+	var name, c, srv
+
+	if irc.focus < 0
+		-> void
+	;;
+	if args.len == 0
+		c = curchan(irc)
+		if c == irc.self
+			-> void
+		;;
+		name = c.name
+	elif args.len == 1
+		name = args[0]
+	else
+		status(irc, irc.self, "leave: invalid arguments: {j= }", args)
+		-> void
+	;;
+
+	srv = irc.srv[irc.focus]
+	for var i = 0; i < srv.chan.len; i++
+		c = srv.chan[i]
+		if std.sleq(name, c.name)
+			send(irc, srv, "PART {}\r\n", name)
+			std.sldel(&srv.chan, i)
+			if i == srv.focus
+				irc.focus = -1
+			;;
+			freechan(c)
+			break
+		;;
+
+	;;
+}
+
+const chancycle = {irc, delta
+	var srv
+
+	if irc.srv.len == 0
+		-> void
+	;;
+
+	irc.chandirty = true
+	irc.cmddirty = true
+	for var i = 0; i < irc.srv.len; i++
+		if irc.focus < 0 || irc.focus >= irc.srv.len
+			irc.focus = 0
+		;;
+
+		srv = irc.srv[irc.focus]
+		srv.focus += delta
+		if srv.focus < 0 || srv.focus >= srv.chan.len
+			irc.focus += delta
+			if irc.focus < 0
+				irc.focus += irc.srv.len
+			;;
+			irc.focus %= irc.srv.len
+			irc.srv[irc.focus].focus = 0
+		;;
+		srv = irc.srv[irc.focus]
+		if srv.chan.len > 0
+			srv.chan[srv.focus].stale = false
+			srv.chan[srv.focus].flagged = false
+			break
+		;;
+	;;
+}
+
+const chanswitch = {irc, args
+	/* full, end, internal */
+	var matches = [(-1, -1), (-1, -1), (-1, -1)]
+	var srv
+
+	if args.len != 1
+		status(irc, irc.self, "invalid arguments: {j= }", args)
+	else
+		irc.chandirty = true
+		irc.cmddirty = true
+		match matchrank(irc.self.name, args[0])
+		| `std.None:		/* ok */
+		| `std.Some rank:	matches[rank] = (0, -1)
+		;;
+		for var i = 0; i < irc.srv.len; i++
+			srv = irc.srv[i]
+			for var j = 0; j < srv.chan.len; j++
+				match matchrank(srv.chan[j].name, args[0])
+				| `std.None:		/* ok */
+				| `std.Some rank:
+					matches[rank] = (j, i)
+				;;
+			;;
+		;;
+
+		for (chanidx, srvidx) : matches[:]
+			if chanidx < 0
+				continue
+			;;
+			irc.focus = srvidx
+			if srvidx >= 0
+				irc.srv[srvidx].focus = chanidx
+				irc.srv[srvidx].chan[chanidx].stale = false
+				irc.srv[srvidx].chan[chanidx].flagged = false
+			;;
+			break
+		;;
+	;;
+}
+
+const matchrank = {str, name
+	match std.strfind(str, name)
+	| `std.None:
+		-> `std.None
+	| `std.Some idx:
+		match (idx, idx + name.len == str.len)
+		| (0, true):	-> `std.Some 0
+		| (_, true):	-> `std.Some 1
+		| (0, false):	-> `std.Some 1
+		| (_, _):	-> `std.Some 2
+		;;
+	;;
+}
+
+const quit = {irc, args
+	if args.len != 0
+		status(irc, irc.self, "invalid argument for quit: {j= }", args)
+		-> void
+	;;
+	termdraw.free(irc.term)
+	std.exit(0)
+}
+
+const help = {irc, args
+	status(irc, irc.self, "irc.myr commands")
+	status(irc, irc.self, "\t/connect dialstr: connect to server")
+	status(irc, irc.self, "\t/help [cmd...]:   get help [on cmd...]")
+	status(irc, irc.self, "\t/join chan [srv]: join channel on server")
+	status(irc, irc.self, "\t/leave [chan]:    leave current channel")
+	status(irc, irc.self, "\t/chan name:       switch to channnel 'name'")
+	status(irc, irc.self, "\t/win name:        switch to channnel 'name'")
+	status(irc, irc.self, "\t/names:           list nicks in current channel")
+	status(irc, irc.self, "\t/quit:            exit irc.myr")
+}
+
+const send = {irc, srv, fmt, args
+	var s, ap
+
+	ap = std.vastart(&args)
+	s = std.fmtv(fmt, &ap)
+	if !writeall(srv.fd, s)
+		closed(irc, srv, "failed writing")
+	;;
+	std.slfree(s)
+}
+
+const message = {irc, msg
+	var c
+
+	match cursrv(irc)
+	| `std.Some s:
+		c = curchan(irc)
+		if c == irc.self
+			status(irc, irc.self, "can't send to status channel")
+		else
+			send(irc, s, "PRIVMSG {} :{}\r\n", c.name, msg)
+			chanmsg(irc, s, c, s.nick, msg)
+		;;
+	| `std.None:
+		status(irc, irc.self, "no connected server")
+	;;
+}
+
+const action = {irc, msg
+	var b, buf : byte[512]
+	var c
+
+	match cursrv(irc)
+	| `std.Some s:
+		b = std.bfmt(buf[:], "{j= }", msg)
+		c = curchan(irc)
+		puthist(irc, c, (std.now(), `Act (s.nick, std.sldup(b))))
+		send(irc, s, "PRIVMSG {} :\x01ACTION {}\x01\r\n", c.name, b)
+	| `std.None:
+		status(irc, irc.self, "no connected server")
+	;;
+}
+
+const privmsg = {irc, msg
+	var srv, chan, txt
+
+	if msg.len <= 1
+		status(irc, irc.self, "missing message in /msg")
+		-> void
+	elif msg.len > 3 && std.eq(msg[1], "-srv")
+		match findsrv(irc, msg[2])
+		| `std.Some s:	srv = s
+		| `std.None:	-> status(irc, irc.self, "can't message: no server '{}", msg[1])
+		;;
+		txt = std.fmt("{j= }", msg[3:])
+	else
+		match cursrv(irc)
+		| `std.Some s:	srv = s
+		| `std.None:	-> status(irc, irc.self, "cant message: no server\n")
+		;;
+		txt = std.fmt("{j= }", msg[1:])
+	;;
+	chan = name2chan(irc, srv, msg[0])
+	send(irc, srv, "PRIVMSG {} :{}\r\n", chan.name, txt)
+	chanmsg(irc, srv, chan, srv.nick, txt)
+	std.slfree(txt)
+}
+
+const parse = {msg
+	var w, hdr, cmd, args
+
+	match std.charstep(msg)
+	| (':', tl):	(hdr, msg) = word(tl)
+	| (_, _):	hdr = ""
+	;;
+	(cmd, msg) = word(msg)
+
+	args = [][:]
+	msg = std.strfstrip(msg)
+	while msg.len != 0
+		match std.charstep(msg)
+		| (':', tl):
+			std.slpush(&args, tl)
+			break
+		| _:
+			(w, msg) = word(msg)
+			std.slpush(&args, w)
+		;;
+	;;
+
+	-> (hdr, cmd, args)
+}
+
+const word = {s
+	for var i = 0; i < s.len; i++
+		if std.isspace(std.decode(s[i:]))
+			-> (s[:i], std.strfstrip(s[i:]))
+		;;
+	;;
+	-> (s, "")
+
+}
+
+
+const srvmsg = {irc, args
+	if args.len >= 2
+		status(irc, irc.self, "{}", args[1])
+	;;
+}
+
+const status = {irc, chan, fmt, args : ...
+	var s, ap
+
+	ap = std.vastart(&args)
+	s = std.fmtv(fmt, &ap)
+	puthist(irc, chan, (std.now(), `Status s))
+}
+
+const curchan = {irc
+	var s
+
+	if irc.focus < 0 || irc.focus >= irc.srv.len
+		-> irc.self
+	else
+		s = irc.srv[irc.focus] 
+		if s.focus < 0 || s.focus >= s.chan.len
+			-> irc.self
+		;;
+		-> s.chan[s.focus]
+	;;
+}
+
+const cursrv = {irc
+	if irc.focus < 0
+		-> `std.None
+	else
+		-> `std.Some irc.srv[irc.focus] 
+	;;
+}
+
+const findsrv = {irc, name
+	var idx
+
+	idx = -1
+	for var i = 0; i < irc.srv.len; i++
+		match std.strfind(irc.srv[i].ds, name)
+		| `std.Some o:
+			if idx != -1
+				status(irc, irc.self, "ambiguous server name {}", name)
+				-> `std.None
+			else
+				idx = i
+			;;
+		| `std.None:
+		;;
+	;;
+	if idx != -1
+		-> `std.Some irc.srv[idx]
+	else
+		-> `std.None
+	;;
+}
+
+const ctcpmsg = {irc, srv, chan, nick, msg
+	var n
+
+	match word(msg)
+	| ("ACTION", tl):
+		n = std.sldup(displayname(nick))
+		puthist(irc, chan, (std.now(), `Act (n, std.sldup(tl))))
+	| ("PING", tl):	
+		send(irc, srv, "PRIVMSG {} :\x01PING {}\x01\r\n", nick, tl)
+	| ("TIME", tl):
+		send(irc, srv, "PRIVMSG {} :\x01TIME {}\x01\r\n", nick, date.utcnow())
+	| ("VERSION", tl):
+		send(irc, srv, "PRIVMSG {} :\x01VERSION irc.myr\x01\r\n", nick)
+	| ("CLIENTINFO", tl):
+		send(irc, srv,
+		    "PRIVMSG {} :\x01ACTION PING TIME VERSION CLIENTINFO\x01\r\n", nick)
+	| (unknown, tl):
+		status(irc, irc.self, "unknown CTCP message {}\n", msg)
+	;;
+}
+
+const chanmsg = {irc, srv, chan, nick, msg 
+	nick = std.sldup(displayname(nick))
+	puthist(irc, chan, (std.now(), `Msg (nick, std.fmt("{e}", msg))))
+	match std.strfind(msg, srv.nick)
+	| `std.None:	/* nothing */
+	| `std.Some _:
+		if curchan(irc) != chan
+			chan.flagged = true
+		;;
+	;;
+}
+
+const puthist = {irc, chan, entry
+	var tm, d, contents
+
+	std.slpush(&chan.hist, entry)
+	(tm, contents) = entry
+	if chan.log != -1
+		d = date.mkinstant(tm, "local")
+		match contents
+		| `Msg (u, ln):
+			std.fput(chan.log, "{} {} | {}\n", d, u, ln)
+			if chan != curchan(irc)
+				chan.stale = true
+			;;
+		| _:
+			/* nothing */
+		;;
+	;;
+	if chan == curchan(irc)
+		irc.chandirty = true
+	;;
+	if chan.scroll != 0
+		chan.scroll++
+	;;
+}
+
+const writeall = {fd, buf
+	var o
+
+	o = 0
+	while o != buf.len
+		match std.write(fd, buf[o:])
+		| `std.Err e:	-> false
+		| `std.Ok 0:	break
+		| `std.Ok n:	o += n
+		;;
+	;;
+	-> o == buf.len
+}
+
+const uppercase = {cmd
+	for var i = 0; i < cmd.len; i++
+		if cmd[i] >= ('a' : byte) && cmd[i] <= ('z' : byte)
+			cmd[i] = cmd[i] - ('a' : byte) + ('A' : byte)
+		;;
+	;;
+}
--- a/main.myr
+++ b/main.myr
@@ -6,63 +6,10 @@
 use termdraw
 use fileutil	/* for homedir() */
 
-pkg =
-	const send	: (irc : irc#, srv : server#, fmt : byte[:], args : ... -> void)
-	const status	: (irc : irc#, chan : chan#, fmt : byte[:], args : ... -> void)
-;;
+use "types"
+use "draw"
+use "irc"
 
-type irc = struct
-	srv	: server#[:]
-	logdir	: byte[:]
-	self	: chan#
-	focus	: std.size
-	term	: termdraw.term#
-	user	: byte[:]
-	nick	: byte[:]
-
-	/* input editing */
-	off	: std.size
-	cmd	: char[:]
-	yank	: char[:]
-
-	chandirty	: bool
-	cmddirty	: bool
-;;
-
-type server = struct
-	id	: std.size
-	fd	: std.fd
-	user	: byte[:]
-	nick	: byte[:]
-	ds	: byte[:]
-	buf	: byte[512]
-	nbuf	: std.size
-	chan	: chan#[:]
-	focus	: std.size
-	death	: std.time
-;;
-
-type chan = struct
-	log	: std.fd
-	hist	: (std.time, hist)[:]
-	histoff	: std.size	/* scrolling */
-	name	: byte[:]
-	topic	: byte[:]
-	users	: byte[:][:]
-	stale	: bool
-	flagged	: bool
-	gutter	: int
-	scroll	: int
-;;
-
-type hist = union
-	`Msg	(byte[:], byte[:])
-	`Act	(byte[:], byte[:])
-	`Join	byte[:]
-	`Part	byte[:]
-	`Status	byte[:]
-;;
-
 const main = {args
 	var irc : irc#
 	var home, rcfile, logdir
@@ -121,225 +68,40 @@
 	;;
 }
 
-const io = {irc, fd
-	var src, cmd, args
-	var srv, ln
+const poll = {irc
+	var pfd, fd, start, i
+	var out
 
-	srv = fd2srv(irc, fd)
-
-	match std.read(srv.fd, srv.buf[srv.nbuf:])
-	| `std.Ok 0:	closed(irc, srv, "eof")
-	| `std.Ok n:	srv.nbuf += n
-	| `std.Err e:	closed(irc, srv, "error reading")
+	fd = -1
+	pfd = [][:]
+	std.slpush(&pfd, [
+		.fd=(std.In : sys.fd),
+		.events=sys.Pollin,
+		.revents=0,
+	])
+	for s : irc.srv
+		std.slpush(&pfd, [
+			.fd=(s.fd : sys.fd),
+			.events=sys.Pollin,
+			.revents=0,
+		])
 	;;
-
-	while true
-		match nextln(irc, srv)
-		| `std.Some l:	ln = l
-		| `std.None:	-> void
-		;;
-		(src, cmd, args) = parse(ln)
-		uppercase(cmd)
-		match cmd
-		| "001":	srvmsg(irc, args)
-		| "002":	srvmsg(irc, args)
-		| "003":	srvmsg(irc, args)
-		| "004":	srvmsg(irc, args)
-		| "005":	srvmsg(irc, args)
-		| "250":	srvmsg(irc, args)
-		| "251":	srvmsg(irc, args)
-		| "254":	srvmsg(irc, args)
-		| "255":	srvmsg(irc, args)
-		| "265":	srvmsg(irc, args)
-		| "266":	srvmsg(irc, args)
-		| "375":	srvmsg(irc, args)
-		| "372":	srvmsg(irc, args)
-		| "376":	srvmsg(irc, args)
-		| "NOTICE":	srvmsg(irc, args)
-		| "332":	topic(irc, srv, args)
-		| "353":	addusers(irc, srv, args)
-		| "QUIT":	deluser(irc, srv, src)
-		| "PART":	delchanuser(irc, srv, src, args)
-		| "366":	shownames(irc, srv, args, 1)
-		| "PRIVMSG":	recievemsg(irc, srv, src, args)
-		| "PING":	send(irc, srv, "PONG :{}\r\n", args[0])
-		| "JOIN":	joined(irc, srv, src, args)
-		| "NICK":	renamed(irc, srv, src, args)
-		| c:		status(irc, irc.self, "unknown server command {}", ln)
-		;;
-		std.slfree(args)
-		std.slfree(ln)
-	;;
-}
-
-const renamed = {irc, srv, src, args
-	var a, b
-	if args.len > 0
-		for c : srv.chan
-			a = displayname(src)
-			b = displayname(args[0])
-			match std.lsearch(c.users, a, std.strcmp)
-			| `std.None:	/* skip */
-			| `std.Some i:
-				std.slfree(c.users[i])
-				c.users[i] = std.sldup(b)
-				status(irc, c, "changed nick: {} => {}", a, b)
+	if sys.poll(pfd, -1) >= 0
+		/* randomize so that we try to cover all fds evenly */
+		start = std.rand(0, pfd.len)
+		for var j = 0; j != pfd.len; j++
+			i = (j + start) % pfd.len
+			if pfd[i].revents & sys.Pollin != 0
+				fd = (pfd[i].fd : std.fd)
+				out = false
 			;;
 		;;
 	;;
-}
+	std.slfree(pfd)
 
-const joined = {irc, srv, src, args
-	var c, name
-
-	if args.len == 1
-		c = name2chan(irc, srv, args[0])
-		name = displayname(src)
-		c.gutter = std.max(c.gutter, std.cellwidth(name.len))
-		match std.lsearch(c.users, name, std.strcmp)
-		| `std.None:	std.slpush(&c.users, std.sldup(name))
-		| `std.Some _:	/* ignore */
-		;;
-		puthist(irc, c, (std.now(), `Join std.sldup(name)))
-		irc.chandirty = true
-		irc.cmddirty = true
-	;;
+	-> fd
 }
 
-const topic = {irc, srv, args
-	var c
-
-	if args.len >= 3
-		c = name2chan(irc, srv, args[1])
-		std.slfree(c.topic)
-		c.topic = std.sldup(args[2])
-	;;
-}
-
-const recievemsg = {irc, srv, src, args
-	var c
-
-	if std.sleq(args[0], srv.nick)
-		c = name2chan(irc, srv, displayname(src))
-	else
-		c = name2chan(irc, srv, args[0])
-	;;
-	if args.len > 1 && isctcp(args[1]) 
-		std.put("got CTCP {e}\n", args[1])
-		ctcpmsg(irc, srv, c, src, args[1][1:args[1].len-1])
-	else
-		chanmsg(irc, srv, c, src, args[1])
-	;;
-}
-
-const isctcp = {m
-	-> std.hasprefix(m, "\x01") && std.hassuffix(m, "\x01")
-}
-
-const addusers = {irc, srv, args
-	var c
-
-	if args.len != 4
-		-> void
-	;;
-	c = name2chan(irc, srv, args[2])
-	for n : std.bysplit(args[3], " ")
-		n = std.strstrip(n)
-		match std.lsearch(c.users, n, std.strcmp)
-		| `std.None:    std.slpush(&c.users, std.sldup(n))
-		| `std.Some _:  /* ignore */
-		;;
-		c.gutter = std.max(c.gutter, n.len)
-	;;
-}
-
-const deluser = {irc, srv, user
-	for c : srv.chan
-		user = displayname(user)
-		match std.lsearch(c.users, user, std.strcmp)
-		| `std.None:    
-			continue
-		| `std.Some i:  
-			puthist(irc, c, (std.now(), `Part std.sldup(user)))
-			std.slfree(c.users[i])
-			std.sldel(&c.users, i)
-		;;
-	;;
-}
-
-const delchanuser = {irc, srv, user, args
-	var c
-
-	if args.len == 0
-		-> void
-	;;
-	c = name2chan(irc, srv, args[0])
-	user = displayname(user)
-	match std.lsearch(c.users, user, std.strcmp)
-	| `std.None:    /* ignore */
-	| `std.Some i:
-		std.slfree(c.users[i])
-		std.sldel(&c.users, i)
-		puthist(irc, c, (std.now(), `Part std.sldup(user)))
-	;;
-}
-
-const fd2srv = {irc, fd
-	for s : irc.srv
-		if s.fd == fd
-			-> s
-		;;
-	;;
-	std.fatal("missing srv for fd {}", fd)
-}
-
-const name2chan = {irc, srv, chan
-	var new
-
-	for c : srv.chan
-		if std.sleq(c.name, chan)
-			-> c
-		;;
-	;;
-
-	new = mkchan(irc, srv.ds, chan, "")
-	std.slpush(&srv.chan, new)
-	if srv.focus == -1
-		srv.focus = srv.chan.len - 1
-	;;
-	status(irc, new, "joined on {}", date.now("local"))
-	-> new
-}
-
-const nextln = {irc, srv
-	var r
-
-	match std.strfind(srv.buf[:srv.nbuf], "\r\n")
-	| `std.Some i:
-		r = std.sldup(srv.buf[:i])
-		std.slcp(srv.buf[:srv.nbuf - i - 2], srv.buf[i + 2:srv.nbuf])
-		srv.nbuf = srv.nbuf - i - 2
-		-> `std.Some r
-	| `std.None:
-		-> `std.None
-	;;
-}
-
-const closed = {irc, srv, msg
-	if srv.death == 0
-		srv.death = std.now()
-		status(irc, irc.self, "{} closed: {}", srv.ds, msg)
-	;;
-}
-
-
-const displayname = {src
-	match std.strfind(src, "!")
-	| `std.Some i:	-> src[:i]
-	| `std.None:	-> src
-	;;
-}
-
 const mkirc = {rcfile, logdir
 	var irc, term
 	var nick, user
@@ -372,17 +134,6 @@
 	-> irc
 }
 
-const mksrv = {irc, fd, ds -> server#
-	-> std.mk([
-		.fd=fd,
-		.ds=std.sldup(ds),
-		.chan=[][:],
-		.focus=-1,
-		.user=std.sldup(irc.user),
-		.nick=std.sldup(irc.nick),
-	])
-}
-
 const mkchan = {irc, srvname, name, topic
 	var logpath, logfd
 
@@ -401,12 +152,6 @@
 	])
 }
 
-const freechan = {chan
-	std.slfree(chan.name)
-	std.slfree(chan.topic)
-	std.free(chan)
-}
-
 const do = {irc, ln
 	if ln.len == 0
 		-> void
@@ -422,419 +167,6 @@
 	;;
 }
 
-const getstr = {chars
-	var sb = std.mksb()
-	for c : chars
-		std.sbputc(sb, c)
-	;;
-	-> std.sbfin(sb)
-}
-
-const cmd = {irc, text
-	var sp
-
-	sp = std.strtok(text)
-	if sp.len == 0
-		-> void
-	;;
-	match sp[0]
-	| "/connect":	connect(irc, sp[1:])
-	| "/join":	join(irc, sp[1:])
-	| "/leave":	leave(irc, sp[1:])
-	| "/chan":	chanswitch(irc, sp[1:])
-	| "/win":	chanswitch(irc, sp[1:])
-	| "/quit":	quit(irc, sp[1:])
-	| "/help":	help(irc, sp[1:])
-	| "/nick":	changenick(irc, sp[1:])
-	| "/user":	changeuser(irc, sp[1:])
-	| "/srv":	changeserver(irc, sp[1:])
-	| "/names":	listnames(irc, sp[1:])
-	| "/msg":	privmsg(irc, sp[1:])
-	| "/me":	action(irc, sp[1:])
-	| "/":		message(irc, std.strfstrip(text[1:]))
-	| c:		status(irc, irc.self, "unknown command: /{}", text)
-	;;
-	std.slfree(sp)
-}
-
-
-const changenick = {irc, args
-	if args.len != 1
-		status(irc, irc.self, "/nick: invalid args {j= }", args)
-		-> void
-	;;
-
-	match cursrv(irc)
-	| `std.None:
-		std.slfree(irc.nick)
-		irc.nick = std.sldup(args[0])
-		status(irc, irc.self, "default nick changed: {}", args[0])
-	| `std.Some srv:
-		std.slfree(irc.nick)
-		srv.nick = std.sldup(args[0])
-		send(irc, srv, "NICK {}\r\n", srv.nick)
-		status(irc, irc.self, "nick changed for {}: {}", srv.ds, args[0])
-	;;
-}
-
-const changeuser = {irc, args
-	if args.len != 1
-		status(irc, irc.self, "/nick: invalid args {j= }", args)
-		-> void
-	;;
-
-	match cursrv(irc)
-	| `std.None:
-		std.slfree(irc.user)
-		irc.user = std.sldup(args[0])
-	| `std.Some srv:
-		std.slfree(irc.user)
-		srv.user = std.sldup(args[0])
-		send(irc, srv, "NICK {}\r\n", srv.user)
-	;;
-}
-
-const changeserver = {irc, args
-	if args.len != 1
-		status(irc, irc.self, "/nick: invalid args {j= }", args)
-		-> void
-	;;
-	match findsrv(irc, args[0])
-	| `std.Some s:	irc.focus = s.id
-	| `std.None:	status(irc, irc.self, "no server '{}", args[0])
-	;;
-}
-
-const connect = {irc, args
-	var ds, srv
-
-	ds = ""
-	match args.len
-	| 1:	ds = std.netaddr(args[0], "tcp", "ircd")
-	| 0:	status(irc, irc.self, "join: missing server")
-	| _:	status(irc, irc.self, "join: invalid arguments '{j= }'", args)
-	;;
-	if ds.len == 0
-		-> void
-	;;
-
-	for s : irc.srv
-		if std.sleq(s.ds, ds)
-			status(irc, irc.self, "already connected to {j= }", args)
-			-> void
-		;;
-	;;
-
-	status(irc, irc.self, "dialing {}", ds)
-	match std.dial(ds)
-	| `std.Ok fd:
-		srv = mksrv(irc, fd, ds)
-		if handshake(irc, srv)
-			srv.id = irc.srv.len
-			std.slpush(&irc.srv, srv)
-			if irc.focus == -1
-				irc.focus = irc.srv.len - 1
-				srv.death = 0
-			;;
-			status(irc, irc.self, "connected to {}", ds)
-		else
-			status(irc, irc.self, "could not handshake with {j= }", args)
-			std.close(srv.fd)
-			std.free(srv)
-		;;
-	| `std.Err e:
-		status(irc, irc.self, "failed to connect: {}", e)
-	;;
-}
-
-const listnames = {irc, args
-	match cursrv(irc)
-	| `std.Some s:	shownames(irc, s, args, 0)
-	| `std.None:	/* nothing */
-	;;
-}
-
-const shownames = {irc, srv, args, idx
-	var c
-
-	if args.len > idx &&  irc.focus >= 0
-		c = name2chan(irc, srv, args[idx])
-	else
-		c = curchan(irc)
-	;;
-	if c != irc.self
-		status(irc, c, "{j= }", c.users)
-	;;
-}
-
-const handshake = {irc, srv
-	send(irc, srv, "NICK {}\r\n", srv.nick)
-	send(irc, srv, "USER {} 8 * :{}\r\n", srv.user, srv.user)
-	-> true
-}
-
-const join = {irc, args
-	if args.len == 2
-		match findsrv(irc, args[1])
-		| `std.Some s:	send(irc, s, "JOIN {}\r\n", args[0])
-		| `std.None:	status(irc, irc.self, "no server '{}", args[1])
-		;;
-	elif args.len == 1
-		match cursrv(irc)
-		| `std.Some s:	send(irc, s, "JOIN {}\r\n", args[0])
-		| `std.None:	status(irc, irc.self, "no server focused")
-		;;
-	else
-		-> status(irc, irc.self, "invalid arguments: {j= }", args)
-	;;
-}
-
-const leave = {irc, args
-	var name, c, srv
-
-	if irc.focus < 0
-		-> void
-	;;
-	if args.len == 0
-		c = curchan(irc)
-		if c == irc.self
-			-> void
-		;;
-		name = c.name
-	elif args.len == 1
-		name = args[0]
-	else
-		status(irc, irc.self, "leave: invalid arguments: {j= }", args)
-		-> void
-	;;
-
-	srv = irc.srv[irc.focus]
-	for var i = 0; i < srv.chan.len; i++
-		c = srv.chan[i]
-		if std.sleq(name, c.name)
-			send(irc, srv, "PART {}\r\n", name)
-			std.sldel(&srv.chan, i)
-			if i == srv.focus
-				irc.focus = -1
-			;;
-			freechan(c)
-			break
-		;;
-
-	;;
-}
-
-const chancycle = {irc, delta
-	var srv
-
-	if irc.srv.len == 0
-		-> void
-	;;
-
-	irc.chandirty = true
-	irc.cmddirty = true
-	for var i = 0; i < irc.srv.len; i++
-		if irc.focus < 0 || irc.focus >= irc.srv.len
-			irc.focus = 0
-		;;
-
-		srv = irc.srv[irc.focus]
-		srv.focus += delta
-		if srv.focus < 0 || srv.focus >= srv.chan.len
-			irc.focus += delta
-			if irc.focus < 0
-				irc.focus += irc.srv.len
-			;;
-			irc.focus %= irc.srv.len
-			irc.srv[irc.focus].focus = 0
-		;;
-		srv = irc.srv[irc.focus]
-		if srv.chan.len > 0
-			srv.chan[srv.focus].stale = false
-			srv.chan[srv.focus].flagged = false
-			break
-		;;
-	;;
-}
-
-const chanswitch = {irc, args
-	/* full, end, internal */
-	var matches = [(-1, -1), (-1, -1), (-1, -1)]
-	var srv
-
-	if args.len != 1
-		status(irc, irc.self, "invalid arguments: {j= }", args)
-	else
-		irc.chandirty = true
-		irc.cmddirty = true
-		match matchrank(irc.self.name, args[0])
-		| `std.None:		/* ok */
-		| `std.Some rank:	matches[rank] = (0, -1)
-		;;
-		for var i = 0; i < irc.srv.len; i++
-			srv = irc.srv[i]
-			for var j = 0; j < srv.chan.len; j++
-				match matchrank(srv.chan[j].name, args[0])
-				| `std.None:		/* ok */
-				| `std.Some rank:
-					matches[rank] = (j, i)
-				;;
-			;;
-		;;
-
-		for (chanidx, srvidx) : matches[:]
-			if chanidx < 0
-				continue
-			;;
-			irc.focus = srvidx
-			if srvidx >= 0
-				irc.srv[srvidx].focus = chanidx
-				irc.srv[srvidx].chan[chanidx].stale = false
-				irc.srv[srvidx].chan[chanidx].flagged = false
-			;;
-			break
-		;;
-	;;
-}
-
-const matchrank = {str, name
-	match std.strfind(str, name)
-	| `std.None:
-		-> `std.None
-	| `std.Some idx:
-		match (idx, idx + name.len == str.len)
-		| (0, true):	-> `std.Some 0
-		| (_, true):	-> `std.Some 1
-		| (0, false):	-> `std.Some 1
-		| (_, _):	-> `std.Some 2
-		;;
-	;;
-}
-
-const quit = {irc, args
-	if args.len != 0
-		status(irc, irc.self, "invalid argument for quit: {j= }", args)
-		-> void
-	;;
-	termdraw.free(irc.term)
-	std.exit(0)
-}
-
-const help = {irc, args
-	status(irc, irc.self, "irc.myr commands")
-	status(irc, irc.self, "\t/connect dialstr: connect to server")
-	status(irc, irc.self, "\t/help [cmd...]:   get help [on cmd...]")
-	status(irc, irc.self, "\t/join chan [srv]: join channel on server")
-	status(irc, irc.self, "\t/leave [chan]:    leave current channel")
-	status(irc, irc.self, "\t/chan name:       switch to channnel 'name'")
-	status(irc, irc.self, "\t/win name:        switch to channnel 'name'")
-	status(irc, irc.self, "\t/names:           list nicks in current channel")
-	status(irc, irc.self, "\t/quit:            exit irc.myr")
-}
-
-const send = {irc, srv, fmt, args
-	var s, ap
-
-	ap = std.vastart(&args)
-	s = std.fmtv(fmt, &ap)
-	if !writeall(srv.fd, s)
-		closed(irc, srv, "failed writing")
-	;;
-	std.slfree(s)
-}
-
-const message = {irc, msg
-	var c
-
-	match cursrv(irc)
-	| `std.Some s:
-		c = curchan(irc)
-		if c == irc.self
-			status(irc, irc.self, "can't send to status channel")
-		else
-			send(irc, s, "PRIVMSG {} :{}\r\n", c.name, msg)
-			chanmsg(irc, s, c, s.nick, msg)
-		;;
-	| `std.None:
-		status(irc, irc.self, "no connected server")
-	;;
-}
-
-const action = {irc, msg
-	var b, buf : byte[512]
-	var c
-
-	match cursrv(irc)
-	| `std.Some s:
-		b = std.bfmt(buf[:], "{j= }", msg)
-		c = curchan(irc)
-		puthist(irc, c, (std.now(), `Act (s.nick, std.sldup(b))))
-		send(irc, s, "PRIVMSG {} :\x01ACTION {}\x01\r\n", c.name, b)
-	| `std.None:
-		status(irc, irc.self, "no connected server")
-	;;
-}
-
-const privmsg = {irc, msg
-	var srv, chan, txt
-
-	if msg.len <= 1
-		status(irc, irc.self, "missing message in /msg")
-		-> void
-	elif msg.len > 3 && std.eq(msg[1], "-srv")
-		match findsrv(irc, msg[2])
-		| `std.Some s:	srv = s
-		| `std.None:	-> status(irc, irc.self, "can't message: no server '{}", msg[1])
-		;;
-		txt = std.fmt("{j= }", msg[3:])
-	else
-		match cursrv(irc)
-		| `std.Some s:	srv = s
-		| `std.None:	-> status(irc, irc.self, "cant message: no server\n")
-		;;
-		txt = std.fmt("{j= }", msg[1:])
-	;;
-	chan = name2chan(irc, srv, msg[0])
-	send(irc, srv, "PRIVMSG {} :{}\r\n", chan.name, txt)
-	chanmsg(irc, srv, chan, srv.nick, txt)
-	std.slfree(txt)
-}
-
-const parse = {msg
-	var w, hdr, cmd, args
-
-	match std.charstep(msg)
-	| (':', tl):	(hdr, msg) = word(tl)
-	| (_, _):	hdr = ""
-	;;
-	(cmd, msg) = word(msg)
-
-	args = [][:]
-	msg = std.strfstrip(msg)
-	while msg.len != 0
-		match std.charstep(msg)
-		| (':', tl):
-			std.slpush(&args, tl)
-			break
-		| _:
-			(w, msg) = word(msg)
-			std.slpush(&args, w)
-		;;
-	;;
-
-	-> (hdr, cmd, args)
-}
-
-const word = {s
-	for var i = 0; i < s.len; i++
-		if std.isspace(std.decode(s[i:]))
-			-> (s[:i], std.strfstrip(s[i:]))
-		;;
-	;;
-	-> (s, "")
-
-}
-
 const terminput = {irc, chan
 	var ln
 
@@ -958,6 +290,7 @@
 	elif matches.len > 1
 		status(irc, chan, "completions: {j= }", matches)
 	;;
+	std.slfree(matches)
 	std.slfree(pfx)
 }
 
@@ -974,436 +307,3 @@
 	irc.chandirty = true
 }
 
-const redraw = {irc
-	var c, x, y, w, h
-
-	if irc.term.width < 10 || irc.term.height < 5
-		-> void
-	;;
-
-	x = 0
-	y = 0
-	w = irc.term.width
-	h = irc.term.height
-	c = curchan(irc)
-	if irc.chandirty
-		drawbanner(irc, 0, 0, w, 1, c)
-		drawtext(irc, 0, 1, w, h - 2, c)
-		drawlist(irc, 0, h - 2, w, h - 1, c)
-		irc.chandirty = false
-	;;
-	if irc.cmddirty
-		drawinput(irc, 0, h - 1, w, h, c)
-		irc.cmddirty = false
-	;;
-	termdraw.flush(irc.term)
-}
-
-const drawbanner = {irc, x0, y0, x1, y1, c
-	var t
-
-	t = irc.term
-	termdraw.setbg(t, termdraw.Blue)
-	termdraw.clear(t, x0, y0, x1, y1)
-	termdraw.move(t, x0, y0)
-	termdraw.put(t, "{}", c.topic)
-}
-
-const drawtext = {irc, x0, y0, x1, y1, c
-	var height, margin, width, off
-	var t, dx, dy
-	var count
-	var x, y
-
-	t = irc.term
-	termdraw.setbg(t, termdraw.Default)
-	termdraw.clear(t, x0, y0, x1, y1)
-	termdraw.move(t, x0, y0)
-
-	dx = x1 - x0
-	dy = y1 - y0
-	count = 0
-	height = 0
-	width = dx
-	off = 0
-	/* nothing worth drawing... */
-	if dx <= c.gutter
-		-> void
-	;;
-	for (tm, ent) : iter.byreverse(c.hist[:c.hist.len - c.scroll])
-		margin = 0
-		match ent
-		| `Msg (m, ln):
-			margin = strwidth(x0, c.gutter, " | ") + c.gutter
-			width = strwidth(x0, margin, ln)
-		| `Act (m, ln):
-			margin = strwidth(x0, 0, "  *")
-			width = strwidth(x0, margin, m)
-			width += strwidth(x0, margin, " ")
-			width += strwidth(x0, margin, ln)
-		| `Join user:
-			margin = strwidth(x0, 0, "#joined: ")
-			width = strwidth(x0, margin, user)
-		| `Part user:
-			margin = strwidth(x0, 0, "#parted: ")
-			width = strwidth(x0, margin, user)
-		| `Status msg:
-			margin = strwidth(x0, 0, "! ")
-			width = strwidth(x0, margin, msg)
-		;;
-		/* no point in drawing if all we draw is gutter */
-		if dx - margin <=  0
-			-> void
-		;;
-		height++
-		if width > 0
-			height += width / (dx - margin)
-		;;
-		count++
-		if height == dy
-			break
-		elif height > dy
-			off = (dx - margin)*(height - dy)
-			break
-		;;
-	;;
-
-	x = x0
-	y = y0
-	for (tm, h) : c.hist[c.hist.len - c.scroll - count:]
-		match h
-		| `Msg (m, ln):
-			termdraw.setattr(t, termdraw.Bold)
-			x = std.clamp(x0 + c.gutter - strwidth(x0, 0, m), 0, dx)
-			termdraw.move(t, x0 + c.gutter, y)
-			(x, y) = draw(t, m, x, y, x1, y1)
-			(x, y) = draw(t, " | ", x, y, x1, y1)
-			termdraw.setattr(t, lineattr(irc, ln[off:]))
-			(x, y) = draw(t, ln[off:], x, y, x1, y1)
-			termdraw.setattr(t, termdraw.Normal)
-		| `Act (m, ln):
-			termdraw.setattr(t, termdraw.Bold)
-			(x, y) = draw(t, "  *", x0, y, x1, y1)
-			(x, y) = draw(t, m, x, y, x1, y1)
-			termdraw.setattr(t, lineattr(irc, ln[off:]))
-			(x, y) = draw(t, " ", x, y, x1, y1)
-			(x, y) = draw(t, ln[off:], x, y, x1, y1)
-			termdraw.setattr(t, termdraw.Normal)
-		| `Join user:
-			(x, y) = draw(t, "#joined ", x0, y, x1, y1)
-			(x, y) = draw(t, user[off:], x, y, x1, y1)
-		| `Part user:
-			(x, y) = draw(t, "#parted ", x0, y, x1, y1)
-			(x, y) = draw(t, user[off:], x, y, x1, y1)
-		| `Status msg:
-			(x, y) = draw(t, "! ", x0, y, x1, y1)
-			(x, y) = draw(t, msg[off:], x, y, x1, y1)
-		;;
-		y++
-		off = 0
-		if y >= y1
-			break
-		;;
-	;;
-}
-
-const strwidth = {x0, margin, str
-	var x
-
-	x = x0 + margin
-	for c : std.bychar(str)
-		match c
-		| '\t':	x = (x / 8 + 1) * 8
-		| _:	x += (std.cellwidth(c) : int)
-		;;
-	;;
-	-> x - (x0 + margin)
-}
-
-const draw = {t, msg, x0, y0, x1, y1
-	var x, y
-
-	x = x0
-	y = y0
-	for l : std.bychar(msg)
-		match l
-		| '\t':	
-			x = (x / 8 + 1)*8
-		| '\n':	
-			termdraw.put(t, "\\n")
-			x += 2
-		| chr:
-			if x < x1
-				termdraw.move(t, x, y)
-				termdraw.putc(t, chr)
-				x++
-			;;
-		;;
-		if x >= x1
-			x = x0
-			y++
-		;;
-	;;
-	-> (x, y)
-}
-
-const drawlist = {irc, x0, y0, x1, y1, c
-	var t
-
-	t = irc.term
-	termdraw.setbg(t, termdraw.Blue)
-	termdraw.clear(t, x0, y0, x1, y1)
-	termdraw.move(t, x0, y0)
-	termdraw.putc(t, '|')
-	termdraw.put(t, "[{}]", irc.self.name)
-	for s : irc.srv
-		for ch : s.chan
-			if ch.flagged
-				termdraw.setattr(t, termdraw.Bold)
-				termdraw.put(t, "[@{}]", ch.name)
-				termdraw.setattr(t, termdraw.Normal)
-			elif ch.stale
-				termdraw.put(t, "[*{}]", ch.name)
-			else
-				termdraw.put(t, "[{}]", ch.name)
-			;;
-		;;
-		termdraw.setattr(t, termdraw.Normal)
-		termdraw.putc(t, '|')
-	;;
-}
-
-const drawinput = {irc, x0, y0, x1, y1, c
-	var t, chan, ln, cx, dx
-
-	dx = x1 - x0
-	t = irc.term
-	chan = std.fmt("[{}] ", c.name)
-	ln = inputstr(irc, chan, dx)
-	cx = x0 + (std.strcellwidth(chan) + irc.off : int)
-	if cx > dx
-		cx = dx
-	;;
-	termdraw.setbg(t, termdraw.Default)
-	termdraw.move(t, x0, y0)
-	termdraw.clear(t, x0, y0, x1, y1)
-	termdraw.put(t, "{}{}", chan, ln)
-	termdraw.cursorpos(t, cx, y0)
-	std.slfree(chan)
-	std.slfree(ln)
-}
-
-const lineattr = {irc, ln
-	match cursrv(irc)
-	| `std.None:	-> termdraw.Normal
-	| `std.Some srv:
-		match std.strfind(ln, srv.nick)
-		| `std.Some _:	-> termdraw.Italic
-		| `std.None:	-> termdraw.Normal
-		;;
-	;;
-}
-
-const inputstr = {irc, chan, dx
-	var w, s, o
-	var start, end
-
-	s = irc.cmd
-	o = (irc.off : int)
-	dx -= strwidth(0, 0, chan)
-	w = 0
-	start = 0
-	end = 0
-	for var i = 0; i < s.len; i++
-		w += std.cellwidth(s[i])
-		if w >= dx
-			break
-		;;
-		end++
-	;;
-	while end < s.len && irc.off >= (w : std.size)
-		start++
-		while std.cellwidth(s[end]) == 0
-			start++
-		;;
-		w += std.cellwidth(s[end])
-		end++
-		while end < s.len && std.cellwidth(s[end]) == 0
-			end++
-		;;
-	;;
-	-> getstr(s[start:end])
-}
-
-const srvmsg = {irc, args
-	if args.len >= 2
-		status(irc, irc.self, "{}", args[1])
-	;;
-}
-
-const status = {irc, chan, fmt, args : ...
-	var s, ap
-
-	ap = std.vastart(&args)
-	s = std.fmtv(fmt, &ap)
-	puthist(irc, chan, (std.now(), `Status s))
-}
-
-const curchan = {irc
-	var s
-
-	if irc.focus < 0 || irc.focus >= irc.srv.len
-		-> irc.self
-	else
-		s = irc.srv[irc.focus] 
-		if s.focus < 0 || s.focus >= s.chan.len
-			-> irc.self
-		;;
-		-> s.chan[s.focus]
-	;;
-}
-
-const cursrv = {irc
-	if irc.focus < 0
-		-> `std.None
-	else
-		-> `std.Some irc.srv[irc.focus] 
-	;;
-}
-
-const findsrv = {irc, name
-	var idx
-
-	idx = -1
-	for var i = 0; i < irc.srv.len; i++
-		match std.strfind(irc.srv[i].ds, name)
-		| `std.Some o:
-			if idx != -1
-				status(irc, irc.self, "ambiguous server name {}", name)
-				-> `std.None
-			else
-				idx = i
-			;;
-		| `std.None:
-		;;
-	;;
-	if idx != -1
-		-> `std.Some irc.srv[idx]
-	else
-		-> `std.None
-	;;
-}
-
-const poll = {irc
-	var pfd, fd, start, i
-	var out
-
-	fd = -1
-	pfd = [][:]
-	std.slpush(&pfd, [
-		.fd=(std.In : sys.fd),
-		.events=sys.Pollin,
-		.revents=0,
-	])
-	for s : irc.srv
-		std.slpush(&pfd, [
-			.fd=(s.fd : sys.fd),
-			.events=sys.Pollin,
-			.revents=0,
-		])
-	;;
-	if sys.poll(pfd, -1) >= 0
-		/* randomize so that we try to cover all fds evenly */
-		start = std.rand(0, pfd.len)
-		for var j = 0; j != pfd.len; j++
-			i = (j + start) % pfd.len
-			if pfd[i].revents & sys.Pollin != 0
-				fd = (pfd[i].fd : std.fd)
-				out = false
-			;;
-		;;
-	;;
-	std.slfree(pfd)
-
-	-> fd
-}
-
-const ctcpmsg = {irc, srv, chan, nick, msg
-	var n
-
-	match word(msg)
-	| ("ACTION", tl):
-		n = std.sldup(displayname(nick))
-		puthist(irc, chan, (std.now(), `Act (n, std.sldup(tl))))
-	| ("PING", tl):	
-		send(irc, srv, "PRIVMSG {} :\x01PING {}\x01\r\n", nick, tl)
-	| ("TIME", tl):
-		send(irc, srv, "PRIVMSG {} :\x01TIME {}\x01\r\n", nick, date.utcnow())
-	| ("VERSION", tl):
-		send(irc, srv, "PRIVMSG {} :\x01VERSION irc.myr\x01\r\n", nick)
-	| ("CLIENTINFO", tl):
-		send(irc, srv,
-		    "PRIVMSG {} :\x01ACTION PING TIME VERSION CLIENTINFO\x01\r\n", nick)
-	| (unknown, tl):
-		status(irc, irc.self, "unknown CTCP message {}\n", msg)
-	;;
-}
-
-const chanmsg = {irc, srv, chan, nick, msg 
-	nick = std.sldup(displayname(nick))
-	puthist(irc, chan, (std.now(), `Msg (nick, std.fmt("{e}", msg))))
-	match std.strfind(msg, srv.nick)
-	| `std.None:	/* nothing */
-	| `std.Some _:
-		if curchan(irc) != chan
-			chan.flagged = true
-		;;
-	;;
-}
-
-const puthist = {irc, chan, entry
-	var tm, d, contents
-
-	std.slpush(&chan.hist, entry)
-	(tm, contents) = entry
-	if chan.log != -1
-		d = date.mkinstant(tm, "local")
-		match contents
-		| `Msg (u, ln):
-			std.fput(chan.log, "{} {} | {}\n", d, u, ln)
-			if chan != curchan(irc)
-				chan.stale = true
-			;;
-		| _:
-			/* nothing */
-		;;
-	;;
-	if chan == curchan(irc)
-		irc.chandirty = true
-	;;
-	if chan.scroll != 0
-		chan.scroll++
-	;;
-}
-
-const writeall = {fd, buf
-	var o
-
-	o = 0
-	while o != buf.len
-		match std.write(fd, buf[o:])
-		| `std.Err e:	-> false
-		| `std.Ok 0:	break
-		| `std.Ok n:	o += n
-		;;
-	;;
-	-> o == buf.len
-}
-
-const uppercase = {cmd
-	for var i = 0; i < cmd.len; i++
-		if cmd[i] >= ('a' : byte) && cmd[i] <= ('z' : byte)
-			cmd[i] = cmd[i] - ('a' : byte) + ('A' : byte)
-		;;
-	;;
-}
--- /dev/null
+++ b/types.myr
@@ -1,0 +1,56 @@
+use std
+use termdraw
+
+pkg =
+	type irc = struct
+		srv	: server#[:]
+		logdir	: byte[:]
+		self	: chan#
+		focus	: std.size
+		term	: termdraw.term#
+		user	: byte[:]
+		nick	: byte[:]
+
+		/* input editing */
+		off	: std.size
+		cmd	: char[:]
+		yank	: char[:]
+
+		chandirty	: bool
+		cmddirty	: bool
+	;;
+
+	type server = struct
+		id	: std.size
+		fd	: std.fd
+		user	: byte[:]
+		nick	: byte[:]
+		ds	: byte[:]
+		buf	: byte[512]
+		nbuf	: std.size
+		chan	: chan#[:]
+		focus	: std.size
+		death	: std.time
+	;;
+
+	type chan = struct
+		log	: std.fd
+		hist	: (std.time, hist)[:]
+		histoff	: std.size	/* scrolling */
+		name	: byte[:]
+		topic	: byte[:]
+		users	: byte[:][:]
+		stale	: bool
+		flagged	: bool
+		gutter	: int
+		scroll	: int
+	;;
+
+	type hist = union
+		`Msg	(byte[:], byte[:])
+		`Act	(byte[:], byte[:])
+		`Join	byte[:]
+		`Part	byte[:]
+		`Status	byte[:]
+	;;
+;;
--