shithub: irc.myr

Download patch

ref: 49075757a7eb606af381a341cda24df578817c55
author: Ori Bernstein <ori@eigenstate.org>
date: Thu Jun 8 04:37:41 EDT 2017

Initial commit.

--- /dev/null
+++ b/bld.proj
@@ -1,0 +1,3 @@
+bin irc =
+	irc.myr
+;;
--- /dev/null
+++ b/irc.myr
@@ -1,0 +1,782 @@
+use std
+use sys
+use bio
+use termdraw
+
+pkg =
+	const status	: (cli : client#, fmt : byte[:], args : ... -> void)
+	const send	: (cli : client#, srv : server#, fmt : byte[:], args : ... -> void)
+;;
+
+const Cfg = ".ircrc"
+
+type client = struct
+	srv	: server#[:]
+	self	: chan#
+	focus	: std.size
+	term	: termdraw.term#
+	user	: byte[:]
+	nick	: byte[:]
+	input	: byte[:]
+;;
+
+type server = struct
+	fd	: std.fd
+	user	: byte[:]
+	nick	: byte[:]
+	ds	: byte[:]
+	buf	: byte[512]
+	off	: std.size
+	chan	: chan#[:]
+	focus	: std.size
+	live	: bool
+;;
+
+type chan = struct
+	log	: std.fd
+	buf	: byte[:]
+	name	: byte[:]
+	topic	: byte[:]
+	users	: byte[:][:]
+	stale	: bool
+	gutter	: int
+;;
+
+const main = {
+	var cli : client#
+	var c
+	var fd
+
+	cli = mkclient()
+	while true
+		c = curchan(cli)
+		redraw(cli)
+		fd = poll(cli)
+		if fd == std.In
+			terminput(cli, c)
+		else
+			io(cli, fd)
+		;;
+	;;
+}
+
+const io = {cli, fd
+	var src, cmd, args
+	var srv, ln
+
+	srv = fd2srv(cli, fd)
+
+	match std.read(srv.fd, srv.buf[srv.off:])
+	| `std.Ok 0:	closed(cli, srv, "eof")
+	| `std.Ok n:	srv.off += n
+	| `std.Err e:	closed(cli, srv, "error reading")
+	;;
+
+	while true
+		match nextln(cli, srv)
+		| `std.Some l:	ln = l
+		| `std.None:	-> void
+		;;
+		(src, cmd, args) = parse(ln)
+		uppercase(cmd)
+		match cmd
+		| "001":	srvmsg(cli, args)
+		| "002":	srvmsg(cli, args)
+		| "003":	srvmsg(cli, args)
+		| "004":	srvmsg(cli, args)
+		| "005":	srvmsg(cli, args)
+		| "250":	srvmsg(cli, args)
+		| "251":	srvmsg(cli, args)
+		| "254":	srvmsg(cli, args)
+		| "255":	srvmsg(cli, args)
+		| "265":	srvmsg(cli, args)
+		| "266":	srvmsg(cli, args)
+		| "375":	srvmsg(cli, args)
+		| "372":	srvmsg(cli, args)
+		| "376":	srvmsg(cli, args)
+		| "NOTICE":	srvmsg(cli, args)
+		| "332":	topic(cli, srv, args)
+		| "353":	names(cli, srv, args)
+		| "PRIVMSG":	recievemsg(cli, srv, src, args)
+		| "PING":	send(cli, srv, "PONG :{}\r\n", args[0])
+		| "JOIN":	joinchan(cli, srv, src, args, ln)
+		| "PART":	joinchan(cli, srv, src, args, ln)
+		| c:	
+			status(cli, "unknown server command {}\n", ln)
+		;;
+		std.slfree(args)
+		std.slfree(ln)
+	;;
+}
+
+const joinchan = {cli, srv, src, args, ln
+	var c
+
+	if args.len == 1
+		c = name2chan(srv, args[0])
+		status(cli, "join {}: {}\n", args[0], src)
+	;;
+}
+
+const topic = {cli, srv, args
+	var c
+
+	if args.len >= 3
+		status(cli, "topic: {}\n", args)
+		c = name2chan(srv, args[1])
+		std.slfree(c.topic)
+		c.topic = std.sldup(args[2])
+	;;
+}
+
+const recievemsg = {cli, srv, src, args
+	var c
+
+	c = name2chan(srv, args[0])
+	chanmsg(cli, c, src, args[1])
+}
+
+const names = {cli, srv, args
+	var c
+
+	if args.len != 4
+		-> void
+	;;
+	c = name2chan(srv, args[2])
+	for n in std.bysplit(args[3], " ")
+		std.slpush(&c.users, std.sldup(n))
+		c.gutter = std.max(c.gutter, n.len + 4)
+	;;
+}
+
+const fd2srv = {cli, fd
+	for s in cli.srv
+		if s.fd == fd
+			-> s
+		;;
+	;;
+	std.fatal("missing srv for fd {}\n", fd)
+}
+
+const name2chan = {srv, chan
+	var new
+
+	for c in srv.chan
+		if std.sleq(c.name, chan)
+			-> c
+		;;
+	;;
+	new = mkchan(chan, "")
+	std.slpush(&srv.chan, new)
+	if srv.focus == -1
+		srv.focus = srv.chan.len - 1
+	;;
+	-> new
+}
+
+const nextln = {cli, srv
+	var r
+
+	match std.strfind(srv.buf[:srv.off], "\r\n")
+	| `std.Some i:
+		r = std.sldup(srv.buf[:i])
+		std.slcp(srv.buf[:srv.off - i - 2], srv.buf[i + 2:srv.off])
+		srv.off = srv.off - i - 2
+		-> `std.Some r
+	| `std.None:
+		-> `std.None
+	;;
+}
+
+const closed = {cli, srv, msg
+	if srv.live
+		srv.live = false
+		status(cli, "{} closed: {}\n", srv.ds, msg)
+	;;
+}
+
+const mkclient = {
+	var cli, cfg, t
+
+	t = termdraw.mk(std.In)
+	termdraw.raw(t)
+	cli = std.mk([
+		.srv = [][:],
+		.user = std.getenvv("user", std.getenvv("USER", "user")),
+		.nick = std.getenvv("user", std.getenvv("USER", "user")),
+		.self = mkchan("status", "status"),
+		.focus = -1,
+		.term = t,
+	])
+
+	match bio.open(Cfg, bio.Rd)
+	| `std.Ok f:	cfg = f
+	| `std.Err e:	-> cli
+	;;
+
+	for ln in bio.byline(cfg)
+		do(cli, ln)
+	;;
+
+	-> cli
+}
+
+const mksrv = {cli, fd, ds -> server#
+	-> std.mk([
+		.fd=fd,
+		.ds=std.sldup(ds),
+		.chan=[][:],
+		.focus=-1,
+		.user=cli.user,
+		.nick=cli.nick,
+	])
+}
+
+const mkchan = {name, topic
+	-> std.mk([
+		.log = -1, /* FIXME: log to file */
+		.buf = "", /* FIXME: read back from log */
+		.name = std.sldup(name),
+		.topic = std.sldup(topic),
+	])
+}
+
+const freechan = {chan
+	std.slfree(chan.name)
+	std.slfree(chan.topic)
+	std.free(chan)
+}
+
+const do = {cli, ln
+	match std.strstep(ln)
+	| ('/', rest):
+		match std.strstep(ln)
+		| (' ', m):	message(cli, m)
+		| (_, _):	cmd(cli, rest)
+		;;
+	| _:
+		message(cli, ln)
+	;;
+}
+
+const cmd = {cli, text
+	var sp
+
+	sp = std.strtok(text)
+	if sp.len == 0
+		-> void
+	;;
+	match sp[0]
+	| "connect":	connect(cli, sp[1:])
+	| "join":	join(cli, sp[1:])
+	| "leave":	leave(cli, sp[1:])
+	| "chan":	switch(cli, sp[1:])
+	| "win":	switch(cli, sp[1:])
+	| "quit":	quit(cli, sp[1:])
+	| "help":	help(cli, sp[1:])
+	| c:		status(cli, "unknown command: /{}\n", text)
+	;;
+	std.slfree(sp)
+}
+
+const connect = {cli, args
+	var ds, srv
+
+	ds = ""
+	match args.len
+	| 1:	ds = std.netaddr(args[0], "tcp", "ircd")
+	| 0:	status(cli, "join: missing server\n")
+	| _:	status(cli, "join: invalid arguments '{j= }'\n", args)
+	;;
+	if ds.len == 0
+		-> void
+	;;
+
+	for s in cli.srv
+		if std.sleq(s.ds, ds)
+			status(cli, "already connected to {j= }\n", args)
+			-> void
+		;;
+	;;
+
+	status(cli, "dialing {}\n", ds)
+	match std.dial(ds)
+	| `std.Ok fd:
+		srv = mksrv(cli, fd, ds)
+		if handshake(cli, srv)
+			std.slpush(&cli.srv, srv)
+			if cli.focus == -1
+				cli.focus = cli.srv.len - 1
+				srv.live = true
+			;;
+			status(cli, "connected to {}\n", ds)
+		else
+			status(cli, "could not handshake with {j= }\n", args)
+			std.close(srv.fd)
+			std.free(srv)
+		;;
+	| `std.Err e:
+		status(cli, "failed to connect: {}\n", e)
+	;;
+}
+
+const handshake = {cli, srv
+	send(cli, srv, "NICK {}\r\n", srv.nick)
+	send(cli, srv, "USER {} 8 * :{}\r\n", srv.user, srv.user)
+	-> true
+}
+
+const join = {cli, args
+	if args.len == 2
+		match findsrv(cli, args[1])
+		| `std.Some s:	send(cli, s, "JOIN {}\r\n", args[0])
+		| `std.None:	status(cli, "no server '{}\n", args[1])
+		;;
+	elif args.len == 1
+		match cursrv(cli)
+		| `std.Some s:	send(cli, s, "JOIN {}\r\n", args[0])
+		| `std.None:	status(cli, "no server focused\n")
+		;;
+	else
+		-> status(cli, "invalid arguments: {j= }\n", args)
+	;;
+}
+
+const leave = {cli, args
+	var name, c, idx
+
+	if args.len == 0
+		c = curchan(cli)
+		if c == cli.self
+			-> void
+		;;
+		name = c.name
+	elif args.len == 1
+		name = args[0]
+	else
+		status(cli, "leave: invalid arguments: {j= }\n", args)
+		-> void
+	;;
+
+	for srv in cli.srv
+		idx = 0
+		for chan in srv.chan
+			idx++
+			if !std.sleq(chan.name, name)
+				continue
+			;;
+			send(cli, srv, "PART {}\n", name)
+			std.sldel(&srv.chan, idx)
+			freechan(chan)
+		;;
+	;;
+}
+
+const switch = {cli, args
+	var idx, srvidx
+	var ch, srv
+
+	if args.len != 1
+		status(cli, "invalid arguments: {j= }\n", args)
+	else
+		srvidx = -1
+		idx = -1
+		match std.strfind(cli.self.name, args[0])
+		| `std.None:	/* ok */
+		| `std.Some i:	idx = i
+		;;
+		for var i = 0; i < cli.srv.len; i++
+			srv = cli.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(cli, "ambiguous channel name {}\n", args[0])
+						-> void
+					else
+						srvidx = i
+						idx = j
+					;;
+				;;
+			;;
+		;;
+		if idx != -1
+			if idx == -1 || srvidx == -1
+				cli.focus = -1
+			else
+				cli.focus = srvidx
+				cli.srv[srvidx].focus = idx
+				cli.srv[srvidx].chan[idx].stale = false
+			;;
+		;;
+	;;
+}
+
+const quit = {cli, args
+	if args.len != 0
+		status(cli, "invalid argument for quit: {j= }\n", args)
+		-> void
+	;;
+	termdraw.free(cli.term)
+	std.exit(0)
+}
+
+const help = {cli, args
+	status(cli, "irc.myr commands\n")
+	status(cli, "\t/connect dialstr: connect to server\n")
+	status(cli, "\t/help [cmd...]:   get help [on cmd...]\n")
+	status(cli, "\t/join chan [srv]: join channel on server\n")
+	status(cli, "\t/leave [chan]:    leave current channel\n")
+	status(cli, "\t/chan name:       switch to channnel 'name'\n")
+	status(cli, "\t/win name:        switch to channnel 'name'\n")
+	status(cli, "\t/quit:            exit irc.myr\n")
+}
+
+const send = {cli, srv, fmt, args
+	var s, ap
+
+	ap = std.vastart(&args)
+	s = std.fmtv(fmt, &ap)
+	if !writeall(srv.fd, s)
+		closed(cli, srv, "failed writing")
+	;;
+	std.slfree(s)
+}
+
+const message = {cli, msg
+	var c
+
+	match cursrv(cli)
+	| `std.Some s:
+		c = curchan(cli)
+		if c == cli.self
+			status(cli, "can't send to status channel\n")
+		else
+			send(cli, s, "PRIVMSG {} :{}\r\n", c.name, msg)
+			chanmsg(cli, c, s.nick, msg)
+		;;
+	| `std.None:
+		status(cli, "no connected server\n")
+	;;
+}
+
+
+const parse = {msg
+	var w, hdr, cmd, args
+
+	match std.strstep(msg)
+	| (':', tl):	(hdr, msg) = word(tl)
+	| (_, _):	hdr = ""
+	;;
+	(cmd, msg) = word(msg)
+
+	args = [][:]
+	msg = std.strfstrip(msg)
+	while msg.len != 0
+		match std.strstep(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 = {cli, chan
+	var s
+
+	match termdraw.event(cli.term)
+	| `termdraw.Kc '\n':
+		do(cli, cli.input)
+		std.slfree(cli.input)
+		cli.input = ""
+	| `termdraw.Kc c:
+		s = std.fmt("{}", c)
+		std.sljoin(&cli.input, s)
+		std.slfree(s)
+	| `termdraw.Kbksp:
+		s = cli.input
+		while s.len > 0
+			s= s[:s.len - 1]
+			if s.len == 0 || s[s.len - 1] & 0x80 == 0
+				break
+			;;
+		;;
+		cli.input = s
+	| `termdraw.Ctrl 'u':
+		std.slfree(cli.input)
+		cli.input = ""
+	| r:
+		s = std.fmt("{}\n", r)
+		std.sljoin(&cli.self.buf, s)
+		std.slfree(s)
+	;;
+}
+
+const redraw = {cli
+	var c, x, y, w, h
+
+	if cli.term.width < 3 || cli.term.height < 4
+		termdraw.cls(cli.term)
+	;;
+
+	x = 0
+	y = 0
+	w = cli.term.width
+	h = cli.term.height
+	c = curchan(cli)
+	drawbanner(cli, 0, 0, w, 1, c)
+	drawtext(cli, 0, 1, w, h - 2, c)
+	drawlist(cli, 0, h - 2, w, h - 1, c)
+	drawinput(cli, 0, h - 1, w, h, c)
+	termdraw.flush(cli.term)
+}
+
+const drawbanner = {cli, x0, y0, x1, y1, c
+	var t
+
+	t = cli.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 = {cli, x0, y0, x1, y1, c
+	var t
+	var b, x, y
+
+	x = x0
+	y = y0
+	t = cli.term
+	b = visiblerange(cli, c, y1 - y0)
+
+	termdraw.setbg(t, termdraw.Default)
+	termdraw.clear(t, x0, y0, x1, y1)
+	for l in std.bychar(b)
+		match l
+		| '\t':	
+			x = (x / 8 + 1)*8
+		| '\n':	
+			x = 0
+			y++
+		| chr:
+			if x < x1
+				termdraw.move(t, x, y)
+				termdraw.putc(t, chr)
+				x++
+			;;
+		;;
+		if x >= x1
+			x = 0
+			y++
+		;;
+	;;
+}
+
+const drawlist = {cli, x0, y0, x1, y1, c
+	var t
+
+	t = cli.term
+	termdraw.setbg(t, termdraw.Blue)
+	termdraw.clear(t, x0, y0, x1, y1)
+	termdraw.move(t, x0, y0)
+	termdraw.putc(t, '|')
+	termdraw.put(t, "[{}]", cli.self.name)
+	for s in cli.srv
+		for ch in s.chan
+			if ch.stale
+				termdraw.put(t, "[*{}]", ch.name)
+			else
+				termdraw.put(t, "[{}]", ch.name)
+			;;
+		;;
+		termdraw.putc(t, '|')
+	;;
+}
+
+const drawinput = {cli, x0, y0, x1, y1, c
+	var t
+
+	t = cli.term
+	termdraw.setbg(t, termdraw.Default)
+	termdraw.clear(t, x0, y0, x1, y1)
+	termdraw.move(t, x0, y0)
+	termdraw.put(t, "[{}] {}", c.name, cli.input)
+}
+
+const visiblerange = {cli, c, dy
+	var n, i
+
+	n = 0
+	if c.buf.len == 0
+		-> ""
+	;;
+	for i = c.buf.len - 1; i > 0; i--
+		if n == dy
+			break
+		;;
+		if c.buf[i] == ('\n' : byte)
+			n++
+		;;
+	;;
+	-> c.buf[i:]
+}
+
+const srvmsg = {cli, args
+	if args.len >= 2
+		status(cli, "{}\n", args[1])
+	;;
+}
+
+const status = {cli, fmt, args : ...
+	var c, s, ap
+
+	c = cli.self
+	ap = std.vastart(&args)
+	s = std.fmtv(fmt, &ap)
+	std.sljoin(&c.buf, s)
+	std.slfree(s)
+
+	if c != curchan(cli)
+		c.stale = true
+	;;
+}
+
+const curchan = {cli
+	var s
+
+	if cli.focus < 0 || cli.focus >= cli.srv.len
+		-> cli.self
+	else
+		s = cli.srv[cli.focus] 
+		if s.focus < 0 || s.focus >= s.chan.len
+			-> cli.self
+		;;
+		-> s.chan[s.focus]
+	;;
+}
+
+const cursrv = {cli
+	if cli.focus < 0
+		-> `std.None
+	else
+		-> `std.Some cli.srv[cli.focus] 
+	;;
+}
+
+const findsrv = {cli, name
+	var idx
+
+	idx = -1
+	for var i = 0; i < cli.srv.len; i++
+		match std.strfind(cli.srv[i].ds, name)
+		| `std.Some o:
+			if idx != -1
+				status(cli, "ambiguous server name {}\n", name)
+				-> `std.None
+			else
+				idx = i
+			;;
+		| `std.None:
+		;;
+	;;
+	if idx != -1
+		-> `std.Some cli.srv[idx]
+	else
+		-> `std.None
+	;;
+}
+
+const poll = {cli
+	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 in cli.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 = {cli, chan, sender, msg 
+	var nick
+
+	match std.strfind(sender, "!")
+	| `std.Some i:	nick = sender[:i]
+	| `std.None:	nick = sender
+	;;
+	std.sljoin(&chan.buf, nick)
+	for var i = 0; i < chan.gutter - nick.len; i++
+		std.sljoin(&chan.buf, " ")
+	;;
+	std.sljoin(&chan.buf, "| ")
+	std.sljoin(&chan.buf, msg)
+	std.sljoin(&chan.buf, "\n")
+
+	if chan != curchan(cli)
+		chan.stale = true
+	;;
+
+}
+
+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)
+		;;
+	;;
+}
--