Create new demonstration code

This replaces demo-it, but I will be bringing some, more general
functions, over from it.
This commit is contained in:
Howard Abrams 2024-10-19 13:23:36 -07:00
parent 990cc577dc
commit 2bd14a876a
4 changed files with 259 additions and 57 deletions

View file

@ -815,15 +815,6 @@ When having your point on a key entry, you can copy fields to kill-ring using:
- ~b~ :: user name
- ~c~ :: password
* Demo It
Making demonstrations /within/ Emacs with my [[https://github.com/howardabrams/demo-it][demo-it]] project. While on MELPA, I want to use my own cloned version to make sure I can keep debugging it.
#+begin_src emacs-lisp
(use-package demo-it
:straight (:local-repo "~/other/demo-it")
;; :straight (:host github :repo "howardabrams/demo-it")
:commands (demo-it-create demo-it-start))
#+end_src
* PDF Viewing
Why not [[https://github.com/politza/pdf-tools][view PDF files]] better? If you have standard build tools installed on your system, run [[help:pdf-tools-install][pdf-tools-install]], as this command will an =epdfinfo= program to PDF displays.

258
ha-demos.org Normal file
View file

@ -0,0 +1,258 @@
#+title: Demonstrations in Emacs
#+author: Howard X. Abrams
#+date: 2024-10-18
#+filetags: emacs hamacs
#+lastmod: [2024-10-19 Sat]
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.
;;
;; *NB:* Do not edit this file. Instead, edit the original literate file at:
;; /Users/howard.abrams/src/hamacs/ha-demos.org
;; And tangle the file to recreate this one.
;;
;;; Code:
#+end_src
* Introduction
Once 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)
:custom (demo-it--insert-test-speed :faster))
#+end_src
But I feel I should replace it.
* Presentations with Org
Used to use [[https://github.com/takaxp/org-tree-slide][org-tree-slide]] for showing org files as presentations. Converted to use [[https://github.com/rlister/org-present][org-present]]. I love the /hooks/ as that makes it easier to handle. My concern with =org-present= is how it solely displays top-level headers.
#+begin_src emacs-lisp
(use-package org-present
:config
(defvar ha-org-present-mode-line mode-line-format "Cache previous mode-line format state")
(defun ha-org-blocks-hide-headers ()
"Make the headers and other block metadata invisible.
See `ha-org-blocks-show-headers'."
(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 (1+ (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))
(defun ha-org-present-start ()
(unless ha-org-present-mode-line
(setq ha-org-present-mode-line mode-line-format))
(goto-char (point-min)) (re-search-forward (rx bol "*"))
(ha-org-blocks-hide-headers)
(org-present-big)
(org-display-inline-images)
(org-present-read-only)
(jinx-mode -1) ; Turn off spell checking
(evil-normal-state)
(setq org-image-actual-width nil)
;; Clear the demonstration state cache:
(clrhash ha-demo-prev-state)
(setq mode-line-format nil)
(org-present-hide-cursor))
(defun ha-org-present-end ()
(org-present-small)
(org-present-read-write)
(ha-org-blocks-show-headers)
(setq mode-line-format ha-org-present-mode-line)
(jinx-mode) ; Turn on spell checking
(org-present-show-cursor))
:hook
(org-present-mode . ha-org-present-start)
(org-present-mode-quit . ha-org-present-end)
: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
"p" #'org-present-prev
"r" #'org-present-read-only
"w" #'org-present-read-write
"q" #'org-present-quit))
#+end_src
* New Demonstration
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
(define-ha-demo ha-simple-demo
(:head "New Demonstration" :i 0) (message "Howdy")
(:head "New Demonstration" :i 1) (message "Hi there"))
(global-set-key (kbd "<f6>") 'ha-simple-demo)
(global-set-key (kbd "<f5>") 'org-present-next)
(global-set-key (kbd "S-<f5>") 'org-present-previous)
(global-set-key (kbd "C-<f5>") 'org-present-quit)
#+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 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-demo demo1
(:buffer \"demonstrations.py\") (message \"In a buffer\")
(:mode 'dired-mode) (message \"In a dired\")
(:head \"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
:head (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))))))
#+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 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 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))=.
Lets 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? Lets 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:
#+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= doesnt contain an =:i=, it matches every time when meeting the other conditions.
Lets 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
* 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

View file

@ -390,53 +390,6 @@ However, I'm just going to have to write a function to clean this.
(replace-match (match-string 1) nil :no-error)))
#+end_src
Now that is some complicated regular expressions.
* Presentations
Used to use [[https://github.com/takaxp/org-tree-slide][org-tree-slide]] for showing org files as presentations. Converted to use [[https://github.com/rlister/org-present][org-present]]. I love the /hooks/ as that makes it easier to pull out much of my =demo-it= configuration. My concern with =org-present= is that it only jumps from one top-level to another top-level header.
#+begin_src emacs-lisp
(use-package org-present
:config
(defvar ha-org-present-mode-line mode-line-format "Cache previous mode-line format state")
(defun ha-org-blocks-hide-headers ()
"Make the headers and other block metadata invisible.
See `ha-org-blocks-show-headers'."
(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 (1+ (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))
(defun ha-org-present-start ()
(unless ha-org-present-mode-line
(setq ha-org-present-mode-line mode-line-format))
(goto-char (point-min)) (re-search-forward (rx bol "*"))
(ha-org-blocks-hide-headers)
(org-present-big)
(org-display-inline-images)
(setq mode-line-format nil)
(sit-for 3) ; Wait for the cursor to stop blinking
(org-present-hide-cursor))
(defun ha-org-present-end ()
(org-present-small)
(ha-org-blocks-show-headers)
(setq mode-line-format ha-org-present-mode-line)
(org-present-show-cursor))
:hook
(org-present-mode . ha-org-present-start)
(org-present-mode-quit . ha-org-present-end))
#+end_src
* Technical Artifacts :noexport:
Note, according to [[https://www.reddit.com/r/emacs/comments/vahsao/orgmode_use_capitalized_property_keywords_or/][this discussion]] (and especially [[https://scripter.co/org-keywords-lower-case/][this essay]]), Im switching over to lower-case version of org properties. Using this helper function:

View file

@ -3,7 +3,7 @@
#+date: 2020-09-18
#+tags: emacs org
#+startup: inlineimages
#+lastmod: [2024-09-02 Mon]
#+lastmod: [2024-10-18 Fri]
A literate programming file for configuring org-mode and those files.