💾 Archived View for gem.librehacker.com › gemlog › starlog › 20240315-0.gmi captured on 2024-03-21 at 15:07:48. Gemini links have been rewritten to link to archived content

View Raw

More Information

➡️ Next capture (2024-06-16)

-=-=-=-=-=-=-

Emacs: Completion And Function Bundles (publ. 2024-03-15)

Something has worked very well for me, due to the rapidity of calling commands through fuzzy completion, is to wrap simple tasks in Emacs functions with descriptive names. This is particularly convenient due to the availability of the shell-command and async-shell-command functions, which allow me to call any command that I would normally call in a shell. E.g.:

(defvar filesystem-string "/dev/mapper/cryptroot")

(defun filesystem-size ()
  (interactive)
  (shell-command (concat "df -Th " filesystem-string)))

Then M-x filesystem-size gives

Filesystem            Type  Size  Used Avail Use% Mounted on
/dev/mapper/cryptroot ext4  229G   98G  120G  45% /

Where this starts to become cumbersome, however, is if we have a lot of very similar functions. We want some way to group them. E.g., one helpful application is a function that calls browse-url-xdg-open multiple times to quickly open a bundle of Web pages in the Web browser:

(defun weather-reports-fairbanks ()
  (interactive)
  (map nil
       (lambda (url)
         (browse-url-xdg-open url))
       (list
        "https://clearoutside.com/forecast/64.82/-147.86"
        "https://w1.weather.gov/obhistory/PAFA.html"
        "https://forecast.weather.gov/MapClick.php?\
lat=64.84057000000007&lon=-147.86436499999996"
        "https://forecast.weather.gov/MapClick.php?\

This function opens four weather-related Web pages at the same time. Very convenient. But what if we want another function that opens news-related Web pages, and another function that opens work-related Web pages, and so on? I want some kind of grouping.

One solution is Emacs' transient system, where calling a command gets you a page with keypress shortcuts. But, I don't like that. I want to deal with easy to remember words and phrases, not single letters attached to commands.

I want to keep calling commands like I am already doing, while holding onto the convenience of completion. Thankfully completion, including helm completion, is tied into the completing-read function:

(completing-read PROMPT COLLECTION &optional PREDICATE REQUIRE-MATCH
INITIAL-INPUT HIST DEF INHERIT-INPUT-METHOD)
Read a string in the minibuffer, with completion.
PROMPT is a string to prompt with; normally it ends in a colon and a space.
COLLECTION can be a list of strings, an alist, an obarray or a hash table.
COLLECTION can also be a function to do the completion itself.
PREDICATE limits completion to a subset of COLLECTION.

Basically, we can just call this function anywhere — passing it a list of possible keys — and it will prompt the user, along with completion — built-in completion, helm completion, or anything else that is layered on top of it. It can take an association list as well, which will prove convenient.

I pushed forward with the idea of having "bundles" of functions, which are just association lists with keywords for the car and lambdas for the cdr. I have one general function that takes a bundle, prompts the user, and then calls the appropriate function from the bundle.

(defun call-function-bundle (bundle)
  (let* ((key
          (completing-read "call: " bundle))
         (f (alist-get (intern key) bundle)))
    (funcall f)))

Here is a bundle I have right now, with one helper function:

(defun open-links (url-list)
  (map nil
       (lambda (url)
         (browse-url-xdg-open url)) url-list))

(defvar web-pages-bundle nil)

(setq web-pages-bundle
      `((weather
         . ,(lambda ()
              (open-links
               '("https://clearoutside.com/forecast/64.82/-147.86"
                 "https://w1.weather.gov/obhistory/PAFA.html"
                 "https://forecast.weather.gov/MapClick.php?\
lat=64.84057000000007&lon=-147.86436499999996"
                 "https://forecast.weather.gov/MapClick.php?\
lat=64.8406&lon=-147.8644&unit=0&lg=english&FcstType=graphical"))))
        (news
         . ,(lambda ()
              (open-foi-news-digest)
              (open-links
               '("https://www.marklevinshow.com/"))))))

The advantage here of using lambdas, instead of just having URLs that are processed, is it allows me to insert more advanced functionality when needed, like the open-foi-news-digest function used above, which I have mentioned previously:

(defun open-foi-news-digest (&optional time)
  "Constructs the URL for today's Friends of Israel news digest and submits it to xdg-open."
  (interactive)
  (let ((time (if time time (current-time))))
    (browse-url-xdg-open
     (format-time-string "https://www.foi.org/news/news-digest-%-m-%-d-%y/" time))))

And here is the top-level function for the web pages bundle:

(defun web-pages ()
  (interactive)
  (call-function-bundle web-pages-bundle))

This approach is quite simple and easy to use, assuming we are comfortable with basic elisp. I'm thinking it might be nice though to add some syntax — via macro — to make building the function bundle a little cleaner. And maybe another macro for making the top level function. Or maybe one macro that does both.