Robert Kozikowski's blog

Set up ERC, Emacs IRC client, with automated windowed layout and connection

Introduction

I’ve set up an Emacs function my-open-irc that connects to all IRC channels I am interested in and sets up a 2×2 layout with the channels:

#ensime/ensime-emacs (gitter) #emacs (freenode)
#ensime/ensime-server (gitter) #archlinux (freenode)

See the screenshot:

my-open-irc called at any time either (re)connects to irc and sets up the layout or restores the old buffers. It also saves anything I have been doing before under register ?8 so I can come back to it with C-x r j 8. If you are not aware of this functionality, see the documentation of window-configuration-to-register.

You can think of as my-open-irc as “turn IRC on” and C-x r j 8 as “turn IRC off”, all from within emacs. I have bind the my-open-irc to some key with the global-set-key.

It lets me to take a quick glance at “what’s going on”, and if anything appears interesting investigate it further.

My configuration

Code is not of “production” quality, I may clean it up one day if it becomes problematic. I know I shouldn’t hardcode passwords in my .el config, but I do not expose my config and irc is not sensitive.

Just add below to your emacs config, add your nick/password in my-open-irc, and call my-open-irc. Joining my take a few seconds, but you don’t need to do anything manually besides calling my-open-irc.

(setq my-freenode-channels '("#archlinux" "#emacs"))
(setq my-gitter-channels '("#ensime/ensime-emacs" "#ensime/ensime-server"))
(setq my-erc-channel-buffer-regexps '("^#ensime\/ensime-emacs" "#ensime\/ensime-server" "^#archlinux" "^#emacs"))

(defun my-buffer-match-regex (buf)
  (string-match buffer-regex (buffer-name buf)))

(defun my-switch-to-buffer-by-regex (buffer-regex)
  (switch-to-buffer (find-if 'my-buffer-match-regex (buffer-list))))

(require 'erc-sasl)

(setq erc-hide-list '("JOIN" "PART" "QUIT"))
(setq erc-prompt-for-password nil)

;;;;; JOIN Channels after connection ;;;;;

(defun my-join-freenode-channel (channel)
  (message "Attempting to connect to freenode channel %s" channel)
  (my-switch-to-buffer-by-regex "^irc.freenode.net")
  (erc-cmd-JOIN channel))

(defun my-join-gitter-channel (channel)
  (my-switch-to-buffer-by-regex "^irc.gitter.im")
  (erc-cmd-JOIN channel))

(defun myerc-autojoin-channels (server nick)
  (message "Connected to IRC server %s" server)
  (when (s-contains-p "freenode.net" server)
    (mapc 'my-join-freenode-channel my-freenode-channels))
  (when (s-contains-p "gitter.im" server)
    (mapc 'my-join-freenode-channel my-freenode-channels)))

(add-hook 'erc-after-connect 'myerc-autojoin-channels)

;;;;; Set up 2x2 layout when getting all 4 channels ;;;;;

(defun my-setup-windows-in-two-by-two ()
  (call-interactively 'delete-other-windows)
  (my-switch-to-buffer-by-regex (nth 0 my-erc-channel-buffer-regexps))
  (split-window-vertically)
  (split-window-horizontally)
  (windmove-down)
  (split-window-horizontally)
  (my-switch-to-buffer-by-regex (nth 1 my-erc-channel-buffer-regexps))
  (windmove-right)
  (my-switch-to-buffer-by-regex (nth 2 my-erc-channel-buffer-regexps))
  (windmove-up)
  (my-switch-to-buffer-by-regex (nth 3 my-erc-channel-buffer-regexps))
  (windmove-left)
  (window-configuration-to-register ?9)
  (erc-fill-mode -1) ;; Do not wrap lines
  )

(defun my-is-wanted-erc-channel (channel-name)
  "Erc sometimes joins me to random channels lol"
  (-any-p (lambda (regex) (string-match regex channel-name)) my-erc-channel-buffer-regexps))

(setq my-irc-joined-count 0) ;; so hack.
(defun my-post-join-on-hook ()
  (message "Got channel %s in my-post-join-on-hook" (buffer-name))
  (when (my-is-wanted-erc-channel (buffer-name))
    (progn
      (setq my-irc-joined-count (+ 1 my-irc-joined-count))
      (message "Connected to IRC channel %s" (buffer-name))
      (message "my-open-irc, my-irc-joined-count: %s" my-irc-joined-count)))
  (when (equal my-irc-joined-count 4)
    (my-setup-windows-in-two-by-two)))
(add-hook 'erc-join-hook 'my-post-join-on-hook)


;;;;; Auto re-connect function ;;;;;

(setq erc-join-buffer 'bury)

(defun my-maybe-reconnect-buffer (buffer)
  (message "Attempting to reconnect %s" buffer)
  (my-switch-to-buffer-by-regex buffer)
  ;; (when (not erc-server-connected)
  ;;   (message "%s not connected ! %s\n" buffer (not erc-server-connected))
  ;;   (erc-server-reconnect))
  )

(defun my-maybe-reconnect-irc ()
  (mapc 'my-maybe-reconnect-buffer my-erc-channel-buffer-regexps))

;;;;; Tie it all together ;;;;;
(defun my-open-irc ()
  (interactive)
  (window-configuration-to-register ?8) ;; Save old layout under 8
  (if (> 4 my-irc-joined-count)
      (progn
        (erc-tls :server "irc.gitter.im" :port 6697 :nick "kozikow" :password "")
        (erc :server "irc.freenode.net" :port 6667 :nick "kozikow" :password "")
        (my-freenode-force-join))
    (progn
      (my-maybe-reconnect-irc)
      (jump-to-register ?9)
      )
    )

  (setq erc-modified-channels-alist nil) ;; Clean up old notifications
  (erc-modified-channels-update) ;; Update changes
  (erc-fill-mode -1))

(setq erc-insert-timestamp-function 'erc-insert-timestamp-left)
(global-set-key (kbd "C-S-c i") 'my-open-irc)
(define-key erc-mode-map (kbd "M-b") 'erc-button-press-button)

(require 'notify)
(erc-track-disable)
(defun my-erc-message-on-hook (erc-message)
  (when (or (s-contains-p "ensime" (buffer-name))
            (s-contains-p "kozikow" erc-message))
    (notify (format "Irc %s" (buffer-name)) (format "%s" erc-message))))
(add-hook 'erc-insert-pre-hook 'my-erc-message-on-hook)

Problems I encountered

After all of this I think ERC is more of a library that you implement your own IRC client on, rather than actual IRC client.

Erc support for sasl

Erc support for sasl is a bit hacky. Alternative emacs IRC client, circe, supports sasl “out of the box”, but gitter connection was buggy using circe. At that point, I didn’t try out ERC yet, so after a bit of debugging I decided to try out the ERC.

To set up ERC with sasl I did:

  • Install erc with package-install. erc-sasl is not in any emacs repo, so I prefered to install plain erc to avoid manual updates.
  • In erc-sasl add (require 'erc) and anywhere in your emacs config (require 'erc-sasl).

Motivation for such configuration is that erc-sasl is a bit outdated. erc may one day get merged sasl support. Depending only on single erc-sasl file will make it easier to migrate to hypothetical updated erc version.

gitter irc support.

Gitter joining random channels I didn’t tell it about

Beware that gitter joins you to more channels than I want. Initially, gitter did not auto connect me to any channel after just connecting to the gitter server. When I connected to at least one of the channels it connected me to all channels I ever joined through the website.

It caused some bugs in my code running erc-join-hook. To solve it, I simply check if joined channels are in my-erc-channels with (-contains-p my-erc-channels (buffer-name)).

erc-track is not easily configurable

I spent at least half an hour trying to make erc-track do what I want. It’s much easier to disable it and do what you want in erc-insert-pre-hook. This hook allows you to get the message and channel name. I use notify package, that lets me send native system notifications regardless of my host operating system and implement my desired filtering using elisp.

(require 'notify)
(erc-track-disable)
(defun my-erc-message-on-hook (erc-message)
  (when (or (s-contains-p "ensime" (buffer-name))
            (s-contains-p "kozikow" erc-message))
    (notify (format "Irc %s" (buffer-name)) (format "%s" erc-message))))
(add-hook 'erc-insert-pre-hook 'my-erc-message-on-hook)

Auto re-connect

By default you need to guess when ERC lost connection and reconnect it. my-maybe-reconnect-irc works ok for me, but it sadly does not preserve IRC history.

Line length

Default line wrapping and timestamps does not work well with windowed IRC layout. I disabled the timestamp “on the right” and disabled the line wrapping.

(erc-fill-mode -1)
(setq erc-insert-timestamp-function 'erc-insert-timestamp-left)

Problem with connecting to channels on startup

For some reason, ERC did not auto connect to all channels, even when told so. It is quite simple to do it “manually” from elisp, so I went ahead and did it. E.g. for freenode it is:

(switch-to-buffer "irc.freenode.net:6667")
(erc-cmd-JOIN channel))

ERC non deterministic buffer names

ERC sometimes named buffers simply by “#emacs” and sometimes “#emacs@freenode”. Good enough solution is using regexps. To switch to channel by regex:

(defun my-buffer-match-regex (buf)
  (string-match buffer-regex (buffer-name buf)))
(defun my-switch-to-buffer-by-regex (buffer-regex)
  (switch-to-buffer (find-if 'my-buffer-match-regex (buffer-list))))
(my-switch-to-buffer-by-regex "^#emacs")

How I use it

  • At any time, when I feel like checking out IRC I call the my-open-irc via the keybinding. It either connects, reconnects or opens the existing ERC buffers in 2×2 layout.
  • When I feel like doing some work after chatting on IRC I call C-x r j 8.
  • If there are any notifications, my-open-irc cleans them all (I still have some bug when it leaves some notifications hanging in mode buffer).