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