PicoLisp IRC Client

Over the years I used several IRC clients. There are plenty of them, and yet another one is surely not what the world needs most.

But I was never completely happy with any of them. XChat, for example, is nice, but its GTK GUI doesn't support X-Window arguments like -geometry, so it pops up in places where I don't want it to, and I can't control it. Weechat is even better, it runs fine in an XTerm, but it is awfully complicated to configure.

What came closest for me is the plain, simple ircII, the mother of all IRC clients. It is straightforward, and doesn't have too many bells and whistles. Unfortunately, the Debian version is hopelessly outdated (it doesn't, for example, support UTF-8 yet). You script it in TCL, which is not really my cup of tea. What disturbed me most, is that - though it supports TAB expansion for nicknames - it does so only for /msg commands, not inside normal IRC messages (yes, yes, I know, delving into TCL scripting could fix that, but that's much more trouble than writing a new client). Also, it doesn't beep when my nickname is in a message.

One feature which I find disturbing, and which all clients (except ircII in "dumb" mode) seem to have, is that commands I type in are not echoed to the screen, so that it is hard to figure out which output was caused by which command. Unlike a Lisp REPL.

Another minor issue that those clients who support TAB expansion always append a space to the nickname, so that I have to hit backspace when I want to say "Hi nick!", and so on.

Writing an IRC client turned out surprisingly easy. The IRC protocol is text-based running over TCP. I never needed ICB or DCC, so I didn't bother.

Since a few days, I'm using this client now exclusively. It has the following features:

Usage

Copy/paste the source code at the end of this article to a file "irc" in some directory for binaries, or download it from http://software-lab.de/irc, and set it to executable (chmod +x irc).

The invocation syntax is

   bin/irc <server> <nick> <name> [<pass>] [#chan1 #chan2 ..]

To connect to the PicoLisp channel, for example, start it as

   $ bin/irc irc.freenode.net myNickname 'My Name' myPassword '#picolisp'

You'll see the typical dump of the server's initial messages. Then the client waits for input.

Everything you type will go to the channel, except lines starting with a slash '/' in the very first column. Such lines are commands.

Normal commands, like /help, /away some-text or /quit, are passed through and handled by the server.

Five special commands, each consisting of a single character, are intercepted by the client and handled locally:

   /?                      # Print channel and user info
   /! nick some text       # Send "some text" to user 'nick'
   /+ #channel             # Join a channel
   /-                      # Leave the current channel
   /- #channel             # Leave given channel
   /2                      # Digit 1..9: Set nth channel to current

Strictly speaking, some are just convenient shortcuts, and not absolutely necessary. For example, instead of "+" you could write "join", and "/- #channel" is equivalent to "/part #channel".

Inside a message, you can type one or more initial letters and then press TAB. You can cycle through all nicknames starting with these letter(s).

Pressing TAB in the first column of a message will expand to "/! nick " (i.e. to a direct message), where "nick" is the last nickname you had sent a direct message to.

The Code

For a full listing, refer to the end of this article. Let me explain some details.

The first expression connects to the server passed on the command line:

   (unless (setq *Irc (connect (setq *Host (opt)) 6667))
      (quit "Can't connect to IRC") )

The standard IRC port 6667 is hard-coded here, but could be easily changed.

Then the remaining command line arguments are parsed, and corresponding commands sent to the server:

   (out *Irc
      (prinl "NICK " (setq *Nick (chop (opt))) "^M")
      (prinl "USER " *Nick " localhost " *Host " :" (opt) "^M")
      (and (opt) (prinl "PASS " @ "^M"))
      (for C (flip (argv))
         (prinl "JOIN " C "^M") ) )

We define a function to log output from the server to the terminal window. To avoid messing up the currently edited line, it is first cleared, the message printed, and then the line restored:

   (de termlog @
      (do (length *Line)
         (prin "^H ^H") )
      (pass prinl)
      (prin (reverse *Line))
      (flush) )

We define a second function, a utility to send a private message (i.e. to a channel or to another user) to the server:

   (de privmsg (Dst Txt)
      (out *Irc (prinl "PRIVMSG " Dst " :" Txt "^M")) )



Next we start a background task to accept and process messages from the server:

   (task *Irc
      (in @
         (if (eof)
            (bye)

When end of file is detected (i.e. the server closed the connection), the client terminates with (bye).

Otherwise, we read a line, and split it on space delimiters. If the line is empty, it is ignored. Otherwise, the user, the command and its parameters are extracted:

            (let? L (split (line) " ")
               (let Usr *Host
                  (when (= ":" (caar L))
                     (setq Usr (cdar (split (pop 'L) "!"))) )
                  (let (Cmd (pack (pop 'L))  Par (pop 'L))
                     (and (= ":" (car Par)) (pop 'Par))
                     (and (= ":" (caar L) (pop L)))

The following case statement performs the proper actions. Only a few commands are explicitly checked for, as needed to keep track of channels and nicknames. The rest is simply logged to the terminal window.

The final expression is an infinite loop to handle keyboard input:

   (loop
      (case (key)
         (("^J" "^M")
            ...
                  (case (prog1 (cadr L) (setq L (clip (cddr L))))
                     ("?"  # Channel and user info
                        ...
                     ("!"  # Direct message <user> <text>
                        ...
                     ("+"  # JOIN <chan>
                        ...
                     ("-"  # PART [<chan>]
                        ...
                     (("1" "2" "3" "4" "5" "6" "7" "8" "9")
                        ...
                     (T (out *Irc (prinl @ L "^M"))) ) ) )
            ...
         (("^H" "^?")
            ..
         ("^I"
            ..
         (T (prin (push '*Line @)) (off *Complete)) ) )

This handles return or newline to send a message or command, backspace for basic line editing, and Ctrl-I (TAB) for nickname expansion. Any other key is appended to the line.

A note on the input line in the *Line global: You see that it it contains the input characters in reverse order. This makes line editing a bit easier, as new characters are pushed into the line, and backspace simply removes the CAR.

Complete Source Code


   #!/usr/bin/picolisp /usr/lib/picolisp/lib.l
   # 14jan15abu

   ## bin/irc <server> <nick> <name> [<pass>] [#chan1 #chan2 ..]

   (unless (setq *Irc (connect (setq *Host (opt)) 6667))
      (quit "Can't connect to IRC") )

   (out *Irc
      (prinl "NICK " (setq *Nick (chop (opt))) "^M")
      (prinl "USER " *Nick " localhost " *Host " :" (opt) "^M")
      (and (opt) (prinl "PASS " @ "^M"))
      (for C (flip (argv))
         (prinl "JOIN " C "^M") ) )

   (de termlog @
      (do (length *Line)
         (prin "^H ^H") )
      (pass prinl)
      (prin (reverse *Line))
      (flush) )

   (de privmsg (Dst Txt)
      (out *Irc (prinl "PRIVMSG " Dst " :" Txt "^M")) )

   (task *Irc
      (in @
         (if (eof)
            (bye)
            (let? L (split (line) " ")
               (let Usr *Host
                  (when (= ":" (caar L))
                     (setq Usr (cdar (split (pop 'L) "!"))) )
                  (let (Cmd (pack (pop 'L))  Par (pop 'L))
                     (and (= ":" (car Par)) (pop 'Par))
                     (and (= ":" (caar L) (pop L)))
                     (case Cmd
                        (("315" "353" "366"))
                        (("352")
                           (termlog
                              "   " (push1 '*Nicknames (get L 5))
                              " " (get L 6)
                              " (" (glue " " (nth L 8)) ")" ) )
                        ("PING" (out *Irc (prinl "PONG^M")))
                        ("PONG")
                        ("JOIN"
                           (and (= Usr *Nick) (setq *Chan (cons Par (delete Par *Chan))))
                           (termlog "[" (push1 '*Nicknames Usr) "] joined " Par) )
                        ("PART"
                           (and (= Usr *Nick) (del Par '*Chan))
                           (termlog "[" Usr "] left " Par) )
                        ("PRIVMSG"
                           (when (or (= Par *Nick) (find '((X) (head *Nick X)) L))
                              (beep) )
                           (termlog
                              "<" Usr
                              (unless (= Par (car *Chan)) (cons ":" Par))
                              "> " (glue " " L) )
                           (push1 '*Nicknames Usr) )
                        (T (termlog "[" Usr "] " Cmd " (" Par ") " (glue " " L))) ) ) ) ) ) ) )

   (loop
      (case (key)
         (("^J" "^M")
            (let? L (flip *Line)
               (ifn (= "/" (car L))
                  (privmsg (car *Chan) L)
                  (case (prog1 (cadr L) (setq L (clip (cddr L))))
                     ("?"  # Channel and user info
                        (out *Irc (prinl "WHO " (car *Chan) "^M"))
                        (prin " " (glue " " *Chan)) )
                     ("!"  # Direct message <user> <text>
                        (privmsg
                           (setq *Direct (car (setq L (split L " "))))
                           (glue " " (cdr L)) ) )
                     ("+"  # JOIN <chan>
                        (out *Irc (prinl "JOIN " L "^M")) )
                     ("-"  # PART [<chan>]
                        (out *Irc (prinl "PART " (or L (car *Chan)) "^M")) )
                     (("1" "2" "3" "4" "5" "6" "7" "8" "9")
                        (let? C (get *Chan (format @))
                           (del C '*Chan)
                           (prin " " (push '*Chan C)) ) )
                     (T (out *Irc (prinl @ L "^M"))) ) ) )
            (prinl)
            (off *Line *Complete) )
         (("^H" "^?")
            (when *Line
               (prin "^H ^H")
               (pop '*Line) )
            (off *Complete) )
         ("^I"
            (cond
               (*Line
                  (let (L @  P)
                     (while (and L (not (sp? (car L))))
                        (push 'P (pop 'L)) )
                     (when
                        (default *Complete
                           (filter '((Nm) (head P Nm)) *Nicknames) )
                        (do (length P)
                           (prin "^H ^H") )
                        (prin (setq P (car (rot *Complete))))
                        (setq *Line (conc (reverse P) L)) ) ) )
               (*Direct
                  (do (length *Line)
                     (prin "^H ^H") )
                  (prin
                     (reverse
                        (setq *Line
                           (reverse (append '("/" "!" " ") *Direct '(" "))) ) ) ) ) ) )
         (T (prin (push '*Line @)) (off *Complete)) ) )

   # vi:et:ts=3:sw=3

http://picolisp.com/wiki/?ircclient

01apr16    abu