12 KiB
Moss and Puddles
A literate programming file for a Comint-based MUD client.
Introduction
This project is a simple MUD client for Emacs, based on COM-INT MUD client based on Mickey Petersen’s essay on Comint.
The default connects to Moss ‘n Puddles, my own MUD which I invite you to join.
(defvar pud-default-world "moss-n-puddles"
"The default world to connect.")
This uses telnet
(at the moment) for the connection, so you will need to install that first. On Mac, this would be:
brew install telent
And use a similar command on Linux.
User Credentials
You may want to customize your connections to more worlds.
(defgroup pud nil
"An overly simplistic MUD client that works with Evennia."
:group 'processes)
(defcustom pud-worlds
(list (vector pud-default-world "localhost" "4000" "guest" "guest"))
"List of worlds you play in.
You need to define the worlds you play in before you can get
started. In most worlds, you can start playing using a guest account.
Each element WORLD of the list has the following form:
\[NAME HOST PORT CHARACTER PASSWORD]
NAME identifies the connection, HOST and PORT specify the network
connection, CHARACTER and PASSWORD are used to connect automatically.
Note that this will be saved in your `custom-file' -- including your
passwords! If you don't want that, specify nil as your password."
:type '(repeat
(vector :tag "World"
(string :tag "Name")
(string :tag "Host")
(integer :tag "Port")
(string :tag "Char" :value "guest")
(string :tag "Pwd" :value "guest")))
:group 'pud)
Next, open your authinfo file, and insert the following line, substituting [user]
and [pass]
with your credentials as well as the first entry to match your world:
machine moss-n-puddles login [name] password [pass]
Now, let’s play! Type run-pud
, and optionally select a world. If you get disconnected, re-run it, or even pud-reconnect
.
The rest of this file describes the code to implement this project.
Code
The following function will return the default world:
(defun pud-get-default-world ()
"Return the connection information for the `pud-default-world'.
If only one world listed in `pud-worlds', return that."
(if (length= pud-worlds 1)
(seq-first pud-worlds)
(seq-find
(lambda (w) (string-equal (aref w 0) pud-default-world))
pud-worlds)))
And accessibility functions.
(defsubst pud-world-name (&optional world)
"Return the name for WORLD as a string."
;; (concat (aref world 3) "@" (aref world 0))
(if (vectorp world)
(aref world 0)
world))
(defsubst pud-world-network (&optional world)
"Return the network details for WORLD as a cons cell (HOST . PORT)."
(unless world
(setq world (pud-get-default-world)))
(list (aref world 1) (aref world 2)))
(defsubst pud-world-character (&optional world)
"Return the character for WORLD as a string.
Override the customized setting if the world has an entry in authinfo."
(unless world
(setq world (pud-get-default-world)))
(if-let ((auth-results (auth-source-search :host (aref world 0))))
(thread-first auth-results
(first)
(plist-get :user))
(aref world 3)))
(defsubst pud-world-password (&optional world)
"Return the password for WORLD as a string."
(unless world
(setq world (pud-get-default-world)))
(if-let ((auth-results (auth-source-search :host (aref world 0))))
(thread-first auth-results
(first)
(plist-get :secret)
(funcall))
(aref world 4)))
And some basic functions that really need to be expanded.
(ert-deftest pud-world-name-test ()
(should (string-equal (pud-world-name "foobar") "foobar"))
(should (string-equal (pud-world-name ["foobar" "localhost" "4000" "guest" "guest"]) "foobar")))
(ert-deftest pud-world-network-test ()
(should (equal (pud-world-network) '("localhost" "4000")))
(should (equal (pud-world-network ["foobar" "overthere" "4000" "guest" "guest"]) '("overthere" "4000"))))
(ert-deftest pud-world-character-test ()
(should (equal (pud-world-character) "guest")))
Choosing a world… er, connection using a completing-read
allowing you to choose a world. If pud-worlds
contains a single value, might as well just return that.
(defvar pud-world-history nil
"History for `pud-get-world'.")
(defun pud-get-world ()
"Let the user choose a world from `pud-worlds'.
The return value is a cons cell, the car is the name of the connection,
the cdr holds the connection defails from `pud-worlds'."
(if (length= pud-worlds 1)
(seq-first pud-worlds))
(let ((world-completions
(mapcar (lambda (w)
(cons (pud-world-name w) w))
pud-worlds)))
(cond
((and world-completions (length= world-completions 1))
(thread-first world-completions
(first)
(cdr)))
(world-completions
(thread-first
(completing-read "World: " world-completions nil t nil pud-world-history)
(assoc world-completions)
(cdr)))
(t (customize-option 'pud-worlds)))))
And a function for the full credentials, which just happens to be what we need to pass to telnet
.
(defun pud-credentials (&optional world)
"Reset the credentials from WORLD from the authinfo system."
(setf (elt 3 world) (pud-world-character world))
(setf (elt 4 world) (pud-world-password world))
world)
Basics
Using Comint, and hoping to have the ANSI colors displayed.
(require 'comint)
(load "ansi-color" t)
I’m going to use good ‘ol fashion telnet
for the connection:
(defvar pud-cli-file-path "/usr/local/bin/telnet"
"Path to the program used by `run-pud'")
The pud-cli-arguments, holds a list of commandline arguments: the port.
The empty and currently disused mode map for storing our custom keybindings inherits from comint-mode-map
, so we get the same keys exposed in comint-mode
.
(defvar pud-mode-map
(let ((map (nconc (make-sparse-keymap) comint-mode-map)))
;; example definition
(define-key map "\t" 'completion-at-point)
map)
"Basic mode map for `run-pud'.")
This holds a regular expression that matches the prompt style for the MUD. Not sure if this is going to work, since MUDs typically don’t have prompts.
(defvar pud-prompt-regexp "" ; "^\\(?:\\[[^@]+@[^@]+\\]\\)"
"Prompt for `run-pud'.")
The name of the buffer:
(defvar pud-buffer-name "*Moss and Puddles*"
"Name of the buffer to use for the `run-pud' comint instance.")
(defun pud-buffer-name (&optional world)
"Return the buffer name associated with WORLD."
(format "*%s*" (if world
(pud-world-name world)
pud-default-world)))
The main entry point to the program is the run-pud
function:
(defun run-pud (world)
"Run an inferior instance of `pud-cli' inside Emacs."
(interactive (list (pud-get-world)))
(let* ((pud-program pud-cli-file-path)
(pud-args (pud-world-network world))
(buffer (get-buffer-create (pud-buffer-name world)))
(proc-alive (comint-check-proc buffer))
(process (get-buffer-process buffer)))
;; if the process is dead then re-create the process and reset the
;; mode.
(unless proc-alive
(with-current-buffer buffer
(apply 'make-comint-in-buffer "Pud" buffer pud-program nil pud-args)
(pud-mode)
(visual-line-mode 1)
(pud-reconnect world)))
;; Regardless, provided we have a valid buffer, we pop to it.
(when buffer
(pop-to-buffer buffer))))
Connection and/or re-connection:
(defun pud-reconnect (world)
"docstring"
(interactive (list (pud-get-world)))
(pop-to-buffer (pud-buffer-name world))
(sit-for 1)
(let* ((username (pud-world-character world))
(password (pud-world-password world))
(conn-str (format "connect %s %s\n" username password))
(process (get-buffer-process (current-buffer))))
(if process
(comint-send-string process conn-str)
(insert conn-str))))
Pud Mode
The previous snippet of code dealt with creating and maintaining the buffer and process, and this piece of code enriches it with font locking and mandatory setup. Namely comint-process-echoes which, depending on the mode and the circumstances, may result in prompts appearing twice. Setting it to t is usually a requirement, but do experiment.
(defun pud--initialize ()
"Helper function to initialize Pud."
(setq comint-process-echoes t)
(setq comint-use-prompt-regexp nil))
(define-derived-mode pud-mode comint-mode "Pud"
"Major mode for `run-pud'.
\\<pud-mode-map>"
;; this sets up the prompt so it matches things like: [foo@bar]
;; (setq comint-prompt-regexp pud-prompt-regexp)
;; this makes it read only; a contentious subject as some prefer the
;; buffer to be overwritable.
(setq comint-prompt-read-only t)
;; this makes it so commands like M-{ and M-} work.
;; (set (make-local-variable 'paragraph-separate) "\\'")
;; (set (make-local-variable 'font-lock-defaults) '(pud-font-lock-keywords t))
;; (set (make-local-variable 'paragraph-start) pud-prompt-regexp)
)
(add-hook 'pud-mode-hook 'pud--initialize)
(defconst pud-keywords
'("connect" "get" "look" "use")
"List of keywords to highlight in `pud-font-lock-keywords'.")
(defvar pud-font-lock-keywords
(list
;; highlight all the reserved commands.
`(,(concat "\\_<" (regexp-opt pud-keywords) "\\_>") . font-lock-keyword-face))
"Additional expressions to highlight in `pud-mode'.")