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