A bootstrapping Emacs-30.0.50 setup for technical PDF reports using Org-Mode, Org-Babel, Lisp, Python, and LaTeX.
Go to file
Gerard Vermeulen 107587e01f Freeze many changes
* Implement headers and footers for export to LaTeX.
* Implement a title page for export to LaTeX.
* Improve the text in many places.
* Configure export to LaTeX using buffer local variables.
2021-12-23 15:29:03 +01:00
.githooks Fix org-babel-tangle-file usage 2021-11-30 11:57:07 +01:00
.gitignore Freeze many changes 2021-12-23 15:29:03 +01:00
.ignore Fix ripgrep by overriding the .gitignore file 2021-12-01 11:52:28 +01:00
Emacs-logo.png Freeze many changes 2021-12-23 15:29:03 +01:00
Makefile Fix org-babel-tangle-file usage 2021-11-30 11:57:07 +01:00
Org-mode-unicorn.png Freeze many changes 2021-12-23 15:29:03 +01:00
README.org Freeze many changes 2021-12-23 15:29:03 +01:00
org-babel-tangle-file Freeze many changes 2021-12-23 15:29:03 +01:00

README.org

Emacs setup for use with LaTeX, Org, and Python

#

#

#

#

#

#

Quick start

Backup your user-emacs-directory (defaults often to ~/.emacs.d on Linux, Unix, or Darwin) directory and execute the commands in listing lst:prepare-user-emacs-directory. After normal (interactive mode, no batch mode, neither server mode), Emacs will ask you to install a selected set of packages. Quit Emacs and invoke Emacs again.

  cd ~
  git clone ccdr@mercury.grenoble.cnrs.fr:SERVER/emacs.d.git .emacs.d
  make --directory=.emacs.d init
  emacs &

Introduction

This Emacs setup aims to install automatically a minimal set of extension packages that allows to handle my reports and presentations. The file format of the reports is Org Mode plain text with Python source code blocks and the file format of the presentations is LaTeX.

This org file (more precisely the original org source file of this file) illustrates three methods in my work-flow:

  1. How to tangle (or export) source blocks from org files. This file contains source blocks to produce the files early-init.el, init.el, latexmkrc, org-store-link, and example.py by tangling.
  2. How to export org files to other formats such as HTML, LaTeX, and PDF.
  3. How org hyperlinks (info) allow to link inside and outside Org Mode: hover over or click on the links to experiment.

The AUCTeX - Aalborg University Center TeX extension package provides a powerful Text-based User Interface (TUI) environment to edit the LaTeX presentations.

The citar extension package provides quick filtering and selecting of bibliographic entries, and the option to run different commands on those selections. Citar requires Org-9.5 (info), which is already part of Emacs-28.1. Citar exploits the enhancements of Emacs' builtin selection mechanism provided by the extension packages vertico, orderless, embark, marginalia, and consult. The citeproc extension package provides CSL: citation style language processing capabilities to citar and Org Mode.

The pdf-tools extension package renders PDF file with the possibility to annotate the file or to click on anchors in the PDF file that link back to the original LaTeX file of the document. An example of my work-flow are the steps to convert this org file to PDF and to see the result with pdf-tools in Emacs: execute the commands pdf-tools-install, org-babel-tangle, org-latex-export-latex-to-latex, and compile. This sets up an infinite LaTeX compilation loop to update and redisplay the PDF file after excution of the org-latex-export-latex-to-latex command in this buffer.

Here follows a list of interesting Emacs configurations:

  1. Musa Al-hassy's configuration is an impressive example of producing the Emacs initialization files and other files by tangling an org file. His methodology is impressive, as his Elisp Cheat Sheet and org-special-block-extra package show. To me, this is a configuration to admire, but his methodology is way over my head.
  2. Omar Antolín Camarena's configuration exploits built-in packages, Omar's own small packages, and large external packages. Omar is the author of orderless and embark. I have stolen his idea of using custom-set-variables.
  3. Pierre Neirhardt's configuration implements lazy loading without help of external packages. I have stolen his approach of using lazy loading to silently ignore the setup stanzas of uninstalled extension packages.
  4. Sacha Chua's configuration is a practical example of producing the Emacs initialization files by tangling an org file. It gives me the impression that she is a very practical person trying to achieve her goals by the most efficient means. I have stolen her idea of using quelpa to install packages from any source.
  5. Steve Purcell's configuration is well organized, a showcase of readable code, as well helpful commit and issue histories. See for instance the discussion on the correctness of order of company candidates in Emacs lisp mode.
  6. Timothy E. Chapman's configuration

Early Init File (info)

Try to load no-littering as early as possible, since it helps to keep ~/.emacs.d clean.

  ;;; early-init.el --- user early-init file        -*- lexical-binding: t -*-
  ;;; Commentary:
  ;;; Code:
  (setq load-prefer-newer t)

  (require 'no-littering nil 'noerror)

  (provide 'early-init)
  ;; Emacs looks for "Local variables:" after the last "?\n?\f".
  
  ;; Local Variables:
  ;; indent-tabs-mode: nil
  ;; End:
  ;;; earl-init.el ends here

In order to get help in understanding the code block above in a buffer showing the original Org source file, move point (or cursor) to one of the items of the list the and type C-c C-c:

  1. src_emacs-lisp{(describe-variable #'load-prefer-newer t)}
  2. src_emacs-lisp{(apropos-library "no-littering")}
  3. src_emacs-lisp{(find-function #'hack-local-variables)}

to execute the code between the curly braces for access to help.

This shows why Emacs is a self-documenting editor.

Init File (info) header

The user-init-file header requires cl-lib and customizes Emacs variables. It consists of three parts in listing lst:1st-custom-set-variables-call, lst:2nd-custom-set-variables-call, and lst:3rd-custom-set-variables-call in order to limit the length of the listings for exporting to LaTeX. The quoting (info) and the backquote (info) pages explain how to understand the reader macros ' (quote), ` (backquote), , (substitute) and @, (splice) in the custom-set-variables function calls in listing lst:1st-custom-set-variables-call, lst:2nd-custom-set-variables-call, and lst:3rd-custom-set-variables-call. A tutorial of how to use those reader macros is the didactic emacs-lisp macro example. The init file (info) does not load the custom-file as saving customizations (info) recommends because of the custom-set-variables function calls.

  ;;; init.el --- user init file                    -*- lexical-binding: t -*-
  ;;; Commentary:
  ;;; Code:
  (require 'cl-lib)

  (custom-set-variables
   '(after-save-hook #'executable-make-buffer-file-executable-if-script-p)
   '(column-number-mode t)
   '(cursor-type 'box)
   `(custom-file ,(locate-user-emacs-file "custom.el"))
   '(epg-pinentry-mode 'loopback)
   '(global-hl-line-mode t)
   '(global-hl-line-sticky-flag t)
   '(indent-tabs-mode nil)
   '(inhibit-startup-buffer-menu t)
   '(inhibit-startup-screen t)
   '(initial-buffer-choice t)
   '(initial-scratch-message "")
   `(insert-directory-program ,(or (executable-find "gls")
                                   (executable-find "ls")))
   '(kill-ring-max 300)
   '(package-archives '(("gnu" . "https://elpa.gnu.org/packages/")
                        ("nongnu" . "https://elpa.nongnu.org/nongnu/")
                        ("melpa" . "https://melpa.org/packages/"))))
  (custom-set-variables
   `(package-selected-packages
     `(,@(when (version< emacs-version "28.0")
           '(org))                ; plain text thought organizer
       anaconda-mode              ; strangles python-mode
       async                      ; asynchroneous processing
       auctex                     ; Aalborg University Center TeX
       blacken                    ; Black Python-code formatter client
       citar                      ; bibliography handling
       citeproc                   ; bibliography handling
       company                    ; complete anything
       company-anaconda           ; complete anything in anaconda-mode
       consult                    ; consult completing-read
       eglot                      ; Emacs polyGLOT LSP client
       electric-operator          ; automatic spacing around operators
       elfeed                     ; web feed reader
       embark                     ; act on any buffer selection
       emms                       ; Emacs Multi-Media System
       htmlize                    ; convert buffer contents to HTML
       iedit                      ; simultaneous multi-entity editing
       laas                       ; LaTeX Auto-Activating Snippets
       leuven-theme               ; beautiful color theme
       magit                      ; Git Text-based User Interface
       marginalia                 ; minibuffer margin notes
       markdown-mode              ; markdown text mode
       no-littering               ; keep `user-emacs-directory' clean
       nov                        ; EPUB reader
       orderless                  ; Emacs completion style
       pdf-tools                  ; interactive docview replacement
       pdf-view-restore           ; add view history to pdf-tools
       pyenv-mode                 ; Python environment selector
       quelpa                     ; install Emacs packages from source
       rainbow-mode               ; set background color to color string
       smartparens                ; smart editing of character pairs
       toml-mode                  ; Tom's Obvious Minimal Language mode
       vertico                    ; VERTical Interactive Completion
       wgrep                      ; open a writable grep buffer
       which-key                  ; on the fly key-binding help
       wordnut                    ; WordNet lexical database
       writegood-mode             ; bullshit and weasel-word detector
       ws-butler                  ; remove trailing whitespace
       xr                         ; undo rx to grok regular expressions
       yasnippet)))               ; code or text template expansion
  (custom-set-variables
   '(recentf-mode t)
   '(save-place-mode t)
   '(scroll-bar-mode nil)
   '(tab-always-indent 'complete)
   '(tab-width 8)
   '(tool-bar-mode nil)
   '(url-cookie-trusted-urls nil)
   '(url-cookie-untrusted-urls '(".*"))
   '(use-dialog-box nil)
   '(use-short-answer t)
   '(view-read-only t))

  (when (eq system-type 'darwin)
    (custom-set-variables
     '(ns-alternate-modifier nil)
     '(ns-command-modifier 'meta)
     '(ns-right-command-modifier 'super)))

  (when (eq window-system 'ns)
    (add-to-list 'initial-frame-alist '(height . 51))
    (add-to-list 'initial-frame-alist '(width . 180)))

Install the selected packages

Emacs installs packages from archives on the internet. This setup uses three archives:

  1. The GNU Emacs Lisp Package Archive
  2. The NonGNU Emacs Lisp Package Archive.
  3. The Milkypostmans Emacs Lisp Package Archive (MELPA).

Finally, the quelpa tool allows to fetch code from any source and build a package on your computer before installation.

The code in listing lst:install-selected-packages assumes that the package system is in a virgin state in case the package no-littering is not present. Refreshing the contents of available packages at least once is a requirement in order to be able to install and load any packages, hence also no-littering.

The call src_emacs-lisp{(package-install-selected-packages)} checks the installation status of all packages in src_emacs-lisp{package-selected-packages} and installs the missing packages after the user has agreed to its prompt. In case of normal Emacs usage, src_emacs-lisp{(package-list-packages)} refreshes the contents of packages and allows to update packages to the latest version.

  (unless (require 'no-littering nil 'noerror)
    (package-refresh-contents)
    (package-install 'no-littering)
    (require 'no-littering))

  (unless noninteractive
    (package-install-selected-packages))

Using Emacs as a server (info)

Emacs can act as a server that listens to a socket to share its state (for instance buffers and command history) with other programs by means of a shell command emacsclient. Section #sec:latexmk-save-compile-display-loop and #sec:qutebrowser-userscript show how to use emacsclient to:

  1. Install an asynchronous (or background) loop of saving a LaTeX file, compiling it, and redisplaying the output in Emacs.
  2. Make qutebrowser send html links with document titles to Emacs.

The code in listing lst:start-emacs-server starts the Emacs server.

  (when window-system
    (unless (or noninteractive (daemonp))
      (add-hook 'after-init-hook #'server-start)))

Latexmk save-compile-display loop

The latexmk resource file in the next source code block shows how to use emacsclient to (re)display the PDF file in Emacs after each succesful (re)compilation on condition that the settings of the compile-command local variable in section are compatible. The local variable compile-command in the local variables section (only visible in org files, but not in html and pdf files) shows how to use the latexmkrc file.

  # pdf previewer and update pdf previewer
  $pdf_previewer = "emacsclient -e '(find-file-other-window %S)'";
  $pdf_update_method = 4;  # 4 runs a command to force the update
  $pdf_update_command = "emacsclient -e '(with-current-buffer (find-buffer-visiting %S) (pdf-view-revert-buffer nil t))'";
  # see for instance glossary.latexmkrc
  add_cus_dep( 'acn', 'acr', 0, 'makeglossaries' );
  add_cus_dep( 'glo', 'gls', 0, 'makeglossaries' );
  $clean_ext .= " acr acn alg bbl glo gls glg ist lol run.xml";
  sub makeglossaries {
      my ($name, $path) = fileparse( $$Psource );
      return system "makeglossaries -d '$path' '$name'";
  }
  # Emacs looks for "Local variables:" after the last "?\n?\f".
  
  # Local Variables:
  # mode: perl
  # End:

Qutebrowser userscript

The next block contains an userscript that sends a store-link org-protocol message with the url and the title from qutebrowser to emacsclient. The function urlencode translates the url and the title for the message. The Python urllib examples show how to use urlencode. The final execvp call deals with a qutebrowser userscript requirement: the emacsclient process must get the PID of the userscript that must kill itself after the take-over. Termination of the emacsclient process hands control back to qutebrowser.

On a POSIX system, you can run the userscript from qutebrowser or from a terminal to see whether it works. In case you try to run it from Emacs, Emacs may hang or die.

  #!/usr/bin/env python
  from urllib.parse import urlencode
  from os import environ, execvp

  url = environ.get("QUTE_URL", "https://orgmode.org")
  title = environ.get("QUTE_TITLE", "Org Mode")
  parameters = urlencode({"url": url, "title": title})
  print(payload := f"org-protocol://store-link?{parameters}")
  execvp("emacsclient", ("-n", payload))

TODO Look into: org-protocol handling with other browser on Darwin

Completion

Vertico (info) provides a performant and minimalistic vertical completion UI based on the default completion system and behaves therefore correctly under all circumstances. Using Vertico, Marginalia, Consult, and Embark links to a video demonstration. Vertico integrates well with fully supported complementary packages to enrich the completion UI:

  1. Orderless (info) for an advanced completion style,
  2. Embark (info) for minibuffer actions with context menus,
  3. Marginalia (info) for rich annotations in the minibuffer, and
  4. Consult (info) for useful search and navigation commands,

where the order is that of enhancing citar's experience and the configuration steps below.

Finally, company: a modular complete-anything framework for Emacs provides completion in any buffer and minibuffer-history-completion provides completion on previous input in the minibuffer.

Vertico (info)

Listing lst:enable-vertico-mode configures and enables savehist-mode and enables vertico-mode. The documentation src_emacs-lisp{(describe-function 'savehist-mode)} why it is best to turn on savehist-mode in the Emacs init file.

  (unless noninteractive
    (custom-set-variables
     '(history-delete-duplicates t)
     '(history-length 500)
     '(savehist-additional-variables
       '(eww-history
         kill-ring
         regexp-search-string
         search-ring
         search-string)))
    (savehist-mode +1)
    (when (fboundp 'vertico-mode)
      (vertico-mode +1)))

Orderless (info)

Listing lst:configure-orderless enables orderless.

  (unless noninteractive
    (when (fboundp 'orderless-filter)
      (custom-set-variables
       ;; https://github.com/purcell/emacs.d/issues/778
       '(completion-styles '(basic completion-partial orderless))
       '(completion-category-defaults nil)
       '(completion-category-overrides
         '((file (styles partial-completion)))))
      (add-hook 'minibuffer-setup-hook
                (defun my-on-minibuffer-setup-hook()
                  (setq-default completion-styles '(substring orderless))))))

Embark (info)

Listing lst:configure-embark configures embark.

  (unless noninteractive
    (when (cl-every #'fboundp '(embark-act embark-bindings embark-dwim))
      (global-set-key (kbd "C-,") #'embark-act)
      (global-set-key (kbd "C-:") #'embark-dwim)
      (global-set-key (kbd "C-h B") #'embark-bindings)))

Marginalia (info)

Listing lst:enable-marginalia-mode enables marginalia-mode.

  (unless noninteractive
    (when (fboundp 'marginalia-mode)
      (marginalia-mode +1)))

Consult (info)

Listing lst:configure-consult configures consult.

  (unless noninteractive
    (when (fboundp 'consult-apropos)
      (custom-set-variables
       '(consult-project-root-function #'vc-root-dir))
      ;; C-c bindings (mode-specific-map)
      (global-set-key (kbd "C-c h") #'consult-history)
      (global-set-key (kbd "C-c m") #'consult-mode-command)
      ;; C-x bindings (ctl-x-map)
      (global-set-key (kbd "C-x M-:") #'consult-complex-command)
      (global-set-key (kbd "C-x b") #'consult-buffer)
      (global-set-key (kbd "C-x 4 b") #'consult-buffer-other-window)
      (global-set-key (kbd "C-x 5 b") #'consult-buffer-other-frame)
      (global-set-key (kbd "C-x r x") #'consult-register)
      (global-set-key (kbd "C-x r b") #'consult-bookmark)
      ;; M-g bindings (goto-map)
      (global-set-key (kbd "M-g g") #'consult-goto-line)
      (global-set-key (kbd "M-g M-g") #'consult-goto-line)
      (global-set-key (kbd "M-g o") #'consult-outline)
      (global-set-key (kbd "M-g m") #'consult-mark)
      (global-set-key (kbd "M-g k") #'consult-global-mark)
      (global-set-key (kbd "M-g i") #'consult-imenu-project)
      (global-set-key (kbd "M-g e") #'consult-error)
      ;; M-s bindings (search-map)
      (global-set-key (kbd "M-s g") #'consult-git-grep)
      (global-set-key (kbd "M-s f") #'consult-find)
      (global-set-key (kbd "M-s k") #'consult-keep-lines)
      (global-set-key (kbd "M-s l") #'consult-line)
      (global-set-key (kbd "M-s m") #'consult-multi-occur)
      (global-set-key (kbd "M-s u") #'consult-focus-lines)
      ;; Other bindings
      (global-set-key (kbd "M-y") #'consult-yank-pop)
      (global-set-key (kbd "<help> a") #'consult-apropos)
      ;; Tweak functions
      (advice-add 'completing-read-multiple
                  :override #'consult-completing-read-multiple)
      (fset 'multi-occur #'consult-multi-occur)))

Company: a modular complete anything framework for Emacs

Listing lst:configure-company configures company.

  (unless noninteractive
    (when (fboundp 'company-mode)
      (custom-set-variables
       ;; https://github.com/purcell/emacs.d/issues/778
       '(company-transformers '(company-sort-by-occurrence)))
      (dolist (hook '(LaTeX-mode-hook
                      org-mode-hook
                      emacs-lisp-mode-hook
                      lisp-interaction-mode-hook
                      python-mode-hook
                      ielm-mode-hook))
        (add-hook hook #'company-mode))))

Minibuffer history completion

See Juri Linkov (Emacs Developer mailing list) for how to allow completion on previous input in the minibuffer. Listing lst:enable-minibuffer-history-completion enables minibuffer history completion.

  (defun minibuffer-setup-history-completions ()
    (unless (or minibuffer-completion-table minibuffer-completion-predicate)
      (setq-local minibuffer-completion-table
                  (symbol-value minibuffer-history-variable))))

  (add-hook 'minibuffer-setup-hook 'minibuffer-setup-history-completions)

  ;; Stolen from Emacs-28.1 for Emacs-27.2:
  (unless (fboundp 'minibuffer--completion-prompt-end)
    (defun minibuffer--completion-prompt-end ()
      (let ((end (minibuffer-prompt-end)))
        (if (< (point) end)
            (user-error "Can't complete in prompt")
          end))))

  ;; Adapted from minibuffer-complete:
  (defun my-minibuffer-complete-history ()
    "Allow minibuffer completion on previous input."
    (interactive)
    (completion-in-region (minibuffer--completion-prompt-end) (point-max)
                          (symbol-value minibuffer-history-variable)
                          nil))

  (define-key minibuffer-local-map [C-tab] 'my-minibuffer-complete-history)

Prefix key-binding help

Configure which-key-mode so that typing C-h after a prefix key displays all keys available after the prefix key. Listing lst:enable-which-key-mode enables which-key-mode.

  (when (fboundp 'which-key-mode)
    (custom-set-variables
     '(which-key-idle-delay 10000)
     '(which-key-idle-secondary-delay 0.05)
     '(which-key-show-early-on-C-h t))
    (which-key-mode +1))

Reading

Reading EPUB files

Listing lst:enable-nov-mode enables nov-mode.

  (when (fboundp 'nov-mode)
    (add-to-list 'auto-mode-alist `(,(rx ".epub" eos) . nov-mode)))

Reading PDF files

The pdf-tools package exploits the poppler library to render and to let you annotate PDF files. It also exploits the SyncTeX library to link anchors in PDF files produced with LaTeX to the original LaTeX sources.

In order to use pdf-tools, you have to type M-x pdf-tools-install after installation of pdf-tools from MELPA or after each update of poppler to build or rebuild the epdfinfo executable that serves the PDF files to Emacs.

  ;; 'pdf-loader-install' is the lazy equivalent of 'pdf-tools-install':
  ;; see the README file.
  (when (fboundp 'pdf-loader-install)
    (pdf-loader-install))

  (with-eval-after-load 'pdf-view
    (when (fboundp 'pdf-view-restore-mode)
      (add-hook 'pdf-view-mode-hook #'pdf-view-restore-mode)))

Writing

Writing LaTeX files

Loading tex.el immediately instead of lazily ensures proper initialization of the AUCTeX. For instance, the TeX-master safe local variable in the tex.el elisp library file has no autoload cookie. Without prior loading of tex.el, Emacs will complain that TeX-master is no safe local variable in case it reads a LaTeX file that sets TeX-master.

  ;; Try to get the `safe-local-variable' predicate for `TeX-master'
  (when (require 'tex nil 'noerror)
    (custom-set-variables
     '(TeX-auto-save t)
     '(TeX-parse-self t)))
  (with-eval-after-load 'bibtex
    (custom-set-variables
     '(bibtex-dialect 'BibTeX)))
  (with-eval-after-load 'font-latex
    (custom-set-variables
     '(font-latex-fontify-sectioning 1.0)))
  (with-eval-after-load 'latex
    (custom-set-variables
     '(LaTeX-section-hook '(LaTeX-section-heading
                            LaTeX-section-title
                            LaTeX-section-toc
                            LaTeX-section-section
                            LaTeX-section-label))))

Out of the box, AUCTeX does not indent text between square brackets. The code in listing lst:configure-tex corrects this by advising to override TeX-brace-count-line with my-TeX-brace-count-line.

  (with-eval-after-load 'tex
    ;; https://emacs.stackexchange.com/questions/17396/
    ;; indentation-in-square-brackets
    (defun my-TeX-brace-count-line ()
      "Count number of open/closed braces."
      (save-excursion
        (let ((count 0) (limit (line-end-position)) char)
          (while (progn
                   (skip-chars-forward "^{}[]\\\\" limit)
                   (when (and (< (point) limit) (not (TeX-in-comment)))
                     (setq char (char-after))
                     (forward-char)
                     (cond ((eq char ?\{)
                            (setq count (+ count TeX-brace-indent-level)))
                           ((eq char ?\})
                            (setq count (- count TeX-brace-indent-level)))
                           ((eq char ?\[)
                            (setq count (+ count TeX-brace-indent-level)))
                           ((eq char ?\])
                            (setq count (- count TeX-brace-indent-level)))
                           ((eq char ?\\)
                            (when (< (point) limit)
                              (forward-char) t))))))
          count)))
    (advice-add 'TeX-brace-count-line :override #'my-TeX-brace-count-line))

TODO Improve the AUCTeX configuration slowly

Writing Org files

Org activation (info)

  ;; Inspect:
  ;; function with "C-h f"
  ;; symbols with "C-h o"
  ;; variables with "C-h v"

  (global-set-key (kbd "C-c a") #'org-agenda)
  (global-set-key (kbd "C-c c") #'org-capture)
  (global-set-key (kbd "C-c l") #'org-store-link)
  (global-set-key (kbd "C-c C-l") #'org-insert-link-global)

Org customization

The code in listing lst:customize-org-babel, lst:customize-org, and lst:customize-org-export does basic customization of Org Mode variables.

  (with-eval-after-load 'ob-core
    (custom-set-variables
     '(org-confirm-babel-evaluate nil)))

  (with-eval-after-load 'ob-python
    (custom-set-variables
     '(org-babel-python-command "python -E")))
  (with-eval-after-load 'org
    (custom-set-variables
     '(org-babel-load-languages '((C . t)
                                  (calc . t)
                                  (dot . t)
                                  (emacs-lisp . t)
                                  (eshell . t)
                                  (fortran . t)
                                  (gnuplot . t)
                                  (latex . t)
                                  (lisp . t)
                                  (maxima . t)
                                  (org . t)
                                  (perl . t)
                                  (python . t)
                                  (scheme . t)
                                  (shell . t)))
     '(org-file-apps '((auto-mode . emacs)
                       (directory . emacs)
                       ("\\.mm\\'" . default)
                       ("\\.x?html?\\'" . default)
                       ("\\.pdf\\'" . emacs)))
     '(org-modules '(ol-bibtex
                     ol-doi
                     ol-eww
                     ol-info
                     org-id
                     org-protocol
                     org-tempo))
     '(org-structure-template-alist '(("a" . "export ascii")
                                      ("c" . "center")
                                      ("C" . "comment")
                                      ("e" . "example")
                                      ("E" . "export")
                                      ("h" . "export html")
                                      ("l" . "export latex")
                                      ("q" . "quote")
                                      ("s" . "src")
                                      ("p" . "src python :session :async")
                                      ("v" . "verse")))))
  (with-eval-after-load 'ox-latex
    (custom-set-variables
     '(org-latex-compiler "lualatex")
     '(org-latex-hyperref-template nil)
     '(org-latex-listings 'minted)
     '(org-latex-minted-options '(("bgcolor" "LightGoldenrodYellow")))
     '(org-latex-logfiles-extensions '("blg" "lof" "log" "lot" "out" "toc"))
     '(org-latex-prefer-user-labels t)))

Citar: citing bibliography with Org Mode

Citar is a completing-read front-end to browse and act on BibTeX, BibLaTeX, as well as CSL JSON bibliographic data with LaTeX, markdown, and org-cite editing support. In combination with vertico, orderless, embark, marginalia, and consult, Citar provides quick filtering and selecting of bibliographic entries from the minibuffer, as well as the option to run different commands on those selections. Listing lst:configure-oc-cite+citar configures org-cite, citar, and org.

  (with-eval-after-load 'oc
    (require 'oc-biblatex)
    (require 'oc-csl)

    (custom-set-variables
     '(org-cite-export-processors '((latex biblatex)
                                    (t csl)))
     '(org-cite-global-bibliography '("~/VCS/research/refs.bib")))

    (when (require 'citar nil 'noerror)
      (custom-set-variables
       '(org-cite-activate-processor 'citar)
       '(org-cite-follow-processor 'citar)
       '(org-cite-insert-processor 'citar))))

  (with-eval-after-load 'org
    (when (require 'citar nil 'noerror)
      (custom-set-variables
       '(citar-bibliography '("~/VCS/research/refs.bib"))
       '(citar-file-extensions '("djvu" "pdf"))
       '(citar-library-paths '("~/VCS/research/papers/"))))

    (define-key org-mode-map (kbd "C-c b") 'org-cite-insert))

Translate capital keywords (old) to lower case (new)

  (with-eval-after-load 'org
    (defun org-syntax-convert-keyword-case-to-lower ()
      "Convert all #+KEYWORDS to #+keywords."
      (interactive)
      (save-excursion
        (goto-char (point-min))
        (let ((count 0)
              (case-fold-search nil))
          (while (re-search-forward "^[ \t]*#\\+[A-Z_]+" nil t)
            (unless (s-matches-p "RESULTS" (match-string 0))
              (replace-match (downcase (match-string 0)) t)
              (setq count (1+ count))))
          (message "Replaced %d keywords" count)))))

Evaluate specific source blocks at load-time

  (defun my-org-eval-blocks-named (infix)
    "Evaluate all source blocks having INFIX in their name."
    (when (eq major-mode 'org-mode)
      (let ((blocks
             (org-element-map
                 (org-element-parse-buffer 'greater-element nil) 'src-block
               (lambda (block)
                 (message "%s" (org-element-property :name block))
                 (when (string-match-p infix (org-element-property :name block))
                   block)))))
        (dolist (block blocks)
          (goto-char (org-element-property :begin block))
          (org-babel-execute-src-block)))))

  ;; Emacs looks for "Local variables:" after the last "?\n?\f".
  (add-to-list 'safe-local-eval-forms
               '(apply 'my-org-eval-blocks-named '("emacs-lisp-setup")))
  (add-to-list 'safe-local-eval-forms
               '(apply 'my-org-eval-blocks-named '("python-setup")))

Advanced Export Configuration (info)

Stolen from ox-extra.el

  (with-eval-after-load 'ox
    (defun org-latex-header-blocks-filter (backend)
      "Convert marked LaTeX export blocks to \"#+latex_header: \" lines.
  The marker is a line \"#+header: :header yes\" preceding the block.

  For instance, the LaTeX export block

  ,#+header: :header yes
  ,#+begin_export latex
  % This line converts to a LaTeX header line.
  ,#+end_export

  converts to

  \"#+latex_header: % This line converts to a LaTeX header line.\"."
      (when (org-export-derived-backend-p backend 'latex)
        (let ((blocks
               (org-element-map
                   (org-element-parse-buffer 'greater-element nil) 'export-block
                 (lambda (block)
                   (let ((type (org-element-property :type block))
                         (header (org-export-read-attribute :header block :header)))
                     (when (and (string= type "LATEX") (string= header "yes"))
                       block))))))
          (mapc (lambda (block)
                  ;; Set point to where to insert LaTeX header lines
                  ;; after deleting the block.
                  (goto-char (org-element-property :post-affiliated block))
                  (let ((lines
                         (split-string (org-element-property :value block) "\n")))
                    (delete-region (org-element-property :begin block)
                                   (org-element-property :end block))
                    (dolist (line lines)
                      (insert (concat "#+latex_header: "
                                      (replace-regexp-in-string "\\` *" "" line)
                                      "\n")))))
                ;; Reverse to go upwards to avoid wrecking the list of
                ;; block positions in the file that would occur in case
                ;; of going downwards.
                (reverse blocks))))))

How to customize org-latex-title-command only in this buffer

  (with-eval-after-load 'ox
    (make-variable-buffer-local 'org-export-before-parsing-hook)
    (cl-pushnew #'org-latex-header-blocks-filter
                org-export-before-parsing-hook))
  (when (require 'ox-latex nil 'noerror)
    (make-variable-buffer-local 'org-latex-classes)
    (cl-pushnew '("article-local"
                  "\\documentclass[11pt]{article}
  [NO-DEFAULT-PACKAGES]
  [PACKAGES]
  [EXTRA]"
                  ("\\section{%s}" . "\\section*{%s}")
                  ("\\subsection{%s}" . "\\subsection*{%s}")
                  ("\\subsubsection{%s}" . "\\subsubsection*{%s}")
                  ("\\paragraph{%s}" . "\\paragraph*{%s}")
                  ("\\subparagraph{%s}" . "\\subparagraph*{%s}"))
                org-latex-classes :key #'car :test #'equal)

    (make-variable-buffer-local 'org-latex-title-command)
    (setq org-latex-title-command (concat title-page))

    (make-variable-buffer-local 'org-latex-toc-command)
    (setq org-latex-toc-command
          (mapconcat 'identity '(""
                                 "\\tableofcontents\\label{toc}"
                                 "\\listoffigures"
                                 "\\listoflistings"
                                 "\\listoftables"
                                 "\\newpage"
                                 "") "\n"))

    (make-variable-buffer-local 'org-latex-subtitle-format)
    (setq org-latex-subtitle-format ""))
  (defun my-ox-latex-export-buffer-local-variables (title-page)
    (with-eval-after-load 'ox
      (make-variable-buffer-local 'org-export-before-parsing-hook)
      (cl-pushnew #'org-latex-header-blocks-filter
                  org-export-before-parsing-hook))
    (when (require 'ox-latex nil 'noerror)
      (make-variable-buffer-local 'org-latex-classes)
      (cl-pushnew '("article-local"
                    "\\documentclass[11pt]{article}
  [NO-DEFAULT-PACKAGES]
  [PACKAGES]
  [EXTRA]"
                    ("\\section{%s}" . "\\section*{%s}")
                    ("\\subsection{%s}" . "\\subsection*{%s}")
                    ("\\subsubsection{%s}" . "\\subsubsection*{%s}")
                    ("\\paragraph{%s}" . "\\paragraph*{%s}")
                    ("\\subparagraph{%s}" . "\\subparagraph*{%s}"))
                  org-latex-classes :key #'car :test #'equal)

      (make-variable-buffer-local 'org-latex-title-command)
      (setq org-latex-title-command (concat title-page))

      (make-variable-buffer-local 'org-latex-toc-command)
      (setq org-latex-toc-command
            (mapconcat 'identity '(""
                                   "\\tableofcontents\\label{toc}"
                                   "\\listoffigures"
                                   "\\listoflistings"
                                   "\\listoftables"
                                   "\\newpage"
                                   "") "\n"))

      (make-variable-buffer-local 'org-latex-subtitle-format)
      (setq org-latex-subtitle-format "")
      org-latex-title-command))

lst:emacs-lisp-setup-call

  (my-ox-latex-export-buffer-local-variables title-page)
  \begin{titlepage}
    %% https://tex.stackexchange.com/questions/506102/
    %% adding-header-and-footer-to-custom-titlepage
    \thispagestyle{titlepage}
    \begin{center}
      %% Title
      \begin{Huge}
        {\bf %t} \\
        \vspace{1em}
      \end{Huge}
      %% Author
      \begin{Large}
        {\bf %a} \\
        \vspace{1em}
      \end{Large}
    \end{center}
  \end{titlepage}

Editing

Enable disabled commands and inform

Execute src_emacs-lisp{(find-library "novice")} to see how Emacs prevents new users from shooting themselves in the feet.

  (setq disabled-command-function
        (defun my-enable-this-command (&rest _args)
          "Called when a disabled command is executed.
  Enable it and re-execute it."
          (put this-command 'disabled nil)
          (message "You typed %s.  %s was disabled until now."
                   (key-description (this-command-keys)) this-command)
          (sit-for 0)
          (call-interactively this-command)))

Narrowing

Narrowing means focusing in on some portion of the buffer and widening means focussing out on the whole buffer. This allows to concentrate temporarily on for instance a particular function or paragraph by removing clutter. The "Do What I Mean" narrow-or-widen-dwim function allows to toggle between narrowed and widened buffer states.

  (defun narrow-or-widen-dwim (p)
    "Widen if buffer is narrowed, narrow-dwim otherwise.
  Dwim means: region, org-src-block, org-subtree, or defun,
  whichever applies first. Narrowing to org-src-block actually
  calls `org-edit-src-code'.
  With prefix P, don't widen, just narrow even if buffer is
  already narrowed."
    (interactive "P")
    (declare (interactive-only))
    (cond ((and (buffer-narrowed-p) (not p)) (widen))
          ((and (bound-and-true-p org-src-mode) (not p))
           (org-edit-src-exit))
          ((region-active-p)
           (narrow-to-region (region-beginning) (region-end)))
          ((derived-mode-p 'org-mode)
           (or (ignore-errors (org-edit-src-code))
               (ignore-errors (org-narrow-to-block))
               (org-narrow-to-subtree)))
          ((derived-mode-p 'latex-mode)
           (LaTeX-narrow-to-environment))
          ((derived-mode-p 'tex-mode)
           (TeX-narrow-to-group))
          (t (narrow-to-defun))))

  (define-key ctl-x-map (kbd "C-n") #'narrow-or-widen-dwim)

Synchronal multiple-region editing

  (unless noninteractive
    (require 'iedit nil 'noerror))

Extraneous whitespace trimming

  (unless noninteractive
    (when (require 'ws-butler nil 'noerror)
      (custom-set-variables
       '(ws-butler-keep-whitespace-before-point nil))
      (add-hook 'prog-mode-hook #'ws-butler-mode)
      (add-hook 'text-mode-hook #'ws-butler-mode)))

Structure editing

Structure editing keeps character pairs (for instance parentheses, curly and square brackets as well as single and double quotes) balanced to leave code (for instance Lisp and Python) and text (for instance LaTeX and Org) structure intact. I use smartparens which offers a normal mode (smartparens-mode) and a a strict mode (smartparens-strict-mode). Although both modes insert character pairs, the normal mode allows to easily delete one of the paired characters while the strict mode does not. Therefore, the strict mode is more for code editing since it never breaks programming language rules and the normal mode is more for text editing where structure is a matter of convention instead of programming language rules.

For instance, the strict mode in Python allows to delete entire lists, tuples, or the arguments after the cursor (what Emacs calls point) in a function call without breaking the character pair balance. In order to repair a broken character pair balance, insert a single character by prefixing it with "C-q" bound to quoted-insert.

The smartparens documentation targets experienced Emacs users. The following links show how to put the documentation to practical use:

  1. Omar Antolin's gist "my-smartparens-config.el" is the first place to look for how to tweak smartparens. However, the gist may be partially obsolete, since it is not part of his current Emacs configuration.
  2. How to enable smartparens in the minibuffer after eval-expression explains how the machinery after the first and after later usages of eval-expression differ and discusses options how to handle those differences.

Listing lst:configure-smartparens aims to configure smartparens for Elisp, LaTeX, Org, and Python.

  (unless noninteractive
    ;; To disables pairing of the quote character for lisp modes,
    ;; require smartparens-config instead of smartparens.
    (when (require 'smartparens-config nil 'noerror)
      (custom-set-variables
       '(sp-base-key-bindings 'sp)
       '(sp-override-key-bindings '(("C-(" . sp-backward-barf-sexp)
                                    ("C-)" . sp-forward-slurp-sexp))))

      (dolist (hook '(prog-mode-hook
                      text-mode-hook))
        (add-hook hook #'smartparens-mode))

      ;; Hook on the specific `eval-expression-minibuffer-setup-hook'
      ;; and not on the general `minibuffer-setup-hook'.
      (dolist (hook '(emacs-lisp-mode-hook
                      eval-expression-minibuffer-setup-hook
                      ielm-mode-hook
                      inferior-emacs-lisp-mode-hook
                      python-mode-hook))
        (add-hook hook #'smartparens-strict-mode))

      ;; Tweak for the call to `smartparens-strict-mode' hooked on
      ;; `eval-expression-minibuffer-setup-hook'.
      (sp-with-modes '(fundamental-mode           ; first usage.
                       minibuffer-inactive-mode)  ; later usage.
        (sp-local-pair "'" nil :actions nil))

      ;; https://xenodium.com/emacs-smartparens-auto-indent/index.html
      (defun indent-between-pair (&rest _ignored)
        (newline)
        (indent-according-to-mode)
        (forward-line -1)
        (indent-according-to-mode))

      (sp-local-pair 'prog-mode "(" nil :post-handlers '((indent-between-pair "RET")))
      (sp-local-pair 'prog-mode "[" nil :post-handlers '((indent-between-pair "RET")))
      (sp-local-pair 'prog-mode "{" nil :post-handlers '((indent-between-pair "RET")))

      (show-smartparens-global-mode +1)))

Electric operators

  (when (fboundp 'electric-operator-mode)
    (add-hook 'c-mode-common #'electric-operator-mode)
    (add-hook 'python-mode-hook #'electric-operator-mode))

Smart snippets

  (when (require 'yasnippet nil 'noerror)
    (custom-set-variables
     '(yas-alias-to-yas/prefix-p nil))
    (yas-global-mode +1))

Coding

Emacs-lisp coding

Python coding

The Python Programming in Emacs wiki page lists options to enhance Emacs's built-in python-mode. Here, the focus is on two packages:

  1. Eglot - Emacs polyGLOT: an Emacs LSP client that stays out of your way. The maintainer also contributes to Emacs itself and has a deep understanding of the Way of Emacs. He refuses to add new features without seeing how they fit into the Way of Emacs as this discussion on org-mode source code blocks shows.
  2. Anaconda - code navigation, documentation lookup, and completion for Python.
  (with-eval-after-load 'eglot
    (add-to-list 'eglot-server-programs '(python-mode "pylsp"))
    (setq-default
     eglot-workspace-configuration
     `((:pylsp . (:plugins (:jedi_completion (:eager nil))))
       (:pylsp . (:plugins (:jedi_completion (:cache_for ,(vconcat '("astropy"
                                                                     "numpy"
                                                                     "scipy")))))))))

The snippet below initializes anaconda. See elpy-module-company for how to handle company-backends as a local variable and the call to advice-add opens Python org-mode edit-buffers in anaconda-mode.

  (with-eval-after-load 'python
    (with-eval-after-load 'company
      (when (and (fboundp 'anaconda-mode)
                 (fboundp 'company-anaconda))
        (defun my-disable-anaconda-mode ()
          (when (derived-mode-p 'python-mode)
            (anaconda-mode -1)
            (make-variable-buffer-local 'company-backends)
            (setq company-backends
                  (delq 'company-anaconda
                        (mapcar #'identity company-backends)))
            (anaconda-eldoc-mode -1)))
        (defun my-enable-anaconda-mode ()
          (when (derived-mode-p 'python-mode)
            (anaconda-mode +1)
            (make-variable-buffer-local 'company-backends)
            (setq company-backends
                  (cons 'company-anaconda
                        (delq 'company-semantic
                              (delq 'company-capf
                                    (mapcar #'identity company-backends)))))
            (anaconda-eldoc-mode
             (if (file-remote-p default-directory) -1 1))))))

    (unless (and (fboundp 'my-disable-anaconda-mode)
                 (fboundp 'my-enable-anaconda-mode))
      (when (fboundp 'anaconda-mode)
        (defun my-disable-anaconda-mode ()
          (when (derived-mode-p 'python-mode)
            (anaconda-mode -1)
            (anaconda-eldoc-mode -1)))
        (defun my-enable-anaconda-mode ()
          (when (derived-mode-p 'python-mode)
            (anaconda-mode +1)
            (anaconda-eldoc-mode
             (if (file-remote-p default-directory) -1 1))))))

    (when (fboundp 'my-enable-anaconda-mode)
      (advice-add 'org-edit-src-code :after #'my-enable-anaconda-mode))
    (when (and (fboundp 'my-disable-anaconda-mode)
               (fboundp 'my-enable-anaconda-mode))
      (defun my-toggle-anaconda-mode ()
        "Toggle anaconda-mode with bells and whistles."
        (interactive)
        (if (bound-and-true-p anaconda-mode)
            (my-disable-anaconda-mode)
          (my-enable-anaconda-mode)))))
  import numpy
  import astropy.units as apu

  a = numpy.arange(0, 11)
  a = numpy.linspace(0, 10, num=11)
  q = apu.Quantity(a, apu.meter)
  print(q)
  (with-eval-after-load 'python
    (custom-set-variables
     '(python-indent-guess-indent-offset nil)
     '(python-shell-interpreter-args "-i -E")))
  (when (and (executable-find "pyenv")
             (require 'pyenv-mode nil 'noerror))
    (pyenv-mode +1)
    (pyenv-mode-set "3.9.9/envs/python-3.9.9"))
  (with-eval-after-load 'info
    (add-to-list 'Info-directory-list
                 (expand-file-name "~/.local/share/info")))

Look into:

  1. Emacs extension to insert numpy style docstrings in function definitions

Appearance

See the note on mixed font heights in Emacs for how to setup fonts properly. It boils down to two rules:

  1. The height of the default face must be an integer number to make the height a physical quantity.
  2. The heights of all other faces must be real numbers to scale those heights with respect to the height of the face (those heights default to 1.0 for no scaling).

The code in listing lst:configure-face-attributes source implements those rules.

  (unless noninteractive
    ;; Set face attributes.
    (cond
     ((eq system-type 'darwin)
      (set-face-attribute 'default nil :family "Hack" :height 120)
      (set-face-attribute 'fixed-pitch nil :family "Hack")
      (set-face-attribute 'variable-pitch nil :family "FiraGo"))
     ((eq system-type 'gnu/linux)
      (set-face-attribute 'default nil :family "Hack" :height 110)
      (set-face-attribute 'fixed-pitch nil :family "Hack")
      (set-face-attribute 'variable-pitch nil :family "FiraGo"))
     (t
      (set-face-attribute 'default nil :family "Hack" :height 110)
      (set-face-attribute 'fixed-pitch nil :family "Hack")
      (set-face-attribute 'variable-pitch nil :family "DejaVu Sans"))))

In case of proper initialization of all face heigths, font scaling is easy as the the code in lst:my-set-default-face-height shows.

  (defun my-set-default-face-height ()
    "Set the default face height in all current and future frames.

  Scale all other faces with a height that is a real number."
    (interactive)
    (let* ((prompt (format "face heigth (%s): "
                           (face-attribute 'default :height)))
           (choices (mapcar #'number-to-string
                            (number-sequence 50 200 10)))
           (height (string-to-number
                    (completing-read prompt choices nil 'require-match))))
      (message "Setting the height of the default face to %s" height)
      (set-face-attribute 'default nil :height height)))

Allow swapping fhe foreground and background colors of the default face on all frames.

  (defun my-invert-default-face ()
    "Invert the default face."
    (interactive)
    (invert-face 'default))

Enable rainbow-mode to colorize color names in buffers for debugging.

  (when (fboundp 'rainbow-mode)
    (custom-set-variables
     '(rainbow-x-colors-major-mode-list
       '(c++-mode
         c-mode
         emacs-lisp-mode
         inferior-emacs-lisp-mode
         java-mode
         lisp-interaction-mode
         org-mode
         python-mode)))
    (rainbow-mode +1))

This setup prefers the leuven and leuven-dark themes because the modus-operandi and modus-vivendi themes feel quirky: for instance those themes fail to display hl-line-mode properly with Emacs-27.2 on Darwin.

How to change custom theme faces

  (unless noninteractive
    ;; Try to detect `leuven-theme` from MELPA.
    (when (fboundp 'leuven-scale-font)
      (custom-set-variables
       '(leuven-scale-org-agenda-structure 1.0)
       '(leuven-scale-org-document-title 1.0)
       '(leuven-scale-outline-headlines 1.0)
       '(leuven-scale-volatile-highlight 1.0)))

    (defun my-leuven-hook-function ()
      (when (member 'leuven custom-enabled-themes)
        (let ((custom-inhibit--theme-enable nil)
              (ol1 (list :height 1.0 :weight 'bold
                         :foreground "#3C3C3C" :background "#F0F0F0"))
              (ol2 (list :height 1.0 :weight 'bold
                         :foreground "#123555" :background "#E5F4FB")))
          (custom-theme-set-faces
           'leuven
           `(font-latex-sectioning-2-face ((t ,ol1)))
           `(font-latex-sectioning-3-face ((t ,ol2)))
           `(info-title-1 ((t ,ol1)))
           `(markdown-header-face-1 ((t ,ol1)))
           `(markdown-header-face-2 ((t ,ol2)))
           `(org-level-1 ((t ,ol1)))
           `(org-level-2 ((t ,ol2)))
           `(outline-1 ((t ,ol1)))
           `(outline-2 ((t ,ol1)))))
        (enable-theme 'leuven)))

    ;; (load-theme 'leuven 'no-confirm nil)

    (dolist (hook '(Info-mode-hook
                    LaTeX-mode-hook
                    markdown-mode-hook
                    org-mode-hook
                    outline-mode-hook))
      (add-hook hook #'my-leuven-hook-function)))
  (unless noninteractive
    ;; https://karthinks.com/software/batteries-included-with-emacs/
    ;; https://www.reddit.com/r/emacs/comments/jwhr6g/batteries_included_with_emacs/
    (defun my-pulse-one-line (&rest _)
      "Pulse the current line."
      (let ((pulse-iterations 16)
            (pulse-delay 0.1))
        (pulse-momentary-highlight-one-line (point))))
    (dolist (command '(scroll-up-command
                       scroll-down-command
                       recenter-top-bottom
                       other-window))
      (advice-add command :after #'my-pulse-one-line)))

Applications

Feed reader

  (autoload 'elfeed "elfeed" nil t)
  (global-set-key (kbd "C-x w") #'elfeed)

  (with-eval-after-load 'elfeed
    (custom-set-variables
     '(elfeed-feeds
       '(("http://www.howardism.org/index.xml" h-abrams)
         ("https://ambrevar.xyz/atom.xml" p-neirhardt)
         ("https://emacshorrors.com/feed.atom" v-schneidermann)
         ("https://emacsninja.com/emacs.atom" v-schneidermann)
         ("https://feeds.feedburner.com/InterceptedWithJeremyScahill" j-scahill)
         ("https://nullprogram.com/feed/" c-wellons)
         ("https://oremacs.com/atom.xml" o-krehel)
         ("https://planet.emacslife.com/atom.xml" planet-emacs)
         ("https://protesilaos.com/codelog.xml" p-stavrou)
         ("https://sachachua.com/blog/category/emacs/feed" s-chua)
         ("https://sciencescitoyennes.org/feed/" sciences)
         ("https://updates.orgmode.org/feed/updates" org-updates)
         ("https://www.aclu.org/taxonomy/feed-term/2152/feed" aclu)
         ("https://www.bof.nl/rss/" bof)
         ("https://www.democracynow.org/podcast-video.xml" dn)
         ("https://www.laquadrature.net/fr/rss.xml" lqdn)
         ("https://www.lemonde.fr/blog/huet/feed/" sciences)))))

Multi-media system

  (custom-set-variables
   '(emms-mode-line-format "")
   '(emms-player-list '(emms-player-mpd emms-player-mpv))
   `(emms-player-mpd-music-directory ,(expand-file-name "~/Music"))
   '(emms-player-mpd-server-name "localhost")
   '(emms-player-mpd-server-port "6600")
   '(emms-player-mpd-verbose t)
   '(emms-playing-time-display-format " %s ")
   '(emms-playlist-mode-center-when-go t))

  (defun my-emms-print-metadata-find ()
    (require 'find-func)
    (locate-file
     "emms-print-metadata"
     (expand-file-name
      "src"
      (file-name-directory (find-library-name "emms")))
     exec-suffixes #'file-executable-p))

  (with-eval-after-load 'emms
    (require 'emms-info-libtag)
    (let ((emms-print-metadata (my-emms-print-metadata-find)))
      (when emms-print-metadata
        (custom-set-variables
         '(emms-info-functions nil)
         `(emms-info-libtag-program-name ,emms-print-metadata))
        (add-hook 'emms-info-functions #'emms-info-libtag))))

  (with-eval-after-load 'elfeed-show
    (when (require 'emms-setup nil 'noerror)
      (emms-all)))

  (autoload 'emms-streams "emms-streams" nil 'interactive)
  (with-eval-after-load 'emms-streams (emms-all))

Local variables linking to Latexmk save-compile-display-loop

Only the Org source file shows the local variables footer.