Demonstrations in Emacs, Part II

A decade ago, when faced with the prospect of demonstrating Emacs and my use of Org mode (I called literate devops), I enhanced some home-grown Lisp code, and released a package for Emacs, demo-it. While I’ve been using it for years, my level of frustration for its sequential pedanticism seemed unfit for my current job.

After crafting a new solution, I realized I had something more general that may have wider application. So come along with me on another excursion in Hacking Emacs Lisp …

Initial Idea: Function per Org Header

My frustrations with demo-it include:

  • Forced sequence. Can’t go backward without restarting the entire process.
  • Code all the Steps. Moving through a presentation means you have to code all the next steps like moving to the next slide.
  • Forced steps. Feel like you need to skip something? Nope, you must iterate through all the steps.

While creating a long presentation for my teammates, I merely needed to run two commands, so instead of crafting a long demo-it sequence, I figured I would write two functions, demo-step-1 and demo-step-2 and call them with M-x. Simple, eh?

I noticed all my demonstrations, even back then, revolve around an Org file that served as the presentation. This got me thinking, what if, during a presentation, I kick off a function based on the heading?

Almost trivial, for instance:

(defun demo-step ()
  "Run some functions based on the current heading."
  (interactive)
  (pcase (org-get-heading)
    ("Slide One"  (browse-url "http://someplace.io"))
    ("Slide Four" (find-file "~/foo/bar/something.py"))
    ("Slide Five" (eshell)
    ;; ...


(global-set-key (kbd "<f6>") 'demo-step)

This works well! At any particular place in the Org presentation, I can hit the f6 to run this function, and if the current Org heading matches, it runs the associated function. I could even put a default condition of _ to the pcase that could advance my presentation. This made demonstration-presentations considerably easier.

Any downside? Yeah, I always need to return to the Org file, so my triggering function, demo-step, has something to key from. My first solution was to have each called function automatically return to the presentation, for instance:

(defun demo-step-4 ()
  (find-file "~/foo/bar/something.py")
  (pop-to-buffer "my-presentation.org"))

;; ...

(defun demo-step ()
  "Call some functions based on current heading."
  (interactive)
  (pcase (org-get-heading)
    ("Slide One"  (demo-step-1))
    ("Slide Four" (demo-step-4))
    ("Slide Five" (demo-step-5))
  ;; ...

Second Attempt: Multiple Functions per Header

I noticed I sometimes need to trigger a series of functions from the same headline.

Let’s replace the org-get-heading with a function that returns both the current heading and the number of times called. To do this, I keep track of the last heading seen, and compare it to the current heading. Different? Return 0 (for the first time seen), otherwise, return an incremented version:

(defun demo-heading ()
  "New approach to making demonstrations in Emacs"
  (interactive)
  (let ((heading (org-get-heading)))
    (if (equal heading demo-last-heading)
        (setq demo-last-heading-entry
              (1+ demo-last-heading-entry))
      (setq demo-last-heading heading
            demo-last-heading-entry 0))
    (list demo-last-heading-entry
          demo-last-heading)))

This requires a couple of global variables:

(defvar demo-last-heading nil
  "The last heading used for a mini-demo.")

(defvar demo-last-entry 0
  "Number of times calling the current heading.
When 0, it means this is the first time.")

Now the new step version can trigger on both the headline, as well as the iteration:

(defun demo-step ()
  "Trigger a function based on an Org heading.
This new demonstration approach runs its logic from
and org file that serves as a presentation."
  (interactive)
  (pcase (demo-heading)
    ('(0 "Slide One")  (browse-url "http://place.io"))
    ('(0 "Slide Four") (demo-step-4a))
    ('(1 "Slide Four") (demo-step-4b))
    ('(0 "Slide Five") (demo-step-5)
    ;; ...

I still need to return to my Org file for the trigger, but …

Third Try: Matching a “State”

What if, instead of triggering solely on an Org headline, I could trigger on the name of a buffer, or a major mode, or … in other words, the state of the cursor’s location.

For instance, I could trigger a function on

(:buffer "something.py")

or

(:heading "Slide Four")

or even a combination of more states.

Let’s create a function that could accept a list of triggering keys, and then compare that with another list representing the “current state” of the point, including the buffer, the mode, or the heading in an Org file. In this case, the magic happens by calling seq-difference:

(defun demo-match (triggers state)
  "Return t if all elements of TRIGGERS are in STATE.
Where TRIGGERS and STATE are lists of key/value tuple
pairs, e.g. `((:a 1) (:b 2))'."
  ;; If difference returns anything, we've failed:
  (not (seq-difference triggers state)))

I love having a function where the documentary is longer than the code.

Which we can almost prove with some unit tests:

(ert-deftest demo-match-test ()
  (let ((state '((:a 1) (:b 4) (:c 6))))
    (should      (demo-match '((:b 4) (:a 1)) state))
    (should (not (demo-match '((:b 4) (:c 2)) state)))
    (should      (demo-match '((:c 6)       ) state))
    (should (not (demo-match '((:c 2)       ) state)))))

But can I check if I have triggered a state once before? Let’s keep track of the states that have returned true before, in a hash table where the key is the state (a list of :buffer, :mode, :head, etc.) and the value is the number of times triggered at that state:

(defvar demo-prev-state
  (make-hash-table :test 'equal)
  "Matched states in keys, and store number
of matches as values.")

Now, we have a new match function takes the state and triggers, where the trigger could include an iteration, :i that limits a match. For instance:

(:buffer "foobar.txt" :i 0)
triggers the first time we call this function in this buffer.
(:buffer "foobar.txt" :i 1)
triggers the second time we call this function in this buffer.

If the triggers doesn’t contain an :i, it matches every time when meeting the other conditions.

We look in our demo-prev-state cache for the number of times we have triggered that state, and add that value into a new state variable we use to match, :itful-state (yeah, naming is hard).

If we match, we want to return non-nil, and store this new incremented value back in our cache:

(defun demo-state-match (triggers state)
  "Return non-nil if STATE contains all TRIGGERS.
The state also includes the number of times the triggers
matched during previous calls. We do this by keeping track
of the number of successful calls, and incrementing
the iteration... if this function returns non-nil."
  ;; If the first element is either parameter is NOT a list,
  ;; we group it into a list of tuples:
  (when (not (listp (car triggers)))
    (setq triggers (seq-partition triggers 2)))
  (when (not (listp (car state)))
    (setq state (seq-partition state 2)))

  (let* ((iteration    (gethash state demo-prev-state 0))
         (itful-state  (cons `(:i ,iteration) state)))
    (when (demo-match triggers itful-state)
      (puthash state (1+ iteration) demo-prev-state))))

Notice the two when expressions for using seq-partition for converting a property-style list like (:a 1 :b 2 :c 3) into an more standard associative list, like ((:a 1) (:b 2) (:c 3)).

Let’s test:

(ert-deftest demo-state-match-test ()
  ;; Not specifying a state should always work:
  (should (demo-state-match
           '(:a 1)      '((:a 1) (:b 2) (:c 4))))
  (should (demo-state-match
           '(:a 1)      '((:a 1) (:b 2) (:c 4))))

  ;; Reset number of iterations of possible states:
  (clrhash demo-prev-state)

  ;; With a clear hash, we should match on the
  ;; first (0) iteration:
  (should (demo-state-match
           '(:a 1 :i 0) '((:a 1) (:b 3) (:c 4))))
  ;; Which should then match the next state:
  (should (demo-state-match
           '(:a 1 :i 1) '((:a 1) (:b 3) (:c 4))))
  ;; But should not match any other state:
  (should (not (demo-state-match
                '(:a 1 :i 5) '((:a 1) (:b 2) (:c 3))))))

With this -state-match predicate, our new replacement for the step function needs to use cond, for example:

(defun demo-step ()
  "Trigger a function based on the state of the point.
This new demonstration approach runs its logic from
where the point is, for instance:"
  (interactive)
  (let ((state (list :buffer (buffer-name)
                     :mode major-mode
                     :head (when (eq major-mode 'org-mode)
                             (org-get-heading)))))
    (cond
     ((demo-state-match '(:head "Slide One") state)
      (browse-url "https://go-someplace.io"))

     ((demo-state-match '(:head "Slide Four" :i 0) state)
      (find-file "~/foo/bar/something.py"))

     ((demo-state-match '(:head "Slide Four" :i 1) state)
      (find-file "~/foo/bar/something-else.py"))
    ;; ...

It works, but it smells funny.

Cleaning the Code with a Macro

My -step function that encapsulates my demonstration became more complicated and harder to comprehend. When I see code with duplicated boilerplate, I toy with encapsulating the complexity in a macro … then I pour myself a drink and talk myself out it.

This time, I pressed forward.

To do this, you first wait for the full moon, and align your candles, blackened by the blood and berries of Taxus baccata, and repeat after me, Ⲡⲉⲛϣⲁϫⲉ ⲥⲛⲟϥ ⲙⲡⲣⲟⲛⲟⲓⲁ

Joking aside, let me show you how I did this feat of magic…

Essays and books I’ve read about writing Lisp macros often seem like the How to Draw an Owl meme:

how-to-draw-an-owl.jpg

I’ll try to explain my process iteratively, but if not interested, feel to free to jump to the answer key at the back of the book end of the essay.

When I write a macro, I start with an expression that generates the code I wrote by hand. For instance if we put “what I want to write” in a variable (called forms):

(let ((forms '(
  (:head "Slide One")       (browse-url "https://place.io")

  (:head "Slide Four" :i 0) (find-file "~/foo/bar.py")

  (:head "Slide Four" :i 1) (find-file "~/foo/baz.py")
)))

...)

And note what I want the output in code to be:

(let ((state (list :buffer (buffer-name)
                   :mode major-mode
                   :head (when (eq major-mode 'org-mode)
                           (org-get-heading)))))
  (cond
   ((demo-state-match '(:head "Slide One") state)
    (browse-url "https://place.io"))

   ((demo-state-match '(:head "Slide Four" :i 0) state)
    (find-file "~/foo/bar.py"))

   ((demo-state-match '(:head "Slide Four" :i 1) state)
    (find-file "~/foo/baz.py"))))

Seems like I’ve got two aspects, a bunch of static code that surrounds everything, and the inside transformation of my list into a cond expression. Adding the static part, I get:

(let ((forms '(
  (:head "Slide One")       (browse-url "https://place.io")
  (:head "Slide Four" :i 0) (find-file "~/foo/bar.py")
  (:head "Slide Four" :i 1) (find-file "~/foo/baz.py"))))

  `(let ((state (list :buffer (buffer-name)
                       :mode major-mode
                       :head (when (eq major-mode 'org-mode)
                               (org-get-heading)))))
     (cond ...magic ✩₊˚.⋆☾⋆⁺₊✧ here...

As you know, the backtick, ` is a special version of the normal tick, ' that allows us to go from static words to evaluated expressions. To create the static part, I begin with a `(let and define the state variable created at run time each time called.

I’m a big fan of the seq- library, as it has all the goodies for manipulating lists (and Emacs supplies this library in its standard distribution). The seq-map transforms the body into the cond pairs. Before this,, I need to change my even list into a group of pairs with seq-partition:

;; ...
(cond
 ,@(seq-map (λ (pair) ✩₊˚.⋆☾⋆⁺₊✧)
           (seq-partition forms 2)))))

When we started with the back-tick, we tell Lisp the following text will be static until you get to a comma, and then revert back to evaluation mode. Before the seq-map expression, we add the ,. But what about the @ symbol? Well, the result of the seq-map is a list, but I want the contents of that list given to cond, not the list itself. This happens so often that the Good Folks at Lisp, Inc.™ added the ,@ as a symbol for this.

The seq-map takes a lambda expression that will do the conversion. We are passing in a list of two elements, where the first is the trigger and the second is the function to call. Let’s use seq-let to split that up and assign them to two variables, like:

;; ...
(lambda (tf-pair) (seq-let (trigger func) tf-pair
               ;; ... rest of the transformation ...
    ))

The cond expects a two element list, where the first is the condition to match, (demo-state-match trigger state), and the second will be the function, func.

(list
   `(demo-state-match ',trigger state)
   func)

Since we are (due to the previous ,) in evaluation mode, we need another back-tick, to get back to literal mode. Note that a regular tick (which we need to begin the list, we pass to demo-state-match) Lisp inserts as-is, but we will need a , before trigger to have that expanded. The , applies to the next expression (the symbol, trigger), so Lisp parses state as a variable.

Yes, that is the trickiest part to get right.

The entire expression that returns a list of code:

(let ((forms '(
  (:head "Slide One")       (browse-url "https://place.io")
  (:head "Slide Four" :i 0) (find-file "~/foo/bar.py")
  (:head "Slide Four" :i 1) (find-file "~/foo/baz.py"))))

  `(let ((state (list :buffer (buffer-name)
                       :mode major-mode
                       :head (when (eq major-mode 'org-mode)
                               (org-get-heading)))))
     (cond
      ,@(seq-map (lambda (tf-pair)
                  (seq-let (trigger func) tf-pair
                    (list
                     `(demo-state-match ',trigger state)
                     func)))
                (seq-partition forms 2)))))

Returns the following:

(let ((state (list :buffer (buffer-name)
                   :mode major-mode
                   :head (when (eq major-mode 'org-mode)
                           (org-get-heading)))))
  (cond
   ((demo-state-match
     '(:head Slide One) state)
    (browse-url https://place.io))

   ((demo-state-match
     '(:head Slide Four :i 0) state)
    (find-file ~/foo/bar.py))

   ((demo-state-match
     '(:head Slide Four :i 1) state)
    (find-file ~/foo/baz.py))))

Since the output looks like the code I want, converting that expression into a macro wasn’t much work:

(defmacro demo-step (&rest forms)
  "Execute a function based matching list of states at point.
Where FORMS is an even number of _matcher_ and _function_ to call.

Probably best to explain this in an example:

(demo-step
 (:buffer \"demonstrations.py\") (message \"In a buffer\")
 (:mode 'dired-mode)             (message \"In a dired\")
 (:head \"Raven Civilizations\"  (message \"In an org file\")))

Calling this function displays a message based on position of the
point in a particular buffer or place in a heading in an Org file.

You can use the `:i' to specify different forms to call when
the trigger matches the first time, versus the second time, etc.

(demo-step
 (:buffer \"demonstrations.org\" :i 0) (message \"First time\")
 (:buffer \"demonstrations.org\" :i 1) (message \"Second time\"))"
  (interactive)
  `(let ((state (list :buffer (buffer-name)
                      :mode major-mode
                      :head (when (eq major-mode 'org-mode)
                              (org-get-heading)))))
     (cond
      ,@(seq-map (lambda (tf-pair)
                   (seq-let (trigger func) tf-pair
                     (list
                      `(demo-state-match ',trigger state)
                      func)))
                 (seq-partition forms 2)))))

And an interactive validation:

(demo-step
 (:buffer "demonstrations-part-two.org" :i 0)
 (message "First time")

 (:buffer "demonstrations-part-two.org" :i 1)
 (message "Second time"))

I hope that was helpful.

Summary

We could expand this demo-step function with more state, and use it at any time to trigger context-specific functions, for instance, if a project matches, trigger a particular compile command.

As a tool for automating the demonstration parts of a presentation, this approach gives you flexibility during the performance. Running org-present means you can jump back and forth along the slide, and then hit a f6 to trigger some interesting demonstration based on the current context.