emacsconf.org/2023/talks/emacsconf
Starting from the next slide, you can press “play” once in the audio controls (or type “a”) to start the presentation, which advances automatically afterwards.
# of audio: #+OER_REVEAL_AUDIO_SLIDESHOW_CONFIG: audio: { advance: 0, suffix: '.opus', autoplay: false, defaultDuration: 1, defaultAudios: false, defaultAudioRate: window.location.search.match( /audio-speed/gi )? parseFloat((new URL(window.location.href)).searchParams.get('audio-speed')) : 1.0, playerOpacity: 0.8, playerStyle: 'position: fixed; bottom: 9.5vh; left: 0%; width: 30%; height:30px; z-index: 33;', prefix: 'audio/presentation' } #+HTML_HEAD: #+Title: EmacsConf.org #+Author: Sacha Chua # COMMENT +REVEAL_ROOT: https://cdn.jsdelivr.net/npm/reveal.js@4.6.1/ # COMMENT +REVEAL_ROOT: file:///home/sacha/vendor/reveal.js # COMMENT REVEAL_ROOT: http://localhost:8000/ #+REVEAL_ROOT: ./reveal-js * Backstage :noexport: You probably won't be able to republish this file on your own because it refers to files on my computer, but I'm sharing it just in case the general ideas are handy. ** Serving the files Because I refer to media files in this presentation, it can't be loaded from just a =file://= URL. I needed to use a small web server to serve it. [[https://www.npmjs.com/package/live-server][live-server]] is a NodeJS server that was quick to install and run (assuming you have NodeJS): #+begin_src sh :eval no npm install -g live-server live-server --host=127.0.0.1 #+end_src ** Libraries I decided to experiment with using RevealJS for my presentation because I wanted people to be able to click on links, copy text, and so on. I was curious about this [[https://oer.gitlab.io/emacs-reveal-howto/tts-howto.html][oer-reveal + text-to-speech]] as a way to have a fallback in case I didn't have time to record some audio narration, but I wasn't happy with any of the voices and I was able to figure out an audio workflow for recording things, so I didn't need it. After a bit of hacking around, I managed to get [[package:org-re-reveal]] and [[package:oer-reveal]] to work. I patched reveal.js to [[https://github.com/hakimel/reveal.js/pull/3491][handle speaker notes for fragments]]. It hasn't been merged yet, so I'll refer to my copy in reveal.js. I dynamically include source code from [[https://git.emacsconf.org/emacsconf-el][emacsconf-el]] and [[https://git.emacsconf.org/emacsconf-ansible][emacsconf-ansible]]. Some code in the presentation assumes the file locations, though. Someday I'll figure out how to make that more dynamic! Extra libraries that I refer to in my presentation (including defuns, etc.) that aren't autoloaded: #+begin_src emacs-lisp (require 'emacsconf-mail) (require 'emacsconf-spookfox) #+end_src #+RESULTS: :results: emacsconf-spookfox :end: ** Emacs configuration #+begin_src emacs-lisp (use-package org-re-reveal :config (setq org-re-reveal-revealjs-version "4") (setq org-re-reveal-history t)) (use-package oer-reveal :config (setq oer-reveal-plugin-4-config "audioslideshow RevealAudioSlideshow plugin/audio-slideshow/plugin.js anything RevealAnything https://cdn.jsdelivr.net/npm/reveal.js-plugins@latest/anything/plugin.js")) #+end_src ** Licenses *** org-re-reveal, oer-reveal: GPL-3.0-or-later *** RevealJS: MIT License [[https://github.com/hakimel/reveal.js][RevealJS]] Copyright (C) 2011-2023 Hakim El Hattab, http://hakim.se, and reveal.js contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. *** audio-slideshow: MIT License [[https://github.com/rajgoel/reveal.js-plugins/tree/master/audio-slideshow][audio-slideshow]] The MIT License (MIT) Copyright (c) 2021 Asvin Goel Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ** Progression #+NAME: progression | Slide | Highlight | Additional elements | |-------------------+--------------------------------+-----------------------------------------------------------------------------------| | props-map | h-email;h-properties | t-email,email-properties,t-properties | | file-prefixes | h-properties;h-filename | t-filename,properties-filename | | renaming | h-filename;h-renaming | t-renaming,filename-renaming | | shell-scripts | h-renaming;h-shell-scripts | renaming-shell-scripts,t-shell-scripts | | availability | h-properties;h-timezone | t-timezone,properties-timezone | | schedule | h-timezone;h-schedule | t-schedule,timezone-schedule | | emailing-speakers | h-timezone,h-mail-merge;h-emailing-speakers | schedule-emailing-speakers,t-emailing-speakers | | template | h-properties;h-template | t-template,properties-template | | wiki | h-template;h-wiki | t-wiki,template-wiki,schedule-wiki | | pad | h-template;h-pad | template-pad,t-pad | | mail-merge | h-template;h-mail-merge | t-mail-merge,template-mail-merge,schedule-mail-merge,emailing-speakers-mail-merge | | bbb | h-bbb | t-bbb | | checkin | h-mail-merge;h-checkin | t-checkin,bbb-checkin | | redirect | h-bbb;h-redirect | t-redirect,bbb-redirect | | shortcuts | h-email;h-shortcuts | t-shortcuts,email-shortcuts | | logbook | h-shortcuts;h-logbook | shortcuts-logbook,t-logbook | | captions | h-captions | t-captions,captions-wiki | | tramp | h-captions;h-tramp | t-tramp,captions-tramp | | crontab | h-tramp;h-crontab | tramp-crontab,bbb-crontab,t-crontab | | transitions | h-crontab;h-transitions | shell-scripts-transitions,t-transitions,shortcuts-transitions,transitions-crontab | | irc | h-transitions;h-irc | t-irc,transitions-irc | Check progression #+begin_src emacs-lisp :var progression=progression (defun my-check-progression () "Make sure all the rows have corresponding slides. Check order, too." (let ((ids (org-map-entries (lambda () (org-entry-get (point) "CUSTOM_ID")) "CUSTOM_ID={.}")) (has-map (org-map-entries (lambda () (org-entry-get-with-inheritance "CUSTOM_ID")) "map")) last current result) (dolist (p progression) (setq current (seq-position ids (car p))) (if current (progn (when (and last (< current last)) (add-to-list 'result (concat (car p) " out of order: not after " (elt ids last)))) (setq last current)) (add-to-list 'result (concat (car p) " not found")))) (dolist (p (seq-difference (mapcar 'car progression) has-map)) (add-to-list 'result (format "%s does not have map" p))) (when result (add-to-list 'result (concat "Actual order should be: " (string-join has-map ",")))) (string-join (reverse result) "\n"))) (my-check-progression) #+end_src #+RESULTS: :results: :end: ** Notes Target: (* 140 20) 2800 words #+begin_src emacs-lisp (defvar my-note-words-target 2800) (defun my-org-collect-notes () (let (results) (org-block-map (lambda () (unless (org-in-commented-heading-p) (let ((elem (org-element-at-point))) (when (string= (org-element-property :type elem) "notes") (setq results (cons (string-trim (buffer-substring-no-properties (org-element-property :contents-begin elem) (org-element-property :contents-end elem))) results))))))) (reverse results))) (defun my-org-create-notes-buffer () (interactive) (let ((notes (my-org-collect-notes))) (with-current-buffer (get-buffer-create "*Notes*") (insert (string-join notes "\n\n")) (switch-to-buffer (current-buffer))))) (defun my-count-words-in-notes () (interactive) (let ((notes (my-org-collect-notes))) (with-temp-buffer (insert (string-join notes "\n")) (let ((num (count-words-region (point-min) (point-max)))) (message "%d words (%.f%% of %d, %d to go)" num (/ (* 100.0 num) my-note-words-target) my-note-words-target (- my-note-words-target num)))))) #+end_src #+RESULTS: :results: my-count-words-in-notes :end: ** Audio #+begin_src emacs-lisp :results silent :eval no ;; Make sure everything with notes has an :audio ;; or :REVEAL_EXTRA_ATTR: data-audio-src="audio/overview.opus" (defun my-reveal-slide-id () (replace-regexp-in-string "^-\\|-$" "" (replace-regexp-in-string "[^A-Za-z0-9]+" "-" (downcase (concat (string-join (org-get-outline-path) "-") "-" (org-entry-get (point) "ITEM")))))) (org-map-entries (lambda () (org-entry-put (point) "REVEAL_EXTRA_ATTR" (format "data-audio-src=\"audio/%s.opus\"" (my-reveal-slide-id)))) "-REVEAL_EXTRA_ATTR={.}") #+end_src Compile the VTT for use with subed-record #+begin_src emacs-lisp (defun my-reveal-notes-vtt () (interactive) (let ((ms 0) results) (org-block-map (lambda () (unless (org-in-commented-heading-p) (let ((elem (org-element-at-point))) (when (string= (org-element-property :type elem) "notes") (let ((text (string-trim (buffer-substring-no-properties (org-element-property :contents-begin elem) (org-element-property :contents-end elem)))) audio-output prev-fragment prev-heading prop) (save-excursion (setq prev-heading (org-back-to-heading))) (save-excursion (setq prev-fragment (re-search-backward "#\\+ATTR_REVEAL:.*:audio \\(.+\\)" prev-heading t))) (if prev-fragment (setq audio-output (match-string 1)) (setq prop (org-entry-get (point) "REVEAL_EXTRA_ATTR")) (when (string-match "data-audio-src=\"\\(.+?\\)\"" prop) (setq audio-output (match-string 1 prop)))) (add-to-list 'results (list nil ms (+ 1000 ms) text (format "#+OUTPUT: %s" audio-output)) t) (setq ms (+ 1000 ms)))))))) (with-current-buffer (get-buffer-create "*notes.vtt*") (erase-buffer) (subed-vtt-mode) (subed-auto-insert) (subed-append-subtitle-list results) (display-buffer (current-buffer))))) #+end_src #+RESULTS: :results: my-reveal-notes-vtt :end: ** Store audio length #+begin_src emacs-lisp (defun my-reveal-cache-duration () (let ((proj-dir "~/proj/emacsconf-2023-emacsconf")) (org-map-entries (lambda () (let ((prop (org-entry-get (point) "REVEAL_EXTRA_ATTR"))) (when (and prop (string-match "audio/[-A-Za-z0-9]+?\\.opus" prop)) (org-entry-put (point) "AUDIO_DURATION_MS" (number-to-string (compile-media-get-file-duration-ms (expand-file-name (match-string 0 prop) proj-dir))))))) "REVEAL_EXTRA_ATTR={.}"))) #+end_src #+RESULTS: :results: my-reveal-cache-duration :end: #+begin_src emacs-lisp (defun my-reveal-cache-video-duration (&optional force) (let ((proj-dir "~/proj/emacsconf-2023-emacsconf")) (save-excursion (goto-char (point-min)) (while (re-search-forward "\\(?:video:\\([-~/A-Za-z0-9]+.webm\\)\\|\\(https:[^ ]+\\.gif\\)\\|file:\\([-A-Za-z0-9:]+.gif\\)\\)" nil t) (unless (and (null force) (save-match-data (org-entry-get (point) "VIDEO_DURATION"))) (org-entry-put (point) "VIDEO_DURATION" (number-to-string (compile-media-get-file-duration-ms (or (expand-file-name (match-string 1)) (match-string 2) (expand-file-name (match-string 3) proj-dir)))))))))) #+end_src #+RESULTS: :results: my-reveal-cache-video-duration :end: ** Squeeze videos as needed #+begin_src emacs-lisp (defun my-reveal-squeeze-videos () (let* ((proj-dir "~/proj/emacsconf-2023-emacsconf") (default-directory proj-dir) filename) (save-excursion (goto-char (point-min)) (while (re-search-forward "\\(?:video:\\([-A-Za-z0-9]+.webm\\)\\|\\(https:[^ ]+\\.gif\\)\\|file:\\([-A-Za-z0-9:]+.gif\\)\\)" nil t) (let* ((source (or (match-string 1) (match-string 2) (match-string 3))) (audio-duration (save-match-data (string-to-number (org-entry-get (point) "AUDIO_DURATION_MS")))) (video-duration (save-match-data (string-to-number (org-entry-get (point) "VIDEO_DURATION")))) (size (save-match-data (compile-media-video-dimensions source))) (compile-media-output-video-width (car size)) (compile-media-output-video-height (cdr size))) (unless (save-match-data (string-match "/video/" source)) (when (> video-duration audio-duration) (save-match-data (setq filename (expand-file-name (concat (my-reveal-slide-id) ".webm") (expand-file-name "video" proj-dir))) (compile-media-sync `((video (:source ,source :duration ,audio-duration))) filename)) (when (file-exists-p filename) (replace-match (concat "video:" filename) t))))))))) #+end_src ** Including the comments #+begin_src emacs-lisp (defvar my-subed-list nil) (defun my-subed-copy-comment () (interactive) (let* ((re (concat "^ *" (regexp-quote (replace-regexp-in-string "[^A-Za-z0-9]+" "" (subed-subtitle-text))))) (match (seq-find (lambda (o) (string-match re (replace-regexp-in-string "[^A-Za-z0-9]+" "" (elt o 3)))) my-subed-list))) (when match (subed-jump-to-subtitle-time-start) (insert (elt match 4))) (subed-forward-subtitle-text))) #+end_src Now I need to compare my scripted text with the actual text. There's the semi-accurate text from Google Translate, and I could also use Whisper. The talk recording has longer text. I can show the matching subtitles in a posframe. [[file:~/sync/Phone/Emacsconf talk recording.vtt]] [[file:~/proj/emacsconf-2023-emacsconf/emacsconf-compiled.vtt]] ** Experimenting with exporting only maps #+begin_src emacs-lisp (save-excursion (goto-char (org-babel-find-named-block "progression-css")) (org-babel-execute-src-block)) (let ((org-tags-exclude-from-inheritance "map") (org-export-select-tags '("map"))) (oer-reveal-export-to-html)) #+end_src #+RESULTS: :results: ~/proj/emacsconf-2023-emacsconf/index.html :end: ** Export and browse to current slide if custom_id is specified #+begin_src emacs-lisp (defun my-oer-reveal-export-to-html-and-browse-this-slide (&optional async subtreep visible-only body-only ext-plist) (interactive) (let ((filename (expand-file-name (oer-reveal-export-to-html async subtreep visible-only body-only ext-plist)))) (browse-url (concat (browse-url-file-url filename) (if (org-entry-get (point) "CUSTOM_ID") (concat "#slide-" (org-entry-get (point) "CUSTOM_ID")) ""))))) #+end_src #+begin_src emacs-lisp :results silent (defun my-org-re-reveal-set-footer-to-details () (interactive) (org-map-entries (lambda () (let ((details (org-entry-get (point) "DETAILS"))) (org-map-entries (lambda () (unless (org-entry-get (point) "NOSLIDE") (org-entry-put (point) "REVEAL_SLIDE_FOOTER" (format "%s" details details)))) nil 'tree))) "DETAILS={.}")) (my-org-re-reveal-set-footer-to-details) #+end_src #+RESULTS: :results: ((nil) (nil) (nil) (nil nil nil nil) (nil) (nil nil nil nil) (nil nil nil nil nil nil) (nil nil nil nil nil nil) (nil) (nil nil nil nil nil nil nil nil) (nil nil nil nil) (nil) (nil nil nil nil nil nil) (nil) (nil nil nil nil nil) (nil nil nil nil) (nil nil nil)) :end: #+begin_src emacs-lisp :results silent (save-excursion (my-org-re-reveal-set-footer-to-details)) #+end_src *** Check if all the audio files got produced #+begin_src emacs-lisp (save-excursion (goto-char (point-min)) (let (results) (while (re-search-forward "audio/[-A-Za-z0-9]+?\\.opus" nil t) (unless (or (org-in-commented-heading-p) (file-exists-p (expand-file-name (match-string 0) "~/proj/emacsconf-2023-emacsconf"))) (add-to-list 'results (match-string 0) t) )) (string-join results "\n"))) #+end_src #+RESULTS: :results: :REVEAL_EXTRA_ATTR: data-audio-src="audio/crontab.opus" :end: * Overview :PROPERTIES: :CUSTOM_ID: overview :REVEAL_EXTRA_ATTR: data-audio-src="audio/overview.opus" :AUDIO_DURATION_MS: 15860.0 :END: How we use Org Mode and TRAMP to organize and run a multi-track conference #+begin_notes Hi, I'm Sacha Chua. This presentation is a quick tour of some of the things we do to run EmacsConf. Since 2019, we've run it as an entirely online conference, and we do as much of the organization as possible within Emacs itself. #+end_notes ** Reasons for making this presentation :PROPERTIES: :REVEAL_EXTRA_ATTR: data-audio-src="audio/overview-three-reasons.opus" :CUSTOM_ID: reasons :AUDIO_DURATION_MS: 3166.5 :END: #+begin_notes I have three reasons for making this presentation. #+end_notes #+ATTR_REVEAL: :frag t :audio audio/overview-reason-1.opus - Revisit scrambled-together code and document things along the way #+begin_notes The first is entirely selfish: I need to figure out all the stuff I built for last year's EmacsConf, since it was a bit of a crazy scramble. #+end_notes #+ATTR_REVEAL: :frag t :audio audio/overview-reason-2.opus - Show the process of thinking about a complex project and building it up #+begin_notes The second is that I want to show people the process of thinking about a complex project, looking for little things to automate in Emacs, and building things up from small pieces. Maybe you'll get some ideas and start building tools for yourself, too. #+end_notes #+ATTR_REVEAL: :frag t :audio audio/overview-reason-3.opus - Point to more information \\ https://emacsconf.org/2023/talks/emacsconf #+begin_notes The third is that you find any of these little tools interesting, I want to point you to blog posts and source code where you can find out more. That way, you don't need to try to read and understand everything quickly. You can find this presentation and other links on the talk page at emacsconf.org/2023/talks/emacsconf. #+end_notes ** Map :map: :PROPERTIES: :CUSTOM_ID: map :REVEAL_EXTRA_ATTR: data-audio-src="audio/overview-map.opus" :AUDIO_DURATION_MS: 4866.5 :END: #+ATTR_HTML: :class r-stretch [[my-include:~/proj/emacsconf-2023-emacsconf/map.svg?wrap=export html]] #+begin_notes There are a lot of different parts, so I'll try to use this map to help make sense of it all. #+end_notes #+begin_export html #+end_export #+begin_src emacs-lisp :results silent :exports none (defun my-reveal-svg-animation (slide) (string-join (seq-map-indexed (lambda (step-ids i) (format "%s { fill: #f6f396; transition: fill %ds; transition-delay: %ds }" (mapconcat (lambda (id) (format "#slide-%s.present #%s" (car slide) id)) (split-string step-ids ",") ", ") highlight-duration (* i highlight-duration))) (split-string (elt slide 1) ";")) "\n")) (defun my-reveal-svg-highlight-different-colors (slide) (let* ((colors '("#f6f396" "#c6c6c6")) ; reverse (steps (split-string (elt slide 1) ";")) (step-length 0.5)) (string-join (seq-map-indexed (lambda (step-ids i) (format "%s { fill: %s; transition: fill %.1fs; transition-delay: %.1fs }" (mapconcat (lambda (id) (format "#slide-%s.present #%s" (car slide) id)) (split-string step-ids ",") ", ") (elt colors (- (length steps) i 1)) step-length (* i 0.5))) steps)))) #+end_src #+NAME: progression-css #+begin_src emacs-lisp :exports results :var map-progression=progression :var highlight-duration=2 :wrap export html (let (full) (format "" (mapconcat (lambda (slide) (setq full (append (split-string (elt slide 2) ",") full)) (format "#slide-%s.present #text path { opacity: 0.2 } #slide-%s.present #arrows path { opacity: 0.4 } %s { opacity: 1 !important } %s" (car slide) (car slide) (mapconcat (lambda (id) (format "#slide-%s.present #%s" (car slide) id)) full ", ") (my-reveal-svg-highlight-different-colors slide) )) map-progression "\n" ))) #+end_src #+RESULTS: progression-css #+begin_export html #+end_export * Organizing information :PROPERTIES: :CUSTOM_ID: info :REVEAL_EXTRA_ATTR: data-audio-src="audio/info.opus" :AUDIO_DURATION_MS: 5246.5 :END: #+begin_notes There's so much information to work with, so it probably doesn't surprise you that we use Org Mode a lot. #+end_notes #+begin_export html #+end_export #+ATTR_REVEAL: :frag t :audio audio/info-email.opus - E-mail: Notmuch + Emacs #+begin_notes Most of the conference coordination happens over e-mail, which I can quickly search with notmuch. #+end_notes #+ATTR_REVEAL: :frag t :audio audio/info-conf.opus - Private conf.org #+begin_notes Some of the information is private, like emergency contact numbers. We store the talk information in a private Org file. #+end_notes #+ATTR_REVEAL: :frag t :audio audio/info-organizers-notebook.opus - [[https://emacsconf.org/2023/organizers-notebook/][Public organizers' notebook]] #+begin_notes I try to put as much as possible into our public organizers' notebook so that processes and decisions are documented. #+end_notes #+ATTR_REVEAL: :frag t :audio audio/info-public.opus - Public webpages #+begin_notes We need a public website. We use Ikiwiki to make the webpages because we can work with plain text files in a Git repository. We also make a few static HTML pages for things where Ikiwiki is a little awkward. #+end_notes #+ATTR_REVEAL: :frag t :audio audio/info-lists.opus - Mailing lists #+begin_notes We post announcements to mailing lists. We also receive submissions in a private mailing list so that a number of people can review them. #+end_notes #+ATTR_REVEAL: :frag t :audio audio/info-backstage.opus - Backstage area #+begin_notes We have a backstage area for sharing files with volunteers and speakers. #+end_notes #+ATTR_REVEAL: :frag t :audio audio/info-media.opus - Public media files #+begin_notes We share those files publicly when the talk goes live. #+end_notes #+ATTR_REVEAL: :frag t :audio audio/info-config.opus - Configuration, etc. #+begin_notes And there's all the other stuff that goes into running EmacsConf, like shell scripts and configuration files. #+end_notes ** Storing talk information as properties :map: :PROPERTIES: :CUSTOM_ID: props-map :UNNUMBERED: t :REVEAL_EXTRA_ATTR: data-audio-src="audio/organizing-information-storing-talk-information-as-properties.opus" :AUDIO_DURATION_MS: 8966.5 :END: #+ATTR_HTML: :class r-stretch [[my-include:~/proj/emacsconf-2023-emacsconf/map.svg?wrap=export html]] #+begin_notes First, speakers propose a talk by sending an e-mail. We take the info from that e-mail and store it in Org properties so that we can work with it later. #+end_notes ** Basic talk properties :PROPERTIES: :CUSTOM_ID: talk-props :REVEAL_EXTRA_ATTR: data-audio-src="audio/organizing-information-basic-talk-properties.opus" :AUDIO_DURATION_MS: 13766.5 :END: #+begin_src org ,* WAITING_FOR_PREREC EmacsConf.org: How we use Org Mode and TRAMP to organize and run a multi-track conference SCHEDULED: <2023-12-03 Sun 14:55-15:15> :PROPERTIES: :CUSTOM_ID: emacsconf :SLUG: emacsconf :NAME: Sacha Chua :NAME_SHORT: Sacha :END: #+end_src #+begin_notes Every talk is identified with an ID, but since =:ID:= and =:CUSTOM_ID:= have special meanings for Org, I use =:SLUG:= as the keyword. Speakers' names go into the =:NAME:= property, and a short version goes into =:NAME_SHORT:= so that we can include that in a greeting. #+end_notes ** Capturing a talk proposal from an e-mail :PROPERTIES: :DETAILS: https://sachachua.com/blog/2023/09/emacsconf-capturing-submissions-from-e-mails/ :CUSTOM_ID: email-capture :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-capturing-submissions-from-e-mails/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/organizing-information-capturing-a-talk-proposal-from-an-e-mail.opus" :AUDIO_DURATION_MS: 2186.5 :END: https://sachachua.com/blog/2023/09/emacsconf-capturing-submissions-from-e-mails/2023-09-05_13-09-57.png #+begin_notes If people follow the template closely... #+end_notes ** Parsing a submission from e-mail :PROPERTIES: :DETAILS: https://sachachua.com/blog/2023/09/emacsconf-capturing-submissions-from-e-mails/ :CUSTOM_ID: email-parse :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-capturing-submissions-from-e-mails/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/organizing-information-parsing-a-submission-from-e-mail.opus" :AUDIO_DURATION_MS: 8046.5 :END: #+begin_r-stretch [[defun:emacsconf-mail-parse-submission?bare=1][emacsconf-mail-parse-submission: Extract data from EmacsConf 2023 submissions in BODY.]] #+end_r-stretch #+begin_notes ...we can even automatically fill in the Org subtree for their talk. We can use regular expressions to recognize the text and extract the properties. #+end_notes ** Setting the property to a region :PROPERTIES: :CUSTOM_ID: set-property :DETAILS: https://sachachua.com/blog/2023/09/emacsconf-capturing-submissions-from-e-mails/ :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-capturing-submissions-from-e-mails/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/organizing-information-setting-the-property-to-a-region.opus" :AUDIO_DURATION_MS: 12686.5 :END: [[defun:my-org-set-property?bare=1][my-org-set-property: In the current entry, set PROPERTY to VALUE.]] #+begin_notes Other properties need to be set by hand. I often mess things up when I retype them. To avoid typos, I have a function that sets a property based on the current region. I bind that to =C-c C-x p=. #+end_notes ** Setting properties :PROPERTIES: :REVEAL_EXTRA_ATTR: data-audio-src="audio/organizing-information-setting-properties.opus" :AUDIO_DURATION_MS: 5366.5 :VIDEO_DURATION: 16030.000000000002 :END: [[video:video/organizing-information-setting-properties.webm?controls=1&autoplay=1]] #+begin_notes That makes it much easier to set properties that couldn't automatically be recognized. #+end_notes * Calculating and then editing properties :PROPERTIES: :CUSTOM_ID: file-prefixes :DETAILS: https://sachachua.com/blog/2023/10/emacsconf-backstage-file-prefixes/ :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/10/emacsconf-backstage-file-prefixes/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/calculating-and-then-editing-properties.opus" :AUDIO_DURATION_MS: 5906.5 :END: ** :map: :PROPERTIES: :NOSLIDE: t :UNNUMBERED: t :REVEAL_EXTRA_ATTR: data-audio-src="audio/calculating-and-then-editing-properties.opus" :AUDIO_DURATION_MS: 5906.5 :END: #+ATTR_HTML: :class r-stretch :style height:500px [[my-include:~/proj/emacsconf-2023-emacsconf/map.svg?wrap=export html]] #+begin_notes Sometimes it makes sense to dynamically generate a property and then edit it, like with filenames. #+end_notes ** Calculating and then editing properties :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/10/emacsconf-backstage-file-prefixes/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/calculating-and-then-editing-properties-calculating-and-then-editing-properties.opus" :AUDIO_DURATION_MS: 11586.5 :END: #+begin_example :FILE_PREFIX: emacsconf-year-slug--title--speakers #+end_example #+begin_notes We like to name all the talk files the same way, but sometimes special characters in talk titles or speaker names need a little tweaking. I'll put that in a =FILE_PREFIX= property so I can edit it. #+end_notes ** Setting properties that are unset :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/10/emacsconf-backstage-file-prefixes/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/calculating-and-then-editing-properties-setting-properties-that-are-unset.opus" :AUDIO_DURATION_MS: 5786.5 :END: [[defun:emacsconf-set-file-prefixes?bare=1][emacsconf-set-file-prefixes: Set the FILE_PREFIX property for each talk entry that needs it.]] #+begin_notes An Org property match can map over all the talk entries that don't have =FILE_PREFIX= defined. #+end_notes * Renaming files :PROPERTIES: :CUSTOM_ID: renaming :REVEAL_EXTRA_ATTR: data-audio-src="audio/renaming-files.opus" :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/10/emacsconf-backstage-file-prefixes/ :AUDIO_DURATION_MS: 3726.5 :END: ** :map: :PROPERTIES: :NOSLIDE: t :UNNUMBERED: t :REVEAL_EXTRA_ATTR: data-audio-src="audio/renaming-files.opus" :AUDIO_DURATION_MS: 3726.5 :END: #+ATTR_HTML: :class r-stretch :style height:500px [[my-include:~/proj/emacsconf-2023-emacsconf/map.svg?wrap=export html]] #+begin_notes We can use that =FILE_PREFIX= to rename files from Emacs. #+end_notes ** Renaming files with Emacs Lisp :PROPERTIES: :DETAILS: https://git.emacsconf.org/emacsconf-el/tree/emacsconf.el :REVEAL_SLIDE_FOOTER: https://git.emacsconf.org/emacsconf-el/tree/emacsconf.el :REVEAL_EXTRA_ATTR: data-audio-src="audio/renaming-files-renaming-files-with-emacs-lisp.opus" :AUDIO_DURATION_MS: 5986.5 :END: #+begin_stretch [[defun:emacsconf-rename-files?bare=1]] #+end_stretch #+begin_notes With that property, we can then rename files using that prefix, some extra text, and the file extension. #+end_notes * Renaming files with shell scripts :PROPERTIES: :CUSTOM_ID: shell-scripts :DETAILS: https://sachachua.com/blog/2023/10/emacsconf-backstage-file-prefixes/ :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/10/emacsconf-backstage-file-prefixes/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/renaming-files-with-shell-scripts.opus" :AUDIO_DURATION_MS: 6386.5 :END: ** :map: :PROPERTIES: :NOSLIDE: t :UNNUMBERED: t :REVEAL_EXTRA_ATTR: data-audio-src="audio/renaming-files-with-shell-scripts.opus" :AUDIO_DURATION_MS: 6386.5 :END: #+ATTR_HTML: :class r-stretch :style height:500px [[my-include:~/proj/emacsconf-2023-emacsconf/map.svg?wrap=export html]] #+begin_notes Sometimes it's easier to work with the data outside Emacs, like when I want to rename files with a shell script. #+end_notes ** Exporting the information as JSON :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/10/emacsconf-backstage-file-prefixes/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/renaming-files-with-shell-scripts-exporting-the-information-as-json.opus" :AUDIO_DURATION_MS: 3366.5 :END: [[defun:emacsconf-publish-talks-json?bare=1]] #+begin_notes If I export a subset of the data as JSON or JavaScript Object Notation using =json-encode=... #+end_notes ** Using the data with JQ :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/10/emacsconf-backstage-file-prefixes/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/renaming-files-with-shell-scripts-using-the-data-with-jq.opus" :AUDIO_DURATION_MS: 7206.5 :END: [[emacsconf-ansible:roles/prerec/templates/rename-original.sh]] #+INCLUDE: /home/sacha/proj/emacsconf/emacsconf-ansible/roles/prerec/templates/rename-original.sh src sh :eval no #+begin_notes ... then I can extract the data with =jq= and use it in shell scripts. #+end_notes * Parsing availability :PROPERTIES: :CUSTOM_ID: availability :DETAILS: https://sachachua.com/blog/2023/09/emacsconf-converting-timezones/ :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-converting-timezones/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/parsing-availability.opus" :AUDIO_DURATION_MS: 11806.5 :END: ** :map: :PROPERTIES: :UNNUMBERED: t :NOSLIDE: t :REVEAL_EXTRA_ATTR: data-audio-src="audio/parsing-availability.opus" :AUDIO_DURATION_MS: 11806.5 :END: #+ATTR_HTML: :class r-stretch :style height:500px [[my-include:~/proj/emacsconf-2023-emacsconf/map.svg?wrap=export html]] #+begin_notes Another example of semi-structured information is speaker availability. We have speakers from all over the world, so we try to schedule live Q&A sessions when they're around. That means working with timezones. #+end_notes ** Setting the timezone :PROPERTIES: :CUSTOM_ID: set-timezone :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-converting-timezones/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/parsing-availability-setting-the-timezone.opus" :AUDIO_DURATION_MS: 5286.5 :VIDEO_DURATION: 11280.0 :END: video:video/parsing-availability-setting-the-timezone.webm?controls=1&autoplay=1 #+begin_notes Completion makes it much easier to set the timezone property without worrying about typos. #+end_notes ** The code :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-converting-timezones/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/parsing-availability-the-code.opus" :AUDIO_DURATION_MS: 6306.5 :END: [[defun:emacsconf-timezone-set?bare=1]] #+begin_notes We can take advantage of the timezone list from the [[https://github.com/md-arif-shaikh/tzc][tzc]] package, which works with Unix timezone definitions. #+end_notes ** Converting timezones and encoding the availability :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-converting-timezones/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/parsing-availability-converting-timezones-and-encoding-the-availability.opus" :AUDIO_DURATION_MS: 7026.5 :VIDEO_DURATION: 24220.0 :END: #+begin_example <= 11:00 EST #+end_example video:video/parsing-availability-converting-timezones-and-encoding-the-availability.webm?controls=1&autoplay=1 #+begin_notes Then we can convert times using Emacs. Using a standard format to encode the availability makes it easier to parse. #+end_notes ** Validating errors :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-converting-timezones/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/parsing-availability-validating-errors.opus" :AUDIO_DURATION_MS: 5026.5 :VIDEO_DURATION: 11980.0 :END: video:video/parsing-availability-validating-errors.webm?controls=1&autoplay=1 #+begin_notes I can use those availability constraints to report errors when I'm experimenting with the schedule. #+end_notes #+begin_comment TODO Update screenshot to use overlay talk? #+end_comment * Scheduling talks :PROPERTIES: :DETAILS: https://sachachua.com/blog/2023/09/emacsconf-backstage-scheduling-with-svgs/ :CUSTOM_ID: schedule :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-backstage-scheduling-with-svgs/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/scheduling-talks.opus" :AUDIO_DURATION_MS: 3806.5 :END: *** :map: :PROPERTIES: :UNNUMBERED: t :NOSLIDE: t :REVEAL_EXTRA_ATTR: data-audio-src="audio/scheduling-talks.opus" :AUDIO_DURATION_MS: 3806.5 :END: #+ATTR_HTML: :class r-stretch :style height:500px [[my-include:~/proj/emacsconf-2023-emacsconf/map.svg?wrap=export html]] #+begin_notes Now that I have the availability information, I can think about scheduling. #+end_notes *** Schedule as table :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-backstage-scheduling-with-svgs/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/scheduling-talks-schedule-as-table.opus" :AUDIO_DURATION_MS: 11906.5 :END: https://sachachua.com/blog/2023/09/emacsconf-backstage-scheduling-with-svgs/2023-09-10-11-41-42.svg #+begin_notes When we were planning EmacsConf 2022, the schedule was so full, I wanted to see if we could make it more manageable by splitting it up into two tracks. It was hard to think about times with just a table. #+end_notes *** Schedule as SVG :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-backstage-scheduling-with-svgs/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/scheduling-talks-schedule-as-svg.opus" :AUDIO_DURATION_MS: 7526.5 :VIDEO_DURATION: 2940.0 :END: #+begin_notes I was able to turn the schedule information into an SVG to convince the other organizers to get on board with this crazy plan. And the nice thing about SVGs is that they can even be clickable on the wiki. #+end_notes https://sachachua.com/blog/2023/09/emacsconf-backstage-scheduling-with-svgs/opening-talk.gif *** Testing different schedules :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-backstage-scheduling-with-svgs/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/scheduling-talks-testing-different-schedules.opus" :AUDIO_DURATION_MS: 25166.5 :VIDEO_DURATION: 12530.0 :END: https://sachachua.com/blog/2023/09/emacsconf-backstage-scheduling-with-svgs/output-2023-09-10-12:20:14.gif #+begin_notes Being able to quickly make SVGs of different schedules also helped me test scheduling ideas and think out loud. I could change the time between talks, the order of the talks, and even what tracks the talks were in. This was helpful when I needed to include some late submissions or availability changes and I wanted to ask speakers what they thought. They could see the different schedule options themselves. It's really nice to have Emacs Lisp support for working with SVGs. I also love how I can have an Emacs Lisp block in an Org Mode document that updates an SVG that I can view right there in my text editor. #+end_notes * Translating times :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-converting-timezones/ :CUSTOM_ID: emailing-speakers :REVEAL_EXTRA_ATTR: data-audio-src="audio/translating-times.opus" :AUDIO_DURATION_MS: 8986.5 :END: ** :map: :PROPERTIES: :NOSLIDE: t :UNNUMBERED: t :REVEAL_EXTRA_ATTR: data-audio-src="audio/translating-times.opus" :AUDIO_DURATION_MS: 8986.5 :END: #+ATTR_HTML: :class r-stretch :style height:500px [[my-include:~/proj/emacsconf-2023-emacsconf/map.svg?wrap=export html]] #+begin_notes Setting the timezone lets me automatically translate times to the speaker's local timezone when I e-mail them. #+end_notes ** Translating times :PROPERTIES: :REVEAL_EXTRA_ATTR: data-audio-src="audio/translating-times-translating-times.opus" :AUDIO_DURATION_MS: 9246.5 :END: #+begin_example As of the time I write this e-mail, your tentative schedule is: Improving compiler diagnostics with Overlays Dec 2 Sat 1:00 pm EST translated to your local timezone US/Pacific: Dec 2 Sat 10:00 am PST I'm using ">= 11:00 -0500 (08:00 US/Pacific)" as the availability constraint for you when planning the talks, but since I sometimes mess up encoding these things, could you please doublecheck that this works for you? #+end_example =format-time-string= #+begin_notes That's mostly a matter of using =format-time-string= with a timezone. We use that in our templates, which I'll talk about a bit later. #+end_notes * Templates :PROPERTIES: :CUSTOM_ID: template :REVEAL_EXTRA_ATTR: data-audio-src="audio/templates.opus" :AUDIO_DURATION_MS: 3626.5 :END: *** :map: :PROPERTIES: :UNNUMBERED: t :NOSLIDE: t :REVEAL_EXTRA_ATTR: data-audio-src="audio/templates.opus" :AUDIO_DURATION_MS: 3626.5 :END: #+ATTR_HTML: :class r-stretch :style height:500px [[my-include:~/proj/emacsconf-2023-emacsconf/map.svg?wrap=export html]] #+begin_notes It's not just about graphics. There's also a lot of text to work with, which means templates are super handy. #+end_notes ** A little function for templates :PROPERTIES: :DETAILS: https://sachachua.com/blog/2023/09/emacsconf-backstage-adding-a-talk-to-the-wiki/ :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-backstage-adding-a-talk-to-the-wiki/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/templates-a-little-function-for-templates.opus" :AUDIO_DURATION_MS: 28646.5 :END: - tempo? - s-lex-format: =(let ((x 1)) (s-lex-format "x is: ${x}"))= - format: something %s something %s something %s ... ? - concat: hard to get overall view #+begin_notes There are a number of templating functions for Emacs Lisp, like the built-in =tempo.el= or =s-lex-format= from =s.el=. I ended up writing something that works with property lists (plists) instead, since we use plists all over the emacsconf-el library. All it does is replace =${variable}= with the value from a property list. I use this mostly because I have a hard time keeping track of which =%s= is which when I use =format=, and it's hard to get an overall view if I just use =concat=. #+end_notes ** The code :PROPERTIES: :REVEAL_EXTRA_ATTR: data-audio-src="audio/templates-the-code.opus" :AUDIO_DURATION_MS: 6546.5 :END: [[defun:emacsconf-replace-plist-in-string?bare=1][emacsconf-replace-plist-in-string: Replace ${keyword} from ATTRS in STRING.]] #+begin_notes The code looks for the properties and replaces them with the values. I just find it a little easier to think about sometimes. #+end_notes #+begin_src emacs-lisp :eval no (emacsconf-replace-plist-in-string '(:test "Hello" :another-var "world") "Test 1: ${test} Test 2: ${another-var}") ;; Test 1: Hello Test 2: world #+end_src ** Getting all the talk information :PROPERTIES: :REVEAL_EXTRA_ATTR: data-audio-src="audio/templates-getting-all-the-talk-information.opus" :AUDIO_DURATION_MS: 5986.5 :END: [[defun:emacsconf-get-talk-info?bare=1]] #+begin_notes Getting all the information is just a matter of going over all the talk entries using =org-map-entries=. #+end_notes ** Talk info functions :PROPERTIES: :REVEAL_EXTRA_ATTR: data-audio-src="audio/templates-talk-info-functions.opus" :AUDIO_DURATION_MS: 3166.5 :END: [[defun:emacsconf-get-talk-info-for-subtree?bare=1]] [[defvar:emacsconf-talk-info-functions?bare=1][emacsconf-talk-info-functions: Functions to collect information.]] #+begin_notes This builds the talk info by running a bunch of functions. #+end_notes ** Getting info from the Org file :PROPERTIES: :REVEAL_EXTRA_ATTR: data-audio-src="audio/templates-getting-info-from-the-org-file.opus" :AUDIO_DURATION_MS: 2926.5 :END: [[defun:emacsconf-get-talk-abstract-from-subtree?bare=1]] #+begin_notes Some functions get the information from the Org file. #+end_notes ** Modifying talk info :PROPERTIES: :REVEAL_EXTRA_ATTR: data-audio-src="audio/templates-modifying-talk-info.opus" :AUDIO_DURATION_MS: 2726.5 :END: [[defun:emacsconf-add-talk-status?bare=1]] #+begin_notes Other functions use the info already collected. #+end_notes ** Performance :PROPERTIES: :REVEAL_EXTRA_ATTR: data-audio-src="audio/templates-performance.opus" :AUDIO_DURATION_MS: 11346.5 :END: [[https://github.com/skeeto/emacs-memoize][memoize]] for performance #+begin_notes This can take a while to do again and again. It's useful to =memoize= this function when I know I'll be using it a lot, like when I export the organizers notebook. Memoize caches recent values. #+end_notes * Updating the conference wiki :PROPERTIES: :CUSTOM_ID: wiki :DETAILS: https://sachachua.com/blog/2023/09/emacsconf-backstage-adding-a-talk-to-the-wiki/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/updating-the-conference-wiki.opus" :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-backstage-adding-a-talk-to-the-wiki/ :AUDIO_DURATION_MS: 15986.5 :END: ** :map: :PROPERTIES: :UNNUMBERED: t :NOSLIDE: t :REVEAL_EXTRA_ATTR: data-audio-src="audio/updating-the-conference-wiki.opus" :AUDIO_DURATION_MS: 15986.5 :END: #+ATTR_HTML: :class r-stretch :style height:500px [[my-include:~/proj/emacsconf-2023-emacsconf/map.svg?wrap=export html]] #+begin_notes We combine this templating function with the talk information to fill in the conference wiki, since that's a matter of writing templated strings to files. The talk pages are generated once and then left alone for manual editing, while the navigation is regenerated every time we change the details. #+end_notes ** The conference wiki :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-backstage-adding-a-talk-to-the-wiki/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/updating-the-conference-wiki-the-conference-wiki.opus" :AUDIO_DURATION_MS: 12046.5 :END: https://sachachua.com/blog/2023/09/emacsconf-backstage-adding-a-talk-to-the-wiki/2023-09-26_14-09-07.png #+begin_notes Here are some examples of how we fill in the conference wiki. We put in the format of the talk, how Q&A works, and what the status is. Once the talk is live, we include the video and the links to the files, too. #+end_notes ** The code :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-backstage-adding-a-talk-to-the-wiki/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/updating-the-conference-wiki-the-code.opus" :AUDIO_DURATION_MS: 10966.5 :END: #+begin_src emacs-lisp ;; from emacsconf-publish-format-talk-schedule-info (emacsconf-replace-plist-in-string (append o (list :irc-info (format "Discuss on IRC: [#%s](%s) \n" (plist-get o :channel) (plist-get o :webchat-url)) ; ... more entries )) "[[!toc ]] Format: ${format} ${pad-info}${irc-info}${status-info}${schedule-info}\n ${alternate-apac-info}\n") #+end_src #+begin_notes The code is a little bit long, but the important part is that we fill in a plist with the values we calculate, and then we can use =emacsconf-replace-plist-in-string= to put that all together. #+end_notes ** Schedule :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-backstage-adding-a-talk-to-the-wiki/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/updating-the-conference-wiki-schedule.opus" :AUDIO_DURATION_MS: 8246.5 :END: https://sachachua.com/blog/2023/09/emacsconf-backstage-adding-a-talk-to-the-wiki/2023-09-26_14-00-19.png #+begin_notes The schedule is a little more complicated. I wrote an Ikiwiki directive so that the markup is more manageable, and the Emacs Lisp function uses that. #+end_notes *** The Ikiwiki directive :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-backstage-adding-a-talk-to-the-wiki/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/updating-the-conference-wiki-schedule-the-ikiwiki-directive.opus" :AUDIO_DURATION_MS: 4006.5 :END: The Ikiwiki markup looks like this: #+begin_example [[!template id=sched time="""20""" q-and-a="""BBB""" startutc="""2023-12-02T19:50:00+0000""" endutc="""2023-12-02T20:10:00+0000""" start="""2:50""" end="""3:10""" title="""EmacsConf.org: How we use Org Mode and TRAMP to organize and run a multi-track conference""" url="""/2023/talks/emacsconf""" speakers="""Sacha Chua""" track="""Development""" watch="""https://emacsconf.org/2023/watch/dev""" slug="""emacsconf""" note=""""""]] #+end_example #+begin_notes The Ikiwiki directive takes all the data and turns it into HTML... #+end_notes *** Emacs Lisp :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-backstage-adding-a-talk-to-the-wiki/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/updating-the-conference-wiki-schedule-emacs-lisp.opus" :AUDIO_DURATION_MS: 6766.5 :END: =emacsconf-publish-sched-directive= in [[emacsconf-el:emacsconf-publish.el]] #+begin_src emacs-lisp (format "[[!template id=sched%s]]" (let ((result "") (attrs ;; ... some code to make a property list based on the talk ... )) (while attrs (let ((field (pop attrs)) (val (pop attrs))) (when val (setq result (concat result " " (substring (symbol-name field) 1) "=\"\"\"" val "\"\"\""))))) result)) #+end_src #+begin_notes ...so we can use Emacs Lisp to iterate over a slightly smaller property list and put them into the format Ikiwiki expects. #+end_notes ** Navigation :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-backstage-adding-a-talk-to-the-wiki/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/updating-the-conference-wiki-navigation.opus" :AUDIO_DURATION_MS: 4786.5 :END: https://sachachua.com/blog/2023/09/emacsconf-backstage-adding-a-talk-to-the-wiki/2023-09-26_14-05-51.png #+begin_notes It's nice to be able to navigate between talks without going back to the schedule page each time. #+end_notes ** Emacs Lisp :PROPERTIES: :REVEAL_SLIDE_FOOTER: https://sachachua.com/blog/2023/09/emacsconf-backstage-adding-a-talk-to-the-wiki/ :REVEAL_EXTRA_ATTR: data-audio-src="audio/updating-the-conference-wiki-emacs-lisp.opus" :AUDIO_DURATION_MS: 11226.5 :END: [[defun:emacsconf-publish-nav-pages?bare=1][emacsconf-publish-nav-pages: Generate links to the next and previous talks.]] #+begin_notes This is handled by keeping two extra copies of the list: one with the first talk popped off, and one with an extra element added to the beginning. Then we can use the heads of those lists for next/previous links. #+end_notes * Etherpad :PROPERTIES: :CUSTOM_ID: pad :REVEAL_EXTRA_ATTR: data-audio-src="audio/etherpad.opus" :AUDIO_DURATION_MS: 1846.5 :END: ** :map: :PROPERTIES: :NOSLIDE: t :UNNUMBERED: t :END: #+ATTR_HTML: :class r-stretch :style height:500px [[my-include:~/proj/emacsconf-2023-emacsconf/map.svg?wrap=export html]] #+begin_notes Links to the next talks are also handy on the collaborative Etherpad documents that we use for collecting questions, answers, and notes during each talk. #+end_notes ** API :PROPERTIES: :REVEAL_EXTRA_ATTR: data-audio-src="audio/etherpad-api.opus" :AUDIO_DURATION_MS: 1846.5 :END: [[defun:emacsconf-pad-set-html?bare=1][emacsconf-pad-set-html: Set PAD-ID contents to the given HTML.]] #+begin_notes Etherpad has an API... #+end_notes ** Pad template :PROPERTIES: :DETAILS: emacsconf-pad-initial-content :REVEAL_SLIDE_FOOTER: emacsconf-pad-initial-content :REVEAL_EXTRA_ATTR: data-audio-src="audio/etherpad-pad-template.opus" :AUDIO_DURATION_MS: 3526.5 :END: #+begin_example