Compare commits

...

6 commits

Author SHA1 Message Date
Howard Abrams
a351eeadfa Better search for a header in a demo 2025-06-09 09:48:27 -07:00
Howard Abrams
27eb55b154 Autocomplete is quite annoying
As one can't actually fix some mistakes on the command line.
2025-06-09 09:48:27 -07:00
Howard Abrams
f1e282d713 Remove lsp-rename that doesn't exist 2025-06-08 19:53:06 -07:00
Howard Abrams
a4ce781544 Use a fix-width font on exported code blocks 2025-06-08 19:51:48 -07:00
Howard Abrams
d598ec1e46 Fix connection bug with pud-login 2025-06-08 19:50:26 -07:00
Howard Abrams
cf79016484 Use community RSS for Org instead of my copy 2025-06-08 14:41:49 -07:00
7 changed files with 122 additions and 486 deletions

View file

@ -1,414 +0,0 @@
;;; ox-rss.el --- RSS 2.0 Back-End for Org Export Engine
;; Copyright (C) 2013-2015 Bastien Guerry
;; Author: Bastien Guerry <bzg@gnu.org>
;; Keywords: org, wp, blog, feed, rss
;; This file is not yet part of GNU Emacs.
;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; This library implements a RSS 2.0 back-end for Org exporter, based on
;; the `html' back-end.
;;
;; It requires Emacs 24.1 at least.
;;
;; It provides two commands for export, depending on the desired output:
;; `org-rss-export-as-rss' (temporary buffer) and `org-rss-export-to-rss'
;; (as a ".xml" file).
;;
;; This backend understands two new option keywords:
;;
;; #+RSS_EXTENSION: xml
;; #+RSS_IMAGE_URL: http://myblog.org/mypicture.jpg
;;
;; It uses #+HTML_LINK_HOME: to set the base url of the feed.
;;
;; Exporting an Org file to RSS modifies each top-level entry by adding a
;; PUBDATE property. If `org-rss-use-entry-url-as-guid', it will also add
;; an ID property, later used as the guid for the feed's item.
;;
;; The top-level headline is used as the title of each RSS item unless
;; an RSS_TITLE property is set on the headline.
;;
;; You typically want to use it within a publishing project like this:
;;
;; (add-to-list
;; 'org-publish-project-alist
;; '("homepage_rss"
;; :base-directory "~/myhomepage/"
;; :base-extension "org"
;; :rss-image-url "http://lumiere.ens.fr/~guerry/images/faces/15.png"
;; :html-link-home "http://lumiere.ens.fr/~guerry/"
;; :html-link-use-abs-url t
;; :rss-extension "xml"
;; :publishing-directory "/home/guerry/public_html/"
;; :publishing-function (org-rss-publish-to-rss)
;; :section-numbers nil
;; :exclude ".*" ;; To exclude all files...
;; :include ("index.org") ;; ... except index.org.
;; :table-of-contents nil))
;;
;; ... then rsync /home/guerry/public_html/ with your server.
;;
;; By default, the permalink for a blog entry points to the headline.
;; You can specify a different one by using the :RSS_PERMALINK:
;; property within an entry.
;;; Code:
(require 'ox-html)
(declare-function url-encode-url "url-util" (url))
;;; Variables and options
(defgroup org-export-rss nil
"Options specific to RSS export back-end."
:tag "Org RSS"
:group 'org-export
:version "24.4"
:package-version '(Org . "8.0"))
(defcustom org-rss-image-url "http://orgmode.org/img/org-mode-unicorn-logo.png"
"The URL of the an image for the RSS feed."
:group 'org-export-rss
:type 'string)
(defcustom org-rss-extension "xml"
"File extension for the RSS 2.0 feed."
:group 'org-export-rss
:type 'string)
(defcustom org-rss-categories 'from-tags
"Where to extract items category information from.
The default is to extract categories from the tags of the
headlines. When set to another value, extract the category
from the :CATEGORY: property of the entry."
:group 'org-export-rss
:type '(choice
(const :tag "From tags" from-tags)
(const :tag "From the category property" from-category)))
(defcustom org-rss-use-entry-url-as-guid t
"Use the URL for the <guid> metatag?
When nil, Org will create ids using `org-icalendar-create-uid'."
:group 'org-export-rss
:type 'boolean)
;;; Define backend
(org-export-define-derived-backend 'rss 'html
:menu-entry
'(?r "Export to RSS"
((?R "As RSS buffer"
(lambda (a s v b) (org-rss-export-as-rss a s v)))
(?r "As RSS file" (lambda (a s v b) (org-rss-export-to-rss a s v)))
(?o "As RSS file and open"
(lambda (a s v b)
(if a (org-rss-export-to-rss t s v)
(org-open-file (org-rss-export-to-rss nil s v)))))))
:options-alist
'((:description "DESCRIPTION" nil nil newline)
(:keywords "KEYWORDS" nil nil space)
(:with-toc nil nil nil) ;; Never include HTML's toc
(:rss-extension "RSS_EXTENSION" nil org-rss-extension)
(:rss-image-url "RSS_IMAGE_URL" nil org-rss-image-url)
(:rss-categories nil nil org-rss-categories))
:filters-alist '((:filter-final-output . org-rss-final-function))
:translate-alist '((headline . org-rss-headline)
(comment . (lambda (&rest args) ""))
(comment-block . (lambda (&rest args) ""))
(timestamp . (lambda (&rest args) ""))
(plain-text . org-rss-plain-text)
(section . org-rss-section)
(template . org-rss-template)))
;;; Export functions
;;;###autoload
(defun org-rss-export-as-rss (&optional async subtreep visible-only)
"Export current buffer to a RSS buffer.
If narrowing is active in the current buffer, only export its
narrowed part.
If a region is active, export that region.
A non-nil optional argument ASYNC means the process should happen
asynchronously. The resulting buffer should be accessible
through the `org-export-stack' interface.
When optional argument SUBTREEP is non-nil, export the sub-tree
at point, extracting information from the headline properties
first.
When optional argument VISIBLE-ONLY is non-nil, don't export
contents of hidden elements.
Export is done in a buffer named \"*Org RSS Export*\", which will
be displayed when `org-export-show-temporary-export-buffer' is
non-nil."
(interactive)
(let ((file (buffer-file-name (buffer-base-buffer))))
(org-icalendar-create-uid file 'warn-user)
(org-rss-add-pubdate-property))
(org-export-to-buffer 'rss "*Org RSS Export*"
async subtreep visible-only nil nil (lambda () (text-mode))))
;;;###autoload
(defun org-rss-export-to-rss (&optional async subtreep visible-only)
"Export current buffer to a RSS file.
If narrowing is active in the current buffer, only export its
narrowed part.
If a region is active, export that region.
A non-nil optional argument ASYNC means the process should happen
asynchronously. The resulting file should be accessible through
the `org-export-stack' interface.
When optional argument SUBTREEP is non-nil, export the sub-tree
at point, extracting information from the headline properties
first.
When optional argument VISIBLE-ONLY is non-nil, don't export
contents of hidden elements.
Return output file's name."
(interactive)
(let ((file (buffer-file-name (buffer-base-buffer))))
(org-icalendar-create-uid file 'warn-user)
(org-rss-add-pubdate-property))
(let ((outfile (org-export-output-file-name
(concat "." org-rss-extension) subtreep)))
(org-export-to-file 'rss outfile async subtreep visible-only)))
;;;###autoload
(defun org-rss-publish-to-rss (plist filename pub-dir)
"Publish an org file to RSS.
FILENAME is the filename of the Org file to be published. PLIST
is the property list for the given project. PUB-DIR is the
publishing directory.
Return output file name."
(let ((bf (get-file-buffer filename)))
(if bf
(with-current-buffer bf
(org-icalendar-create-uid filename 'warn-user)
(org-rss-add-pubdate-property)
(write-file filename))
(find-file filename)
(org-icalendar-create-uid filename 'warn-user)
(org-rss-add-pubdate-property)
(write-file filename) (kill-buffer)))
(org-publish-org-to
'rss filename (concat "." org-rss-extension) plist pub-dir))
;;; Main transcoding functions
(defun org-rss-headline (headline contents info)
"Transcode HEADLINE element into RSS format.
CONTENTS is the headline contents. INFO is a plist used as a
communication channel."
(unless (or (org-element-property :footnote-section-p headline)
;; Only consider first-level headlines
(> (org-export-get-relative-level headline info) 1))
(let* ((author (and (plist-get info :with-author)
(let ((auth (plist-get info :author)))
(and auth (org-export-data auth info)))))
(htmlext (plist-get info :html-extension))
(hl-number (org-export-get-headline-number headline info))
(hl-home (file-name-as-directory (plist-get info :html-link-home)))
(hl-pdir (plist-get info :publishing-directory))
(hl-perm (org-element-property :RSS_PERMALINK headline))
(anchor (org-export-get-reference headline info))
(category (org-rss-plain-text
(or (org-element-property :CATEGORY headline) "") info))
(pubdate0 (org-element-property :PUBDATE headline))
(pubdate (let ((system-time-locale "C"))
(if pubdate0
(format-time-string
"%a, %d %b %Y %H:%M:%S %z"
(org-time-string-to-time pubdate0)))))
(title (or (org-element-property :RSS_TITLE headline)
(replace-regexp-in-string
org-bracket-link-regexp
(lambda (m) (or (match-string 3 m)
(match-string 1 m)))
(org-element-property :raw-value headline))))
(publink
(or (and hl-perm (concat (or hl-home hl-pdir) hl-perm))
(concat
(or hl-home hl-pdir)
(file-name-nondirectory
(file-name-sans-extension
(plist-get info :input-file))) "." htmlext "#" anchor)))
(guid (if org-rss-use-entry-url-as-guid
publink
(org-rss-plain-text
(or (org-element-property :ID headline)
(org-element-property :CUSTOM_ID headline)
publink)
info))))
(if (not pubdate0) "" ;; Skip entries with no PUBDATE prop
(format
(concat
"<item>\n"
"<title>%s</title>\n"
"<link>%s</link>\n"
"<author>%s</author>\n"
"<guid isPermaLink=\"false\">%s</guid>\n"
"<pubDate>%s</pubDate>\n"
(org-rss-build-categories headline info) "\n"
"<description><![CDATA[%s]]></description>\n"
"</item>\n")
title publink author guid pubdate contents)))))
(defun org-rss-build-categories (headline info)
"Build categories for the RSS item."
(if (eq (plist-get info :rss-categories) 'from-tags)
(mapconcat
(lambda (c) (format "<category><![CDATA[%s]]></category>" c))
(org-element-property :tags headline)
"\n")
(let ((c (org-element-property :CATEGORY headline)))
(format "<category><![CDATA[%s]]></category>" c))))
(defun org-rss-template (contents info)
"Return complete document string after RSS conversion.
CONTENTS is the transcoded contents string. INFO is a plist used
as a communication channel."
(concat
(format "<?xml version=\"1.0\" encoding=\"%s\"?>"
(symbol-name org-html-coding-system))
"\n<rss version=\"2.0\"
xmlns:content=\"http://purl.org/rss/1.0/modules/content/\"
xmlns:wfw=\"http://wellformedweb.org/CommentAPI/\"
xmlns:dc=\"http://purl.org/dc/elements/1.1/\"
xmlns:atom=\"http://www.w3.org/2005/Atom\"
xmlns:sy=\"http://purl.org/rss/1.0/modules/syndication/\"
xmlns:slash=\"http://purl.org/rss/1.0/modules/slash/\"
xmlns:georss=\"http://www.georss.org/georss\"
xmlns:geo=\"http://www.w3.org/2003/01/geo/wgs84_pos#\"
xmlns:media=\"http://search.yahoo.com/mrss/\">"
"<channel>"
(org-rss-build-channel-info info) "\n"
contents
"</channel>\n"
"</rss>"))
(defun org-rss-build-channel-info (info)
"Build the RSS channel information."
(let* ((system-time-locale "C")
(title (plist-get info :title))
(email (org-export-data (plist-get info :email) info))
(author (and (plist-get info :with-author)
(let ((auth (plist-get info :author)))
(and auth (org-export-data auth info)))))
(date (format-time-string "%a, %d %b %Y %H:%M:%S %z")) ;; RFC 882
(description (org-export-data (plist-get info :description) info))
(lang (plist-get info :language))
(keywords (plist-get info :keywords))
(rssext (plist-get info :rss-extension))
(blogurl (or (plist-get info :html-link-home)
(plist-get info :publishing-directory)))
(image (url-encode-url (plist-get info :rss-image-url)))
(ifile (plist-get info :input-file))
(publink
(concat (file-name-as-directory blogurl)
(file-name-nondirectory
(file-name-sans-extension ifile))
"." rssext)))
(format
"\n<title>%s</title>
<atom:link href=\"%s\" rel=\"self\" type=\"application/rss+xml\" />
<link>%s</link>
<description><![CDATA[%s]]></description>
<language>%s</language>
<pubDate>%s</pubDate>
<lastBuildDate>%s</lastBuildDate>
<generator>%s</generator>
<webMaster>%s (%s)</webMaster>
<image>
<url>%s</url>
<title>%s</title>
<link>%s</link>
</image>
"
title publink blogurl description lang date date
(concat (format "Emacs %d.%d"
emacs-major-version
emacs-minor-version)
" Org-mode " (org-version))
email author image title blogurl)))
(defun org-rss-section (section contents info)
"Transcode SECTION element into RSS format.
CONTENTS is the section contents. INFO is a plist used as
a communication channel."
contents)
(defun org-rss-timestamp (timestamp contents info)
"Transcode a TIMESTAMP object from Org to RSS.
CONTENTS is nil. INFO is a plist holding contextual
information."
(org-html-encode-plain-text
(org-timestamp-translate timestamp)))
(defun org-rss-plain-text (contents info)
"Convert plain text into RSS encoded text."
(let (output)
(setq output (org-html-encode-plain-text contents)
output (org-export-activate-smart-quotes
output :html info))))
;;; Filters
(defun org-rss-final-function (contents backend info)
"Prettify the RSS output."
(with-temp-buffer
(xml-mode)
(insert contents)
(indent-region (point-min) (point-max))
(buffer-substring-no-properties (point-min) (point-max))))
;;; Miscellaneous
(defun org-rss-add-pubdate-property ()
"Set the PUBDATE property for top-level headlines."
(let (msg)
(org-map-entries
(lambda ()
(let* ((entry (org-element-at-point))
(level (org-element-property :level entry)))
(when (= level 1)
(unless (org-entry-get (point) "PUBDATE")
(setq msg t)
(org-set-property
"PUBDATE" (format-time-string
(cdr org-time-stamp-formats)))))))
nil nil 'comment 'archive)
(when msg
(message "Property PUBDATE added to top-level entries in %s"
(buffer-file-name))
(sit-for 2))))
(provide 'ox-rss)
;;; ox-rss.el ends here

View file

@ -446,7 +446,11 @@ These interactive functions scroll the “notes” in the other window in anothe
(call-interactively 'org-find-file)) (call-interactively 'org-find-file))
(setq ha-slide-presentation (buffer-name)) (setq ha-slide-presentation (buffer-name))
(when initial-heading (when initial-heading
(imenu initial-heading)) (goto-char (point-min))
(re-search-forward (rx bol
(one-or-more "*")
(one-or-more space)
(literal initial-heading))))
(cond (cond
((fboundp #'dslide-deck-forward) (call-interactively 'dslide-deck-start)) ((fboundp #'dslide-deck-forward) (call-interactively 'dslide-deck-start))
((fboundp #'org-present-next) (call-interactively 'org-present)) ((fboundp #'org-present-next) (call-interactively 'org-present))
@ -510,7 +514,7 @@ To make the contents of the expression easier to write, the =define-ha-demo= as
Probably best to explain this in an example: Probably best to explain this in an example:
\(define-demo demo1 \(define-ha-demo demo1
\(:buffer \"demonstrations.py\") \(message \"In a buffer\"\) \(:buffer \"demonstrations.py\") \(message \"In a buffer\"\)
\(:mode 'dired-mode\) \(message \"In a dired\"\) \(:mode 'dired-mode\) \(message \"In a dired\"\)
\(:heading \"Raven Civilizations\"\) \(message \"In an org file\"\)\) \(:heading \"Raven Civilizations\"\) \(message \"In an org file\"\)\)

View file

@ -27,13 +27,26 @@ A literate programming file for publishing my website using org.
* Introduction * Introduction
While the Emacs community have a plethora of options for generating a static website from org-formatted files, I keep my pretty simple, and use the standard =org-publish= feature. While the Emacs community have a plethora of options for generating a static website from org-formatted files, I keep my pretty simple, and use the standard =org-publish= feature.
The RSS needs UUIDs:
#+BEGIN_SRC emacs-lisp results silent
(use-package uuidgen
:straight (:host github :repo "emacsmirror/uuidgen"))
(defun org-icalendar-create-uid (&rest ignored)
"Returns a UUID."
(uuidgen-1))
#+END_SRC
While the following packages come with Emacs, they aren't necessarily loaded: While the following packages come with Emacs, they aren't necessarily loaded:
#+begin_src emacs-lisp :results silent #+begin_src emacs-lisp :results silent
(use-package ox-rss
:straight (:host github :repo "emacsmirror/ox-rss"))
(use-package org (use-package org
:config :config
(require 'ox-html) (require 'ox-html)
(require 'ox-rss)
(require 'ox-publish)) (require 'ox-publish))
#+end_src #+end_src

View file

@ -3,7 +3,7 @@
#+date: 2020-09-18 #+date: 2020-09-18
#+tags: emacs org #+tags: emacs org
#+startup: inlineimages #+startup: inlineimages
#+lastmod: [2025-03-11 Tue] #+lastmod: [2025-04-17 Thu]
A literate programming file for configuring org-mode and those files. A literate programming file for configuring org-mode and those files.
@ -771,11 +771,14 @@ Splitting out HTML snippets is often a way that I can transfer org-formatted con
(:link (@ :rel "stylesheet" (:link (@ :rel "stylesheet"
:type "text/css" :type "text/css"
:href "https://fonts.googleapis.com/css2?family=Overpass:ital,wght@0,300;0,600;1,300;1,600&display=swap")) :href "https://fonts.googleapis.com/css2?family=Overpass:ital,wght@0,300;0,600;1,300;1,600&display=swap"))
(:link (@ :rel "stylesheet"
:type "text/css"
:href "https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,200..900;1,200..900&display=swap"))
(:style ,(string-join '( (:style ,(string-join '(
"body { font-family: 'Literata', sans-serif; color: #333; }" "body { font-family: 'Literata', sans-serif; color: #333; }"
"h1,h2,h3,h4,h5 { font-family: 'Overpass', sans-serif; color: #333; }" "h1,h2,h3,h4,h5 { font-family: 'Overpass', sans-serif; color: #333; }"
"code { color: steelblue }" "code { font-family: 'Source Code Pro'; color: steelblue }"
"pre { background-color: #eee; border-color: #aaa; }" "pre { font-family: 'Source Code Pro'; background-color: #eee; border-color: #aaa; }"
"a { text-decoration-style: dotted }" "a { text-decoration-style: dotted }"
"@media (prefers-color-scheme: dark) {" "@media (prefers-color-scheme: dark) {"
" body { background-color: #1d1f21; color: white; }" " body { background-color: #1d1f21; color: white; }"
@ -871,7 +874,6 @@ Since this auto-correction needs to happen in /insert/ mode, I have bound a few
Of course I need a thesaurus, and I'm installing [[https://github.com/SavchenkoValeriy/emacs-powerthesaurus][powerthesaurus]]: Of course I need a thesaurus, and I'm installing [[https://github.com/SavchenkoValeriy/emacs-powerthesaurus][powerthesaurus]]:
#+begin_src emacs-lisp #+begin_src emacs-lisp
(use-package powerthesaurus (use-package powerthesaurus
:bind ("s-t" . powerthesaurus-lookup-dwim)
:config :config
(ha-leader (ha-leader
"s t" '(:ignore t :which-key "thesaurus") "s t" '(:ignore t :which-key "thesaurus")
@ -882,14 +884,19 @@ Of course I need a thesaurus, and I'm installing [[https://github.com/SavchenkoV
"s t u" '("usages" . powerthesaurus-lookup-sentences-dwim))) "s t u" '("usages" . powerthesaurus-lookup-sentences-dwim)))
#+end_src #+end_src
The key-bindings, keystrokes, and key-connections work well with ~M-T~ (notice the Shift), but to jump to specifics, we use a leader. The key-bindings, keystrokes, and key-connections work well with a hyper-command, but to jump to specifics, I use a leader.
#+BEGIN_SRC emacs-lisp
(use-package powerthesaurus
:bind ("s-t" . powerthesaurus-lookup-dwim))
#+END_SRC
*** Definitions *** Definitions
Since the /definitions/ do not work, so let's use the [[https://github.com/abo-abo/define-word][define-word]] project: Since the /definitions/ do not work, so let's use the [[https://github.com/abo-abo/define-word][define-word]] project:
#+begin_src emacs-lisp #+begin_src emacs-lisp
(use-package define-word (use-package define-word
:bind ("s-d" . define-word-at-point)
:config :config
(ha-leader :keymaps 'text-mode-map (ha-leader :keymaps 'text-mode-map
"s d" '(:ignore t :which-key "dictionary") "s d" '(:ignore t :which-key "dictionary")
@ -897,6 +904,13 @@ Since the /definitions/ do not work, so let's use the [[https://github.com/abo-a
"s d a" '("define any word" . define-word))) "s d a" '("define any word" . define-word)))
#+end_src #+end_src
And what about a binding when Im in insert mode:
#+BEGIN_SRC emacs-lisp
(use-package define-word
:bind ("s-d" . define-word-at-point))
#+END_SRC
After my enamoring of Noah Websters 1913 dictionary (originally due to reading [[https://janusworx.com/blog/thank-god-for-noah/][this essay]] by Mario Jason Braganza who referred to James Somers original [[https://jsomers.net/blog/dictionary][2014 blog entry]]), I easily followed the instructions from [[https://github.com/ponychicken/WebsterParser][WebsterParser]], a Github project, with the dictionary: After my enamoring of Noah Websters 1913 dictionary (originally due to reading [[https://janusworx.com/blog/thank-god-for-noah/][this essay]] by Mario Jason Braganza who referred to James Somers original [[https://jsomers.net/blog/dictionary][2014 blog entry]]), I easily followed the instructions from [[https://github.com/ponychicken/WebsterParser][WebsterParser]], a Github project, with the dictionary:
1. Download [[https://github.com/ponychicken/WebsterParser/releases/latest/download/websters-1913.dictionary.zip][the dictionary]] file. 1. Download [[https://github.com/ponychicken/WebsterParser/releases/latest/download/websters-1913.dictionary.zip][the dictionary]] file.
2. Unzip the archive … have a *Finder* window open to the =.dictionary= file. 2. Unzip the archive … have a *Finder* window open to the =.dictionary= file.
@ -935,9 +949,7 @@ The [[https://github.com/bnbeckwith/writegood-mode][writegood-mode]] is effectiv
#+end_src #+end_src
And it reports obnoxious messages. And it reports obnoxious messages.
Hrm::hook ((org-mode . writegood-mode) Note: Instead of hooking the =writegood-mode= to Org files, I will hook it to =flycheck= instead.
(gfm-mode . writegood-mode)
(markdown-mode) . writegood-mode)
We install the =write-good= NPM: We install the =write-good= NPM:
#+begin_src shell #+begin_src shell

View file

@ -347,8 +347,7 @@ Now that the [[file:ha-programming.org::*Language Server Protocol (LSP) Integrat
("Server" ("Server"
(("l" python-lsp/body "LSP...")) (("l" python-lsp/body "LSP..."))
"Edit" "Edit"
(("r" lsp-rename "Rename") (("=" lsp-format-region "Format"))
("=" lsp-format-region "Format"))
"Navigate" "Navigate"
(("A" lsp-workspace-folders-add "Add Folder") (("A" lsp-workspace-folders-add "Add Folder")
("R" lsp-workspace-folders-remove "Remove Folder")) ("R" lsp-workspace-folders-remove "Remove Folder"))

119
pud.org
View file

@ -2,7 +2,7 @@
#+author: Howard X. Abrams #+author: Howard X. Abrams
#+date: 2025-01-18 #+date: 2025-01-18
#+filetags: emacs hamacs #+filetags: emacs hamacs
#+lastmod: [2025-03-21 Fri] #+lastmod: [2025-06-08 Sun]
A literate programming file for a Comint-based MUD client. A literate programming file for a Comint-based MUD client.
@ -30,17 +30,31 @@ A literate programming file for a Comint-based MUD client.
This project is a simple MUD client for Emacs, based on COM-INT MUD client I learn about on Mickey Petersens [[https://www.masteringemacs.org/article/comint-writing-command-interpreter][essay on Comint]]. This project is a simple MUD client for Emacs, based on COM-INT MUD client I learn about on Mickey Petersens [[https://www.masteringemacs.org/article/comint-writing-command-interpreter][essay on Comint]].
This uses =telnet= (at the moment) for the connection, so you will need to install that first. On Mac, this would be: This uses eithr =ssh= or good ol =telnet= for the connection. Surprised that one can still install in on a Mac, like:
#+BEGIN_SRC sh #+BEGIN_SRC sh
brew install telent brew install telnet
#+END_SRC #+END_SRC
And use a similar command on Linux. Use your favorite way to install Emacs packages, for instance, with Emacs 30, once can install it, and customize some 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 ** Customization
You may want to customize your connections to more worlds. 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.
The default connects to *Moss n Puddles*, my own MUD which I invite you to join.
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(defgroup pud nil (defgroup pud nil
@ -88,27 +102,12 @@ The default connects to *Moss n Puddles*, my own MUD which I invite you to jo
:group 'pud) :group 'pud)
#+END_SRC #+END_SRC
For instance: 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 #+BEGIN_SRC emacs-lisp :tangle no :eval no
(use-package pud (setopt pud-worlds
:custom '(["Moss-n-Puddles" ssh "howardabrams.com" 4004 "howard" "" "connect %s %s"]
(pud-worlds ["Moss-n-Puddles" ssh "howardabrams.com" 4004 "rick" "" "connect %s %s"]
'(["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
Hidden:
#+BEGIN_SRC emacs-lisp :tangle no :eval no
(setq 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"]
["Local-Moss" telnet "localhost" 4000 "howard" "" ""] ["Local-Moss" telnet "localhost" 4000 "howard" "" ""]
["Local-Moss" telnet "localhost" 4000 "rick" "" ""])) ["Local-Moss" telnet "localhost" 4000 "rick" "" ""]))
#+END_SRC #+END_SRC
@ -129,7 +128,7 @@ Next, open [[file:~/.authinfo.gpg][your authinfo file]], and insert the followin
machine howardabrams.com login [name] port 4000 password [pass] machine howardabrams.com login [name] port 4000 password [pass]
#+END_SRC #+END_SRC
Now, lets play! Type =run-pud=, and optionally select a world. If you get disconnected, re-run it, or even =pud-reconnect=. Now, lets 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. The rest of this file describes the code implementing this project.
* Code * Code
@ -163,7 +162,6 @@ Choosing a world… er, connection using a =completing-read= allowing you to cho
(t (customize-option 'pud-worlds))))) (t (customize-option 'pud-worlds)))))
#+END_SRC #+END_SRC
The following functions are accessibility functions to the world entry. The following functions are accessibility functions to the world entry.
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
@ -182,7 +180,7 @@ The following functions are accessibility functions to the world entry.
(defun pud-world-creds (world) (defun pud-world-creds (world)
"Return the username and password from WORLD. "Return the username and password from WORLD.
Multiple search queries for the .authinfo file." Multiple search queries for the .authinfo file."
(seq-let (label host port user) world (seq-let (label conn-type host port user) world
(if-let ((auth-results (first (auth-source-search (if-let ((auth-results (first (auth-source-search
:host host :host host
:port port :port port
@ -200,8 +198,8 @@ And some basic functions I should expand.
(ert-deftest pud-world-name-test () (ert-deftest pud-world-name-test ()
(should (string-equal (pud-world-name "foobar") "foobar")) (should (string-equal (pud-world-name "foobar") "foobar"))
(should (string-equal (pud-world-name ["foobar" "localhost" "4000"]) "foobar")) (should (string-equal (pud-world-name ["foobar" "localhost" "4000"]) "foobar"))
(should (string-equal (pud-world-name ["foobar" "localhost" "4000" nil]) "foobar"))
(should (string-equal (pud-world-name ["foobar" "localhost" "4000" ""]) "foobar")) (should (string-equal (pud-world-name ["foobar" "localhost" "4000" ""]) "foobar"))
(should (string-equal (pud-world-name ["foobar" "localhost" "4000" nil]) "foobar"))
(should (string-equal (pud-world-name ["foobar" "localhost" "4000" "guest" "guest"]) "guest@foobar"))) (should (string-equal (pud-world-name ["foobar" "localhost" "4000" "guest" "guest"]) "guest@foobar")))
(ert-deftest pud-world-network-test () (ert-deftest pud-world-network-test ()
@ -221,9 +219,6 @@ And some basic functions I should expand.
#+END_SRC #+END_SRC
* Basics * Basics
:LOGBOOK:
CLOCK: [2025-03-03 Mon 11:57]--[2025-03-03 Mon 12:10] => 0:13
:END:
Using Comint, and hoping to have the ANSI colors displayed. Using Comint, and hoping to have the ANSI colors displayed.
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
@ -235,12 +230,12 @@ Im going to use good ol fashion =telnet= for the connection:
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(defcustom pud-telnet-path "telnet" (defcustom pud-telnet-path "telnet"
"Path to the program used by `run-pud' to connect using telnet." "Path to the program used by `pud-run' to connect using telnet."
:type '(string) :type '(string)
:group 'pud) :group 'pud)
(defcustom pud-ssh-path "ssh" (defcustom pud-ssh-path "ssh"
"Path to the program used by `run-pud' to connect using ssh." "Path to the program used by `pud-run' to connect using ssh."
:type '(string) :type '(string)
:group 'pud) :group 'pud)
#+END_SRC #+END_SRC
@ -259,7 +254,6 @@ Command string to use, given a =world= with a connection type:
"Return a command string to pass to the shell. "Return a command string to pass to the shell.
The WORLD is a vector with the hostname, see `pud-worlds'." The WORLD is a vector with the hostname, see `pud-worlds'."
(seq-let (host port) (pud-world-network world) (seq-let (host port) (pud-world-network world)
(message "Dealing with: %s %s %s" host port (aref world 1))
(cl-case (aref world 1) (cl-case (aref world 1)
(telnet (append (cons pud-telnet-path pud-cli-arguments) (telnet (append (cons pud-telnet-path pud-cli-arguments)
(list host port))) (list host port)))
@ -286,14 +280,14 @@ The empty and currently disused mode map for storing our custom keybindings inhe
(let ((map (nconc (make-sparse-keymap) comint-mode-map))) (let ((map (nconc (make-sparse-keymap) comint-mode-map)))
(define-key map "\t" 'completion-at-point) (define-key map "\t" 'completion-at-point)
map) map)
"Basic mode map for `run-pud'.") "Basic mode map for `pud-run'.")
#+END_SRC #+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 dont have prompts. This holds a regular expression that matches the prompt style for the MUD. Not sure if this is going to work, since MUDs typically dont have prompts.
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(defvar pud-prompt-regexp "" ; "^\\(?:\\[[^@]+@[^@]+\\]\\)" (defvar pud-prompt-regexp "" ; "^\\(?:\\[[^@]+@[^@]+\\]\\)"
"Prompt for `run-pud'.") "Prompt for `pud-run'.")
#+END_SRC #+END_SRC
The name of the buffer: The name of the buffer:
@ -305,13 +299,14 @@ The name of the buffer:
#+END_SRC #+END_SRC
** Run and Connect ** Run and Connect
The main entry point to the program is the =run-pud= function: The main entry point to the program is the =pud-run= function:
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(defun run-pud (world) (defun pud-run (world)
"Run an inferior instance of `pud-cli' inside Emacs. "Run an inferior instance of `pud-cli' inside Emacs.
The WORLD should be vector containing the following: The WORLD should be vector containing the following:
- label for the world - label for the world
- server type, 'ssh or 'telnet
- server hostname - server hostname
- server port - server port
- username (can be overridden) - username (can be overridden)
@ -328,7 +323,7 @@ The main entry point to the program is the =run-pud= function:
(apply 'make-comint-in-buffer "Pud" buffer (car pud-cli) nil (cdr pud-cli)) (apply 'make-comint-in-buffer "Pud" buffer (car pud-cli) nil (cdr pud-cli))
(pud-mode) (pud-mode)
(visual-line-mode 1) (visual-line-mode 1)
(pud-reconnect world))) (pud-login world)))
;; Regardless, provided we have a valid buffer, we pop to it. ;; Regardless, provided we have a valid buffer, we pop to it.
(when buffer (when buffer
(pop-to-buffer buffer)))) (pop-to-buffer buffer))))
@ -337,29 +332,57 @@ The main entry point to the program is the =run-pud= function:
Connection and/or re-connection: Connection and/or re-connection:
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
(defun pud-reconnect (world) (defun pud-login (world)
"Collect and send a `connect' sequence to 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." 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))) (interactive (list (pud-get-world)))
(when (called-interactively-p) (when (called-interactively-p)
(pop-to-buffer (pud-buffer-name world))) (pop-to-buffer (pud-buffer-name world)))
(sit-for 1) (sit-for 1)
(message "Attempting to log in...") (length world)
(message "Attempting to log in to %s..." (pud-world-name world))
(seq-let (username password) (pud-world-creds world) (seq-let (username password) (pud-world-creds world)
(let* ((conn-str (if (length> world 5) (let* ((local-conn (when (length> world 6)
(aref world 5) (aref world 6 )))
(conn-str (if (and local-conn
(not (string-blank-p local-conn)))
local-conn
pud-default-connection-string)) pud-default-connection-string))
(conn-full (format conn-str username password)) (conn-full (format conn-str username password))
(process (get-buffer-process (current-buffer)))) (process (get-buffer-process (current-buffer))))
(message "proc: %s str: '%s'" process conn-full)
(goto-char (point-max)) (goto-char (point-max))
(if process (if process
(comint-send-string process conn-full) (comint-send-string process conn-full)
(insert conn-full))))) (insert conn-full)))))
#+END_SRC #+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 * 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. 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.
@ -370,7 +393,7 @@ Note that =comint-process-echoes=, depending on the mode and the circumstances,
(setq comint-use-prompt-regexp nil)) (setq comint-use-prompt-regexp nil))
(define-derived-mode pud-mode comint-mode "Pud" (define-derived-mode pud-mode comint-mode "Pud"
"Major mode for `run-pud'. "Major mode for `pud-run'.
\\<pud-mode-map>" \\<pud-mode-map>"
;; this sets up the prompt so it matches things like: [foo@bar] ;; this sets up the prompt so it matches things like: [foo@bar]

View file

@ -18,7 +18,7 @@ This creates the following files:
#!/usr/bin/env zsh #!/usr/bin/env zsh
# #
# My complete Zshell configuration. Don't edit this file. # My complete Zshell configuration. Don't edit this file.
# Instead edit: ~/technical/zshell.org and tangle it. # Instead edit: zshell.org and tangle it.
# #
#+END_SRC #+END_SRC
@ -26,7 +26,7 @@ This creates the following files:
#!/usr/bin/env zsh #!/usr/bin/env zsh
# #
# Non-interactive, mostly easily settable environment variables. Don't # Non-interactive, mostly easily settable environment variables. Don't
# edit this file. # Instead edit: ~/technical/zshell.org and tangle. # edit this file. Instead edit: zshell.org and tangle.
# #
#+END_SRC #+END_SRC
@ -133,9 +133,9 @@ If you type something wrong, Zshell, by default, prompts to see if you wanted to
ENABLE_CORRECTION="true" ENABLE_CORRECTION="true"
#+END_SRC #+END_SRC
What about just /fixing it/? For this, we update the [[https://zsh.sourceforge.io/Doc/Release/Zsh-Line-Editor.html][ZShell line editor]]: What about just /fixing it/? For this, I thought to update the [[https://zsh.sourceforge.io/Doc/Release/Zsh-Line-Editor.html][ZShell line editor]] with something like:
#+BEGIN_SRC zsh #+BEGIN_SRC zsh :tangle no
autocorrect() { autocorrect() {
zle .spell-word zle .spell-word
zle .$WIDGET zle .$WIDGET
@ -145,12 +145,7 @@ What about just /fixing it/? For this, we update the [[https://zsh.sourceforge.i
zle -N magic-space autocorrect zle -N magic-space autocorrect
#+END_SRC #+END_SRC
Bind the ability to auto-correct the word to the left with =Space= or =Enter=: Now you cant insert a space as it attempts to correct it. Not worth the space savings.
#+BEGIN_SRC zsh
bindkey ' ' magic-space
#+END_SRC
** Waiting Indication ** Waiting Indication
Display red dots whilst waiting for commands to complete. Display red dots whilst waiting for commands to complete.
@ -374,3 +369,7 @@ To let us know we read the =~/.zshrc= file:
#+options: num:nil toc:t todo:nil tasks:nil tags:nil date:nil #+options: num:nil toc:t todo:nil tasks:nil tags:nil date:nil
#+options: skip:nil author:nil email:nil creator:nil timestamp:nil #+options: skip:nil author:nil email:nil creator:nil timestamp:nil
#+infojs_opt: view:nil toc:t ltoc:t mouse:underline buttons:0 path:http://orgmode.org/org-info.js #+infojs_opt: view:nil toc:t ltoc:t mouse:underline buttons:0 path:http://orgmode.org/org-info.js
# Local Variables:
# eval: (add-hook 'after-save-hook #'org-babel-tangle t t)
# End: