#+TITLE:  Demonstrations in Emacs
#+AUTHOR: Howard X. Abrams
#+DATE:   2024-10-18
#+FILETAGS: emacs hamacs
#+LASTMOD: [2025-01-14 Tue]
#+STARTUP: showstars

A literate programming file for creating and running demonstrations

#+begin_src emacs-lisp :exports none
  ;;; ha-demos --- creating and running demonstrations -*- lexical-binding: t; -*-
  ;;
  ;; © 2024 Howard X. Abrams
  ;;   Licensed under a Creative Commons Attribution 4.0 International License.
  ;;   See http://creativecommons.org/licenses/by/4.0/
  ;;
  ;; Author: Howard X. Abrams <http://gitlab.com/howardabrams>
  ;; Maintainer: Howard X. Abrams
  ;; Created: October 18, 2024
  ;;
  ;; While obvious, GNU Emacs does not include this file or project.
  ;;
  ;;; Commentary:
  ;;
  ;;  This replaces my original demo-it project encapsulating the following goals:
  ;;
  ;;  - Flexible presentation that can use either `org-present' or
  ;;    continue to `org-tree-slide'
  ;;
  ;;  - Simpler support functions for showing side windows and whatnot
  ;;
  ;;  - Most importantly, a more flexible demonstration where trigger
  ;;    events based on the current state.
  ;;
  ;; *NB:* Do not edit this file. Instead, edit the original literate file at:
  ;;            ~/src/hamacs/ha-demos.org
  ;;       And tangle the file to recreate this one.
  ;;
  ;;; Code:
#+end_src

* Introduction
Once I made demonstrations /within/ Emacs with my [[https://github.com/howardabrams/demo-it][demo-it]] project. While on MELPA, I wanted to use my own cloned version to make sure I can keep debugging it.

#+begin_src emacs-lisp :tangle no
    (use-package demo-it
      :straight (:local-repo "~/src/demo-it")
      ;; :straight (:host github :repo "howardabrams/demo-it")
      :commands (demo-it-create demo-it-start demo-it-hide-mode-line
                                demo-it--presentation-display-set)
      :custom (demo-it--insert-test-speed :faster))
#+end_src

But I feel I should replace it, and this project encapsulates the following goals:

  - Flexible presentation that can use either =org-present= or continue to =org-tree-slide=
  - Simpler support functions for showing side windows and whatnot
  - Most importantly, a more flexible demonstration where trigger events based on the current /state/.

* Presentations with Org
A demonstration begins with an Org file where the screen shows a /single heading/ with a larger font. Not much more. I’m playing around with /all/ the projects available, including writing my own.
** My Presentation Views
Regardless of the presentation package I use, I make them all look similar with the following code. Much of this is getting rid of Emacs visual elements, like the cursor and the mode-line, as well as stopping minor modes that add visual changes, like spellchecking and the gutter. I can call this function from any presentation software used.

#+BEGIN_SRC emacs-lisp
  (defun ha-slide-setup (&optional frame-name)
    "Configure the look I want for presentations.
  The frame associated with FRAME-NAME is tidied
  by removing the gutters and other informative
  widgets not needed for a presentation."
    (org-indent-mode -1)
    ;; (org-modern-mode -1)

    (setq org-image-actual-width nil)
    (org-display-inline-images)
    (ha-org-blocks-hide-headers)
    ;; (ha-org-hide-stars)
    (font-lock-update)
    (ha-demo-hide-mode-line)
    (ha-demo-hide-cursor)
    (ha-demo-presentation-frame frame-name)

    (ha-set-favorite-font-size 42)                  ; instead of (text-scale-set 4)

    (diff-hl-mode -1)
    (flycheck-mode -1)
    (jinx-mode -1)

    ;; Clear the demonstration state cache:
    (clrhash ha-demo-prev-state)

    (evil-normal-state))
#+END_SRC

And after a presentation finishes, this function cleans up by restoring minor modes, etc:

#+BEGIN_SRC emacs-lisp
  (defun ha-slide-teardown ()
    "Reset the Org after a presentation."
    (org-indent-mode 1)
    ;; (org-modern-mode 1)

    (ha-org-blocks-show-headers)
    (font-lock-update)
    (ha-demo-show-mode-line)
    (ha-demo-show-cursor)
    (ha-demo-normalize-frame)

    (ha-mac-monitor-fontsize)                 ; (text-scale-set 0)
    (diff-hl-mode)
    (flycheck-mode)
    (jinx-mode))
#+END_SRC

The =dslide= seems to reset /everything/ on each slide display, so:

#+BEGIN_SRC emacs-lisp
  (defun ha-slide-reset ()
    "Reset the current slide."
    (interactive)
    (ha-org-blocks-hide-headers)
    (font-lock-update))
    #+END_SRC

** Org Tree Slide
I’ve used [[https://github.com/takaxp/org-tree-slide][org-tree-slide]] for years for showing org files as presentations. I like the /simple/ presentation and it seems to shows all the images.

#+BEGIN_SRC emacs-lisp :tangle no
  (use-package org-tree-slide
    :config
    (setq org-tree-slide-heading-emphasis nil
          org-tree-slide-activate-message "† This demonstration is running in Emacs"
          org-tree-slide-indicator '(:next nil :previous nil :content nil)
          org-tree-slide-cursor-init nil)
    (org-tree-slide-simple-profile)

    :bind
    (:map org-tree-slide-mode-map
     ;; ("<f5>" . org-tree-slide-move-next-tree)
     ;; ("S-<f5>" . org-tree-slide-move-previous-tree)
     ("M-<f5>" . org-tree-slide-content)
     ("C-<f5>" . (lambda () (interactive) (org-tree-slide-mode -1))))

    :general
    (:states 'normal :keymaps 'org-tree-slide-mode-map
             "C" #'ha-demo-toggle-cursor
             "n" #'org-tree-slide-move-next-tree
             "p" #'org-tree-slide-move-previous-tree
             "Q" (lambda () (interactive) (org-tree-slide-mode -1)))

    :hook
    ((org-tree-slide-play . ha-slide-setup)
     (org-tree-slide-stop . ha-slide-teardown)))
#+END_SRC
** Org Present
Converted to use [[https://github.com/rlister/org-present][org-present]]. I love the /hooks/ as that makes it easier to handle. My problem with =org-present= is that it doesn’t always display images based on how it handles overlays.

#+begin_src emacs-lisp
  (use-package org-present
    :config
    (defvar ha-org-present-mode-line mode-line-format
      "Cache previous mode-line format state")

    :bind
    (:map org-present-mode-keymap
          ;; ("<f5>" . org-present-next)
          ;; ("S-<f5>" . org-present-previous)
          ("C-<f5>" . org-present-quit))

    :general
    (:states 'normal :keymaps 'org-present-mode-keymap
             "+" #'org-present-big
             "-" #'org-present-small
             "<" #'org-present-beginning
             ">" #'org-present-end
             "c" #'org-present-hide-cursor
             "C" #'org-present-show-cursor
             "n" #'org-present-next
             "j" #'org-present-next
             "k" #'org-present-prev
             "p" #'org-present-prev
             "r" #'org-present-read-only
             "w" #'org-present-read-write
             "Q" #'org-present-quit)

    :hook
    (org-present-mode . ha-slide-setup)
    (org-present-mode-quit . ha-slide-teardown))
#+end_src
** DSlide
The [[https://github.com/positron-solutions/dslide][dslide project]] is flexible, interesting, and can perform actions based on blocks /in the slide/ as opposed to my previous approaches of having external control.

With some a startup bug that I haven’t been able to resolve, I’m not using it =dslide= at moment.

#+begin_src emacs-lisp :tangle no
  (use-package dslide
    :straight (dslide :host github :repo "positron-solutions/dslide")
    :commands (dslide-deck-start dslide-deck-stop)
    :custom
    (dslide-start-from 'point)
    ;; Let's keep our presentations simple:
    (dslide-slide-in-effect nil)
    (dslide-header nil)
    (dslide-header-date nil)
    (dslide-header-author nil)
    (dslide-header-email nil)
    (dslide-breadcrumb-separator nil)

    :general
    (:states 'normal :no-autoload t :keymaps 'dslide-mode-map
             "q"       '("quit presentation" . dslide-deck-stop)
             "<left>"  '("previous slide"    . dslide-deck-backward)
             "<right>" '("next slide"        . dslide-deck-forward)
             "C"       '("show cursor"       . ha-demo-show-cursor)
             "c"       '("hide cursor"       . ha-demo-hide-cursor)
             "<up>"    '("previous slide"    . previous-line)
             "<down>"  '("next slide"        . next-line))

    :bind
    (:map dslide-mode-map
          ;; ("<f5>"   . ha-dslide-deck-forward)
          ;; ("S-<f5>" . ha-dslide-deck-backward)
          ("C-<f5>" . dslide-deck-stop))

    :hook ((dslide-start  . ha-slide-setup)
           ;; (dslide-narrow . ha-slide-reset)
           (dslide-stop   . ha-slide-teardown)))
#+end_src

Let’s try it out by loading this example.[[file:~/.emacs.d/straight/repos/dslide/test/demo.org][ demo.org]]

What features do I like and want to take advantage of?
  - Inline Children show: =:DSLIDE_SLIDE_ACTION: dslide-slide-action-inline=
  - Flat Slide (shows children section immediately): =:DSLIDE_SLIDE_ACTION: dslide-slide-action-flat=
  - Show images only? I guess we could use my own thing, but it is nice and easy: =:DSLIDE_ACTIONS: dslide-action-image :slide-display nil=
  - No header slides: =:DSLIDE_SLIDE_ACTION: dslide-slide-action-child :header nil=
  - Re-execute Babel blocks: =:DSLIDE_SLIDE_ACTION: dslide-slide-babel=
  - Hiding Blocks
  - Results Only

Fixes and improvements for the dslide:

Call the =ha-slide-notes-update= function automatically after updating a slide. With =dslide=, we add a hook:

#+BEGIN_SRC emacs-lisp :tangle no
  (use-package dslide
    :straight (dslide :host github :repo "positron-solutions/dslide")
    :commands (dslide-narrow-hook)
    :hook (dslide-narrow . 'ha-slide-notes-update))
#+END_SRC

*** Master of Ceremonies
The [[https://github.com/positron-solutions/moc][Master of Ceremonies]] package (moc) is to help when recording Emacs screens. Early in development, but it looks to have some potential. Not sure how to use it yet.

#+BEGIN_SRC emacs-lisp :tangle no
  (use-package default-text-scale)

  (use-package moc
    :straight (:type git :host github
               :repo "positron-solutions/moc"))
#+END_SRC

Select text, and call =moc-focus= (call =moc-focus-quit= to stop). Highlight more text, and call =moc-focus-highlight= to brighten it, or =moc-focus-obscure= to hide it.

The =moc-screenshot= seems to only work on Linux.

An interesting approach for making presentations, that I’m not sure I will need.

*** TopSpace

The [[https://github.com/trevorpogue/topspace][topspace]] project can pad the top of a buffer, to make the first line in the center of the window. Helpful for presentations:

#+BEGIN_SRC emacs-lisp
  (use-package topspace
    :straight (:type git :host github :repo "trevorpogue/topspace"))
    #+END_SRC

*** Showing Something associated with a Headline
:PROPERTIES:
:DSLIDE_ACTIONS: dslide-action-babel
:END:

When I give a /demonstration/ (uising my [[New Demonstration]] project), I could, instead, use a custom =dslide= action.

But how would I get it to close? Maybe we use a combination of actions and my “demo” code for everything else?

*Note:* Code blocks with =exports= set to =none= are not displayed.

#+begin_src elisp :tangle no :exports none :results none :eval no
  (ha-demo-show-file "ha-org.org" :position 'right
                     :focus 'presentation :heading "Meetings"
                     :shift 0)
#+end_src

#+BEGIN_SRC emacs-lisp :tangle no :exports none :results none :eval no
  (ha-demo-highlight-buffer :buffer "ha-org.org"
                            :hi-lines "268-274")
#+END_SRC

#+begin_src elisp :tangle no :exports none :results none
  (delete-other-windows)
#+end_src

*** Bullet/Paragraph Highlighting
I would like to highlight a bullet point or a paragraph while talking.
To do this, add =:DSLIDE_ACTIONS: dslide-action-highlight-paragraphs= to the properties of a section.

#+begin_src elisp emacs-lisp :tangle no
  (use-package dslide
    :straight (:host github :repo "positron-solutions/dslide")
    :config
    (defclass dslide-action-highlight-paragraphs (dslide-action)
      ((overlays :initform nil))
      "Paint the paragraphs with the highlight color, one by one.")

    ;; In this case, the Default no-op `dslide-begin' works.
    ;; Default implementation of `dslide-end', plays forward to the end.

    ;; Remove any remaining overlays when calling final.
    (cl-defmethod dslide-final :after ((obj dslide-action-highlight-paragraphs))
      (mapc #'delete-overlay (oref obj overlays)))

    ;; Find the next paragraph and add an overlay if it exists
    (cl-defmethod dslide-forward ((obj dslide-action-highlight-paragraphs))
      ;; This line removes all overlays allowing us to highlight a new one.
      ;; Remove this if you want the paragraphs to _linger_.
      (mapc #'delete-overlay (oref obj overlays))
      (when-let ((paragraph (dslide-section-next obj 'paragraph)))
        (let* ((beg (org-element-property :begin paragraph))
               (end (org-element-property :end paragraph))
               (new-overlay (make-overlay beg end)))
          (overlay-put new-overlay 'face 'highlight)
          (push new-overlay (oref obj overlays))
          ;; Return non-nil indicates we made progress.  This also informs the
          ;; highlight when following the slides in the base buffer.
          beg)))

    (cl-defmethod dslide-backward ((obj dslide-action-highlight-paragraphs))
      (when-let* ((overlay (pop (oref obj overlays))))
        (delete-overlay overlay)
        ;; If there is a preceding overlay, move to its beginning else move to the
        ;; beginning of the heading.
        (if-let ((overlay (car (oref obj overlays))))
            (dslide-marker obj (overlay-start overlay))
          (dslide-marker obj (org-element-property :begin (dslide-heading obj)))))))
#+end_src
*** Custom Action Demo
:PROPERTIES:
:DSLIDE_ACTIONS: dslide-action-highlight-paragraphs
:END:
Phasellus at dui in ligula mollis ultricies.  Phasellus lacus.  Fusce commodo.  Nulla posuere.  Nunc rutrum turpis sed pede.  Pellentesque tristique imperdiet tortor.  Nullam libero mauris, consequat quis, varius et, dictum id, arcu.  Phasellus lacus.  Sed diam.  Nullam tristique diam non turpis.

  * Donec vitae dolor.
  * Fusce commodo.
  * Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.

Nunc porta vulputate tellus.  Lorem ipsum dolor sit amet, consectetuer adipiscing elit.  Donec posuere augue in quam.  Sed id ligula quis est convallis tempor.  Integer placerat tristique nisl.  Nunc rutrum turpis sed pede.  Nullam rutrum.  Sed id ligula quis est convallis tempor.
** My Presentation Notes View
A /full/ presentation requires my /notes/ on one frame, and the presentation on the other.

To use this, following:
  1. Select the Org mode presentation
  2. Run the function, =ha-slide-make-notes-frame=
  3. Reference the notes file associated with the presentation

The end result is two frames, where updating the presentation, updates the location of the other frame to match the same headline.

#+begin_src emacs-lisp
  (defvar ha-slide-notes-frame-name "Demonstration Notes"
    "The name of the frame that displays the presentation notes.")

  (defvar ha-slide-notes-frame nil
    "Frame containing the presentation notes.")

  (defvar ha-slide-notes-window nil
    "Window containing the presentation notes.")

  (defun ha-slide-make-notes-frame (filename &optional heading)
    "Display the notes, FILENAME, in a new frame.
  With HEADING, jump to that `org-mode' headline."
    (interactive "fNotes File: ")
    (let ((f (selected-frame)))
      (setq ha-slide-notes-frame
            (make-frame `((name . ,ha-slide-notes-frame-name))))
      (set-frame-position ha-slide-notes-frame 1310 0)
      (set-frame-size ha-slide-notes-frame 920 1420 t)

      ;; While I could call `find-file-other-frame', I want to make
      ;; sure I get the file loaded in the correct frame:
      (x-focus-frame ha-slide-notes-frame)
      (find-file filename)
      (goto-char (point-min))
      (when heading
        (re-search-forward (rx bol (one-or-more "*") (one-or-more space) (literal heading)))
        (recenter-top-bottom 0))

      (setq ha-slide-notes-window (selected-window))
      (delete-other-windows)

      ;; Highlight the original window containing the presentation:
      (x-focus-frame f)))
#+end_src

These interactive functions scroll the “notes” in the other window in another frame:

#+begin_src emacs-lisp
  (defun ha-slide-notes-scroll-up ()
    "Scroll the frame/window containing the notes, up."
    (interactive)
    (when ha-slide-notes-window
      (with-selected-window ha-slide-notes-window
        (scroll-up -10))))

  (defun ha-slide-notes-scroll-down ()
    "Scroll the frame/window containing the notes, down."
    (interactive)
    (when ha-slide-notes-window
      (with-selected-window ha-slide-notes-window
        (scroll-up 10))))

  (defun ha-slide-notes-update ()
    "Function to move the notes headline to current buffers.
  Assuming the buffer is showing an org-file, and have
  called `ha-slide-make-notes-frame', this function moves
  the point in that buffer to the same headline."
    (interactive)
    (when ha-slide-notes-window
      (let ((heading (thread-first
                       (org-get-heading t t t t)
                       (substring-no-properties))))
        (with-selected-window ha-slide-notes-window
          (goto-char (point-min))
          (re-search-forward (rx (literal heading)) nil t)
          (recenter-top-bottom 0)))))
#+end_src

** My Presentation Interface
#+BEGIN_SRC emacs-lisp
  (defvar ha-slide-presentation nil
    "The buffer name of the starting presentation.")

  (defun ha-slide-deck-start (&optional initial-heading)
    "Start (and remember) a dslide presentation."
    (interactive)
    (unless (eq major-mode 'org-mode)
      (call-interactively 'org-find-file))
    (setq ha-slide-presentation (buffer-name))
    (when 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))
     ((fboundp #'org-tree-slide-mode) (call-interactively 'org-tree-slide-mode))
     (t   (message "No presentation software was loaded."))))

  (defun ha-slide-deck-forward ()
    "Switch to current running presentation, and advance slide deck."
    (interactive)
    (when ha-slide-presentation
      (pop-to-buffer ha-slide-presentation))
    (setq ha-slide-presentation (buffer-name))

    ;; Which presentation software am I using?
    (cond
     ((fboundp #'dslide-deck-forward) (dslide-deck-forward))
     ((fboundp #'org-present-next) (org-present-next))
     ((fboundp #'org-tree-slide-move-next-tree) (org-tree-slide-move-next-tree))))

  (defun ha-slide-deck-backward ()
    "Switch to current running presentation, and reverse slide deck."
    (interactive)
    (when ha-slide-presentation
      (pop-to-buffer ha-slide-presentation))

    ;; Which presentation software am I using?
    (cond
     ((fboundp #'dslide-deck-forward) (dslide-deck-backward))
     ((fboundp #'org-present-previous) (org-present-previous))
     ((fboundp #'org-tree-slide-move-previous-tree) (org-tree-slide-move-previous-tree))))

  (set-face-attribute 'highlight nil :background 'unspecified :foreground "lightblue")

  (global-set-key (kbd "<f5>") 'ha-slide-deck-forward)
  (global-set-key (kbd "S-<f5>") 'ha-slide-deck-backward)
#+END_SRC

* New Demonstration
:LOGBOOK:
CLOCK: [2025-03-26 Wed 09:04]--[2025-03-26 Wed 09:29] =>  0:25
:END:
Instead of executing a sequence of demonstration steps, demonstrations key on “state”, that is, the active buffer or major-mode, or the heading of an Org file, etc. I described the [[https://howardism.org/Technical/Emacs/demonstrations-part-two.html][guts of writing this code]], but we bind a key to calling =ha-demo-step= with a list of /state matchers/ to functions to call when matched. For instance:

#+BEGIN_SRC emacs-lisp :tangle no :eval no
  (define-ha-demo ha-simple-demo
                  (:heading "New Demonstration" :i 0) (message "Howdy")
                  (:heading "New Demonstration" :i 1) (message "Hi there"))

  (global-set-key (kbd "<f6>") 'ha-simple-demo)
#+END_SRC

To make the contents of the expression easier to write, the =define-ha-demo= as a macro. Otherwise we write a complicated =cond= with lots of duplicated calls to =ha-demo-state-match= (defined later). This macro creates a function, so the first parameter is the name of the function:

#+BEGIN_SRC emacs-lisp
  (defmacro define-ha-demo (demo-name &rest forms)
    "Create a demonstration sequence from FORMS as DEMO-NAME function.

  Call DEMO-NAME (as an interactive function), executes 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:

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

  Calling `(demo1)' 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.

  \(define-demo demo2
   \(:buffer \"demonstrations.org\" :i 0\) \(message \"First time\"\)
   \(:buffer \"demonstrations.org\" :i 1\) \(message \"Second time\"\)\)"
    `(defun ,demo-name ()
       (interactive)
       (let ((state (list :buffer (buffer-name)
                          :mode major-mode
                          :heading (when (eq major-mode 'org-mode)
                                  (org-get-heading)))))
         (cond
          ,@(seq-map (lambda (tf-pair)
                       (seq-let (trigger func) tf-pair
                         (list
                          `(ha-demo-state-match ',trigger state)
                          func)))
                     (seq-partition forms 2))
          (t (ha-slide-deck-forward))))))
#+END_SRC

The matching function, =ha-demo-state-match= looks in a cache, the =demo-prev-state= hash table, 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).

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

#+BEGIN_SRC emacs-lisp
  (defun ha-demo-state-match (triggers state)
    "Return non-nil if STATE has 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 ha-demo-prev-state 0))
           (itful-state  (cons `(:i ,iteration) state)))
      (when (ha-demo-match triggers itful-state)
        (puthash state (1+ iteration) ha-demo-prev-state))))
#+END_SRC

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:

#+BEGIN_SRC emacs-lisp :tangle no
  (ert-deftest ha-demo-state-match-test ()
    ;; Not specifying a state should always work:
    (should (ha-demo-state-match
             '(:a 1)      '((:a 1) (:b 2) (:c 4))))
    (should (ha-demo-state-match
             '(:a 1)      '((:a 1) (:b 2) (:c 4))))

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

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

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=, =:heading=, etc.) and the /value/ is the number of times triggered at that state:

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

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.

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

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

* Demonstration Support
What sort of functions will I often be doing?

** Hiding Blocks
When showing a presentation, I never want the =#+business= to lines to completely disappear. First attempt turned the foreground color to the background color, but that still leaves a blank, but occupied line. Using the invisible overlays removes them completely:

#+BEGIN_SRC emacs-lisp
  (defun ha-org-blocks-hide-headers ()
    "Make the headers and other block metadata invisible.
  See `ha-org-blocks-show-headers' to return their appearance."
    (let ((pattern (rx bol (zero-or-more space)
                       (or ":" "#")
                       (zero-or-more any) eol)))
      (save-excursion
        (goto-char (point-min))
        (while (re-search-forward pattern nil t)
          (let* ((start (match-beginning 0)) (end (1+ (match-end 0)))
                 (ovlay (make-overlay start end)))
            (overlay-put ovlay 'invisible t))))))

  (defun ha-org-blocks-show-headers ()
    "Un-invisibilize the headers and other block metadata invisible.
  In other words, this undoes what `ha-org-blocks-hide-headers' did."
    (delete-all-overlays))
#+END_SRC

What about deleting the initial bullets in =org-indent-mode=:

#+BEGIN_SRC emacs-lisp
  (defun ha-org-hide-stars ()
    "Create overlay to hide all initial astericks in Org headlines."
    (let ((pattern (rx bol (one-or-more "*") (one-or-more space))))
      (save-excursion
        (goto-char (point-min))
        (while (re-search-forward pattern nil t)
          (let* ((start (match-beginning 0))
                 (end   (1+ (match-end 0)))
                 (ovlay (make-overlay start end)))
            (overlay-put ovlay 'invisible t))))))
#+END_SRC

** Hide and Show the Cursor
The typical presentation software has an issue for hiding the cursor when working with Evil mode, and since setting =cursor-type= to =nil= doesn’t work in a graphical display (where we typically run a presentation), the following functions turn on/off the displayed cursor.

#+BEGIN_SRC emacs-lisp
  (defvar ha-demo-cursor nil
    "List of cursor states stored during `ha-demo-hide-cursor'.
  Used to restore with `ha-demo-show-cursor'.")

  (defun ha-demo-hide-cursor ()
    "Hide the cursor for the current frame."
    (interactive)
    (unless ha-demo-cursor
      (setq ha-demo-cursor
            (list cursor-type
                  t ; (when (boundp 'evil-default-cursor) evil-default-cursor)
                  (when (boundp 'evil-emacs-state-cursor) evil-emacs-state-cursor)
                  (when (boundp 'evil-normal-state-cursor) evil-normal-state-cursor)
                  (default-value blink-cursor-mode)
                  (when (display-graphic-p)
                    (frame-parameter (selected-frame) 'cursor-type))))

      ;; Turn off the cursor blinking minor mode:
      (blink-cursor-mode -1)

      ;; Change the cursor types for normal and Evil states:
      (setq-local cursor-type nil)
      (when (boundp 'evil-default-cursor)
        (setq-local
         evil-default-cursor nil
         evil-emacs-state-cursor nil
         evil-normal-state-cursor nil))

      ;; And most importantly, turn off the cursor for the selected frame:
      (set-frame-parameter (selected-frame) 'cursor-type nil)))

  (defun ha-demo-show-cursor ()
    "Restore cursor properties turned off by `ha-demo-hide-cursor'."
    (interactive)
    (when ha-demo-cursor
      (setq cursor-type (car ha-demo-cursor))

      (when (boundp 'evil-default-cursor)
        (setq-local
         evil-default-cursor (nth 1 ha-demo-cursor)
         evil-emacs-state-cursor (nth 2 ha-demo-cursor)
         evil-normal-state-cursor (nth 3 ha-demo-cursor)))

      (when (nth 4 ha-demo-cursor) (blink-cursor-mode 1))

      (set-frame-parameter (selected-frame)
                           'cursor-type (nth 5 ha-demo-cursor))
      (setq ha-demo-cursor nil)))

  (defun ha-demo-toggle-cursor ()
    "Toggle cursor display from shown or hidden."
    (interactive)
    (if ha-demo-cursor
        (ha-demo-show-cursor)
      (ha-demo-hide-cursor)))
    #+END_SRC

** Hide and Show the Modeline
For Org file displayed as presentations as well as images, we probably don’t want the distraction associated with the modeline, but when we finish the presentation, let’s turn it back on …

#+BEGIN_SRC emacs-lisp
  (defvar ha-demo-mode-line nil)
  (make-variable-buffer-local 'ha-demo-mode-line)

  (defun ha-demo-hide-mode-line ()
    "Hide mode line for a particular buffer."
    (interactive)
    (when mode-line-format
      (setq ha-demo-mode-line mode-line-format)
      (setq mode-line-format nil)))

  (defun ha-demo-show-mode-line ()
    "Restore mode hidden with `ha-demo-hide-mode-line'."
    (interactive)
    (if ha-demo-mode-line
        (setq mode-line-format ha-demo-mode-line)))
#+END_SRC

** Presentation Frame Properties
Like the work I’m doing to the mode-line, can we make the frame cleaner for a presentation?

#+BEGIN_SRC emacs-lisp
  (defvar ha-demo-frame-state nil
    "Store frame properties during `ha-demo-presentation-frame'.
  Stored before alteration, as to restore them with
  `ha-demo-normalize-frame'.")

  (defun ha-demo-presentation-frame (&optional name)
    "Remove the fringe and other frame settings.
  See `ha-demo-normalize-frame' for restoration.
  The NAME, if given, is the name of the frame."
    (interactive)
    (setq ha-demo-frame-state
          (list
           (frame-parameter (selected-frame) 'left-fringe)
           (frame-parameter (selected-frame) 'right-fringe)))

    (when name
      (set-frame-parameter (selected-frame) 'name name)))

  (defun ha-demo-normalize-frame ()
    "Restore frame state from `ha-demo-presentation-frame'."
    (interactive)
    (set-frame-parameter (selected-frame) 'left-fringe (nth 0 ha-demo-frame-state))
    (set-frame-parameter (selected-frame) 'right-fringe (nth 1 ha-demo-frame-state)))
#+END_SRC

** Side Window Helpers
The following sections create side windows (potentially) and run stuff inside them.

#+BEGIN_SRC emacs-lisp
  (cl-defun ha-demo-create-side-window (&key position keep-windows)
    "Display a side window.
  POSITION can be 'full 'right or 'below and positions the window.
  Deletes other windows unless KEEP-WINDOWS is non-nil."
    (unless position
      (setq position :right))

    ;; Remove any other windows that may be shown:
    (unless keep-windows
      (ignore-errors
        (delete-other-windows)))

    (pcase position
      ('above (progn (split-window-vertically)))
      ('up    (progn (split-window-vertically)))
      ('left  (progn (split-window-horizontally)))
      ('right (progn (split-window-horizontally) (other-window 1)))
      ('above (progn (split-window-vertically) (other-window 1)))
      ('below (progn (split-window-vertically) (other-window 1)))))

  (cl-defun ha-demo-set-side-window (&key size modeline cursor)
    "Standard settings for demonstration windows.
  SIZE is an integer for the font size based on the default size.
  Show MODELINE if non-nil, default is to hide it.
  The CURSOR can be 'show / 'yes or 'hide / 'no."
    (when size
      (text-scale-set size))

    (unless modeline
      (setq-local mode-line-format nil))

    (when cursor
      (if (or (eq cursor 'yes) (eq cursor 'show))
          (ha-demo-show-cursor)
        (ha-demo-hide-cursor))))
    #+END_SRC

** Display File
Displaying a File with:
  - On the side or covering the entire frame
  - Larger font size
  - Modeline or no modeline
  - Going to a particular text or line
  - Moving the cursor to the top or middle of the buffer window

All options? Should I use Common Lisp’s =cl-defun= for the keyword parameters?

#+BEGIN_SRC emacs-lisp
  (cl-defun ha-demo-show-file (filename &key position size modeline
                                        line heading shift cursor
                                        hi-lines hi-face
                                        commands keep-windows focus)
    "Show a file, FILENAME, in a buffer based on keyed parameters.
  POSITION can be 'full 'right or 'below and positions the window.
  SIZE is an integer for the font size based on the default size.
  MODELINE is shown if non-line, default is to hide it.
  LINE is either a line number or a regular expression to match.
  HEADING is a headline from the currently display Org file.
  SHIFT is the number of lines above the point to show, in case
  the LINE shouldn't be at the top of the window.
  The CURSOR can be 'show / 'yes or 'hide / 'no.
  The FOCUS can be 'presentation to return the cursor to the
  calling buffer.

  COMMANDS is a lambda expression that can contain any other
  instructions to happen to the buffer display."
    (let ((orig-buf (current-buffer)))
      (ha-demo-create-side-window :position position :keep-windows keep-windows)

      (if (file-exists-p filename)
          (find-file filename)
        (switch-to-buffer filename))

      (if image-minor-mode
          (image-transform-fit-to-window)
        (goto-char (point-min)))

      (ha-demo-set-side-window :size size :modeline modeline
                               :cursor cursor)
      ;; (when (fboundp 'topspace-mode)
      ;;   (topspace-mode 1))

      (ha-demo-highlight-buffer :line line :heading heading :shift shift
                                :hi-lines hi-lines :hi-face hi-face
                                :commands commands)

      (when (and focus (eq focus 'presentation))
        (pop-to-buffer orig-buf))))
#+END_SRC

Let try it all together:

#+BEGIN_SRC emacs-lisp :tangle no :eval no
  (ha-demo-show-file "ha-config.org" :position 'right :size 1 :modeline nil :line 418 :shift 4)
#+END_SRC

Or:

#+BEGIN_SRC emacs-lisp :tangle no :eval no
  (ha-demo-show-file "ha-config.org" :modeline t
                     :heading "Text Expanders"
                     :commands (lambda () (jinx-mode -1)))
#+END_SRC

** Highlight Text in Buffer
Perhaps when we call =ha-demo-show-file=, we want to highlight different parts of the file?

#+BEGIN_SRC emacs-lisp
  (defface ha-demo-highlight-1 '((t :weight ultra-heavy))
    "Face used for highlighting alternate buffers.")

  (defface ha-demo-highlight-2 '((t :slant italic))
    "Face used for highlighting alternate buffers.")

  (defface ha-demo-highlight-3 '((t :background "#0000a0" :extend t))
    "Face used for highlighting alternate buffers.")

  (cl-defun ha-demo-highlight-buffer (&key buffer line heading shift
                                           hi-lines hi-face commands)
    "Move to a section of a buffer, and possibly highlight text.
  If BUFFER is given, call `pop-to-buffer' on that.
  If LINE, HEADING, or SHIFT is given, move to that section.
  If HI-LINES is given, create an overlay for those lines
  based on the face, HI-FACE (if that isn't given, bold those lines).
  Finally execute COMMANDS, if given."
    (let ((orig-buf (current-buffer)))

      (when buffer
        (pop-to-buffer buffer))

      (when line
        (if (integerp line)
            (forward-line line)
          (re-search-forward line nil t)))

      (when heading
        (re-search-forward (rx bol (one-or-more "*") (one-or-more space)
                               (literal heading))
                           nil t))

      ;; If SHIFT is positive integer, left that many line above point,
      ;; otherwise don't do anything to leave it in the middle.
      ;; If SHIFT is null, move it to the top of the buffer window:
      (when shift
        (if (integerp shift)
            (recenter-top-bottom shift))
        (recenter-top-bottom 0))

      (remove-overlays)
      (when hi-lines
        (seq-let (first-line last-line) (string-split hi-lines (rx (or ":" "-")))
          (save-excursion
            (let* ((beg (goto-line (string-to-number first-line)))
                   (end (progn
                          (goto-line (string-to-number last-line))
                          (line-end-position)))
                   (new-overlay (make-overlay beg end)))

              (if hi-face
                  (overlay-put new-overlay 'face hi-face)
                (overlay-put new-overlay 'face 'ha-demo-highlight-3))

              ;; (push new-overlay (oref obj overlays))
              ))))

      (when commands (funcall commands))

      (when buffer
        (pop-to-buffer orig-buf))))
    #+END_SRC

Example:

#+BEGIN_SRC emacs-lisp :tangle no
  (ha-demo-highlight-buffer :hi-lines "874-881" :hi-face 'ha-demo-highlight-3)
#+END_SRC


** Shell Commands

Demo-like wrapper around the [[file:~/other/hamacs/ha-remoting.org::*Programmatic Interface][ha-shell]] commands, where I can make bigger shell terminals.

We would normally just have a single shell for a demonstration, with a name associated with the directory:

#+BEGIN_SRC emacs-lisp
  (defvar ha-demo-shell-dir (getenv "HOME")
    "Store the directory for repeated commands")
  #+END_SRC

And we can open the shell in a window:

#+BEGIN_SRC emacs-lisp
  (cl-defun ha-demo-shell (&key directory position size modeline
                                cursor command focus)
    "Open a shell, and potentially send COMMAND to it.
  POSITION can be 'full 'right or 'below and positions the window.
  SIZE is an integer for the font size based on the default size.
  Show MODELINE when non-nil, default is to hide it.
  The CURSOR can be 'show / 'yes or 'hide / 'no.
  The FOCUS can be 'presentation to return the cursor to the
  calling buffer."
    (let ((orig-buf (current-buffer)))
      (ha-demo-create-side-window :position position)

      (when directory
        (setq ha-demo-shell-dir directory))

      ;; We could also do ha-ssh
      (ha-shell ha-demo-shell-dir)

      (ha-demo-set-side-window :size size :modeline modeline :cursor cursor)

      (when command
        (sit-for 1)
        (ha-shell-send command))

      (when (and focus (eq focus 'presentation))
        (pop-to-buffer orig-buf))))

  (defun ha-demo-shell-send (command)
    "Send COMMAND to the currently opened shell, `ha-demo-shell'."
    (ha-shell-send command))

  (defun ha-demo-shell-quit ()
    "Close the window associated with a shell."
    (ha-shell-send "exit")
    (delete-other-windows))
#+END_SRC

Try it out:

#+BEGIN_SRC emacs-lisp :tangle no
  (ha-demo-shell :position 'right :directory "/tmp" :command "ls -l")
#+END_SRC

 And:
#+BEGIN_SRC emacs-lisp :tangle no
  (ha-demo-shell-send "date > now.txt")

  (ha-demo-shell-send "cat now.txt")
#+END_SRC

** Delete Specific Windows
While often safe to call =delete-other-windows=, being able to delete a particular window that hosts a particular buffer seems helpful.

#+BEGIN_SRC emacs-lisp
  (defun ha-demo-delete-window (bufname)
    "Delete the window associated with BUFNAME."
    (ignore-errors
      (delete-window (get-buffer-window bufname))))
    #+END_SRC

* Technical Artifacts                              :noexport:
Let's =provide= a name so we can =require= this file:

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

#+DESCRIPTION: creating and running demonstrations

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

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

# Local Variables:
# jinx-local-words: "Modeline"
# End:
