Added package definition.
[lurk.git] / lurk.el
diff --git a/lurk.el b/lurk.el
index 9ef2fbb..8114597 100644 (file)
--- a/lurk.el
+++ b/lurk.el
@@ -1,13 +1,13 @@
 ;;; lurk.el --- Little Unibuffer iRc Klient -*- lexical-binding:t -*-
 
 ;;; lurk.el --- Little Unibuffer iRc Klient -*- lexical-binding:t -*-
 
-;; Copyright (C) 2021 Tim Vaughan
+;; Copyright (C) 2021--2024 plugd
 
 
-;; Author: Tim Vaughan <timv@ughan.xyz>
+;; Author: plugd <plugd@thelambdalab.xyz>
 ;; Created: 14 June 2021
 ;; Created: 14 June 2021
-;; Version: 1.0
-;; Keywords: network
+;; Version: 2.0
 ;; Homepage: http://thelambdalab.xyz/lurk
 ;; Homepage: http://thelambdalab.xyz/lurk
-;; Package-Requires: ((emacs "26"))
+;; Keywords: comm
+;; Package-Requires: ((emacs "26.1"))
 
 ;; This file is not part of GNU Emacs.
 
 
 ;; This file is not part of GNU Emacs.
 
 
 ;;; Commentary:
 
 
 ;;; Commentary:
 
+;; A very simple IRC client which uses only a single buffer.
+
 ;;; Code:
 
 (provide 'lurk)
 
 ;;; Code:
 
 (provide 'lurk)
 
+(require 'cl-lib)
+
 
 ;;; Customizations
 
 ;;; Customizations
-;;
 
 (defgroup lurk nil
 
 (defgroup lurk nil
-  "Little Unibuffer iRc Klient."
+  "Multinetwork Unibuffer iRc Klient"
   :group 'network)
 
   :group 'network)
 
-(defcustom lurk-nick "plugd"
-  "Default nick.")
+(defcustom lurk-default-nick "plugd"
+  "Default nick."
+  :type '(string))
 
 (defcustom lurk-default-quit-msg "Bye"
 
 (defcustom lurk-default-quit-msg "Bye"
-  "Default quit message when none supplied.")
+  "Default quit message when none supplied."
+  :type '(string))
 
 (defcustom lurk-networks
   '(("libera" "irc.libera.chat" 6697)
 
 (defcustom lurk-networks
   '(("libera" "irc.libera.chat" 6697)
-    ("freenode" "chat.freenode.net" 6697)
     ("tilde" "tilde.chat" 6697)
     ("tilde" "tilde.chat" 6697)
-    ("mbr" "mbrserver.com" 6667 :notls)
-    ("local" "localhost" 6697))
-  "IRC networks.")
-
-(defcustom lurk-allow-ipv6 nil
-  "Set to non-nil to allow use of IPv6.")
+    ("sdf" "irc.sdf.org" 6697))
+  "IRC networks."
+  :type '(alist :key-type string))
 
 (defcustom lurk-show-joins nil
   "Set to non-nil to be notified of joins, parts and quits.")
 
 (defcustom lurk-display-header t
 
 (defcustom lurk-show-joins nil
   "Set to non-nil to be notified of joins, parts and quits.")
 
 (defcustom lurk-display-header t
-  "If non-nil, use buffer header to display information on current host and channel.")
+  "If non-nil, use buffer header to display current host and channel."
+  :type '(boolean))
+
+(defcustom lurk-autoreply-table nil
+  "Table of autoreply messages.
+
+Each autoreply is a list of two elements: (matcher reply)
+
+Here matcher is a list:
+
+(network src cmd params ...)
+
+and reply is another list:
+
+ (cmd params ...)
+
+Each entry in the matcher list is a regular expression tested against the
+corresponding values in the incomming message.  Entries can be nil,
+in which case they match anything."
+  :type '(list (list) (list)))
+
 
 ;;; Faces
 ;;
 
 (defface lurk-text
   '((t :inherit default))
 
 ;;; Faces
 ;;
 
 (defface lurk-text
   '((t :inherit default))
-  "Face used for Lurk text.")
+  "Face used for lurk text.")
 
 (defface lurk-prompt
 
 (defface lurk-prompt
-  '((t :inherit org-priority))
+  '((t :inherit font-lock-keyword-face))
   "Face used for the prompt.")
 
 (defface lurk-context
   "Face used for the prompt.")
 
 (defface lurk-context
-  '((t :inherit org-tag))
+  '((t :inherit lurk-context))
   "Face used for the context name in the prompt.")
 
 (defface lurk-faded
   "Face used for the context name in the prompt.")
 
 (defface lurk-faded
-  '((t :inherit org-agenda-dimmed-todo-face))
-  "Face used for faded Lurk text.")
+  '((t :inherit shadow))
+  "Face used for faded lurk text.")
 
 (defface lurk-timestamp
 
 (defface lurk-timestamp
-  '((t :inherit org-agenda-dimmed-todo-face))
+  '((t :inherit shadow))
   "Face used for timestamps.")
 
 (defface lurk-error
   "Face used for timestamps.")
 
 (defface lurk-error
-  '((t :inherit font-lock-regexp-grouping-construct))
-  "Face used for Lurk error text.")
+  '((t :inherit error))
+  "Face used for lurk error text.")
 
 (defface lurk-notice
 
 (defface lurk-notice
-  '((t :inherit org-upcoming-deadline))
-  "Face used for Lurk notice text.")
+  '((t :inherit warning))
+  "Face used for lurk notice text.")
+
 
 ;;; Global variables
 ;;
 
 
 ;;; Global variables
 ;;
 
-(defvar lurk-version "Lurk v0.1"
+(defvar lurk-version "Lurk v0.0"
   "Value of this string is used in response to CTCP version queries.")
 
 (defvar lurk-notice-prefix "-!-")
   "Value of this string is used in response to CTCP version queries.")
 
 (defvar lurk-notice-prefix "-!-")
-
 (defvar lurk-error-prefix "!!!")
 (defvar lurk-error-prefix "!!!")
-
 (defvar lurk-prompt-string ">")
 
 (defvar lurk-debug nil
 (defvar lurk-prompt-string ">")
 
 (defvar lurk-debug nil
     nil))
 
 
     nil))
 
 
-;;; Network process
+;;; Network processes
 ;;
 
 ;;
 
-(defvar lurk-response "")
+(defvar lurk-connection-table nil
+  "An alist associating networks to connection information.
+This includes the process and the response string.")
 
 
-(defun lurk-filter (proc string)
-  (dolist (line (split-string (concat lurk-response string) "\n"))
-    (if (string-suffix-p "\r" line)
-        (lurk-eval-msg-string (string-trim line))
-      (setq lurk-response line))))
+(defun lurk-connection-process (network)
+  (elt (assoc network lurk-connection-table) 1))
 
 
-(defun lurk-sentinel (proc string)
-  (unless (equal "open" (string-trim string))
-    (lurk-display-error "Disconnected from server.")
-    (clrhash lurk-contexts)
-    (lurk-set-current-context nil)
-    (lurk-render-prompt)
-    (cancel-timer lurk-ping-timer)))
+(defun lurk-connection-nick (network)
+  (elt (assoc network lurk-connection-table) 2))
+
+(defun lurk-set-connection-nick (network nick)
+  (setf (elt (assoc network lurk-connection-table) 2) nick))
+
+(defun lurk-connection-response (network)
+  (elt (assoc network lurk-connection-table) 3))
+
+(defun lurk-set-connection-response (network string)
+  (setf (elt (assoc network lurk-connection-table) 3) string))
+
+(defun lurk-connection-new (network process nick)
+  (add-to-list 'lurk-connection-table
+               (list network process nick "")))
+
+(defun lurk-connection-remove (network)
+  (setq lurk-connection-table
+        (seq-remove (lambda (row) (equal (car row) network))
+                    lurk-connection-table)))
+
+(defun lurk-make-network-filter (network)
+  (lambda (_proc string)
+    (dolist (line (split-string (concat (lurk-connection-response network) string)
+                                "\n"))
+      (if (string-suffix-p "\r" line)
+          (lurk-eval-msg-string network (string-trim line))
+        (lurk-set-connection-response network line)))))
+
+(defun lurk-make-network-sentinel (network)
+  (lambda (_proc string)
+    (unless (equal "open" (string-trim string))
+      (lurk-display-error "Disconnected from network.")
+      (lurk-connection-remove network)
+      (lurk-remove-network-contexts network)
+      (lurk-highlight-current-context)
+      (lurk-render-prompt))))
 
 (defun lurk-start-process (network)
   (let* ((row (assoc network lurk-networks))
          (host (elt row 1))
          (port (elt row 2))
          (flags (seq-drop row 3)))
 
 (defun lurk-start-process (network)
   (let* ((row (assoc network lurk-networks))
          (host (elt row 1))
          (port (elt row 2))
          (flags (seq-drop row 3)))
-    (make-network-process :name "lurk"
+    (make-network-process :name (concat "lurk-" network)
                           :host host
                           :service port
                           :host host
                           :service port
-                          :family (if lurk-allow-ipv6 nil 'ipv4)
-                          :filter #'lurk-filter
-                          :sentinel #'lurk-sentinel
+                          :family nil
+                          :filter (lurk-make-network-filter network)
+                          :sentinel (lurk-make-network-sentinel network)
                           :nowait nil
                           :tls-parameters (if (memq :notls flags)
                                               nil
                           :nowait nil
                           :tls-parameters (if (memq :notls flags)
                                               nil
                                                    :hostname host)))
                           :buffer "*lurk*")))
 
                                                    :hostname host)))
                           :buffer "*lurk*")))
 
-(defvar lurk-ping-timer nil)
 (defvar lurk-ping-period 60)
 
 (defvar lurk-ping-period 60)
 
-(defun lurk-ping-function ()
-  (lurk-send-msg (lurk-msg nil nil "PING" (car (process-contact (get-process "lurk")))))
-  (setq lurk-ping-timer (run-with-timer lurk-ping-period nil #'lurk-ping-function)))
+;; IDEA: Have a single ping timer which pings all connected hosts
 
 (defun lurk-connect (network)
 
 (defun lurk-connect (network)
-  (if (get-process "lurk")
-      (lurk-display-error "Already connected.  Disconnect first.")
+  (if (assoc network lurk-connection-table)
+      (lurk-display-error "Already connected to this network")
     (if (not (assoc network lurk-networks))
         (lurk-display-error "Network '" network "' is unknown.")
     (if (not (assoc network lurk-networks))
         (lurk-display-error "Network '" network "' is unknown.")
-      (clrhash lurk-contexts)
-      (lurk-set-current-context nil)
-      (lurk-start-process network)
-      (lurk-send-msg (lurk-msg nil nil "USER" lurk-nick 0 "*" lurk-nick))
-      (lurk-send-msg (lurk-msg nil nil "NICK" lurk-nick))
-      (setq lurk-ping-timer (run-with-timer lurk-ping-period nil #'lurk-ping-function)))))
-
-(defun lurk-connected-p ()
-  (let ((proc (get-process "lurk")))
-    (and proc (eq (process-status proc) 'open))))
-
-(defun lurk-send-msg (msg)
+      (let ((proc (lurk-start-process network)))
+        (lurk-connection-new network proc lurk-default-nick))
+      (lurk-send-msg network (lurk-msg nil nil "USER" lurk-default-nick 0 "*" lurk-default-nick))
+      (lurk-send-msg network (lurk-msg nil nil "NICK" lurk-default-nick))
+      (lurk-add-context (list network))
+      (lurk-highlight-current-context)
+      (lurk-render-prompt))))
+
+(defun lurk-send-msg (network msg)
   (if lurk-debug
       (lurk-display-string nil nil (lurk-msg->string msg)))
   (if lurk-debug
       (lurk-display-string nil nil (lurk-msg->string msg)))
-  (let ((proc (get-process "lurk")))
+  (let ((proc (lurk-connection-process network)))
     (if (and proc (eq (process-status proc) 'open))
         (process-send-string proc (concat (lurk-msg->string msg) "\r\n"))
     (if (and proc (eq (process-status proc) 'open))
         (process-send-string proc (concat (lurk-msg->string msg) "\r\n"))
-      (lurk-display-error "No server connection established.")
-      (error "No server connection established"))))
+      (lurk-display-error "No network connection established"))))
 
 
 
 
-;;; Server messages
+;;; network messages
 ;;
 
 (defun lurk-msg (tags src cmd &rest params)
 ;;
 
 (defun lurk-msg (tags src cmd &rest params)
    (opt (group (+ not-newline))))
   "Regex used to parse IRC messages.
 Note that this regex is incomplete.  Noteably, we discard the non-nick
    (opt (group (+ not-newline))))
   "Regex used to parse IRC messages.
 Note that this regex is incomplete.  Noteably, we discard the non-nick
-portion of the source component of the message, as LURK doesn't use this.")
+portion of the source component of the message, as lurk doesn't use this.")
 
 (defun lurk-string->msg (string)
   (if (string-match lurk-msg-regex string)
 
 (defun lurk-string->msg (string)
   (if (string-match lurk-msg-regex string)
@@ -235,13 +277,13 @@ portion of the source component of the message, as LURK doesn't use this.")
              (params-str (match-string 4 string))
              (params
               (if params-str
              (params-str (match-string 4 string))
              (params
               (if params-str
-                  (let* ((idx (cl-search ":" params-str))
+                  (let* ((idx (seq-position params-str ?:))
                          (l (split-string (string-trim (substring params-str 0 idx))))
                          (r (if idx (list (substring params-str (+ 1 idx))) nil)))
                     (append l r))
                 nil)))
         (apply #'lurk-msg (append (list tags src cmd) params)))
                          (l (split-string (string-trim (substring params-str 0 idx))))
                          (r (if idx (list (substring params-str (+ 1 idx))) nil)))
                     (append l r))
                 nil)))
         (apply #'lurk-msg (append (list tags src cmd) params)))
-    (error "Failed to parse string " string)))
+    (error "Failed to parse string %s" string)))
 
 (defun lurk-msg->string (msg)
   (let ((tags (lurk-msg-tags msg))
 
 (defun lurk-msg->string (msg)
   (let ((tags (lurk-msg-tags msg))
@@ -263,79 +305,152 @@ portion of the source component of the message, as LURK doesn't use this.")
 ;;; Contexts
 ;;
 
 ;;; Contexts
 ;;
 
-(defvar lurk-current-context nil)
-(defvar lurk-contexts (make-hash-table :test #'equal))
-
-(defun lurk-add-context (name)
-  (puthash name nil lurk-contexts))
-
-(defun lurk-del-context (name)
-  (remhash name lurk-contexts))
-
-(defun lurk-get-context-users (name)
-  (gethash name lurk-contexts))
-
-(defun lurk-add-context-users (context users)
-  (puthash context
-           (append users
-                   (gethash context lurk-contexts))
-           lurk-contexts))
-
-(defun lurk-del-context-user (context user)
-  (puthash context
-           (remove user (gethash context lurk-contexts))
-           lurk-contexts))
-
-(defun lurk-del-user (user)
-  (dolist (context (lurk-get-context-list))
-    (lurk-del-context-user context user)))
-
-(defun lurk-rename-user (old-nick new-nick)
-  (dolist (context (lurk-get-context-list))
-    (lurk-del-context-user context old-nick)
-    (lurk-add-context-users context (list new-nick))))
-
-(defun lurk-get-context-type (name)
-  (cond
-   ((string-prefix-p "#" name) 'channel)
-   ((string-match-p (rx (or "." "localhost")) name) 'host)
-   (t 'nick)))
-
-(defun lurk-get-context-list ()
-  (let ((res nil))
-    (maphash (lambda (key val)
-               (cl-pushnew key res))
-             lurk-contexts)
-    res))
-
-(defun lurk-get-next-context (&optional prev)
-  (if lurk-current-context
-      (let* ((context-list (if prev
-                               (reverse (lurk-get-context-list))
-                             (lurk-get-context-list)))
-             (context-list* (member lurk-current-context context-list)))
-        (if (> (length context-list*) 1)
-            (cadr context-list*)
-          (car context-list)))
+;; A context is a list (network channel) identifying the network
+;; and channel.  The tail of the list contains the nicks of users
+;; present in the channel.
+;;
+;; Each network has a special context (network) used for messages
+;; to/from the network itself.
+
+(defvar lurk-contexts nil
+  "List of currently-available contexts.
+The head of this list is always the current context.")
+
+(defun lurk-current-context ()
+  "Return the current context."
+  (if lurk-contexts
+      (car lurk-contexts)
     nil))
 
     nil))
 
-(defun lurk-set-current-context (context)
-  (setq lurk-current-context context)
-  (lurk-highlight-context context)
-  (if lurk-zoomed
-      (lurk-zoom-in lurk-current-context)))
+(defun lurk-contexts-equal (c1 c2)
+  (if (lurk-network-context-p c1)
+      (and (lurk-network-context-p c2)
+           (equal (lurk-context-network c1)
+                  (lurk-context-network c2)))
+    (and (not (lurk-network-context-p c2))
+         (equal (seq-take c1 2)
+                (seq-take c2 2)))))
+
+(defun lurk-context-network (ctx)
+  (elt ctx 0))
+
+(defun lurk-context-channel (ctx)
+  (elt ctx 1))
+
+(defun lurk-network-context-p (ctx)
+  (not (cdr ctx)))
+
+(defun lurk-add-context (ctx)
+  (add-to-list 'lurk-contexts ctx))
+
+(defun lurk-remove-context (ctx)
+  (setq lurk-contexts
+        (seq-remove
+         (lambda (this-ctx)
+           (lurk-contexts-equal this-ctx ctx))
+         lurk-contexts)))
+
+(defun lurk-remove-network-contexts (network)
+  (setq lurk-contexts
+        (seq-remove (lambda (row) (equal (car row) network))
+                    lurk-contexts)))
+
+(defun lurk-context->string (ctx)
+  (concat
+   (if (lurk-network-context-p ctx)
+       ""
+     (concat (lurk-context-channel ctx) "@"))
+   (lurk-context-network ctx)))
+
+(defun lurk-string->context (string)
+  (if (not (string-prefix-p "#" string))
+      (lurk-get-context string)
+    (let* ((parts (string-split string "@"))
+           (channel (elt parts 0))
+           (network (elt parts 1)))
+      (lurk-get-context network channel))))
+
+(defun lurk-get-context (network &optional channel)
+  (if (and channel (string-prefix-p "#" channel))
+      (let ((test-ctx (list network channel)))
+        (seq-find (lambda (ctx)
+                    (equal (seq-take ctx 2) test-ctx))
+                  lurk-contexts))
+    (car (member (list network) lurk-contexts))))
+
+(defun lurk-cycle-contexts (&optional reverse)
+  (setq lurk-contexts
+        (if reverse
+            (let ((nminus1 (- (length lurk-contexts) 1)))
+              (cons
+               (elt lurk-contexts nminus1)
+               (seq-take lurk-contexts nminus1)))
+          (append (cdr lurk-contexts) (list (car lurk-contexts))))))
+
+(defun lurk-switch-to-context (ctx)
+  (setq lurk-contexts
+        (let* ((new-head (memq ctx lurk-contexts))
+               (new-tail (take (- (length lurk-contexts)
+                                  (length new-head))
+                               lurk-contexts)))
+          (append new-head new-tail))))
+
+
+;;; Context users
+;;
 
 
-(defun lurk-cycle-contexts (&optional rev)
-  (if lurk-current-context
-      (progn
-        (lurk-set-current-context (lurk-get-next-context rev))
-        (lurk-render-prompt))
-    (lurk-display-error "No channels joined.")))
+(defvar lurk-context-users nil
+  "Association list between channel contexts and users.")
+
+(defun lurk-get-context-users (ctx)
+  (cdr (assoc ctx lurk-context-users)))
+
+(defun lurk-set-context-users (ctx users)
+  (setq lurk-context-users
+        (cons (cons ctx users) (assoc-delete-all ctx lurk-context-users))))
+
+(defun lurk-add-context-users (ctx users)
+  (lurk-set-context-users
+   ctx
+   (cl-union users (lurk-get-context-users ctx))))
+
+(defun lurk-del-context-user (ctx user)
+  (lurk-set-context-users
+   ctx
+   (delete user (lurk-get-context-users ctx))))
+
+(defun lurk-del-all-context-users (ctx)
+  (lurk-set-context-users ctx nil))
+
+(defun lurk-del-network-user (network user)
+  (dolist (ctx lurk-contexts)
+    (if (and (equal (lurk-context-network ctx) network)
+             (not (lurk-network-context-p ctx)))
+        (lurk-del-context-user ctx user))))
+
+(defun lurk-del-all-network-users (network)
+  (dolist (ctx lurk-contexts)
+    (if (and (equal (lurk-context-network ctx) network)
+             (not (lurk-network-context-p ctx)))
+        (lurk-del-all-context-users ctx))))
+
+(defun lurk-rename-network-user (network old-nick new-nick)
+  (dolist (ctx lurk-contexts)
+    (when (and (equal (lurk-context-network ctx) network)
+               (member old-nick (lurk-get-context-users ctx)))
+      (lurk-del-context-user ctx old-nick)
+      (lurk-add-context-users ctx (list new-nick)))))
 
 
 ;;; Buffer
 ;;
 
 
 
 ;;; Buffer
 ;;
 
+(defvar lurk-prompt-marker nil
+  "Marker for prompt position in lurk buffer.")
+
+(defvar lurk-input-marker nil
+  "Marker for prompt position in lurk buffer.")
+
 (defun lurk-render-prompt ()
   (with-current-buffer "*lurk*"
     (let ((update-point (= lurk-input-marker (point)))
 (defun lurk-render-prompt ()
   (with-current-buffer "*lurk*"
     (let ((update-point (= lurk-input-marker (point)))
@@ -350,13 +465,16 @@ portion of the source component of the message, as LURK doesn't use this.")
           (delete-region lurk-prompt-marker lurk-input-marker)
           (goto-char lurk-prompt-marker)
           (insert
           (delete-region lurk-prompt-marker lurk-input-marker)
           (goto-char lurk-prompt-marker)
           (insert
-           (propertize (if lurk-current-context
-                           lurk-current-context
-                         "")
+           (propertize (let ((ctx (lurk-current-context)))
+                         (if ctx
+                             (lurk-context->string ctx)
+                           ""))
                        'face 'lurk-context
                        'read-only t)
                        'face 'lurk-context
                        'read-only t)
-           (propertize (concat lurk-prompt-string " ")
+           (propertize lurk-prompt-string
                        'face 'lurk-prompt
                        'face 'lurk-prompt
+                       'read-only t)
+           (propertize " " ; Need this to be separate to mark it as rear-nonsticky
                        'read-only t
                        'rear-nonsticky t)))
         (set-marker-insertion-type lurk-input-marker nil))
                        'read-only t
                        'rear-nonsticky t)))
         (set-marker-insertion-type lurk-input-marker nil))
@@ -366,32 +484,25 @@ portion of the source component of the message, as LURK doesn't use this.")
         (if (car v)
             (set-window-point (cadr v) lurk-input-marker))))))
   
         (if (car v)
             (set-window-point (cadr v) lurk-input-marker))))))
   
-(defvar lurk-prompt-marker nil
-  "Marker for prompt position in LURK buffer.")
-
-(defvar lurk-input-marker nil
-  "Marker for prompt position in LURK buffer.")
-
 (defun lurk-setup-header ()
   (with-current-buffer "*lurk*"
     (setq-local header-line-format
                 '((:eval
 (defun lurk-setup-header ()
   (with-current-buffer "*lurk*"
     (setq-local header-line-format
                 '((:eval
-                   (let ((proc (get-process "lurk")))
-                     (if proc
-                         (concat
-                          "Host: " (car (process-contact proc))
-                          ", Context: "
-                          (if lurk-current-context
+                   (let* ((ctx (lurk-current-context)))
+                     (if ctx
+                         (let ((network (lurk-context-network ctx)))
+                           (concat
+                            "Network: " network ", "
+                            (if (lurk-network-context-p ctx)
+                                "network"
                               (concat
                               (concat
-                               lurk-current-context
+                               "Channel: "
+                               (lurk-context-channel ctx)
                                " ("
                                (number-to-string
                                " ("
                                (number-to-string
-                                (length (lurk-get-context-users lurk-current-context)))
-                               " users)")
-                            "Server"))
-                       "No connection")))
-                  (:eval
-                   (if lurk-zoomed " [ZOOMED]" ""))))))
+                                (length (lurk-get-context-users ctx)))
+                               ")"))))
+                       "No connection")))))))
 
 (defun lurk-setup-buffer ()
   (with-current-buffer (get-buffer-create "*lurk*")
 
 (defun lurk-setup-buffer ()
   (with-current-buffer (get-buffer-create "*lurk*")
@@ -404,10 +515,17 @@ portion of the source component of the message, as LURK doesn't use this.")
         (set-marker lurk-input-marker (point-max))
       (setq lurk-input-marker (point-max-marker)))
     (goto-char (point-max))
         (set-marker lurk-input-marker (point-max))
       (setq lurk-input-marker (point-max-marker)))
     (goto-char (point-max))
+    (lurk-highlight-current-context)
     (lurk-render-prompt)
     (if lurk-display-header
         (lurk-setup-header))))
 
     (lurk-render-prompt)
     (if lurk-display-header
         (lurk-setup-header))))
 
+(defun lurk-clear-buffer ()
+  "Completely erase all non-prompt and non-input text from lurk buffer."
+  (with-current-buffer "*lurk*"
+    (let ((inhibit-read-only t))
+      (delete-region (point-min) lurk-prompt-marker))))
+
 
 ;;; Output formatting and highlighting
 ;;
 
 ;;; Output formatting and highlighting
 ;;
@@ -430,7 +548,7 @@ portion of the source component of the message, as LURK doesn't use this.")
   "List of seen contexts and associated face lists.")
 
 (defun lurk-get-context-facelist (context)
   "List of seen contexts and associated face lists.")
 
 (defun lurk-get-context-facelist (context)
-  (let ((facelist (gethash context lurk-context-facelists)))
+  (let* ((facelist (gethash context lurk-context-facelists)))
     (unless facelist
       (setq facelist (list 'lurk-text))
       (puthash context facelist lurk-context-facelists))
     (unless facelist
       (setq facelist (list 'lurk-text))
       (puthash context facelist lurk-context-facelists))
@@ -446,14 +564,17 @@ portion of the source component of the message, as LURK doesn't use this.")
       (buffer-string))))
 
 (defun lurk-display-string (context prefix &rest strings)
       (buffer-string))))
 
 (defun lurk-display-string (context prefix &rest strings)
-  (with-current-buffer (get-buffer-create "*lurk*")
+  (with-current-buffer "*lurk*"
     (save-excursion
       (goto-char lurk-prompt-marker)
       (let* ((inhibit-read-only t)
              (old-pos (marker-position lurk-prompt-marker))
              (padded-timestamp (concat (format-time-string "%H:%M ")))
              (padded-prefix (if prefix (concat prefix " ") ""))
     (save-excursion
       (goto-char lurk-prompt-marker)
       (let* ((inhibit-read-only t)
              (old-pos (marker-position lurk-prompt-marker))
              (padded-timestamp (concat (format-time-string "%H:%M ")))
              (padded-prefix (if prefix (concat prefix " ") ""))
-             (context-atom (if context (intern context) nil)))
+             (context-atom (if context
+                               (intern (lurk-context->string context))
+                             nil))
+             (context-face (lurk-get-context-facelist context)))
         (insert-before-markers
          (lurk--fill-strings
           80
         (insert-before-markers
          (lurk--fill-strings
           80
@@ -470,36 +591,54 @@ portion of the source component of the message, as LURK doesn't use this.")
                       'invisible context-atom)
           (lurk-add-formatting
            (propertize (concat (apply #'lurk-buttonify-urls strings) "\n")
                       'invisible context-atom)
           (lurk-add-formatting
            (propertize (concat (apply #'lurk-buttonify-urls strings) "\n")
-                       'face (lurk-get-context-facelist context)
+                       'face context-face
                        'read-only t
                        'context context
                        'read-only t
                        'context context
-                       'invisible context-atom))))))))
+                       'invisible context-atom)))))))
+  (lurk-scroll-windows-to-last-line))
+
+(defun lurk-click-context (button)
+  (lurk-switch-to-context (button-get button 'context))
+  (lurk-highlight-current-context)
+  (lurk-render-prompt)
+  (if lurk-zoomed
+      (lurk-zoom-in (lurk-current-context))))
+
+(defun lurk-make-context-button (context &optional string)
+  (with-temp-buffer
+    (let ((label (or string (lurk-context->string context))))
+      (insert-text-button label
+                          'action #'lurk-click-context
+                          'context context
+                          'follow-link t
+                          'help-echo "Switch context"))
+    (buffer-string)))
 
 
-(defun lurk-display-message (from to text)
-  (let ((context (if (eq 'channel (lurk-get-context-type to))
-                     to
-                   (if (equal to lurk-nick) from to))))
+(defun lurk-display-message (network from to text)
+  (let ((context (if (string-prefix-p "#" to)
+                     (lurk-get-context network to)
+                   (lurk-get-context network))))
     (lurk-display-string
      context
      (propertize
     (lurk-display-string
      context
      (propertize
-      (pcase (lurk-get-context-type to)
-        ('channel (concat to " <" from ">"))
-        ('nick (concat "[" from " -> " to "]"))
-        (_
-         (error "Unsupported context type")))
+      (if (lurk-network-context-p context)
+          (concat "[" from "->" to "]")
+        (concat
+         (lurk-make-context-button context)
+         " <" from ">"))
       'face (lurk-get-context-facelist context))
      text)))
 
       'face (lurk-get-context-facelist context))
      text)))
 
-(defun lurk-display-action (from to action-text)
-  (let ((context (if (eq 'channel (lurk-get-context-type to))
-                     to
-                   (if (equal to lurk-nick) from to))))
+(defun lurk-display-action (network from to action-text)
+  (let ((context (if (string-prefix-p "#" to)
+                     (lurk-get-context network to)
+                   (lurk-get-context network))))
     (lurk-display-string
      context
      (propertize
     (lurk-display-string
      context
      (propertize
-      (concat context " * " from)
+      (concat (lurk-context->string context) " *")
       'face (lurk-get-context-facelist context))
       'face (lurk-get-context-facelist context))
-     action-text)))
+     from " " action-text)))
 
 (defun lurk-display-notice (context &rest notices)
   (lurk-display-string
 
 (defun lurk-display-notice (context &rest notices)
   (lurk-display-string
@@ -513,13 +652,14 @@ portion of the source component of the message, as LURK doesn't use this.")
    (propertize lurk-error-prefix 'face 'lurk-error)
    (apply #'concat messages)))
 
    (propertize lurk-error-prefix 'face 'lurk-error)
    (apply #'concat messages)))
 
-(defun lurk-highlight-context (context)
-  (maphash
-   (lambda (this-context facelist)
-     (if (equal this-context context)
-         (setcar facelist 'lurk-text)
-       (setcar facelist 'lurk-faded)))
-   lurk-context-facelists)
+(defun lurk-highlight-current-context ()
+  (with-current-buffer "*lurk*"
+    (maphash
+     (lambda (this-context facelist)
+       (if (equal this-context (lurk-current-context))
+           (setcar facelist 'lurk-text)
+         (setcar facelist 'lurk-faded)))
+     lurk-context-facelists))
   (force-window-update "*lurk*"))
 
 (defun lurk-zoom-in (context)
   (force-window-update "*lurk*"))
 
 (defun lurk-zoom-in (context)
@@ -527,21 +667,40 @@ portion of the source component of the message, as LURK doesn't use this.")
     (maphash
      (lambda (this-context _)
        (when this-context
     (maphash
      (lambda (this-context _)
        (when this-context
-         (let ((this-context-atom (intern this-context)))
+         (let ((this-context-atom
+                (intern (lurk-context->string this-context))))
            (if (equal this-context context)
                (remove-from-invisibility-spec this-context-atom)
              (add-to-invisibility-spec this-context-atom)))))
      lurk-context-facelists)
            (if (equal this-context context)
                (remove-from-invisibility-spec this-context-atom)
              (add-to-invisibility-spec this-context-atom)))))
      lurk-context-facelists)
-    (force-window-update "*lurk*")))
+    (force-window-update "*lurk*"))
+  (lurk-scroll-windows-to-last-line))
 
 (defun lurk-zoom-out ()
   (with-current-buffer "*lurk*"
     (maphash
      (lambda (this-context _)
 
 (defun lurk-zoom-out ()
   (with-current-buffer "*lurk*"
     (maphash
      (lambda (this-context _)
-       (let ((this-context-atom (if this-context (intern this-context) nil)))
+       (let ((this-context-atom
+              (if this-context
+                  (intern (lurk-context->string this-context))
+                nil)))
          (remove-from-invisibility-spec this-context-atom)))
      lurk-context-facelists)
          (remove-from-invisibility-spec this-context-atom)))
      lurk-context-facelists)
-    (force-window-update "*lurk*")))
+    (force-window-update "*lurk*"))
+  (lurk-scroll-windows-to-last-line))
+
+(defun lurk--start-of-final-line ()
+  (with-current-buffer "*lurk*"
+    (save-excursion
+      (goto-char (point-max))
+      (line-beginning-position))))
+
+(defun lurk-scroll-windows-to-last-line ()
+  (with-current-buffer "*lurk*"
+    (dolist (window (get-buffer-window-list))
+      (if (>= (window-point window) (lurk--start-of-final-line))
+          (with-selected-window window
+            (recenter -1))))))
 
 (defconst lurk-url-regex
   (rx (:
 
 (defconst lurk-url-regex
   (rx (:
@@ -552,8 +711,8 @@ portion of the source component of the message, as LURK doesn't use this.")
        (opt (group (: ":" (+ digit))))
        (opt (group (: "/"
                       (opt
        (opt (group (: ":" (+ digit))))
        (opt (group (: "/"
                       (opt
-                       (* (any alnum "-/.,#:%=&_?~@"))
-                       (any alnum "-/#:%=&_~@")))))))
+                       (* (any alnum "-/.,#:%=&_?~@+"))
+                       (any alnum "-/#:%=&_~@+")))))))
   "Imperfect regex used to find URLs in plain text.")
 
 (defun lurk-click-url (button)
   "Imperfect regex used to find URLs in plain text.")
 
 (defun lurk-click-url (button)
@@ -584,7 +743,9 @@ portion of the source component of the message, as LURK doesn't use this.")
           (underline nil)
           (strikethrough nil)
           (prev-point (point)))
           (underline nil)
           (strikethrough nil)
           (prev-point (point)))
-      (while (re-search-forward (rx (any "\x02\x1D\x1F\x1E")) nil t)
+      (while (re-search-forward (rx (or (any "\x02\x1D\x1F\x1E\x0F")
+                                        (: "\x03" (* digit) (opt "," (* digit)))))
+                                nil t)
         (let ((beg (+ (match-beginning 0) 1)))
           (if bold
               (add-face-text-property prev-point beg '(:weight bold)))
         (let ((beg (+ (match-beginning 0) 1)))
           (if bold
               (add-face-text-property prev-point beg '(:weight bold)))
@@ -598,7 +759,13 @@ portion of the source component of the message, as LURK doesn't use this.")
             ("\x02" (setq bold (not bold)))
             ("\x1D" (setq italics (not italics)))
             ("\x1F" (setq underline (not underline)))
             ("\x02" (setq bold (not bold)))
             ("\x1D" (setq italics (not italics)))
             ("\x1F" (setq underline (not underline)))
-            ("\x1E" (setq strikethrough (not strikethrough))))
+            ("\x1E" (setq strikethrough (not strikethrough)))
+            ("\x0F" ; Reset
+             (setq bold nil)
+             (setq italics nil)
+             (setq underline nil)
+             (setq strikethrough nil))
+            (_))
           (delete-region (match-beginning 0) (match-end 0))
           (setq prev-point (point)))))
     (buffer-string)))
           (delete-region (match-beginning 0) (match-end 0))
           (setq prev-point (point)))))
     (buffer-string)))
@@ -607,119 +774,143 @@ portion of the source component of the message, as LURK doesn't use this.")
 ;;; Message evaluation
 ;;
 
 ;;; Message evaluation
 ;;
 
-(defun lurk-eval-msg-string (string)
+(defun lurk-eval-msg-string (network string)
   (if lurk-debug
       (lurk-display-string nil nil string))
   (let* ((msg (lurk-string->msg string)))
   (if lurk-debug
       (lurk-display-string nil nil string))
   (let* ((msg (lurk-string->msg string)))
+    (lurk-process-autoreplies network msg)
     (pcase (lurk-msg-cmd msg)
       ("PING"
     (pcase (lurk-msg-cmd msg)
       ("PING"
-       (lurk-send-msg
+       (lurk-send-msg network
         (lurk-msg nil nil "PONG" (lurk-msg-params msg))))
 
       ("PONG")
 
         (lurk-msg nil nil "PONG" (lurk-msg-params msg))))
 
       ("PONG")
 
-      ("001"
+      ("001" ; RPL_WELCOME
        (let* ((params (lurk-msg-params msg))
               (nick (elt params 0))
               (text (string-join (seq-drop params 1) " ")))
        (let* ((params (lurk-msg-params msg))
               (nick (elt params 0))
               (text (string-join (seq-drop params 1) " ")))
-         (setq lurk-nick nick)
-         (lurk-display-notice nil text)))
+         (lurk-set-connection-nick network nick)
+         (lurk-display-notice (lurk-get-context network) text))
+       (let* ((row (assoc network lurk-networks))
+              (channels (if (memq :channels row)
+                            (cdr (memq :channels row))
+                          nil)))
+         (dolist (channel channels)
+           (lurk-command-join (list channel)))))
 
       ("353" ; NAMEREPLY
        (let* ((params (lurk-msg-params msg))
               (channel (elt params 2))
 
       ("353" ; NAMEREPLY
        (let* ((params (lurk-msg-params msg))
               (channel (elt params 2))
-              (names (split-string (elt params 3))))
-         (lurk-add-context-users channel names)))
+              (names (split-string (elt params 3)))
+              (ctx (lurk-get-context network channel)))
+         (if ctx
+             (lurk-add-context-users ctx names)
+           (lurk-display-notice ctx "Users in " channel
+                                ": " (string-join names " ")))))
 
       ("366" ; ENDOFNAMES
        (let* ((params (lurk-msg-params msg))
 
       ("366" ; ENDOFNAMES
        (let* ((params (lurk-msg-params msg))
-              (channel (elt params 1)))
-         (lurk-display-notice
-          channel
-          (lurk--as-string (length (lurk-get-context-users channel)))
-          " users in " channel)))
-
-      ("331"
+              (channel (elt params 1))
+              (ctx (lurk-get-context network channel)))
+         (if ctx
+             (lurk-display-notice
+              ctx
+              (lurk--as-string (length (lurk-get-context-users ctx)))
+              " users in " channel)
+           (lurk-display-notice (lurk-get-context network)
+                                "End of " channel " names list."))))
+
+      ("331" ; RPL_NOTOPIC
        (let* ((params (lurk-msg-params msg))
        (let* ((params (lurk-msg-params msg))
-              (channel (elt params 1)))
-         (lurk-display-notice
-          channel
-          "No topic set.")))
+              (channel (elt params 1))
+              (ctx (lurk-get-context network channel)))
+         (lurk-display-notice ctx "No topic set.")))
 
 
-      ("332"
+      ("332" ; RPL_TOPIC
        (let* ((params (lurk-msg-params msg))
               (channel (elt params 1))
        (let* ((params (lurk-msg-params msg))
               (channel (elt params 1))
-              (topic (elt params 2)))
-         (lurk-display-notice channel "Topic: " topic)))
-
-      ("333") ; Avoid displaying these
+              (topic (elt params 2))
+              (ctx (lurk-get-context network channel)))
+         (lurk-display-notice ctx "Topic: " topic)))
 
       ((rx (= 3 (any digit)))
 
       ((rx (= 3 (any digit)))
-       (lurk-display-notice nil (mapconcat 'identity (cdr (lurk-msg-params msg)) " ")))
+       (lurk-display-notice (lurk-get-context network)
+                            (mapconcat 'identity (cdr (lurk-msg-params msg)) " ")))
 
       ((and "JOIN"
 
       ((and "JOIN"
-            (guard (equal lurk-nick (lurk-msg-src msg))))
-       (let ((channel (car (lurk-msg-params msg))))
-         (lurk-add-context channel)
-         (lurk-set-current-context channel)
-         (lurk-display-notice channel "Joining channel " channel)
+            (guard (equal (lurk-connection-nick network)
+                          (lurk-msg-src msg))))
+       (let* ((channel (car (lurk-msg-params msg)))
+              (context (list network channel)))
+         (lurk-add-context context)
+         (lurk-del-all-context-users context)
+         (lurk-display-notice (lurk-current-context)
+                              "Joining channel " channel " on " network)
+         (lurk-highlight-current-context)
          (lurk-render-prompt)))
 
       ("JOIN"
          (lurk-render-prompt)))
 
       ("JOIN"
-       (let ((channel (car (lurk-msg-params msg)))
-             (nick (lurk-msg-src msg)))
-         (lurk-add-context-users channel (list nick))
+       (let* ((channel (car (lurk-msg-params msg)))
+              (nick (lurk-msg-src msg))
+              (ctx (lurk-get-context network channel)))
+         (lurk-add-context-users ctx (list nick))
          (if lurk-show-joins
          (if lurk-show-joins
-             (lurk-display-notice channel nick " joined channel " channel))))
+             (lurk-display-notice ctx nick " joined channel " channel
+                                  " on " network))))
 
       ((and "PART"
 
       ((and "PART"
-            (guard (equal lurk-nick (lurk-msg-src msg))))
-       (let ((channel (car (lurk-msg-params msg))))
-         (lurk-display-notice channel "Left channel " channel)
-         (lurk-del-context channel)
-         (if (equal channel lurk-current-context)
-             (lurk-set-current-context (lurk-get-next-context)))
+            (guard (equal (lurk-connection-nick network)
+                          (lurk-msg-src msg))))
+       (let* ((channel (car (lurk-msg-params msg)))
+              (context (list network channel)))
+         (lurk-display-notice context "Left channel " channel)
+         (lurk-remove-context context)
+         (lurk-del-all-context-users context)
+         (lurk-highlight-current-context)
          (lurk-render-prompt)))
 
       ("PART"
          (lurk-render-prompt)))
 
       ("PART"
-       (let ((channel (car (lurk-msg-params msg)))
-             (nick (lurk-msg-src msg)))
-         (lurk-del-context-user channel nick)
+       (let* ((channel (car (lurk-msg-params msg)))
+              (nick (lurk-msg-src msg))
+              (ctx (lurk-get-context network channel)))
+         (lurk-del-context-user ctx nick)
          (if lurk-show-joins
          (if lurk-show-joins
-             (lurk-display-notice channel nick " left channel " channel))))
-
-      ((and "KICK")
-       (let ((kicker-nick (lurk-msg-src msg))
-             (channel (car (lurk-msg-params msg)))
-             (nick (cadr (lurk-msg-params msg)))
-             (reason (caddr (lurk-msg-params msg))))
-         (if (equal nick lurk-nick)
-             (progn
-               (lurk-display-notice channel kicker-nick " kicked you from " channel ": " reason)
-               (lurk-del-context channel)
-               (if (equal channel lurk-current-context)
-                   (lurk-set-current-context (lurk-get-next-context)))
-               (lurk-render-prompt))
-           (lurk-del-context-user channel nick)
-           (lurk-display-notice channel kicker-nick " kicked " nick " from " channel ": " reason))))
-
-      ("QUIT"
-       (let ((nick (lurk-msg-src msg))
-             (reason (mapconcat 'identity (lurk-msg-params msg) " ")))
-         (lurk-del-user nick)
-         (if lurk-show-joins
-             (lurk-display-notice nil nick " quit: " reason))))
+             (lurk-display-notice ctx nick " left channel " channel
+                                  " on " network))))
 
       ((and "NICK"
 
       ((and "NICK"
-            (guard (equal lurk-nick (lurk-msg-src msg))))
-       (setq lurk-nick (car (lurk-msg-params msg)))
-       (lurk-display-notice nil "Set nick to " lurk-nick))
+            (guard (equal (lurk-connection-nick network)
+                          (lurk-msg-src msg))))
+       (let ((new-nick (car (lurk-msg-params msg)))
+             (old-nick (lurk-connection-nick network)))
+         (lurk-set-connection-nick network new-nick)
+         (lurk-rename-network-user network old-nick new-nick)
+         (lurk-display-notice (lurk-get-context network)
+                              "Nick set to " new-nick " on " network)))
 
       ("NICK"
        (let ((old-nick (lurk-msg-src msg))
              (new-nick (car (lurk-msg-params msg))))
 
       ("NICK"
        (let ((old-nick (lurk-msg-src msg))
              (new-nick (car (lurk-msg-params msg))))
-         (lurk-display-notice nil old-nick " is now known as " new-nick)
-         (lurk-rename-user old-nick new-nick)))
+         (lurk-display-notice (lurk-get-context network)
+                              old-nick " is now known as " new-nick
+                              " on " network)
+         (lurk-rename-network-user network old-nick new-nick)))
+
+      ("TOPIC"
+       (let ((channel (car (lurk-msg-params msg)))
+             (nick (lurk-msg-src msg))
+             (topic (cadr (lurk-msg-params msg))))
+         (lurk-display-notice (lurk-get-context network channel)
+                              nick " set the topic: " topic)))
+
+      ("QUIT"
+       (let ((nick (lurk-msg-src msg))
+             (reason (mapconcat 'identity (lurk-msg-params msg) " ")))
+         (lurk-del-network-user network nick)
+         (if lurk-show-joins
+             (lurk-display-notice (lurk-get-context network)
+                                  nick " on " network " has quit: " reason))))
 
       ("NOTICE"
        (let ((nick (lurk-msg-src msg))
 
       ("NOTICE"
        (let ((nick (lurk-msg-src msg))
@@ -729,9 +920,10 @@ portion of the source component of the message, as LURK doesn't use this.")
            ((rx (: "\01VERSION "
                    (let version (* (not "\01")))
                    "\01"))
            ((rx (: "\01VERSION "
                    (let version (* (not "\01")))
                    "\01"))
-            (lurk-display-notice nil "CTCP version reply from " nick ": " version))
+            (lurk-display-notice (lurk-get-context network)
+                                 "CTCP version reply from " nick ": " version))
            (_
            (_
-            (lurk-display-notice nil text)))))
+            (lurk-display-notice (lurk-get-context network channel) text)))))
 
       ("PRIVMSG"
        (let* ((from (lurk-msg-src msg))
 
       ("PRIVMSG"
        (let* ((from (lurk-msg-src msg))
@@ -741,26 +933,299 @@ portion of the source component of the message, as LURK doesn't use this.")
          (pcase text
            ("\01VERSION\01"
             (let ((version-string (concat lurk-version " - running on GNU Emacs " emacs-version)))
          (pcase text
            ("\01VERSION\01"
             (let ((version-string (concat lurk-version " - running on GNU Emacs " emacs-version)))
-              (lurk-send-msg (lurk-msg nil nil "NOTICE"
+              (lurk-send-msg network
+                             (lurk-msg nil nil "NOTICE"
                                        (list from (concat "\01VERSION "
                                                           version-string
                                                           "\01")))))
                                        (list from (concat "\01VERSION "
                                                           version-string
                                                           "\01")))))
-            (lurk-display-notice nil "CTCP version request received from " from))
+            (lurk-display-notice (lurk-get-context network)
+                                 "CTCP version request received from "
+                                 from " on " network))
 
            ((rx (let ping (: "\01PING " (* (not "\01")) "\01")))
 
            ((rx (let ping (: "\01PING " (* (not "\01")) "\01")))
-            (lurk-send-msg (lurk-msg nil nil "NOTICE" (list from ping)))
-            (lurk-display-notice from "CTCP ping received from " from))
+            (lurk-send-msg network (lurk-msg nil nil "NOTICE" (list from ping)))
+            (lurk-display-notice (lurk-get-context network)
+                                 "CTCP ping received from " from " on " network))
 
            ("\01USERINFO\01"
 
            ("\01USERINFO\01"
-            (lurk-display-notice from "CTCP userinfo request from " from " (no response sent)"))
+            (lurk-display-notice (lurk-get-context network)
+                                 "CTCP userinfo request from " from
+                                 " on " network " (no response sent)"))
+
+           ("\01CLIENTINFO\01"
+            (lurk-display-notice (lurk-get-context network)
+                                 "CTCP clientinfo request from " from
+                                 " on " network " (no response sent)"))
 
            ((rx (: "\01ACTION " (let action-text (* (not "\01"))) "\01"))
 
            ((rx (: "\01ACTION " (let action-text (* (not "\01"))) "\01"))
-            (lurk-display-action from to action-text))
+            (lurk-display-action network from to action-text))
 
            (_
 
            (_
-            (lurk-display-message from to text)))))
+            (lurk-display-message network from to text)))))
+
       (_
       (_
-       (lurk-display-notice nil (lurk-msg->string msg))))))
+       (lurk-display-notice (lurk-get-context network)
+                            (lurk-msg->string msg))))))
+
+
+;;; User-defined responses
+;;
+
+(defun lurk--lists-equal (l1 l2)
+    (if (and l1 l2)
+        (if (or (not (and (car l1) (car l2)))
+                (string-match (car l1) (car l2)))
+            (lurk--lists-equal (cdr l1) (cdr l2))
+          nil)
+      t))
+
+(defun lurk-process-autoreply (network msg autoreply)
+  (let ((matcher (car autoreply))
+        (reply (cadr autoreply)))
+    (let ((target-network (car matcher)))
+      (when (and (or (not target-network)
+                     (and (equal network target-network)))
+                 (lurk--lists-equal (cdr matcher)
+                                    (append (list (lurk-msg-src msg)
+                                                  (lurk-msg-cmd msg))
+                                            (lurk-msg-params msg))))
+        (lurk-send-msg network
+         (lurk-msg nil nil (car reply) (cdr reply)))))))
+
+(defun lurk-process-autoreplies (network msg)
+  (mapc
+   (lambda (autoreply)
+     (lurk-process-autoreply network msg autoreply))
+   lurk-autoreply-table))
+
+
+;;; Commands
+;;
+
+(defvar lurk-command-table
+  '(("DEBUG" "Toggle debug mode on/off." lurk-command-debug lurk-boolean-completions)
+    ("HEADER" "Toggle display of header." lurk-command-header lurk-boolean-completions)
+    ("SHOWJOINS" "Toggles display of joins/parts." lurk-command-showjoins lurk-boolean-completions)
+    ("NETWORKS" "List known IRC networks." lurk-command-networks)
+    ("CONNECT" "Connect to an IRC network." lurk-command-connect lurk-network-completions)
+    ("QUIT" "Disconnect from current network." lurk-command-quit)
+    ("JOIN" "Join one or more channels." lurk-command-join)
+    ("PART" "Leave channel." lurk-command-part lurk-channel-completions)
+    ("SWITCHCONTEXT" "Switch current context" lurk-command-switch-context lurk-context-completions)
+    ("NICK" "Change nick." lurk-command-nick)
+    ("LIST" "Display details of one or more channels." lurk-command-list)
+    ("TOPIC" "Set/query topic for current channel." lurk-command-topic)
+    ("USERS" "List nicks of users in current channel." lurk-command-users)
+    ("MSG" "Send private message to user." lurk-command-msg lurk-nick-completions)
+    ("ME" "Display action." lurk-command-me)
+    ("VERSION" "Request version of another user's client via CTCP." lurk-command-version lurk-nick-completions)
+    ("CLEAR" "Clear buffer text." lurk-command-clear lurk-context-completions)
+    ("HELP" "Display help on client commands." lurk-command-help lurk-help-completions))
+  "Table of commands explicitly supported by lurk.")
+
+(defun lurk-boolean-completions ()
+  '("on" "off"))
+
+(defun lurk-network-completions ()
+  (mapcar (lambda (row) (car row)) lurk-networks))
+
+(defun lurk-help-completions ()
+  (mapcar (lambda (row) (car row)) lurk-command-table))
+
+(defun lurk-channel-completions ()
+  (mapcar (lambda (ctx)
+            (lurk-context->string ctx))
+          (seq-filter (lambda (ctx)
+                        (not (lurk-network-context-p ctx)))
+                      lurk-contexts)))
+
+(defun lurk-context-completions ()
+  (mapcar (lambda (ctx) (lurk-context->string ctx)) lurk-contexts))
+
+(defun lurk-command-help (params)
+  (if params
+      (let* ((cmd-str (upcase (car params)))
+             (row (assoc cmd-str lurk-command-table #'equal)))
+        (if row
+            (progn
+              (lurk-display-notice nil "Help for \x02" cmd-str "\x02:")
+              (lurk-display-notice nil "  " (elt row 1)))
+          (lurk-display-notice nil "No such (client-interpreted) command.")))
+    (lurk-display-notice nil "Client-interpreted commands:")
+    (dolist (row lurk-command-table)
+      (lurk-display-notice nil "  \x02" (elt row 0) "\x02: " (elt row 1)))
+    (lurk-display-notice nil "Use /HELP COMMAND to display information about a specific command.")))
+
+(defun lurk-command-debug (params)
+  (setq lurk-debug 
+        (if params
+            (if (equal (upcase (car params)) "ON")
+                t
+              nil)
+          (not lurk-debug)))
+  (lurk-display-notice nil "Debug mode now " (if lurk-debug "on" "off") "."))
+
+(defun lurk-command-header (params)
+  (if
+      (if params
+          (equal (upcase (car params)) "ON")
+        (not header-line-format))
+      (progn
+        (lurk-setup-header)
+        (lurk-display-notice nil "Header enabled."))
+    (setq-local header-line-format nil)
+    (lurk-display-notice nil "Header disabled.")))
+
+(defun lurk-command-showjoins (params)
+  (setq lurk-show-joins 
+        (if params
+            (if (equal (upcase (car params)) "ON")
+                t
+              nil)
+          (not lurk-show-joins)))
+  (lurk-display-notice nil "Joins/parts will now be "
+                       (if lurk-show-joins "shown" "hidden") "."))
+
+(defun lurk-command-connect (params)
+  (if params
+      (let ((network (car params)))
+        (lurk-display-notice nil "Attempting to connect to " network "...")
+        (lurk-connect network))
+    (lurk-display-notice nil "Usage: /connect <network>")))
+
+(defun lurk-command-networks (_params)
+  (lurk-display-notice nil "Currently-known networks:")
+  (dolist (row lurk-networks)
+    (seq-let (network network port &rest _others) row
+      (lurk-display-notice nil "\t" network
+                           " [" network
+                           " " (number-to-string port) "]")))
+  (lurk-display-notice nil "(Modify the `lurk-networks' variable to add more.)"))
+
+(defun lurk-command-quit (params)
+  (let ((ctx (lurk-current-context)))
+    (if (not ctx)
+        (lurk-display-error "No current network")
+      (let ((quit-msg (if params (string-join params " ") lurk-default-quit-msg)))
+        (lurk-send-msg
+         (lurk-context-network ctx)
+         (lurk-msg nil nil "QUIT" quit-msg))))))
+
+(defun lurk-command-join (params)
+  (if params
+      (let ((network (lurk-context-network (lurk-current-context))))
+        (dolist (channel params)
+          (lurk-send-msg network (lurk-msg nil nil "JOIN" channel))))
+    (lurk-display-notice nil "Usage: /join channel [channel2 ...]")))
+
+(defun lurk-command-part (params)
+  (let ((ctx (cond
+              ((not params) (lurk-current-context))
+              ((seq-contains (car params) "@") (lurk-string->context (car params)))
+              (t (list (lurk-context-network (lurk-current-context))  (car params))))))
+    (let ((network (lurk-context-network ctx))
+          (channel (lurk-context-channel ctx)))
+      (if channel
+          (lurk-send-msg network (lurk-msg nil nil "PART" channel))
+        (lurk-display-error "Specify which channel to leave")))))
+
+(defun lurk-command-switch-context (params)
+  (if (not params)
+      (lurk-display-notice nil "Usage: /switchcontext #channel@network")
+    (let ((ctx (lurk-string->context (car params))))
+      (lurk-switch-to-context ctx)
+      (lurk-highlight-current-context)
+      (lurk-render-prompt)
+      (if lurk-zoomed
+          (lurk-zoom-in (lurk-current-context))))))
+
+(defun lurk-command-nick (params)
+  (if params
+      (let ((new-nick (string-join params " "))
+            (ctx (lurk-current-context)))
+        (if ctx
+            (lurk-send-msg (lurk-context-network ctx)
+                           (lurk-msg nil nil "NICK" new-nick))
+          (lurk-display-error "No current connection")))
+    (lurk-display-notice nil "Usage: /nick <new-nick>")))
+
+(defun lurk-command-list (params)
+  (let ((ctx (lurk-current-context)))
+    (if ctx
+        (if (not params)
+            (lurk-display-notice nil "This command can generate lots of output."
+                                 " Use `/LIST -yes' if you really want this,"
+                                 " or `/LIST <channel_regexp>' to reduce the output.")
+          (let ((network (lurk-context-network ctx)))
+            (if (equal (upcase (car params)) "-YES")
+                (lurk-send-msg network (lurk-msg nil nil "LIST"))
+              (lurk-send-msg network (lurk-msg nil nil "LIST"
+                                              (car params))))))
+      (lurk-display-error "No current connection"))))
+
+(defun lurk-command-topic (params)
+  (let ((ctx (lurk-current-context)))
+    (if (and ctx (not (lurk-network-context-p ctx)))
+        (let ((network (lurk-context-network ctx))
+              (channel (lurk-context-channel ctx)))
+          (if params
+              (lurk-send-msg network
+                             (lurk-msg nil nil "TOPIC" channel
+                                       (string-join params " ")))
+            (lurk-send-msg network
+                           (lurk-msg nil nil "TOPIC" channel))))
+      (lurk-display-notice nil "No current channel."))))
+
+(defun lurk-command-msg (params)
+  (let ((network (lurk-context-network (lurk-current-context))))
+    (if (and params (>= (length params) 2))
+        (let ((to (car params))
+              (text (string-join (cdr params) " ")))
+          (lurk-send-msg network (lurk-msg nil nil "PRIVMSG" to text))
+          (lurk-display-message network
+                                (lurk-connection-nick network)
+                                to text))
+      (lurk-display-notice nil "Usage: /msg <nick> <message>"))))
+
+(defun lurk-command-me (params)
+  (let* ((ctx (lurk-current-context))
+         (network (lurk-context-network ctx)))
+    (if (and ctx (not (lurk-network-context-p ctx)))
+        (if params
+            (let* ((channel (lurk-context-channel ctx))
+                   (my-nick (lurk-connection-nick network))
+                   (action (string-join params " "))
+                   (ctcp-text (concat "\01ACTION " action "\01")))
+              (lurk-send-msg network
+                             (lurk-msg nil nil "PRIVMSG"
+                                       (list channel ctcp-text)))
+              (lurk-display-action network my-nick channel action))
+          (lurk-display-notice nil "Usage: /me <action>"))
+      (lurk-display-notice nil "No current channel."))))
+
+(defun lurk-command-version (params)
+  (let ((ctx (lurk-current-context)))
+    (if ctx
+        (if params
+            (let ((network (lurk-context-network ctx))
+                  (nick (car params)))
+              (lurk-send-msg network
+                             (lurk-msg nil nil "PRIVMSG"
+                                       (list nick "\01VERSION\01")))
+              (lurk-display-notice ctx "CTCP version request sent to "
+                                   nick " on " network))
+          (lurk-display-notice ctx "Usage: /version <nick>"))
+      (lurk-display-notice nil "No current channel."))))
+
+(defun lurk-command-users (_params)
+  (let ((ctx (lurk-current-context)))
+    (if (and ctx (not (lurk-network-context-p ctx)))
+        (let ((channel (lurk-context-channel ctx))
+              (network (lurk-context-network ctx))
+              (users (lurk-get-context-users ctx)))
+          (lurk-display-notice ctx "Users in " channel " on " network ":")
+          (lurk-display-notice ctx (string-join users " ")))
+      (lurk-display-notice nil "No current channel."))))
 
 
 ;;; Command entering
 
 
 ;;; Command entering
@@ -768,101 +1233,43 @@ portion of the source component of the message, as LURK doesn't use this.")
 
 (defun lurk-enter-string (string)
   (if (string-prefix-p "/" string)
 
 (defun lurk-enter-string (string)
   (if (string-prefix-p "/" string)
-      (pcase (substring string 1)
-        ((rx "DEBUG")
-         (setq lurk-debug (not lurk-debug))
-         (lurk-display-notice nil "Debug mode now " (if lurk-debug "on" "off") "."))
-
-        ((rx "HEADER")
-         (if header-line-format
-           (progn
-             (setq-local header-line-format nil)
-             (lurk-display-notice nil "Header disabled."))
-           (lurk-setup-header)
-           (lurk-display-notice nil "Header enabled.")))
-
-        ((rx (: "CONNECT " (let network (* not-newline))))
-         (lurk-display-notice nil "Attempting to connect to " network "...")
-         (lurk-connect network))
-
-        ((rx (: "TOPIC " (let new-topic (* not-newline))))
-         (lurk-send-msg (lurk-msg nil nil "TOPIC" lurk-current-context new-topic)))
-
-        ((rx (: "ME " (let action (* not-newline))))
-         (let ((ctcp-text (concat "\01ACTION " action "\01")))
-           (lurk-send-msg (lurk-msg nil nil "PRIVMSG"
-                                    (list lurk-current-context ctcp-text)))
-           (lurk-display-action lurk-nick lurk-current-context action)))
-
-        ((rx (: "VERSION" " " (let nick (+ (not whitespace)))))
-         (lurk-send-msg (lurk-msg nil nil "PRIVMSG"
-                                  (list nick "\01VERSION\01")))
-         (lurk-display-notice nil "CTCP version request sent to " nick))
-
-        ((rx "PART" (opt (: " " (let channel (* not-newline)))))
-         (if (or lurk-current-context channel)
-             (lurk-send-msg (lurk-msg nil nil "PART" (if channel
-                                                         channel
-                                                       lurk-current-context)))
-           (lurk-display-error "No current channel to leave.")))
-
-        ((rx "QUIT" (opt (: " " (let quit-msg (* not-newline)))))
-         (lurk-send-msg (lurk-msg nil nil "QUIT"
-                                  (or quit-msg lurk-default-quit-msg))))
-
-        ((rx (: "NICK" (* whitespace) string-end))
-         (lurk-display-notice nil "Current nick: " lurk-nick))
-
-        ((rx (: "NICK" (+ whitespace) (let nick (+ (not whitespace)))))
-         (if (lurk-connected-p)
-             (lurk-send-msg (lurk-msg nil nil "NICK" nick))
-           (setq lurk-nick nick)
-           (lurk-display-notice nil "Set default nick to '" nick "'")))
-
-        ((rx (: "LIST" (* whitespace) string-end))
-         (lurk-display-notice nil "This command can generate lots of output. Use `LIST -yes' if you're sure."))
-
-        ((rx (: "LIST" (+ whitespace) "-YES" (* whitespace) string-end))
-         (lurk-send-msg (lurk-msg nil nil "LIST")))
-
-        ((rx "MSG "
-             (let to (* (not whitespace)))
-             " "
-             (let text (* not-newline)))
-         (lurk-send-msg (lurk-msg nil nil "PRIVMSG" to text))
-         (lurk-display-message lurk-nick to text))
-
-        ((rx (: (let cmd-str (+ (not whitespace)))
-                (opt (: " " (let params-str (* not-newline))))))
-         (lurk-send-msg (lurk-msg nil nil (upcase cmd-str)
-                                  (if params-str
-                                      (split-string params-str)
-                                    nil)))))
-
+      (pcase string
+        ((rx (: "/" (let cmd-str (+ (not whitespace)))
+                (opt (+ whitespace)
+                     (let params-str (+ anychar))
+                     string-end)))
+         (let ((command-row (assoc (upcase  cmd-str) lurk-command-table #'equal))
+               (params (if params-str
+                           (split-string params-str nil t)
+                         nil)))
+           (if (and command-row (elt command-row 2))
+               (funcall (elt command-row 2) params)
+             (lurk-send-msg
+              (lurk-context-network (lurk-current-context))
+              (lurk-msg nil nil (upcase cmd-str) params)))))
+        (_
+         (lurk-display-error "Badly formed command")))
     (unless (string-empty-p string)
     (unless (string-empty-p string)
-      (if lurk-current-context
-          (progn
-            (lurk-send-msg (lurk-msg nil nil "PRIVMSG"
-                                     lurk-current-context
-                                     string))
-            (lurk-display-message lurk-nick lurk-current-context string))
-        (lurk-display-error "No current context.")))))
+      (let ((ctx (lurk-current-context)))
+        (if ctx
+            (if (not (lurk-network-context-p ctx))
+                (let ((network (lurk-context-network ctx))
+                      (channel (lurk-context-channel ctx)))
+                  (lurk-send-msg network
+                                 (lurk-msg nil nil "PRIVMSG" channel string))
+                  (lurk-display-message network
+                                        (lurk-connection-nick network)
+                                        channel string))
+              (lurk-display-error "No current channel"))
+          (lurk-display-error "No current context"))))))
+
+
+;;; Command history
+;;
 
 (defvar lurk-history nil
   "Commands and messages sent in current session.")
 
 
 (defvar lurk-history nil
   "Commands and messages sent in current session.")
 
-
-(defun lurk-enter ()
-  "Enter current contents of line after prompt."
-  (interactive)
-  (with-current-buffer "*lurk*"
-    (let ((line (buffer-substring lurk-input-marker (point-max))))
-      (push line lurk-history)
-      (setq lurk-history-index nil)
-      (let ((inhibit-read-only t))
-        (delete-region lurk-input-marker (point-max)))
-      (lurk-enter-string line))))
-
 (defvar lurk-history-index nil)
 
 (defun lurk-history-cycle (delta)
 (defvar lurk-history-index nil)
 
 (defun lurk-history-cycle (delta)
@@ -877,6 +1284,21 @@ portion of the source component of the message, as LURK doesn't use this.")
       (delete-region lurk-input-marker (point-max))
       (insert (elt lurk-history lurk-history-index)))))
 
       (delete-region lurk-input-marker (point-max))
       (insert (elt lurk-history lurk-history-index)))))
 
+
+;;; Interactive commands
+;;
+
+(defun lurk-enter ()
+  "Enter current contents of line after prompt."
+  (interactive)
+  (with-current-buffer "*lurk*"
+    (let ((line (buffer-substring lurk-input-marker (point-max))))
+      (push line lurk-history)
+      (setq lurk-history-index nil)
+      (let ((inhibit-read-only t))
+        (delete-region lurk-input-marker (point-max)))
+      (lurk-enter-string line))))
+
 (defun lurk-history-next ()
   (interactive)
   (lurk-history-cycle -1))
 (defun lurk-history-next ()
   (interactive)
   (lurk-history-cycle -1))
@@ -885,16 +1307,21 @@ portion of the source component of the message, as LURK doesn't use this.")
   (interactive)
   (lurk-history-cycle +1))
 
   (interactive)
   (lurk-history-cycle +1))
 
-;;; Interactive functions
-;;
-
 (defun lurk-cycle-contexts-forward ()
   (interactive)
 (defun lurk-cycle-contexts-forward ()
   (interactive)
-  (lurk-cycle-contexts))
+  (lurk-cycle-contexts)
+  (lurk-highlight-current-context)
+  (lurk-render-prompt)
+  (if lurk-zoomed
+      (lurk-zoom-in (lurk-current-context))))
 
 (defun lurk-cycle-contexts-reverse ()
   (interactive)
 
 (defun lurk-cycle-contexts-reverse ()
   (interactive)
-  (lurk-cycle-contexts t))
+  (lurk-cycle-contexts t)
+  (lurk-highlight-current-context)
+  (lurk-render-prompt)
+  (if lurk-zoomed
+      (lurk-zoom-in (lurk-current-context))))
 
 (defvar lurk-zoomed nil
   "Keeps track of zoom status.")
 
 (defvar lurk-zoomed nil
   "Keeps track of zoom status.")
@@ -903,20 +1330,39 @@ portion of the source component of the message, as LURK doesn't use this.")
   (interactive)
   (if lurk-zoomed
       (lurk-zoom-out)
   (interactive)
   (if lurk-zoomed
       (lurk-zoom-out)
-    (lurk-zoom-in lurk-current-context))
+    (lurk-zoom-in (lurk-current-context)))
   (setq lurk-zoomed (not lurk-zoomed)))
 
   (setq lurk-zoomed (not lurk-zoomed)))
 
-(defun lurk-complete-nick ()
-  (interactive)
-  (when (and (>= (point) lurk-input-marker) lurk-current-context)
-    (let* ((end (max lurk-input-marker (point)))
-           (space-idx (save-excursion
-                        (re-search-backward " " lurk-input-marker t)))
-           (start (if space-idx (+ 1 space-idx) lurk-input-marker))
-           (completion-ignore-case t))
-      (unless (string-prefix-p "/" (buffer-substring start end))
-        (completion-in-region start end (lurk-get-context-users lurk-current-context))))))
 
 
+(defun lurk-complete-input ()
+  (interactive)
+  (let ((completion-ignore-case t))
+    (when (>= (point) lurk-input-marker)
+      (pcase (buffer-substring lurk-input-marker (point))
+        ((rx (: "/" (let cmd-str (+ (not whitespace))) (+ " ") (* (not whitespace)) string-end))
+         (let ((space-idx (save-excursion
+                            (re-search-backward " " lurk-input-marker t)))
+               (table-row (assoc (upcase cmd-str) lurk-command-table #'equal)))
+           (if (and table-row (elt table-row 3))
+               (let* ((completions-nospace (funcall (elt table-row 3)))
+                      (completions (mapcar (lambda (el) (concat el " ")) completions-nospace)))
+                 (completion-in-region (+ 1 space-idx) (point) completions)))))
+        ((rx (: "/" (* (not whitespace)) string-end))
+         (message (buffer-substring lurk-input-marker (point)))
+         (completion-in-region lurk-input-marker (point)
+                               (mapcar (lambda (row) (concat "/" (car row) " "))
+                                       lurk-command-table)))
+        (_
+         (let* ((end (max lurk-input-marker (point)))
+                (space-idx (save-excursion
+                             (re-search-backward " " lurk-input-marker t)))
+                (start (if space-idx (+ 1 space-idx) lurk-input-marker)))
+           (unless (string-prefix-p "/" (buffer-substring start end))
+             (let* ((users (lurk-get-context-users (lurk-current-context)))
+                    (users-no@ (mapcar
+                                (lambda (u) (car (split-string u "@" t)))
+                                users)))
+               (completion-in-region start end users-no@)))))))))
 
 ;;; Mode
 ;;
 
 ;;; Mode
 ;;
@@ -924,22 +1370,20 @@ portion of the source component of the message, as LURK doesn't use this.")
 (defvar lurk-mode-map
   (let ((map (make-sparse-keymap)))
     (define-key map (kbd "RET") 'lurk-enter)
 (defvar lurk-mode-map
   (let ((map (make-sparse-keymap)))
     (define-key map (kbd "RET") 'lurk-enter)
-    (define-key map (kbd "<tab>") 'lurk-complete-nick)
+    (define-key map (kbd "TAB") 'lurk-complete-input)
     (define-key map (kbd "C-c C-z") 'lurk-toggle-zoom)
     (define-key map (kbd "C-c C-z") 'lurk-toggle-zoom)
-    (define-key map (kbd "<C-tab>") 'lurk-cycle-contexts-forward)
-    (define-key map (kbd "<C-S-tab>") 'lurk-cycle-contexts-reverse)
     (define-key map (kbd "<C-up>") 'lurk-history-prev)
     (define-key map (kbd "<C-down>") 'lurk-history-next)
     (define-key map (kbd "<C-up>") 'lurk-history-prev)
     (define-key map (kbd "<C-down>") 'lurk-history-next)
-    ;; (when (fboundp 'evil-define-key*)
-    ;;   (evil-define-key* 'insert map
-    ;;                     (kbd "<C-Up>") 'lurk-history-prev
-    ;;                     (kbd "<C-Down>") 'lurk-history-next))
+    (define-key map (kbd "<C-tab>") 'lurk-cycle-contexts-forward)
+    (define-key map (kbd "<C-S-iso-lefttab>") 'lurk-cycle-contexts-reverse)
+    (define-key map (kbd "<C-S-tab>") 'lurk-cycle-contexts-reverse)
+    (when (fboundp 'evil-define-key*)
+      (evil-define-key* 'motion map
+        (kbd "TAB") 'lurk-complete-input))
     map))
 
     map))
 
-(defvar lurk-mode-map)
-
 (define-derived-mode lurk-mode text-mode "lurk"
 (define-derived-mode lurk-mode text-mode "lurk"
-  "Major mode for LURK.")
+  "Major mode for lurk.")
 
 (when (fboundp 'evil-set-initial-state)
   (evil-set-initial-state 'lurk-mode 'insert))
 
 (when (fboundp 'evil-set-initial-state)
   (evil-set-initial-state 'lurk-mode 'insert))
@@ -947,15 +1391,15 @@ portion of the source component of the message, as LURK doesn't use this.")
 ;;; Main start procedure
 ;;
 
 ;;; Main start procedure
 ;;
 
+;;;###autoload
 (defun lurk ()
 (defun lurk ()
-  "Switch to *lurk* buffer."
+  "Start lurk or just switch to the lurk buffer if one already exists."
   (interactive)
   (if (get-buffer "*lurk*")
       (switch-to-buffer "*lurk*")
     (switch-to-buffer "*lurk*")
     (lurk-mode)
     (lurk-setup-buffer))
   (interactive)
   (if (get-buffer "*lurk*")
       (switch-to-buffer "*lurk*")
     (switch-to-buffer "*lurk*")
     (lurk-mode)
     (lurk-setup-buffer))
-  "Started LURK.")
-
+  "Started lurk.")
 
 ;;; lurk.el ends here
 
 ;;; lurk.el ends here