Compare commits
6 commits
0743dd1dbc
...
a351eeadfa
Author | SHA1 | Date | |
---|---|---|---|
|
a351eeadfa | ||
|
27eb55b154 | ||
|
f1e282d713 | ||
|
a4ce781544 | ||
|
d598ec1e46 | ||
|
cf79016484 |
7 changed files with 122 additions and 486 deletions
414
elisp/ox-rss.el
414
elisp/ox-rss.el
|
@ -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
|
|
@ -446,7 +446,11 @@ These interactive functions scroll the “notes” in the other window in anothe
|
|||
(call-interactively 'org-find-file))
|
||||
(setq ha-slide-presentation (buffer-name))
|
||||
(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
|
||||
((fboundp #'dslide-deck-forward) (call-interactively 'dslide-deck-start))
|
||||
((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:
|
||||
|
||||
\(define-demo demo1
|
||||
\(define-ha-demo demo1
|
||||
\(:buffer \"demonstrations.py\") \(message \"In a buffer\"\)
|
||||
\(:mode 'dired-mode\) \(message \"In a dired\"\)
|
||||
\(:heading \"Raven Civilizations\"\) \(message \"In an org file\"\)\)
|
||||
|
|
|
@ -27,13 +27,26 @@ A literate programming file for publishing my website using org.
|
|||
* 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.
|
||||
|
||||
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:
|
||||
|
||||
#+begin_src emacs-lisp :results silent
|
||||
(use-package ox-rss
|
||||
:straight (:host github :repo "emacsmirror/ox-rss"))
|
||||
|
||||
(use-package org
|
||||
:config
|
||||
(require 'ox-html)
|
||||
(require 'ox-rss)
|
||||
(require 'ox-publish))
|
||||
#+end_src
|
||||
|
||||
|
|
30
ha-org.org
30
ha-org.org
|
@ -3,7 +3,7 @@
|
|||
#+date: 2020-09-18
|
||||
#+tags: emacs org
|
||||
#+startup: inlineimages
|
||||
#+lastmod: [2025-03-11 Tue]
|
||||
#+lastmod: [2025-04-17 Thu]
|
||||
|
||||
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"
|
||||
:type "text/css"
|
||||
: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 '(
|
||||
"body { font-family: 'Literata', sans-serif; color: #333; }"
|
||||
"h1,h2,h3,h4,h5 { font-family: 'Overpass', sans-serif; color: #333; }"
|
||||
"code { color: steelblue }"
|
||||
"pre { background-color: #eee; border-color: #aaa; }"
|
||||
"code { font-family: 'Source Code Pro'; color: steelblue }"
|
||||
"pre { font-family: 'Source Code Pro'; background-color: #eee; border-color: #aaa; }"
|
||||
"a { text-decoration-style: dotted }"
|
||||
"@media (prefers-color-scheme: dark) {"
|
||||
" 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]]:
|
||||
#+begin_src emacs-lisp
|
||||
(use-package powerthesaurus
|
||||
:bind ("s-t" . powerthesaurus-lookup-dwim)
|
||||
:config
|
||||
(ha-leader
|
||||
"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)))
|
||||
#+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
|
||||
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
|
||||
(use-package define-word
|
||||
:bind ("s-d" . define-word-at-point)
|
||||
:config
|
||||
(ha-leader :keymaps 'text-mode-map
|
||||
"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)))
|
||||
#+end_src
|
||||
|
||||
And what about a binding when I’m 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 Webster’s 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.
|
||||
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
|
||||
And it reports obnoxious messages.
|
||||
|
||||
Hrm::hook ((org-mode . writegood-mode)
|
||||
(gfm-mode . writegood-mode)
|
||||
(markdown-mode) . writegood-mode)
|
||||
Note: Instead of hooking the =writegood-mode= to Org files, I will hook it to =flycheck= instead.
|
||||
|
||||
We install the =write-good= NPM:
|
||||
#+begin_src shell
|
||||
|
|
|
@ -347,8 +347,7 @@ Now that the [[file:ha-programming.org::*Language Server Protocol (LSP) Integrat
|
|||
("Server"
|
||||
(("l" python-lsp/body "LSP..."))
|
||||
"Edit"
|
||||
(("r" lsp-rename "Rename")
|
||||
("=" lsp-format-region "Format"))
|
||||
(("=" lsp-format-region "Format"))
|
||||
"Navigate"
|
||||
(("A" lsp-workspace-folders-add "Add Folder")
|
||||
("R" lsp-workspace-folders-remove "Remove Folder"))
|
||||
|
|
119
pud.org
119
pud.org
|
@ -2,7 +2,7 @@
|
|||
#+author: Howard X. Abrams
|
||||
#+date: 2025-01-18
|
||||
#+filetags: emacs hamacs
|
||||
#+lastmod: [2025-03-21 Fri]
|
||||
#+lastmod: [2025-06-08 Sun]
|
||||
|
||||
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 Petersen’s [[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
|
||||
brew install telent
|
||||
brew install telnet
|
||||
#+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
|
||||
|
||||
You may want to customize your connections to more worlds.
|
||||
The default connects to *Moss ‘n Puddles*, my own MUD which I invite you to join.
|
||||
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
|
||||
|
@ -88,27 +102,12 @@ The default connects to *Moss ‘n Puddles*, my own MUD which I invite you to jo
|
|||
:group 'pud)
|
||||
#+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
|
||||
(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
|
||||
|
||||
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"]
|
||||
(setopt pud-worlds
|
||||
'(["Moss-n-Puddles" ssh "howardabrams.com" 4004 "howard" "" "connect %s %s"]
|
||||
["Moss-n-Puddles" ssh "howardabrams.com" 4004 "rick" "" "connect %s %s"]
|
||||
["Local-Moss" telnet "localhost" 4000 "howard" "" ""]
|
||||
["Local-Moss" telnet "localhost" 4000 "rick" "" ""]))
|
||||
#+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]
|
||||
#+END_SRC
|
||||
|
||||
Now, let’s play! Type =run-pud=, and optionally select a world. If you get disconnected, re-run it, or even =pud-reconnect=.
|
||||
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
|
||||
|
@ -163,7 +162,6 @@ Choosing a world… er, connection using a =completing-read= allowing you to cho
|
|||
(t (customize-option 'pud-worlds)))))
|
||||
#+END_SRC
|
||||
|
||||
|
||||
The following functions are accessibility functions to the world entry.
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
|
@ -182,7 +180,7 @@ The following functions are accessibility functions to the world entry.
|
|||
(defun pud-world-creds (world)
|
||||
"Return the username and password from WORLD.
|
||||
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
|
||||
:host host
|
||||
:port port
|
||||
|
@ -200,8 +198,8 @@ And some basic functions I should expand.
|
|||
(ert-deftest pud-world-name-test ()
|
||||
(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" nil]) "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")))
|
||||
|
||||
(ert-deftest pud-world-network-test ()
|
||||
|
@ -221,9 +219,6 @@ And some basic functions I should expand.
|
|||
#+END_SRC
|
||||
|
||||
* 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.
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
|
@ -235,12 +230,12 @@ 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 `run-pud' to connect using 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 `run-pud' to connect using ssh."
|
||||
"Path to the program used by `pud-run' to connect using ssh."
|
||||
:type '(string)
|
||||
:group 'pud)
|
||||
#+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.
|
||||
The WORLD is a vector with the hostname, see `pud-worlds'."
|
||||
(seq-let (host port) (pud-world-network world)
|
||||
(message "Dealing with: %s %s %s" host port (aref world 1))
|
||||
(cl-case (aref world 1)
|
||||
(telnet (append (cons pud-telnet-path pud-cli-arguments)
|
||||
(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)))
|
||||
(define-key map "\t" 'completion-at-point)
|
||||
map)
|
||||
"Basic mode map for `run-pud'.")
|
||||
"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 `run-pud'.")
|
||||
"Prompt for `pud-run'.")
|
||||
#+END_SRC
|
||||
|
||||
The name of the buffer:
|
||||
|
@ -305,13 +299,14 @@ The name of the buffer:
|
|||
#+END_SRC
|
||||
|
||||
** 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
|
||||
(defun run-pud (world)
|
||||
(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)
|
||||
|
@ -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))
|
||||
(pud-mode)
|
||||
(visual-line-mode 1)
|
||||
(pud-reconnect world)))
|
||||
(pud-login world)))
|
||||
;; Regardless, provided we have a valid buffer, we pop to it.
|
||||
(when 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:
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(defun pud-reconnect (world)
|
||||
(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."
|
||||
|
||||
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)
|
||||
|
||||
(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)
|
||||
(let* ((conn-str (if (length> world 5)
|
||||
(aref world 5)
|
||||
(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))))
|
||||
|
||||
(message "proc: %s str: '%s'" process conn-full)
|
||||
(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.
|
||||
|
||||
|
@ -370,7 +393,7 @@ Note that =comint-process-echoes=, depending on the mode and the circumstances,
|
|||
(setq comint-use-prompt-regexp nil))
|
||||
|
||||
(define-derived-mode pud-mode comint-mode "Pud"
|
||||
"Major mode for `run-pud'.
|
||||
"Major mode for `pud-run'.
|
||||
|
||||
\\<pud-mode-map>"
|
||||
;; this sets up the prompt so it matches things like: [foo@bar]
|
||||
|
|
19
zshell.org
19
zshell.org
|
@ -18,7 +18,7 @@ This creates the following files:
|
|||
#!/usr/bin/env zsh
|
||||
#
|
||||
# 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
|
||||
|
||||
|
@ -26,7 +26,7 @@ This creates the following files:
|
|||
#!/usr/bin/env zsh
|
||||
#
|
||||
# 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
|
||||
|
||||
|
@ -133,9 +133,9 @@ If you type something wrong, Zshell, by default, prompts to see if you wanted to
|
|||
ENABLE_CORRECTION="true"
|
||||
#+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() {
|
||||
zle .spell-word
|
||||
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
|
||||
#+END_SRC
|
||||
|
||||
Bind the ability to auto-correct the word to the left with =Space= or =Enter=:
|
||||
|
||||
#+BEGIN_SRC zsh
|
||||
bindkey ' ' magic-space
|
||||
#+END_SRC
|
||||
|
||||
Now you can’t insert a space as it attempts to correct it. Not worth the space savings.
|
||||
** Waiting Indication
|
||||
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: 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
|
||||
|
||||
# Local Variables:
|
||||
# eval: (add-hook 'after-save-hook #'org-babel-tangle t t)
|
||||
# End:
|
||||
|
|
Loading…
Reference in a new issue