shithub: irc.myr

Download patch

ref: adb285ccf935148ce2e936b870d55c2159ed563e
parent: ba49d299fd4a587c41eb6dbadd5d64b1d0191410
author: Ori Bernstein <ori@eigenstate.org>
date: Sat Nov 25 19:10:52 EST 2017

Rename to irc.myr

	Irc *is* a bit generic.

--- a/bld.proj
+++ b/bld.proj
@@ -1,7 +1,2 @@
-bin irc =
-	irc.myr
-;;
-
-man =
-	irc.1
-;;
+bin irc.myr = main.myr ;;
+man = irc.myr.1 ;;
--- a/irc.1
+++ /dev/null
@@ -1,180 +1,0 @@
-.TH IRC 1
-.SH NAME
-irc
-.SH SYNOPSIS
-.B irc
-.I [-c cfg]
-.I [-l logdir]
-.br
-.SH DESCRIPTION
-.PP
-Irc is a simple curses IRC client. It supports the usual features
-one would expect from an IRC client, including connecting to multiple
-channels and servers.
-
-.SH OPTIONS
-.PP
-Irc supports relatively few options on the command line, as most
-configuration is done via the config file.
-
-.PP
-The supported options are:
-
-.TP
-.B -c cfg
-Use the config file
-.I cfg.
-If not specified, the default
-config file is
-.I $HOME/.ircrc
-
-.TP
-.B -l logdir
-Write logs to the log directory
-.I logdir.
-By default, logs go into
-.I $HOME/.irclogs.
-
-.SH COMMANDS
-
-.PP
-Irc accepts most of the expected commands people want in an
-IRC client, although there are a few notable exceptions. The
-general format of a commands is
-.B /command target operands.
-
-
-.PP
-In general, the commands in irc.myr will
-accept any nonambiguous name fragment when a
-name is required.
-
-.TP
-.B /help
-Show help.
-
-.TP
-.B /connect host
-Connect to a server specified withthe hostname
-.I host.
-The dial syntax is shared with the
-.I netaddr
-function.
-
-.TP
-.B /quit
-Exit the IRC client.
-
-.TP
-.B /join chan [srv]
-Join a channel. If the server is given, then the
-channel is joined on the specified server. Otherwise,
-the channel is joined on the currently focused server.
-
-.TP 
-.B /leave [chan]
-Leave a channel. If the channel name specified, then
-the channel
-.I chan
-is parted. Otherwise, the target is the current channel.
-
-.TP
-.B /chan [name]
-Switch to the channel named
-.I chan.
-
-.TP
-.B /win name
-Same as
-.I /chan.
-
-.TP
-.B /names
-List the names of the users present in the current channel.
-
-.TP
-.B /srv name
-Focus the server
-.I srv.
-For most commands, this doesn't matter, but it affects
-the commands that accept a server target.
-
-.TP
-.B /nick name
-Set your nickname to
-.I nick.
-Defaults to the Unix username that IRC was run under.
-
-.TP /user name
-Set your username to
-.I nick.
-Defaults to the Unix username that IRC was run under.
-
-.SH KEYBINDINGS
-
-.TP
-.B ctrl+n
-Move to next channel
-
-.TP
-.B ctrl+p
-Move to previous channel
-
-.TP
-.B ctrl+l
-Redraw terminal
-
-.TP
-.B up arrow
-Scroll up one line
-
-.TP
-.B down arrow
-Scroll down one line
-
-.TP
-.B page up
-Scroll up one page
-
-.TP
-.B page down
-Scroll down one page
-
-.SH CONFIGURATION
-
-.PP
-The
-.I .ircrc
-configuration file uses the same commands as mentioned in the
-.I commands
-section of this manual, one per line. These commands are
-executed on irc startup.
-
-.PP
-When using a configuration file with multiple servers, the
-.I /join
-directives should be used with the server form, otherwise
-all joins will be done on the first channel.
-
-.EX
-	/connect irc.eigenstate.org
-	/nick Ori
-	/join #myrddin eigenstate
-	/connect irc.freenode.org
-	/nick Ori_B
-	/msg NickServ -srv freenode identify my-pass0rd
-	/join #proglangdesign freenode
-	/join #cat-v freenode
-	/join #more-channels
-.EE
-
-.SH BUGS
-
-.PP
- Connections happen in the foreground, which can cause UI jank.
-.PP
-There are a number of commands that are not implemented (/me, etc)
-.PP
-The chat history is an unbounded log. It should be replaced with a ringbuffer.
-.PP
-Line wrapping is a bit rough, and doens't do word splits
--- a/irc.myr
+++ /dev/null
@@ -1,1316 +1,0 @@
-use std
-use sys
-use bio
-use date
-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)
-;;
-
-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[:])
-	`Join	byte[:]
-	`Part	byte[:]
-	`Status	byte[:]
-;;
-
-const main = {args
-	var irc : irc#
-	var home, rcfile, logdir
-	var c
-	var fd
-	var cmd
-
-	home = fileutil.homedir()
-	rcfile = std.pathcat(home, ".ircrc")
-	logdir = std.pathcat(home, ".irclogs")
-	cmd = std.optparse(args, &[
-		.maxargs=-1,
-		.opts=[
-			[.opt='c', .arg="cfg",
-			 .desc="use config file 'cfg'",
-			 .dest=`std.Some &rcfile],
-			[.opt='l', .arg="log",
-			 .desc="use log dir 'log'",
-			 .dest=`std.Some &logdir],
-		][:]
-	])
-
-	irc = mkirc(rcfile, logdir)
-	while true
-		redraw(irc)
-		c = curchan(irc)
-		fd = poll(irc)
-		/* we can get interrupted by sigwinch */
-		if fd <= 0
-			terminput(irc, c)
-		elif fd >= 0
-			io(irc, fd)
-		;;
-		redial(irc)
-	;;
-}
-
-const redial = {irc
-	for s : irc.srv
-		if s.death == 0 || std.now() - s.death < 60*std.Sec
-			continue
-		;;
-		match std.dial(s.ds)
-		| `std.Ok fd:
-			s.fd = fd
-			if !handshake(irc, s)
-				std.close(s.fd)
-				continue
-			;;
-			s.death = 0
-			for c : s.chan
-				send(irc, s, "JOIN {}\r\n", c.name)
-			;;
-		| `std.Err e:
-		;;
-	;;
-}
-
-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)
-		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])
-	;;
-	chanmsg(irc, srv, c, src, args[1])
-}
-
-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
-
-	term = termdraw.mk(std.In)
-	termdraw.raw(term)
-	termdraw.cursoron(term)
-	user = std.getenvv("user", std.getenvv("USER", "user"))
-	nick = user
-	irc = std.mk([
-		.srv = [][:],
-		.user = std.sldup(user),
-		.nick = std.sldup(nick),
-		.focus = -1,
-		.term = term,
-		.logdir = logdir,
-		.chandirty = true,
-		.cmddirty = true,
-	])
-	irc.self = mkchan(irc, "status", "status", "status")
-
-	match bio.open(rcfile, bio.Rd)
-	| `std.Ok cfg:
-		for ln : bio.byline(cfg)
-			do(irc, ln)
-		;;
-	| `std.Err e:
-	;;
-
-	-> 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
-
-	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 freechan = {chan
-	std.slfree(chan.name)
-	std.slfree(chan.topic)
-	std.free(chan)
-}
-
-const do = {irc, ln
-	ln = std.strstrip(ln)
-	if ln.len == 0
-		-> void
-	;;
-	match std.charstep(ln)
-	| ('/', rest):
-		match std.charstep(ln)
-		| (' ', m):	message(irc, m)
-		| (_, _):	cmd(irc, rest)
-		;;
-	| _:
-		message(irc, ln)
-	;;
-}
-
-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:])
-	| 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
-	var idx, srvidx
-	var ch, srv
-
-	if args.len != 1
-		status(irc, irc.self, "invalid arguments: {j= }", args)
-	else
-		srvidx = -1
-		idx = -1
-		irc.chandirty = true
-		irc.cmddirty = true
-		match std.strfind(irc.self.name, args[0])
-		| `std.None:	/* ok */
-		| `std.Some i:	idx = i
-		;;
-		for var i = 0; i < irc.srv.len; i++
-			srv = irc.srv[i]
-			for var j = 0; j < srv.chan.len; j++
-				ch = srv.chan[j]
-				match std.strfind(ch.name, args[0])
-				| `std.None:	/* ok */
-				| `std.Some n:
-					if idx != -1
-						status(irc, irc.self, "ambiguous channel name {}", args[0])
-						-> void
-					else
-						srvidx = i
-						idx = j
-					;;
-				;;
-			;;
-		;;
-		if idx != -1
-			if idx == -1 || srvidx == -1
-				irc.focus = -1
-			else
-				irc.focus = srvidx
-				irc.srv[srvidx].focus = idx
-				irc.srv[srvidx].chan[idx].stale = false
-				irc.srv[srvidx].chan[idx].flagged = false
-			;;
-		;;
-	;;
-}
-
-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 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
-
-	while true
-		match termdraw.poll(irc.term)
-		| `std.None:
-			break
-		| `std.Some `termdraw.Winsz _:
-			irc.chandirty = true
-			irc.cmddirty = true
-		| `std.Some `termdraw.Kc '\t':
-			complete(irc, chan)
-		| `std.Some `termdraw.Kc '\n':
-			ln = getstr(irc.cmd)
-			do(irc, ln)
-			std.slfree(ln)
-			std.slfree(irc.cmd)
-			irc.cmd = [][:]
-			irc.off = 0
-		| `std.Some `termdraw.Ctrl 'n':
-			chancycle(irc, 1)
-		| `std.Some `termdraw.Ctrl 'p':
-			chancycle(irc, -1)
-		| `std.Some `termdraw.Ctrl 'l':
-			termdraw.cls(irc.term)
-			irc.chandirty = true
-			irc.cmddirty = true
-		| `std.Some `termdraw.Kup:	scroll(irc, chan, 1)
-		| `std.Some `termdraw.Kdown:	scroll(irc, chan, -1)
-		| `std.Some `termdraw.Kpgup:	scroll(irc, chan, std.max(1, irc.term.height - 7))
-		| `std.Some `termdraw.Kpgdn:	scroll(irc, chan, -std.max(1, irc.term.height - 7))
-		| `std.Some ev:
-			if !input(irc, ev)
-				puthist(irc, irc.self, (std.now(), `Status std.fmt("{}", ev)))
-			;;
-		;;
-		irc.cmddirty = true
-		redraw(irc)
-	;;
-}
-
-const input = {irc, ev
-	var b
-	match ev
-	| `termdraw.Khome:	irc.off = 0
-	| `termdraw.Ctrl 'a':	irc.off = 0
-	| `termdraw.Kend:	irc.off = irc.cmd.len
-	| `termdraw.Ctrl 'e':	irc.off = irc.cmd.len
-	| `termdraw.Kc c:
-		std.slput(&irc.cmd, irc.off, c)
-		irc.off++
-	| `termdraw.Kleft:
-		if irc.off > 0
-			irc.off--
-		;;
-	| `termdraw.Kright:
-		if irc.off < irc.cmd.len
-			irc.off++
-		;;
-	| `termdraw.Ctrl 'u':
-		std.slfree(irc.yank)
-		irc.yank = irc.cmd
-		irc.off = 0
-		irc.cmd = [][:]
-	| `termdraw.Ctrl 'y':
-		b = [][:]
-		std.sljoin(&b, irc.cmd[:irc.off])
-		std.sljoin(&b, irc.yank)
-		std.sljoin(&b, irc.cmd[irc.off:])
-		std.slfree(irc.cmd)
-		irc.cmd = b
-		irc.off += irc.yank.len
-	| `termdraw.Ctrl 'w':
-		while irc.off > 0
-			if !std.isspace(irc.cmd[irc.off - 1])
-				break
-			;;
-			irc.off--
-		;;
-		while irc.off > 0
-			if std.isspace(irc.cmd[irc.off - 1])
-				break
-			;;
-			std.sldel(&irc.cmd, irc.off - 1)
-			irc.off--
-		;;
-	| `termdraw.Kbksp:
-		if irc.off > 0
-			irc.off--
-			std.sldel(&irc.cmd, irc.off)
-		;;
-	| _:
-		-> false
-	;;
-	-> true
-}
-
-const complete = {irc, chan
-	var pfx, off, matches
-
-	off = irc.off
-	while off > 0
-		if std.isspace(irc.cmd[off - 1])
-			break
-		;;
-		off--
-	;;
-
-	pfx = getstr(irc.cmd[off:irc.off])
-	matches = [][:]
-	for var i = 0; i < chan.users.len; i++
-		if std.hasprefix(chan.users[i], pfx)
-			std.slpush(&matches, chan.users[i])
-		;;
-	;;
-
-	if matches.len == 1
-		for c : std.bychar(matches[0][pfx.len:])
-			std.slput(&irc.cmd, irc.off, c)
-			irc.off++
-		;;
-	elif matches.len > 1
-		status(irc, chan, "completions: {j= }", matches)
-	;;
-	std.slfree(pfx)
-}
-
-const scroll = {irc, chan, delta
-	chan.scroll = std.clamp(chan.scroll + delta, 0, chan.hist.len)
-	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) : std.byreverse(c.hist[:c.hist.len - c.scroll])
-		margin = 0
-		match ent
-		| `Msg (m, ln):
-			margin = c.gutter + termdraw.strwidth(t, " | ")
-			width = termdraw.strwidth(t, ln)
-		| `Join user:
-			margin = termdraw.strwidth(t, "#joined: ")
-			width = termdraw.strwidth(t, user)
-		| `Part user:
-			margin = termdraw.strwidth(t, "#parted: ")
-			width = termdraw.strwidth(t, user)
-		| `Status msg:
-			margin = termdraw.strwidth(t, "! ")
-			width = termdraw.strwidth(t, 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 - termdraw.strwidth(t, m), 0, dx)
-			(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)
-		| `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 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 -= termdraw.strwidth(irc.term, 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 chanmsg = {irc, srv, chan, nick, msg 
-	nick = std.sldup(displayname(nick))
-	puthist(irc, chan, (std.now(), `Msg (nick, std.sldup(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, date, contents
-
-	irc.chandirty = true
-	std.slpush(&chan.hist, entry)
-	(tm, contents) = entry
-	if chan.log != -1
-		date = date.mkinstant(tm, "local")
-		match contents
-		| `Msg (u, ln):
-			std.fput(chan.log, "{} {} | {}\n", date, u, ln)
-			if chan != curchan(irc)
-				chan.stale = true
-			;;
-		| _:
-			/* nothing */
-		;;
-	;;
-	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/irc.myr.1
@@ -1,0 +1,180 @@
+.TH IRC 1
+.SH NAME
+irc
+.SH SYNOPSIS
+.B irc
+.I [-c cfg]
+.I [-l logdir]
+.br
+.SH DESCRIPTION
+.PP
+Irc is a simple curses IRC client. It supports the usual features
+one would expect from an IRC client, including connecting to multiple
+channels and servers.
+
+.SH OPTIONS
+.PP
+Irc supports relatively few options on the command line, as most
+configuration is done via the config file.
+
+.PP
+The supported options are:
+
+.TP
+.B -c cfg
+Use the config file
+.I cfg.
+If not specified, the default
+config file is
+.I $HOME/.ircrc
+
+.TP
+.B -l logdir
+Write logs to the log directory
+.I logdir.
+By default, logs go into
+.I $HOME/.irclogs.
+
+.SH COMMANDS
+
+.PP
+Irc accepts most of the expected commands people want in an
+IRC client, although there are a few notable exceptions. The
+general format of a commands is
+.B /command target operands.
+
+
+.PP
+In general, the commands in irc.myr will
+accept any nonambiguous name fragment when a
+name is required.
+
+.TP
+.B /help
+Show help.
+
+.TP
+.B /connect host
+Connect to a server specified withthe hostname
+.I host.
+The dial syntax is shared with the
+.I netaddr
+function.
+
+.TP
+.B /quit
+Exit the IRC client.
+
+.TP
+.B /join chan [srv]
+Join a channel. If the server is given, then the
+channel is joined on the specified server. Otherwise,
+the channel is joined on the currently focused server.
+
+.TP 
+.B /leave [chan]
+Leave a channel. If the channel name specified, then
+the channel
+.I chan
+is parted. Otherwise, the target is the current channel.
+
+.TP
+.B /chan [name]
+Switch to the channel named
+.I chan.
+
+.TP
+.B /win name
+Same as
+.I /chan.
+
+.TP
+.B /names
+List the names of the users present in the current channel.
+
+.TP
+.B /srv name
+Focus the server
+.I srv.
+For most commands, this doesn't matter, but it affects
+the commands that accept a server target.
+
+.TP
+.B /nick name
+Set your nickname to
+.I nick.
+Defaults to the Unix username that IRC was run under.
+
+.TP /user name
+Set your username to
+.I nick.
+Defaults to the Unix username that IRC was run under.
+
+.SH KEYBINDINGS
+
+.TP
+.B ctrl+n
+Move to next channel
+
+.TP
+.B ctrl+p
+Move to previous channel
+
+.TP
+.B ctrl+l
+Redraw terminal
+
+.TP
+.B up arrow
+Scroll up one line
+
+.TP
+.B down arrow
+Scroll down one line
+
+.TP
+.B page up
+Scroll up one page
+
+.TP
+.B page down
+Scroll down one page
+
+.SH CONFIGURATION
+
+.PP
+The
+.I .ircrc
+configuration file uses the same commands as mentioned in the
+.I commands
+section of this manual, one per line. These commands are
+executed on irc startup.
+
+.PP
+When using a configuration file with multiple servers, the
+.I /join
+directives should be used with the server form, otherwise
+all joins will be done on the first channel.
+
+.EX
+	/connect irc.eigenstate.org
+	/nick Ori
+	/join #myrddin eigenstate
+	/connect irc.freenode.org
+	/nick Ori_B
+	/msg NickServ -srv freenode identify my-pass0rd
+	/join #proglangdesign freenode
+	/join #cat-v freenode
+	/join #more-channels
+.EE
+
+.SH BUGS
+
+.PP
+ Connections happen in the foreground, which can cause UI jank.
+.PP
+There are a number of commands that are not implemented (/me, etc)
+.PP
+The chat history is an unbounded log. It should be replaced with a ringbuffer.
+.PP
+Line wrapping is a bit rough, and doens't do word splits
--- /dev/null
+++ b/main.myr
@@ -1,0 +1,1316 @@
+use std
+use sys
+use bio
+use date
+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)
+;;
+
+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[:])
+	`Join	byte[:]
+	`Part	byte[:]
+	`Status	byte[:]
+;;
+
+const main = {args
+	var irc : irc#
+	var home, rcfile, logdir
+	var c
+	var fd
+	var cmd
+
+	home = fileutil.homedir()
+	rcfile = std.pathcat(home, ".ircrc")
+	logdir = std.pathcat(home, ".irclogs")
+	cmd = std.optparse(args, &[
+		.maxargs=-1,
+		.opts=[
+			[.opt='c', .arg="cfg",
+			 .desc="use config file 'cfg'",
+			 .dest=`std.Some &rcfile],
+			[.opt='l', .arg="log",
+			 .desc="use log dir 'log'",
+			 .dest=`std.Some &logdir],
+		][:]
+	])
+
+	irc = mkirc(rcfile, logdir)
+	while true
+		redraw(irc)
+		c = curchan(irc)
+		fd = poll(irc)
+		/* we can get interrupted by sigwinch */
+		if fd <= 0
+			terminput(irc, c)
+		elif fd >= 0
+			io(irc, fd)
+		;;
+		redial(irc)
+	;;
+}
+
+const redial = {irc
+	for s : irc.srv
+		if s.death == 0 || std.now() - s.death < 60*std.Sec
+			continue
+		;;
+		match std.dial(s.ds)
+		| `std.Ok fd:
+			s.fd = fd
+			if !handshake(irc, s)
+				std.close(s.fd)
+				continue
+			;;
+			s.death = 0
+			for c : s.chan
+				send(irc, s, "JOIN {}\r\n", c.name)
+			;;
+		| `std.Err e:
+		;;
+	;;
+}
+
+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)
+		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])
+	;;
+	chanmsg(irc, srv, c, src, args[1])
+}
+
+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
+
+	term = termdraw.mk(std.In)
+	termdraw.raw(term)
+	termdraw.cursoron(term)
+	user = std.getenvv("user", std.getenvv("USER", "user"))
+	nick = user
+	irc = std.mk([
+		.srv = [][:],
+		.user = std.sldup(user),
+		.nick = std.sldup(nick),
+		.focus = -1,
+		.term = term,
+		.logdir = logdir,
+		.chandirty = true,
+		.cmddirty = true,
+	])
+	irc.self = mkchan(irc, "status", "status", "status")
+
+	match bio.open(rcfile, bio.Rd)
+	| `std.Ok cfg:
+		for ln : bio.byline(cfg)
+			do(irc, ln)
+		;;
+	| `std.Err e:
+	;;
+
+	-> 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
+
+	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 freechan = {chan
+	std.slfree(chan.name)
+	std.slfree(chan.topic)
+	std.free(chan)
+}
+
+const do = {irc, ln
+	ln = std.strstrip(ln)
+	if ln.len == 0
+		-> void
+	;;
+	match std.charstep(ln)
+	| ('/', rest):
+		match std.charstep(ln)
+		| (' ', m):	message(irc, m)
+		| (_, _):	cmd(irc, rest)
+		;;
+	| _:
+		message(irc, ln)
+	;;
+}
+
+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:])
+	| 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
+	var idx, srvidx
+	var ch, srv
+
+	if args.len != 1
+		status(irc, irc.self, "invalid arguments: {j= }", args)
+	else
+		srvidx = -1
+		idx = -1
+		irc.chandirty = true
+		irc.cmddirty = true
+		match std.strfind(irc.self.name, args[0])
+		| `std.None:	/* ok */
+		| `std.Some i:	idx = i
+		;;
+		for var i = 0; i < irc.srv.len; i++
+			srv = irc.srv[i]
+			for var j = 0; j < srv.chan.len; j++
+				ch = srv.chan[j]
+				match std.strfind(ch.name, args[0])
+				| `std.None:	/* ok */
+				| `std.Some n:
+					if idx != -1
+						status(irc, irc.self, "ambiguous channel name {}", args[0])
+						-> void
+					else
+						srvidx = i
+						idx = j
+					;;
+				;;
+			;;
+		;;
+		if idx != -1
+			if idx == -1 || srvidx == -1
+				irc.focus = -1
+			else
+				irc.focus = srvidx
+				irc.srv[srvidx].focus = idx
+				irc.srv[srvidx].chan[idx].stale = false
+				irc.srv[srvidx].chan[idx].flagged = false
+			;;
+		;;
+	;;
+}
+
+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 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
+
+	while true
+		match termdraw.poll(irc.term)
+		| `std.None:
+			break
+		| `std.Some `termdraw.Winsz _:
+			irc.chandirty = true
+			irc.cmddirty = true
+		| `std.Some `termdraw.Kc '\t':
+			complete(irc, chan)
+		| `std.Some `termdraw.Kc '\n':
+			ln = getstr(irc.cmd)
+			do(irc, ln)
+			std.slfree(ln)
+			std.slfree(irc.cmd)
+			irc.cmd = [][:]
+			irc.off = 0
+		| `std.Some `termdraw.Ctrl 'n':
+			chancycle(irc, 1)
+		| `std.Some `termdraw.Ctrl 'p':
+			chancycle(irc, -1)
+		| `std.Some `termdraw.Ctrl 'l':
+			termdraw.cls(irc.term)
+			irc.chandirty = true
+			irc.cmddirty = true
+		| `std.Some `termdraw.Kup:	scroll(irc, chan, 1)
+		| `std.Some `termdraw.Kdown:	scroll(irc, chan, -1)
+		| `std.Some `termdraw.Kpgup:	scroll(irc, chan, std.max(1, irc.term.height - 7))
+		| `std.Some `termdraw.Kpgdn:	scroll(irc, chan, -std.max(1, irc.term.height - 7))
+		| `std.Some ev:
+			if !input(irc, ev)
+				puthist(irc, irc.self, (std.now(), `Status std.fmt("{}", ev)))
+			;;
+		;;
+		irc.cmddirty = true
+		redraw(irc)
+	;;
+}
+
+const input = {irc, ev
+	var b
+	match ev
+	| `termdraw.Khome:	irc.off = 0
+	| `termdraw.Ctrl 'a':	irc.off = 0
+	| `termdraw.Kend:	irc.off = irc.cmd.len
+	| `termdraw.Ctrl 'e':	irc.off = irc.cmd.len
+	| `termdraw.Kc c:
+		std.slput(&irc.cmd, irc.off, c)
+		irc.off++
+	| `termdraw.Kleft:
+		if irc.off > 0
+			irc.off--
+		;;
+	| `termdraw.Kright:
+		if irc.off < irc.cmd.len
+			irc.off++
+		;;
+	| `termdraw.Ctrl 'u':
+		std.slfree(irc.yank)
+		irc.yank = irc.cmd
+		irc.off = 0
+		irc.cmd = [][:]
+	| `termdraw.Ctrl 'y':
+		b = [][:]
+		std.sljoin(&b, irc.cmd[:irc.off])
+		std.sljoin(&b, irc.yank)
+		std.sljoin(&b, irc.cmd[irc.off:])
+		std.slfree(irc.cmd)
+		irc.cmd = b
+		irc.off += irc.yank.len
+	| `termdraw.Ctrl 'w':
+		while irc.off > 0
+			if !std.isspace(irc.cmd[irc.off - 1])
+				break
+			;;
+			irc.off--
+		;;
+		while irc.off > 0
+			if std.isspace(irc.cmd[irc.off - 1])
+				break
+			;;
+			std.sldel(&irc.cmd, irc.off - 1)
+			irc.off--
+		;;
+	| `termdraw.Kbksp:
+		if irc.off > 0
+			irc.off--
+			std.sldel(&irc.cmd, irc.off)
+		;;
+	| _:
+		-> false
+	;;
+	-> true
+}
+
+const complete = {irc, chan
+	var pfx, off, matches
+
+	off = irc.off
+	while off > 0
+		if std.isspace(irc.cmd[off - 1])
+			break
+		;;
+		off--
+	;;
+
+	pfx = getstr(irc.cmd[off:irc.off])
+	matches = [][:]
+	for var i = 0; i < chan.users.len; i++
+		if std.hasprefix(chan.users[i], pfx)
+			std.slpush(&matches, chan.users[i])
+		;;
+	;;
+
+	if matches.len == 1
+		for c : std.bychar(matches[0][pfx.len:])
+			std.slput(&irc.cmd, irc.off, c)
+			irc.off++
+		;;
+	elif matches.len > 1
+		status(irc, chan, "completions: {j= }", matches)
+	;;
+	std.slfree(pfx)
+}
+
+const scroll = {irc, chan, delta
+	chan.scroll = std.clamp(chan.scroll + delta, 0, chan.hist.len)
+	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) : std.byreverse(c.hist[:c.hist.len - c.scroll])
+		margin = 0
+		match ent
+		| `Msg (m, ln):
+			margin = c.gutter + termdraw.strwidth(t, " | ")
+			width = termdraw.strwidth(t, ln)
+		| `Join user:
+			margin = termdraw.strwidth(t, "#joined: ")
+			width = termdraw.strwidth(t, user)
+		| `Part user:
+			margin = termdraw.strwidth(t, "#parted: ")
+			width = termdraw.strwidth(t, user)
+		| `Status msg:
+			margin = termdraw.strwidth(t, "! ")
+			width = termdraw.strwidth(t, 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 - termdraw.strwidth(t, m), 0, dx)
+			(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)
+		| `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 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 -= termdraw.strwidth(irc.term, 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 chanmsg = {irc, srv, chan, nick, msg 
+	nick = std.sldup(displayname(nick))
+	puthist(irc, chan, (std.now(), `Msg (nick, std.sldup(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, date, contents
+
+	irc.chandirty = true
+	std.slpush(&chan.hist, entry)
+	(tm, contents) = entry
+	if chan.log != -1
+		date = date.mkinstant(tm, "local")
+		match contents
+		| `Msg (u, ln):
+			std.fput(chan.log, "{} {} | {}\n", date, u, ln)
+			if chan != curchan(irc)
+				chan.stale = true
+			;;
+		| _:
+			/* nothing */
+		;;
+	;;
+	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)
+		;;
+	;;
+}
--