#+title:  Moss and Puddles
#+author: Howard X. Abrams
#+date:   2025-01-18
#+filetags: emacs hamacs
#+lastmod: [2025-08-23 Sat]

A literate programming file for a Comint-based MUD client.

#+begin_src emacs-lisp :exports none
  ;;; pud --- a MUD client -*- lexical-binding: t; -*-
  ;;
  ;; © 2025 Howard X. Abrams
  ;;   Licensed under a Creative Commons Attribution 4.0 International License.
  ;;   See http://creativecommons.org/licenses/by/4.0/
  ;;
  ;; Author: Howard X. Abrams <http://gitlab.com/howardabrams>
  ;; Maintainer: Howard X. Abrams
  ;; Created: January 18, 2025
  ;;
  ;; While obvious, GNU Emacs does not include this file or project.
  ;;
  ;; *NB:* Do not edit this file. Instead, edit the original literate file at:
  ;;            /Users/howard/src/hamacs/pud.org
  ;;       And tangle the file to recreate this one.
  ;;
  ;;; Code:
#+end_src

* Introduction

This project is a simple MUD client for Emacs, based on COM-INT MUD client I learn about on Mickey Petersen’s [[https://www.masteringemacs.org/article/comint-writing-command-interpreter][essay on Comint]].

This uses either =ssh= or good ol’ =telnet= for the connection. Surprised that one can still install =telnet= on a Mac, like:

#+BEGIN_SRC sh
  brew install telnet
#+END_SRC

Use your favorite way to install Emacs’ packages, for instance, with Emacs 30, one could install and customize settings in one go with =use-package=:

#+BEGIN_SRC emacs-lisp :tangle no :eval no
  (use-package pud
    :vc (:url "https://howardabrams.com/git/howard/pud")
    :custom
    (pud-worlds
     '(["Remote Moss-n-Puddles" 'ssh "howardabrams.com" 4000 "george"]
                                          ; ↑ No password? Should be in .authinfo.gpg

       ["Local Root" 'telnet "localhost" 4000 "darol" "some-pass"]
                                          ; ↑ This has the password in your custom settings.

                                          ; ↓ Password from authinfo, special connection string:
       ["Local User" 'telnet "localhost" 4000 "rick" nil "login %s %s"])))
#+END_SRC
** Customization

You will want to customize your connections to the connections, as this program defaults to  *Moss ‘n Puddles*, my own MUD which I invite you to join.

#+BEGIN_SRC emacs-lisp
  (defgroup pud nil
    "An overly simplistic MUD client that works with Evennia."
    :group 'processes)

  (defcustom pud-worlds
    '(["Moss-n-Puddles" telnet "howardabrams.com" 4000])
    "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:

    \[CONN-TYPE NAME HOST PORT CHARACTER PASSWORD LOGIN-STR]

    NAME identifies the connection, HOST and PORT specify the network
    connection, CHARACTER and PASSWORD are used to connect automatically.

    The CONN-TYPE can be either 'telnet or 'ssh.

    The LOGIN-STR is a string with two `%s' where this substitutes
    the username and password respectively. Sends this to the server after
    establishing a connection. This can be blank for the default.
    If given, make sure to have a trailing `\n' to automatically send.

    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 "Server World"
                    (string :tag "Name")
                    (radio :tag "Type"
                           (const :tag "Telnet" :value telnet)
                           (const :tag "SSH"    :value ssh))
                    (string  :tag "Hostname")
                    (integer :tag "Port num")
                    (string  :tag "Username" :value "guest")
                    (string  :tag "Password")
                    (string  :tag "Login String"
                             :format "%t: %v%h"
                             :doc "The login string to send after connection.
  This should probably have a \`\\n' at the end to submit it.
  If blank or nil, use the \`pud-default-connection-string'.
  For example: connect %s %s\\n")))
    :group 'pud)
#+END_SRC

For instance, you could set up a call to =setopt= or customize the =pud-worlds= variable, as in:

#+BEGIN_SRC emacs-lisp :tangle no :eval no
  (use-package pud
    :custom
    (pud-worlds
     '(["Remote Moss-n-Puddles" 'ssh "howardabrams.com" 4000 "bobby"]
       ; ↑ No password? Should be in .authinfo.gpg

       ["Local Root" 'telnet "localhost" 4000 "suzy" "some-pass"]
       ; ↑ This has the password in your custom settings.

       ; ↓ Password from authinfo, special connection string:
       ["Local User" 'telnet "localhost" 4000 "rick" nil "login %s %s"])))
#+END_SRC

Or set up the customization in the =init.el= file:

#+BEGIN_SRC emacs-lisp :tangle no :eval no
  (setopt pud-worlds
        '(["Moss-n-Puddles" ssh "howardabrams.com" 4004 "howard" "" "\\nconnect %s %s\\n"]
          ["Moss-n-Puddles" ssh "howardabrams.com" 4004 "rick" "" "\\nconnect %s %s\\n"]
          ["Moss-n-Puddles" ssh "howardabrams.com" 4004 "darol" "" "\\nconnect %s %s\\n"]
          ["Local-Moss" telnet "localhost" 4000 "howard" "" ""]
          ["Local-Moss" telnet "localhost" 4000 "rick" "" ""]))
#+END_SRC

Seems like MUDs have a standard login sequence, but they don’t have to. Here is the default that a user can override in their =pud-worlds= listing:

#+BEGIN_SRC emacs-lisp
  (defcustom pud-default-connection-string "connect %s %s\n"
    "The standard connection string to substitute the username and password."
    :type '(string)
    :group 'pud)
#+END_SRC

** User Credentials
Next, open [[file:~/.authinfo.gpg][your authinfo file]], and insert the following line, substituting =[user]= and =[pass]= with your credentials as well as the first entry to match each host name in your =pud-worlds= entries:

#+BEGIN_SRC conf :tangle no :eval no
  machine howardabrams.com login [name] port 4000 password [pass]
#+END_SRC

Now, let’s play! Type =pud-run=, and optionally select a world. If you get disconnected, re-run it, or even =pud-reconnect=.

The rest of this file describes the code implementing this project.
* Code
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.

#+BEGIN_SRC emacs-lisp
  (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)))))
#+END_SRC

The following functions are accessibility functions to the world entry.

#+BEGIN_SRC emacs-lisp
  (defun pud-world-name (world)
    "Return the name for WORLD as a string."
    (if (vectorp world)
        (if (or (length< world 5) (null (aref world 4)) (string-blank-p (aref world 4)))
            (aref world 0)
          (concat (aref world 4) "@" (aref world 0)))
      world))

  (defun pud-world-network (world)
    "Return the network details for WORLD as a list.
  [Connect-Type Host Port]."
    (list (aref world 1) (aref world 2) (format "%s" (aref world 3))))

  (defun pud-world-creds (world)
    "Return the username and password from WORLD.
  Multiple search queries for the .authinfo file."
    (seq-let (label conn-type host port user) world
      (if-let ((auth-results (first (auth-source-search
                                     :host host
                                     :port port
                                     :user user
                                     :max 1))))
          (list (plist-get auth-results :user)
                (funcall (plist-get auth-results :secret)))
        ;; No match? Just return values from world:
        (list (aref world 4) (aref world 5)))))
#+END_SRC

And some basic functions I should expand.

#+BEGIN_SRC emacs-lisp :tangle no
  (ert-deftest pud-world-name-test ()
    (should (string-equal (pud-world-name "foobar") "foobar"))
    (should (string-equal (pud-world-name ["foobar" 'telnet "localhost" "4000"]) "foobar"))
    (should (string-equal (pud-world-name ["foobar" 'telnet "localhost" "4000" nil]) "foobar"))
    (should (string-equal (pud-world-name ["foobar" 'telnet "localhost" "4000" ""]) "foobar"))
    (should (string-equal (pud-world-name ["foobar" 'telnet "localhost" "4000" "guest" "guest"]) "guest@foobar")))

  (ert-deftest pud-world-network-test ()
    (should (equal (pud-world-network ["foobar" telnet "overthere" "4000" "guest" "guest"]) '(telnet "overthere" "4000")))
    (should (equal (pud-world-network ["foobar" ssh "overthere" 4000 "guest" "guest"]) '(ssh "overthere" "4000"))))

  (ert-deftest pud-world-creds-test ()
    ;; Test with no match in authinfo!
    (should (equal
             (pud-world-creds ["some-place" telnet "some-home" 4000 "a-user" "a-pass"])
             '("a-user" "a-pass")))
    ;; This test works if the following line is in .authinfo:
    ;;  machine localhost port 4000 login george password testpass
    (should (equal
             (pud-world-creds ["nudder-place" ssh "localhost" 4000 "george"])
             '("george" "testpass"))))
#+END_SRC

* Basics
Using Comint, and hoping to have the ANSI colors displayed.

#+BEGIN_SRC emacs-lisp
  (require 'comint)
  (load "ansi-color" t)
#+END_SRC

Get full colors using the [[https://github.com/atomontage/xterm-color][xterm-color]] project:

#+BEGIN_SRC emacs-lisp
  (use-package xterm-color)
#+END_SRC

I’m going to use good ‘ol fashion =telnet= for the connection:

#+BEGIN_SRC emacs-lisp
  (defcustom pud-telnet-path "telnet"
    "Path to the program used by `pud-run' to connect using telnet."
    :type '(string)
    :group 'pud)

  (defcustom pud-ssh-path "ssh"
    "Path to the program used by `pud-run' to connect using ssh."
    :type '(string)
    :group 'pud)
#+END_SRC

The pud-cli-arguments, holds a list of commandline arguments: the port.

#+BEGIN_SRC emacs-lisp
  (defvar pud-cli-arguments nil
    "A list of arguments to use before the connection.")
#+END_SRC

Command string to use, given a =world= with a connection type:

#+BEGIN_SRC emacs-lisp
  (defun pud-cli-command (world)
    "Return a command string to pass to the shell.
  The WORLD is a vector with the hostname, see `pud-worlds'."
    (seq-let (connection-type host port) (pud-world-network world)
      (message "Dealing with: %s %s %s" connection-type host port)
      (cl-case connection-type
        (telnet (append (cons pud-telnet-path pud-cli-arguments)
                         (list host port)))
        (ssh    (append (cons pud-ssh-path pud-cli-arguments)
                         (list "-p" port host)))
        (t (error "Unsupported connection type")))))
#+END_SRC

Some tests:

#+BEGIN_SRC emacs-lisp :tangle no
  (ert-deftest pud-cli-command-test ()
    (should (equal (pud-cli-command ["some-world" telnet "world.r.us" 4000])
                   '("telnet" "world.r.us" "4000")))
    (should (equal (pud-cli-command ["nudder-world" ssh "world.r.us" 4004])
                   '("ssh" "-p" "4004" "world.r.us"))))
    #+END_SRC


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=.

#+BEGIN_SRC emacs-lisp
  (defvar pud-mode-map
    (let ((map (nconc (make-sparse-keymap) comint-mode-map)))
      (define-key map "\t" 'completion-at-point)
      map)
    "Basic mode map for `pud-run'.")
#+END_SRC

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.

#+BEGIN_SRC emacs-lisp
  (defvar pud-prompt-regexp "" ; "^\\(?:\\[[^@]+@[^@]+\\]\\)"
    "Prompt for `pud-run'.")
#+END_SRC

The name of the buffer:

#+BEGIN_SRC emacs-lisp
  (defun pud-buffer-name (world)
    "Return the buffer name associated with WORLD."
    (format "*%s*" (pud-world-name world)))
#+END_SRC

** Run and Connect
The main entry point to the program is the =pud-run= function:

#+BEGIN_SRC emacs-lisp
  (defun pud-run (world)
    "Run an inferior instance of `pud-cli' inside Emacs.
  The WORLD should be vector containing the following:
    - label for the world
    - server type, 'ssh or 'telnet
    - server hostname
    - server port
    - username (can be overridden)
    - password (should be overridden)"
    (interactive (list (pud-get-world)))
    (let* ((pud-cli (pud-cli-command 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.
      (message "Gonna %s with %s" (car pud-cli) (cdr pud-cli))
      (unless proc-alive
        (with-current-buffer buffer
          (apply 'make-comint-in-buffer "Pud" buffer (car pud-cli) nil (cdr pud-cli))
          (pud-mode)
          (visual-line-mode 1)
          (pud-login world)))
      ;; Regardless, provided we have a valid buffer, we pop to it.
      (when buffer
        (pop-to-buffer buffer))))
#+END_SRC

Connection and/or re-connection:

#+BEGIN_SRC emacs-lisp
  (defun pud-login (world)
    "Collect and send a `connect' sequence to WORLD.

  Where WORLD is a vector of world information. NOP if the buffer
  has no connection or no password could be found."
    (interactive (list (pud-get-world)))
    (when (called-interactively-p)
      (pop-to-buffer (pud-buffer-name world)))
    (sit-for 1)

    (length world)
    (message "Attempting to log in to %s..." (pud-world-name world))
    (seq-let (username password) (pud-world-creds world)
      (let* ((local-conn (when (length> world 6)
                           (aref world 6 )))
             (conn-str (if (and local-conn
                                (not (string-blank-p local-conn)))
                           local-conn
                         pud-default-connection-string))
             (conn-full (format conn-str username password))
             (process (get-buffer-process (current-buffer))))

        (goto-char (point-max))
        (if process
            (comint-send-string process conn-full)
          (insert conn-full)))))
#+END_SRC

(setq world (pud-get-world))
** Reconnect
Force a kill process, and restart.

#+BEGIN_SRC emacs-lisp
  (defun pud-reconnect (world)
    "Force stop an inferior instance of `pud-cli'.
  The WORLD should be vector containing the following:
    - label for the world
    - server hostname
    - server port
    - username (can be overridden)
    - password (should be overridden)"
    (interactive (list (pud-get-world)))
    (let* ((pud-cli (pud-cli-command world))
           (buffer (get-buffer-create (pud-buffer-name world)))
           (proc-alive (comint-check-proc buffer))
           (process (get-buffer-process buffer)))
      (when (processp process)
        (kill-process process))
      (pud-run world)))
#+END_SRC

* Pud Mode
Note that =comint-process-echoes=, depending on the mode and the circumstances, may result in prompts appearing twice. Setting =comint-process-echoes= to =t= helps with that.

#+BEGIN_SRC emacs-lisp
  (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 `pud-run'.

    \\<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)

    ;; Get the xterm colorization on:
    (require 'xterm-color)
    (font-lock-mode -1)
    ;; Prevent font-locking from being re-enabled in this buffer
    (make-local-variable 'font-lock-function)
    (setq font-lock-function (lambda (_) nil))
    (add-hook 'comint-preoutput-filter-functions 'xterm-color-filter nil t))

  (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 (rx bol (optional "@")) (regexp-opt pud-keywords)) . font-lock-keyword-face)
     `(,(rx bol "@" (one-or-more)))
     )

    "Additional expressions to highlight in `pud-mode'.")
#+END_SRC

* Org Babel
Wouldn’t it be nice to be able to write commands in an Org file, and send the command to the connected Mud?

#+begin_src emacs-lisp :exports none :tangle ~/.emacs.d/elisp/ob-evennia.el
  ;;; ob-evennia --- Evennia source blocks in Org -*- lexical-binding: t; -*-
  ;;
  ;; © 2025 Howard X. Abrams
  ;;   Licensed under a Creative Commons Attribution 4.0 International License.
  ;;   See http://creativecommons.org/licenses/by/4.0/
  ;;
  ;; Author: Howard X. Abrams <http://gitlab.com/howardabrams>
  ;; Maintainer: Howard X. Abrams
  ;; Created: January 18, 2025
  ;;
  ;; While obvious, GNU Emacs does not include this file or project.
  ;;
  ;; *NB:* Do not edit this file. Instead, edit the original literate file at:
  ;;            /Users/howard/src/hamacs/pud.org
  ;;       And tangle the file to recreate this one.
  ;;
  ;;; Code:
#+end_src
Since I’m connected to more than one MUD, or at least, I often log in with two different characters as two different characters. Let’s have a function that can return all PUD buffers:

#+BEGIN_SRC emacs-lisp :tangle ~/.emacs.d/elisp/ob-evennia.el
  (defun pud-get-all-buffers ()
      "Return a list of all buffers with a live PUD connection."
      (save-window-excursion
        (seq-filter (lambda (buf)
                      (switch-to-buffer buf)
                      (and
                       (eq major-mode 'pud-mode)
                       (get-buffer-process (current-buffer))))
                    (buffer-list))))
#+END_SRC

And a wrapper around =completing-read= for choosing one of the buffers:

#+BEGIN_SRC emacs-lisp :tangle ~/.emacs.d/elisp/ob-evennia.el
  (defun pud-current-world ()
      "Return buffer based on user choice of current PUD connections."
      (let ((pud-buffers (pud-get-all-buffers)))
        (cond
         ((null pud-buffers) nil)
         ((length= pud-buffers 1) (car pud-buffers))
         (t
          (completing-read "Choose connection: "
                           (seq-map (lambda (buf) (buffer-name buf))
                                    pud-buffers))))))
#+END_SRC

Given a buffer and a string, use the =comint-send-string=:

#+BEGIN_SRC emacs-lisp :tangle ~/.emacs.d/elisp/ob-evennia.el
  (defun pud-send-string (buf-name text)
    "Send TEXT to a comint buffer, BUF-NAME."
    (save-window-excursion
      (save-excursion
        (pop-to-buffer buf-name)
        (goto-char (point-max))
        (comint-send-string (get-buffer-process (current-buffer))
                            (format "%s\n" text)))))
#+END_SRC

Let’s send the current line or region.

#+BEGIN_SRC emacs-lisp :tangle ~/.emacs.d/elisp/ob-evennia.el
  (defun pud-send-line (world)
    "Send the current line or region to WORLD."
    (interactive (list (pud-current-world)))
    (unless world
      (error "No current MUD connection."))

    (let ((text (buffer-substring-no-properties
                 (if (region-active-p) (region-beginning)
                   (beginning-of-line-text) (point))
                 (if (region-active-p) (region-end)
                   (end-of-line) (point)))))
      (pud-send-string world text)))

  (global-set-key (kbd "<f6>") 'pud-send-line)
#+END_SRC

Let’s be able to send the current Org block, where all lines in the block are smooshed together to create a single line:

#+BEGIN_SRC emacs-lisp :tangle ~/.emacs.d/elisp/ob-evennia.el
  (defun pud-send-block (world)
    "Send the current Org block to WORLD."
    (interactive (list (pud-current-world)))
    (unless world
      (error "No current MUD connection."))
    (let ((text (thread-last (org-element-at-point)
                             (org-src--contents-area)
                             (nth 2))))
      (pud-send-string world
                       (replace-regexp-in-string
                        (rx (one-or-more space)) " " text))))
#+END_SRC

And code so that we can =(require 'ob-evennia)= to get font-locking working in blocks.

#+BEGIN_SRC emacs-lisp :tangle ~/.emacs.d/elisp/ob-evennia.el
  (require 'ob)

  (defvar org-babel-default-header-args:evennia '())

  (defun org-babel-execute:evennia (body params)
    "Execute evennia BODY.

   PARAMS can contain the following:
     :session - The buffer to send the block

   Called by `org-babel-execute-src-block'."
    (let* ((session (cdr (assq :session params)))
           (buffer (or session (pud-current-world))))
      (if session
          (setq buffer (format "*%s*" session)))

      (pud-send-string buffer
                       (replace-regexp-in-string
                        (rx (one-or-more space)) " " body))
      (message "No connected world.")))

  (defun org-babel-prep-session:evennia (_session _params)
    "Signal error; Evennia does not (currently) support sessions."
    (error "Evennia sessions are nonsensical"))

  (provide 'ob-evennia)
#+END_SRC

This should allow this client to simply =require= it:

#+BEGIN_SRC emacs-lisp
  (require 'ob-evennia)
#+END_SRC

* Evennia Mode
#+begin_src emacs-lisp :exports none :tangle ~/.emacs.d/elisp/evennia-mode.el
  ;;; evennia-mode --- Syntax coloring for Evennia code -*- lexical-binding: t; -*-
  ;;
  ;; © 2025 Howard X. Abrams
  ;;   Licensed under a Creative Commons Attribution 4.0 International License.
  ;;   See http://creativecommons.org/licenses/by/4.0/
  ;;
  ;; Author: Howard X. Abrams <http://gitlab.com/howardabrams>
  ;; Maintainer: Howard X. Abrams
  ;; Created: January 18, 2025
  ;;
  ;; While obvious, GNU Emacs does not include this file or project.
  ;;
  ;; *NB:* Do not edit this file. Instead, edit the original literate file at:
  ;;            /Users/howard/src/hamacs/pud.org
  ;;       And tangle the file to recreate this one.
  ;;
  ;;; Code:
#+end_src

Make a simple mode for basic highlighting of =ev= code. Based on =ruby= (which seems to be close enough).

#+BEGIN_SRC emacs-lisp :tangle ~/.emacs.d/elisp/evennia-mode.el
  (define-derived-mode evennia-mode ruby-mode "Evennia"
    "Major mode for editing evennia batch command files.
      \\{evennia-mode-map}
      Turning on Evennia mode runs the normal hook `evennia-mode-hook'."
    (setq-local require-final-newline mode-require-final-newline)
    (setq-local comment-start "# ")
    (setq-local comment-start-skip "#+\\s-*"))
#+END_SRC

And add it to org blocks:

#+BEGIN_SRC emacs-lisp :tangle no
  (add-to-list 'org-babel-load-languages '(evennia . t))
#+END_SRC

Final stuff to require to include this major-mode:

#+BEGIN_SRC emacs-lisp :tangle ~/.emacs.d/elisp/evennia-mode.el
  ;; Add the mode to the auto-mode-alist for specific file extensions
  (add-to-list 'auto-mode-alist '("\\.ev\\'" . evennia-mode))

  ;; Provide the mode for use
  (provide 'evennia-mode)
#+END_SRC

How does this look?

#+BEGIN_SRC evennia :tangle /tmp/testing.ev
  # Comments, while not used much are comments.

  @one two = "three" :four
#+END_SRC

This client can =require= to depend on this mode.

#+BEGIN_SRC emacs-lisp export none
  (require 'evennia-mode)
#+END_SRC


* Technical Artifacts                                :noexport:

Let's =provide= a name so we can =require= this file:

#+begin_src emacs-lisp :exports none
  (provide 'pud)
  ;;; pud.el ends here
#+end_src

#+DESCRIPTION: a MUD client

#+PROPERTY:    header-args:sh :tangle no
#+PROPERTY:    header-args:emacs-lisp  :tangle yes
#+PROPERTY:    header-args    :results none :eval no-export :comments no mkdirp yes

#+OPTIONS:     num:nil toc:nil todo:nil tasks:nil tags:nil date:nil
#+OPTIONS:     skip:nil author:nil email:nil creator:nil timestamp:nil
#+INFOJS_OPT:  view:nil toc:nil ltoc:t mouse:underline buttons:0 path:http://orgmode.org/org-info.js
