shithub: irc.myr

ref: 8d6c1b01c6e17894cb1054d954d18272e71e5aae
dir: /main.myr/

View raw version
use std
use sys
use bio
use date
use iter
use termdraw
use fileutil	/* for homedir() */

use "types"
use "draw"
use "irc"

const main = {args
	var irc : irc#
	var home, rcfile, logdir
	var c
	var fd
	var cmd

	std.nopipe()
	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
		c = curchan(irc)
		c.stale = false
		c.flagged = false
		redraw(irc)
		fd = poll(irc)
		/* we can get interrupted by sigwinch */
		if fd <= 0
			terminput(irc, c)
		elif fd >= 0
			io(irc, fd)
		;;
		ping(irc)
		redial(irc)
	;;
}

const ping = {irc
	for s : irc.srv
		if s.death == 0 && (std.now() - s.ping) > 60*std.Sec
			send(irc, s, "PING irc.myr\r\n")
			/* let's not flood */
			s.ping = std.now()
		;;
	;;
}

const redial = {irc
	var pingtime, droptime

	for s : irc.srv
		droptime = std.now() - s.death 
		pingtime = std.now() - s.ping
		if s.death != 0 && droptime >= 60*std.Sec
			status(irc, irc.self, "{}: connection dropped\n", s.ds)
		elif pingtime >= 300*std.Sec
			status(irc, irc.self, "{}: ping timed out\n", s.ds)
		else
			continue
		;;
		std.close(s.fd)
		s.fd = -1
		match std.dial(s.ds)
		| `std.Ok fd:
			s.death = 0
			s.fd = fd
			handshake(irc, s)
			for c : irc.onconn
				do(irc, c)
				redraw(irc)
			;;
			for c : s.chan
				send(irc, s, "JOIN {}\r\n", c.name)
				redraw(irc)
				std.usleep(std.Sec)
			;;
		| `std.Err e:
			/* push back our next reconnection attempt */
			s.death = std.now()
		;;
	;;
}

const poll = {irc
	var pfd, fd, start, i

	fd = -1
	pfd = [][:]
	std.slpush(&pfd, [
		.fd=(std.In : sys.fd),
		.events=sys.Pollin,
		.revents=0,
	])
	for s : irc.srv
		if s.fd == -1
			continue
		;;
		std.slpush(&pfd, [
			.fd=(s.fd : sys.fd),
			.events=sys.Pollin,
			.revents=0,
		])
	;;
	if sys.poll(pfd, 60_000) >= 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|sys.Pollerr) != 0
				fd = (pfd[i].fd : std.fd)
				break
			;;
		;;
	;;
	std.slfree(pfd)

	-> fd
}

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.Err e:	/* nothing */
	| `std.Ok cfg:
		for ln : bio.byline(cfg)
			std.usleep(300*std.Msec);
			do(irc, ln)
			redraw(irc)
		;;
		status(irc, irc.self, "doing {}", irc.onconn)
		for c : irc.onconn
			std.usleep(300*std.Msec);
			do(irc, c)
			redraw(irc)
		;;
	;;

	-> irc
}

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 do = {irc, ln
	if ln.len == 0
		-> void
	;;
	match std.charstep(std.strfstrip(ln))
	| ('/', rest):
		match std.charstep(ln)
		| (' ', m):	message(irc, m)
		| (_, _):	cmd(irc, ln)
		;;
	| _:
		message(irc, ln)
	;;
}

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':
		deltext(irc, true)
		deltext(irc, false)
	| `termdraw.Kbksp:
		if irc.off > 0
			irc.off--
			std.sldel(&irc.cmd, irc.off)
		;;
	| _:
		-> false
	;;
	-> true
}

const deltext = {irc, isspace
	while irc.off > 0
		if isspace != std.isspace(irc.cmd[irc.off - 1])
			break
		;;
		std.sldel(&irc.cmd, irc.off - 1)
		irc.off--
	;;
}

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 matchcompletion(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(matches)
	std.slfree(pfx)
}

const matchcompletion = {user, pfx
	match (user[0] : char)
	| '@':	-> std.hasprefix(user[1:], pfx)
	| '+':	-> std.hasprefix(user[1:], pfx)
	| _:	-> std.hasprefix(user, pfx)
	;;
}

const scroll = {irc, chan, delta
	chan.scroll = std.clamp(chan.scroll + delta, 0, chan.hist.len)
	irc.chandirty = true
}