Doom Emacs Configuration

Terence Munro

Disclaimer

Most of these settings come from tecosaur/emacs-config

TODO

Science

Apparently make this file run slightly faster with lexical binding (see this blog post for more info).

;;; config.el -*- lexical-binding: t; -*-

Help

Doom helper macros

Here are some additional functions/macros that could help you configure Doom:

To get information about any of these functions/macros, move the cursor over the highlighted symbol at press K (non-evil users must press C-c c k). This will open documentation for it, including demos of how they are used.

You can also try gd (or C-c c d) to jump to their definition and see how they are implemented.

Functions

Hide prettified under point

(defun org-show-emphasis-markers-at-point ()
  (save-match-data
    (if (and (org-in-regexp org-emph-re 2)
             (>= (point) (match-beginning 3))
             (<= (point) (match-end 4))
             (member (match-string 3) (mapcar 'car org-emphasis-alist)))
        (with-silent-modifications
          (remove-text-properties
           (match-beginning 3) (match-beginning 5)
           '(invisible org-link)))
      (apply 'font-lock-flush (list (match-beginning 3) (match-beginning 5))))))

Settings

Personal Information

(setq user-full-name "Terence Munro"
      user-mail-address "dev@tm.gg")

Secrets

Secrets in Emacs - GPG

(load-library (expand-file-name "secrets.el.gpg" doom-private-dir))
(setq auth-sources '((:source (expand-file-name "authinfo.gpg" doom-private-dir)))
      auth-source-cache-expiry nil) ; default is 7200 (2h)
(setenv "GPG_AGENT_INFO" nil)       ; use emacs key helper

Auto-customisations

I was under the impression that doom-emacs didn't like the usage of the customisation interface. Preferring to configure that stuff in here. But incase it gets used by accident I still want it to be a different file.

(setq-default custom-file (expand-file-name ".custom.el" doom-private-dir))
(when (file-exists-p custom-file)
  (load custom-file))

Defaults

Wish I knew the difference between setq-default, setq and :custom. Also when I should be using one vs the others.

(setq-default
 delete-by-moving-to-trash t            ; Delete files to trash
 tab-width 2                            ; Set width for tabs
 fill-column 100                        ; Fill column indicator
 uniquify-buffer-name-style 'forward    ; Uniquify buffer names
 window-combination-resize t            ; take new window space from all other windows (not just current)
 major-mode 'org-mode                   ; Default to org-mode
 x-stretch-cursor t                     ; Stretch cursor to the glyph width

 history-length 1000
 prescient-history-length 1000)

Preferences

(setq undo-limit 80000000               ; Raise undo-limit to 80Mb
      evil-want-fine-undo t             ; By default while in insert all changes are one big blob. Be more granular
      auto-save-default t               ; Nobody likes to loose work, I certainly don't
      inhibit-compacting-font-caches t  ; When there are lots of glyphs, keep them in memory
      truncate-string-ellipsis "โ€ฆ"      ; Unicode ellispis are nicer than "...", and also save /precious/ space

      yas-triggers-in-field t           ; Nested snippets
      )

Windows

From tecosaurs config

(setq evil-vsplit-window-right t
      evil-split-window-below t)
(defadvice! prompt-for-buffer (&rest _)
  :after '(evil-window-split evil-window-vsplit)
  (+ivy/switch-buffer))
(setq +ivy-buffer-preview t)

Window rotation SPC w r and SPC w R Layout rotation SPC w SPC inspired by tmux

  1. Window title

    Buffer name and if applicable project

    (setq frame-title-format
        '(""
          (:eval
           (if (s-contains-p org-roam-directory (or buffer-file-name ""))
               (replace-regexp-in-string ".*/[0-9]*-?" "๐Ÿข” " buffer-file-name)
             "%b"))
          (:eval
           (let ((project-name (projectile-project-name)))
             (unless (string= "-" project-name)
               (format (if (buffer-modified-p)  " โ—‰ %s" " โ€†โ—โ€† %s") project-name))))))

Editor interaction

(map! :n [mouse-8] #'better-jumper-jump-backward
      :n [mouse-9] #'better-jumper-jump-forward)

Productivity

(setq deft-directory      (expand-file-name "~/Dropbox/notes")
      org-directory       deft-directory
      org-roam-directory  deft-directory
      org-roam-index-file "index.org"

      org-roam-dailies-capture-templates '(("d" "daily" plain (function org-roam-capture--get-point)
                                            ""
                                            :immediate-finish t
                                            :file-name "dailies/%<%Y-%m-%d>"
                                            :head "#+TITLE: %<%Y-%m-%d>\n"))

      org-roam-capture-templates '(("d" "default" plain (function org-roam-capture--get-point)
                                    "%?"
                                    :file-name "%<%Y%m%d%H%M%S>-${slug}"
                                    :head "#+TITLE: ${title}\n"
                                    :unnarrowed t)))

(setq habitica-show-streak t)

;; TODO make these do a directory exists check first
(setq langtool-language-tool-jar "~/Development/jars/languagetool-commandline.jar"
      langtool-language-tool-server-jar "~/Development/jars/languagetool-server.jar"
      langtool-default-language "en-AU"
      langtool-java-user-arguments '("--languagemodel /mnt/Toshiba-3TB/ngrams"
                                     "-Dfile.encoding=UTF-8"))

Visual

  1. Font face

    My favourites

    (setq doom-font (font-spec :family "JetBrains Mono" :size 16)
          doom-big-font (font-spec :family "JetBrains Mono" :size 36)
          doom-variable-pitch-font (font-spec :family "Overpass" :size 16)
          doom-serif-font (font-spec :family "IBM Plex Mono" :weight 'light))
    
    (custom-set-faces!
      '(outline-1 :weight extra-bold :height 1.25)
      '(outline-2 :weight bold :height 1.15)
      '(outline-3 :weight bold :height 1.12)
      '(outline-4 :weight semi-bold :height 1.09)
      '(outline-5 :weight semi-bold :height 1.06)
      '(outline-6 :weight semi-bold :height 1.03)
      '(outline-8 :weight semi-bold)
      '(outline-9 :weight semi-bold))
    
    (after! org
      (custom-set-faces!
        '(org-document-title :height 1.2)))
  2. Symbols

    ;;(use-package! org-pretty-tags
    ;;  :after org
    ;;  :config
    ;;  (setq org-pretty-tags-surrogate-strings
    ;;         `(("test"       . ,(all-the-icons-material "timer"          :face 'all-the-icons-red     :v-adjust 0.01))
    ;;          ("lecture"    . ,(all-the-icons-fileicon "keynote"        :face 'all-the-icons-orange  :v-adjust 0.01))
    ;;          ("email"      . ,(all-the-icons-faicon   "envelope"       :face 'all-the-icons-blue    :v-adjust 0.01))
    ;;          ("read"       . ,(all-the-icons-octicon  "book"           :face 'all-the-icons-lblue   :v-adjust 0.01))
    ;;          ("article"    . ,(all-the-icons-octicon  "file-text"      :face 'all-the-icons-yellow  :v-adjust 0.01))
    ;;          ("web"        . ,(all-the-icons-faicon   "globe"          :face 'all-the-icons-green   :v-adjust 0.01))
    ;;          ("info"       . ,(all-the-icons-faicon   "info-circle"    :face 'all-the-icons-blue    :v-adjust 0.01))
    ;;          ("issue"      . ,(all-the-icons-faicon   "bug"            :face 'all-the-icons-red     :v-adjust 0.01))
    ;;          ("someday"    . ,(all-the-icons-faicon   "calendar-o"     :face 'all-the-icons-cyan    :v-adjust 0.01))
    ;;          ("idea"       . ,(all-the-icons-octicon  "light-bulb"     :face 'all-the-icons-yellow  :v-adjust 0.01))
    ;;          ("emacs"      . ,(all-the-icons-fileicon "emacs"          :face 'all-the-icons-lpurple :v-adjust 0.01))))
    ;;  (org-pretty-tags-global-mode))
    
    (after! org-superstar
      (setq org-superstar-headline-bullets-list '("โ—‰" "โ—‹" "โœธ" "โœฟ" "โœค" "โœœ" "โ—†" "โ–ถ")
            ;;org-superstar-headline-bullets-list '("โ… " "โ…ก" "โ…ข" "โ…ฃ" "โ…ค" "โ…ฅ" "โ…ฆ" "โ…ง" "โ…จ" "โ…ฉ")
            org-superstar-prettify-item-bullets t ))
    (after! org
      (setq org-ellipsis " โ–พ "
            org-priority-highest ?A
            org-priority-lowest ?E
            org-priority-faces
            '((?A . 'all-the-icons-red)
              (?B . 'all-the-icons-orange)
              (?C . 'all-the-icons-yellow)
              (?D . 'all-the-icons-green)
              (?E . 'all-the-icons-blue))))
    (after! org
      (appendq! +pretty-code-symbols
                `(:checkbox      "โ˜"
                  :pending       "โ—ผ"
                  :checkedbox    "โ˜‘"
                  :list_property "โˆท"
                  :results       "๐Ÿ ถ"
                  :property      "โ˜ธ"
                  :properties    "โš™"
                  :end           "โˆŽ"
                  :options       "โŒฅ"
                  :title         "๐™"
                  :subtitle      "๐™ฉ"
                  :author        "๐˜ผ"
                  :date          "๐˜ฟ"
                  :latex_header  "โ‡ฅ"
                  :latex_class   "๐Ÿ„ฒ"
                  :beamer_header "โ† "
                  :begin_quote   "โฎ"
                  :end_quote     "โฏ"
                  :begin_export  "โฏฎ"
                  :end_export    "โฏฌ"
                  :priority_a   ,(propertize "โš‘" 'face 'all-the-icons-red)
                  :priority_b   ,(propertize "โฌ†" 'face 'all-the-icons-orange)
                  :priority_c   ,(propertize "โ– " 'face 'all-the-icons-yellow)
                  :priority_d   ,(propertize "โฌ‡" 'face 'all-the-icons-green)
                  :priority_e   ,(propertize "โ“" 'face 'all-the-icons-blue)
                  :em_dash       "โ€”"))
      (set-pretty-symbols! 'org-mode
        :merge t
        :checkbox      "[ ]"
        :pending       "[-]"
        :checkedbox    "[X]"
        :list_property "::"
        :results       "#+RESULTS:"
        :property      "#+PROPERTY:"
        :property      ":PROPERTIES:"
        :end           ":END:"
        :options       "#+OPTIONS:"
        :title         "#+TITLE:"
        :subtitle      "#+SUBTITLE:"
        :author        "#+AUTHOR:"
        :date          "#+DATE:"
        :latex_class   "#+LATEX_CLASS:"
        :latex_header  "#+LATEX_HEADER:"
        :beamer_header "#+BEAMER_HEADER:"
        :begin_quote   "#+BEGIN_QUOTE"
        :end_quote     "#+END_QUOTE"
        :begin_export  "#+BEGIN_EXPORT"
        :end_export    "#+END_EXPORT"
        :priority_a    "[#A]"
        :priority_b    "[#B]"
        :priority_c    "[#C]"
        :priority_d    "[#D]"
        :priority_e    "[#E]"
        :em_dash       "---"))
    (plist-put +pretty-code-symbols :name "โ") ; or โ€บ could be good?
  3. Theme and modeline

    (setq doom-theme 'doom-Iosvkem)
    (delq! t custom-theme-load-path)
    (custom-set-faces!
      '(doom-modeline-buffer-modified :foreground "orange"))
    (defun doom-modeline-conditional-buffer-encoding ()
      "We expect the encoding to be LF UTF-8, so only show the modeline when this is not the case"
      (setq-local doom-modeline-buffer-encoding
                  (unless (or (eq buffer-file-coding-system 'utf-8-unix)
                              (eq buffer-file-coding-system 'utf-8)))))
    (add-hook 'after-change-major-mode-hook #'doom-modeline-conditional-buffer-encoding)
  4. Miscellaneous

    Prettier buffer names

    (setq doom-fallback-buffer-name "โ–บ Doom"
          +doom-dashboard-name "โ–บ Doom")

    This determines the style of line numbers in effect. If set to `nil', line numbers are disabled. For relative line numbers, set this to `relative'.

    (setq display-line-numbers-type t)

    Make result of eval with gr and gR prettier

    (setq eros-eval-result-prefix "โŸน ")

Daemon

Emacs daemon as a systemd service

[Unit]
Description=Emacs server daemon
Documentation=info:emacs man:emacs(1) https://gnu.org/software/emacs/

[Service]
Type=forking
ExecStart=/usr/bin/emacs --daemon
ExecStop=/usr/bin/emacsclient --eval "(progn (setq kill-emacs-hook nil) (kill emacs))"
Environment=SSH_AUTH_SOCK=%t/keyring/ssh
Restart=on-failure

[Install]
WantedBy=default.target

Enabled by

systemctl --user enable emacs.service

Desktop file

[Desktop Entry]
Name=Emacs client
GenericName=Text Editor
Comment=A flexible platform for end-user applications
MimeType=text/english;text/plain;text/x-makefile;text/x-c++hdr;text/x-c++src;text/x-chdr;text/x-csrc;text/x-java;text/x-moc;text/x-pascal;text/x-tcl;text/x-tex;application/x-shellscript;text/x-c;text/x-c++;
Exec=emacsclient -create-frame --alternate-editor="" %F
Icon=emacs
Type=Application
Terminal=false
Categories=TextEditor;Utility;
StartupWMClass=Emacs
Keywords=Text;Editor;
X-KDE-StartupNotify=false

Package loading

This file shouldn't be byte compiled.

;; -*- no-byte-compile: t; -*-

Loading instructions

This is where you install packages, by declaring them with the package! macro, then running doom refresh on the command line. You'll need to restart Emacs for your changes to take effect! Or at least, run M-x doom/reload.

WARNING: Don't disable core packages listed in ~/.emacs.d/core/packages.el. Doom requires these, and disabling them may have terrible side effects.

Packages in MELPA/ELPA/emacsmirror

To install some-package from MELPA, ELPA or emacsmirror:

(package! some-package)

Packages from git repositories

To install a package directly from a particular repo, you'll need to specify a :recipe. You'll find documentation on what :recipe accepts here:

(package! another-package
  :recipe (:host github :repo "username/repo"))

If the package you are trying to install does not contain a PACKAGENAME.el file, or is located in a subdirectory of the repo, you'll need to specify :files in the :recipe:

(package! this-package
  :recipe (:host github :repo "username/repo"
           :files ("some-file.el" "src/lisp/*.el")))

Disabling built-in packages

If you'd like to disable a package included with Doom, for whatever reason, you can do so here with the :disable property:

(package! builtin-package :disable t)

You can override the recipe of a built in package without having to specify all the properties for :recipe. These will inherit the rest of its recipe from Doom or MELPA/ELPA/Emacsmirror:

(package! builtin-package :recipe (:nonrecursive t))
(package! builtin-package-2 :recipe (:repo "myfork/package"))

Specify a :branch to install a package from a particular branch or tag. This is required for some packages whose default branch isn't 'master' (which our package manager can't deal with; see raxod502/straight.el#279)

(package! builtin-package :recipe (:branch "develop"))

General packages

Stats

(package! wakatime-mode)

Improving features

  1. Evil

    (package! evil-leader)
    (package! evil-nerd-commenter)
  2. Flyspell-lazy

    To alleviate some 9.9.3

    (package! flyspell-lazy)
  3. Magit Delta

    Delta is a git diff syntax highlighter written in rust. The author also wrote a package to hook this into the magit diff view. This requires the delta binary.

    (package! magit-delta :recipe (:host github :repo "dandavison/magit-delta"))
  4. Info colours

    (package! info-colors)
  5. Large files

    The very large files mode loads large files in chunks, allowing one to open ridiculously large files.

    (package! vlf :recipe (:host github :repo "m00natic/vlfi" :files ("*.el")))

    To make VLF available without delaying startup, we'll just load it in quiet moments.

    (use-package! vlf-setup
      :defer-incrementally vlf-tune vlf-base vlf-write vlf-search vlf-occur vlf-follow vlf-ediff vlf)

Email

The org-msg package allows to write emails in org mode, and send as an HTML multipart email. We can setup some CSS to be inlined, render LaTeX fragments, and all those goodies!

(package! org-msg)

Language packages

Language server protocol

(package! company-lsp)

Org Mode

  1. Improve agenda/capture

    (package! org-super-agenda)

    doct (Declarative Org Capture Templates)

    (package! doct
      :recipe (:host github :repo "progfolio/doct"))
  2. Visuals

    (package! org-pretty-table-mode
      :recipe (:host github :repo "Fuco1/org-pretty-table"))

    org-superstar-mode + Pretty tags

    (package! org-pretty-tags)
  3. Extra functionality

    (package! ox-gfm)             ; github flavoured markdown
    (package! org-ref)            ; citations
    
    (package! org-graph-view :recipe (:host github :repo "alphapapa/org-graph-view"))
    (package! org-roam-server)
    (package! org-web-tools)
    (package! habitica)

Systemd

For editing systemd unit files

(package! systemd)

TODO Packages

Evil

(after! evil (evil-escape-mode nil))

(use-package! evil-leader
  :config
  (evil-leader/set-leader ",")
  (evil-leader/set-key
    "t" 'lsp-describe-thing-at-point)
  (global-evil-leader-mode))

(use-package! evil-nerd-commenter
  :after evil-leader
  :config
  (evil-leader/set-key
  "ci" 'evilnc-comment-or-uncomment-lines
  "cl" 'evilnc-quick-comment-or-uncomment-to-the-line
  "ll" 'evilnc-quick-comment-or-uncomment-to-the-line
  "cc" 'evilnc-copy-and-comment-lines
  "cp" 'evilnc-comment-or-uncomment-paragraphs
  "cr" 'comment-or-uncomment-region
  "cv" 'evilnc-toggle-invert-comment-line-by-line
  "."  'evilnc-copy-and-comment-operator
  "\\" 'evilnc-comment-operator))

Language Server Protocol

Scala

(use-package! sbt-mode
  :commands sbt-start sbt-command
  :config
  (substitute-key-definition
  'minibuffer-complete-word
  'self-insert-command
  minibuffer-local-completion-map)
  (setq sbt:program-options `("-Dsbt.supershell=false")))

(use-package lsp-mode
  :hook (scala-mode . lsp)
  (lsp-mode . lsp-lens-mode))

(use-package! lsp-metals)
(use-package! lsp-ui)
(use-package! company-lsp)
(use-package! posframe)
(use-package! dap-mode
  :hook
  (lsp-mode . dap-mode)
  (lsp-mode . dap-ui-mode)
  :config
  (dap-register-debug-template "Example Configuration"
                             (list :type "metals"
                                   :request "launch"
                                   :args ""
                                   :name "Run Configuration")))

(use-package! lsp-treemacs
  :config
  (lsp-metals-treeview)
  (setq lsp-metals-treeview-show-when-views-received t))

Company

(after! company
  (setq company-idle-delay 0.5
        company-minimum-prefix-length 2)
  (setq company-show-numbers t)
  (add-hook 'evil-normal-state-entry-hook #'company-abort)) ;; make aborting less annoying.
** Org
#+begin_src emacs-lisp
(use-package! deft
  :after org
  :custom
  (deft-recursive t)
  (deft-use-filter-string-for-filename t)
  (deft-default-extension "org"))

(use-package org-roam-server
  :after org-roam
  :config
  (setq org-roam-server-host "127.0.0.1"
        org-roam-server-port 8080
        org-roam-server-export-inline-images t
        org-roam-server-authenticate nil
        org-roam-server-label-truncate t
        org-roam-server-label-truncate-length 60
        org-roam-server-label-wrap-length 20)
  (defun org-roam-server-open ()
    "Ensure the server is active, then open the roam graph."
    (interactive)
    (org-roam-server-mode 1)
    (browse-url-xdg-open (format "http://localhost:%d" org-roam-server-port))))

(use-package! org-journal
  :bind
  ("C-c n j" . org-journal-new-entry)
  :config
  (defun org-journal-file-header-func (time)
    "Custom function to create journal header."
    (concat
     (pcase org-journal-file-type
       (`daily "#+TITLE: %A, %d %B %Y - Daily Journal\n#+STARTUP: showeverything")
       (`weekly "#+TITLE: %W %B %Y - Weekly Journal\n#+STARTUP: folded")
       (`monthly "#+TITLE: %B %Y - Monthly Journal\n#+STARTUP: folded")
       (`yearly "#+TITLE: %Y - Yearly Journal\n#+STARTUP: folded"))))
  :custom
  (org-journal-dir (expand-file-name "journal" deft-directory))
  (org-journal-file-format "%Y-%m-%d.org")
  (org-journal-date-format "%A, %d %B %Y")
  (org-journal-date-prefix "#+TITLE: ")
  (org-journal-file-type "weekly")
  (org-journal-file-header 'org-journal-file-header-func))

(use-package! pdf-tools
  :magic ("%PDF" . pdf-view-mode))

Git

(after! magit
   (magit-delta-mode +1))

Projectile

(setq projectile-ignored-projects '("~/" "/tmp" "~/.emacs.d/.local/straight/repos/"))
(defun projectile-ignored-project-function (filepath)
  "Return t if FILEPATH is within any of `projectile-ignored-projects'"
  (or (mapcar (lambda (p) (s-starts-with-p p filepath)) projectile-ignored-projects)))

Treemacs

Mechanism to ignore files

(after! treemacs
  (defvar treemacs-file-ignore-extensions '()
    "File extension which `treemacs-ignore-filter' will ensure are ignored")
  (defvar treemacs-file-ignore-globs '()
    "Globs which will are transformed to `treemacs-file-ignore-regexps' which `treemacs-ignore-filter' will ensure are ignored")
  (defvar treemacs-file-ignore-regexps '()
    "RegExps to be tested to ignore files, generated from `treeemacs-file-ignore-globs'")
  (defun treemacs-file-ignore-generate-regexps ()
    "Generate `treemacs-file-ignore-regexps' from `treemacs-file-ignore-globs'"
    (setq treemacs-file-ignore-regexps (mapcar 'dired-glob-regexp treemacs-file-ignore-globs)))
  (if (equal treemacs-file-ignore-globs '()) nil (treemacs-file-ignore-generate-regexps))
  (defun treemacs-ignore-filter (file full-path)
    "Ignore files specified by `treemacs-file-ignore-extensions', and `treemacs-file-ignore-regexps'"
    (or (member (file-name-extension file) treemacs-file-ignore-extensions)
        (let ((ignore-file nil))
          (dolist (regexp treemacs-file-ignore-regexps ignore-file)
            (setq ignore-file (or ignore-file (if (string-match-p regexp full-path) t nil)))))))
  (add-to-list 'treemacs-ignored-file-predicates #'treemacs-ignore-filter))

TODO Files to ignore

;; I don't realy use LaTeX yet so todo put things I actually want to hide here
(setq treemacs-file-ignore-extensions '(;; LaTeX
                                        "aux"
                                        "ptc"
                                        "fdb_latexmk"
                                        "fls"
                                        "synctex.gz"
                                        "toc"
                                        ;; LaTeX - glossary
                                        "glg"
                                        "glo"
                                        "gls"
                                        "glsdefs"
                                        "ist"
                                        "acn"
                                        "acr"
                                        "alg"
                                        ;; LaTeX - pgfplots
                                        "mw"
                                        ;; LaTeX - pdfx
                                        "pdfa.xmpi"
                                        ))
(setq treemacs-file-ignore-globs '(;; LaTeX
                                   "*/_minted-*"
                                   ;; AucTeX
                                   "*/.auctex-auto"
                                   "*/_region_.log"
                                   "*/_region_.tex"))

Which-key

Let's make this popup a bit faster

(setq which-key-idle-delay 0.5) ;; I need the help, I really do

Change evil- appearing as often

(setq which-key-allow-multiple-replacements t)
(after! which-key
  (pushnew!
   which-key-replacement-alist
   '(("" . "\\`+?evil[-:]?\\(?:a-\\)?\\(.*\\)") . (nil . "โ—‚\\1"))
   '(("\\`g s" . "\\`evilem--?motion-\\(.*\\)") . (nil . "โ—ƒ\\1"))
   ))

The IDK what this is section

Info colors

(use-package! info-colors
  :commands (info-colors-fontify-node))

(add-hook 'Info-selection-hook 'info-colors-fontify-node)

(add-hook 'Info-mode-hook #'mixed-pitch-mode)

Ivy

(setq ivy-read-action-function #'ivy-hydra-read-action
      ivy-sort-max-size 50000)

Flyspell

(after! flyspell (require 'flyspell-lazy) (flyspell-lazy-mode 1))

Language configuration

Plain text

(after! text-mode
  (add-hook! 'text-mode-hook
    ;; Apply ANSI color codes
    (with-silent-modifications
      (ansi-color-apply-on-region (point-min) (point-max)))))

Org-mode

System config

Org mode isn't recognised as it's own mime type by default

<?xml version="1.0" encoding="utf-8"?>
<mime-info xmlns='http://www.freedesktop.org/standards/shared-mime-info'>
  <mime-type type="text/org">
    <comment>Emacs Org-mode File</comment>
    <glob pattern="*.org"/>
    <alias type="text/org"/>
  </mime-type>
</mime-info>
update-mime-database ~/.local/share/mime
xdg-mime default emacs.desktop text/org

Behaviour

  1. Tweaking defaults

    (setq org-use-property-inheritance t              ; it's convenient to have properties inherited
          org-log-done 'time                          ; having the time a item is done sounds convininet
          org-list-allow-alphabetical t               ; have a. A. a) A) list bullets
          org-export-in-background t                  ; run export processes in external emacs process
          org-catch-invisible-edits 'smart            ; try not to accidently do weird stuff in invisible regions)

    No idea what this is

    (setq org-babel-default-header-args '((:session . "none")
                                          (:results . "replace")
                                          (:exports . "code")
                                          (:cache . "no")
                                          (:noweb . "no")
                                          (:hlines . "no")
                                          (:tangle . "no")
                                          (:comments . "link")))

    By default, visual-line-mode is turned on, and auto-fill-mode off by a hook. Apparently this messes with tables in org-mode, and other plaintext files

    (remove-hook 'text-mode-hook #'visual-line-mode)
    (add-hook 'text-mode-hook #'auto-fill-mode)
  2. Extra functionality

    1. Org buffer creation

      Let's also make creating an org buffer just that little bit easier.

      (evil-define-command evil-buffer-org-new (count file)
        "Creates a new ORG buffer replacing the current window, optionally
         editing a certain FILE"
        :repeat nil
        (interactive "P<f>")
        (if file
            (evil-edit file)
          (let ((buffer (generate-new-buffer "*new org*")))
            (set-window-buffer nil buffer)
            (with-current-buffer buffer
              (org-mode)))))
      (map! :leader
        (:prefix "b"
          :desc "New empty ORG buffer" "o" #'evil-buffer-org-new))
    2. List bullet sequence

      List bullets change with depth

      (setq org-list-demote-modify-bullet '(("+" . "-") ("-" . "+") ("*" . "+") ("1." . "a.")))
    3. Citation

      (use-package! org-ref
         :after org
         :config
          (setq org-ref-completion-library 'org-ref-ivy-cite))
    4. Spellcheck

      My spelling is atrocious, so let's get flycheck going.

      (after! org (add-hook 'org-mode-hook 'turn-on-flyspell))
    5. LSP support in src blocks

      Now, by default, LSPs don't really function at all in src blocks.

      (cl-defmacro lsp-org-babel-enable (lang)
          "Support LANG in org source code block."
          (setq centaur-lsp 'lsp-mode)
          (cl-check-type lang stringp)
          (let* ((edit-pre (intern (format "org-babel-edit-prep:%s" lang)))
                 (intern-pre (intern (format "lsp--%s" (symbol-name edit-pre)))))
            `(progn
               (defun ,intern-pre (info)
                 (let ((file-name (->> info caddr (alist-get :file))))
                   (unless file-name
                     (setq file-name (make-temp-file "babel-lsp-")))
                   (setq buffer-file-name file-name)
                    (lsp-deferred)))
               (put ',intern-pre 'function-documentation
                    (format "Enable lsp-mode in the buffer of org source block (%s)."
                            (upcase ,lang)))
               (if (fboundp ',edit-pre)
                   (advice-add ',edit-pre :after ',intern-pre)
                 (progn
                   (defun ,edit-pre (info)
                     (,intern-pre info))
                   (put ',edit-pre 'function-documentation
                        (format "Prepare local buffer environment for org source block (%s)."
                                (upcase ,lang))))))))
        (defvar org-babel-lang-list
          '("python" "ipython" "bash" "sh" "haskell" "json" "javascript" "rust" "scala"))
        (dolist (lang org-babel-lang-list)
          (eval `(lsp-org-babel-enable ,lang)))
    6. View exported file

      'localeader v has no pre-existing binding, so I may as well use it with the same functionality as in LaTeX. Let's try viewing possible output files with this.

      (after! org
        (map! :map org-mode-map
              :localleader
              :desc "View exported file" "v" #'org-view-output-file)
      
        (defun org-view-output-file (&optional org-file-path)
          "Visit buffer open on the first output file (if any) found, using `org-view-output-file-extensions'"
          (interactive)
          (let* ((org-file-path (or org-file-path (buffer-file-name) ""))
                 (dir (file-name-directory org-file-path))
                 (basename (file-name-base org-file-path))
                 (output-file nil))
            (dolist (ext org-view-output-file-extensions)
              (unless output-file
                (when (file-exists-p
                       (concat dir basename "." ext))
                       (setq output-file (concat dir basename "." ext)))))
            (if output-file
                (if (member (file-name-extension output-file) org-view-external-file-extensions)
                    (browse-url-xdg-open output-file)
                  (pop-to-buffer (or (find-buffer-visiting output-file)
                                     (find-file-noselect output-file))))
              (message "No exported file found")))))
      
      (defvar org-view-output-file-extensions '("pdf" "md" "rst" "txt" "tex" "html")
        "Search for output files with these extensions, in order, viewing the first that matches")
      (defvar org-view-external-file-extensions '("html")
        "File formats that should be opened externally.")
  3. Super agenda

    (use-package! org-super-agenda
      :commands (org-super-agenda-mode))
    (after! org-agenda
      (org-super-agenda-mode))
    
    (setq org-agenda-skip-scheduled-if-done t
          org-agenda-skip-deadline-if-done t
          org-agenda-include-deadlines t
          org-agenda-block-separator nil
          org-agenda-tags-column 100 ;; from testing this seems to be a good value
          org-agenda-compact-blocks t)
    
    (setq org-agenda-custom-commands
          '(("o" "Overview"
             ((agenda "" ((org-agenda-span 'day)
                          (org-super-agenda-groups
                           '((:name "Today"
                                    :time-grid t
                                    :date today
                                    :todo "TODAY"
                                    :scheduled today
                                    :order 1)))))
              (alltodo "" ((org-agenda-overriding-header "")
                           (org-super-agenda-groups
                            '((:name "Next to do"
                                     :todo "NEXT"
                                     :order 1)
                              (:name "Important"
                                     :tag "Important"
                                     :priority "A"
                                     :order 6)
                              (:name "Due Today"
                                     :deadline today
                                     :order 2)
                              (:name "Due Soon"
                                     :deadline future
                                     :order 8)
                              (:name "Overdue"
                                     :deadline past
                                     :face error
                                     :order 7)
                              (:name "Issues"
                                     :tag "Issue"
                                     :order 12)
                              (:name "Emacs"
                                     :tag "Emacs"
                                     :order 13)
                              (:name "Projects"
                                     :tag "Project"
                                     :order 14)
                              (:name "Research"
                                     :tag "Research"
                                     :order 15)
                              (:name "To read"
                                     :tag "Read"
                                     :order 30)
                              (:name "Waiting"
                                     :todo "WAITING"
                                     :order 20)
                              (:name "Trivial"
                                     :priority<= "E"
                                     :tag ("Trivial" "Unimportant")
                                     :todo ("SOMEDAY" )
                                     :order 90)
                              (:discard (:tag ("Chore" "Routine" "Daily")))))))))))
  4. Capture

    Let's setup some org-capture templates

    (use-package! doct
      :commands (doct))
    
    (after! org-capture
      (defun org-capture-select-template-prettier (&optional keys)
        "Select a capture template, in a prettier way than default
      Lisp programs can force the template by setting KEYS to a string."
        (let ((org-capture-templates
               (or (org-contextualize-keys
                    (org-capture-upgrade-templates org-capture-templates)
                    org-capture-templates-contexts)
                   '(("t" "Task" entry (file+headline "" "Tasks")
                      "* TODO %?\n  %u\n  %a")))))
          (if keys
              (or (assoc keys org-capture-templates)
                  (error "No capture template referred to by \"%s\" keys" keys))
            (org-mks org-capture-templates
                     "Select a capture template\nโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”"
                     "Template key: "
                     `(("q" ,(concat (all-the-icons-octicon "stop" :face 'all-the-icons-red :v-adjust 0.01) "\tAbort")))))))
      (advice-add 'org-capture-select-template :override #'org-capture-select-template-prettier)
    
      (defun org-mks-pretty (table title &optional prompt specials)
        "Select a member of an alist with multiple keys. Prettified.
    
      TABLE is the alist which should contain entries where the car is a string.
      There should be two types of entries.
    
      1. prefix descriptions like (\"a\" \"Description\")
         This indicates that `a' is a prefix key for multi-letter selection, and
         that there are entries following with keys like \"ab\", \"ax\"โ€ฆ
    
      2. Select-able members must have more than two elements, with the first
         being the string of keys that lead to selecting it, and the second a
         short description string of the item.
    
      The command will then make a temporary buffer listing all entries
      that can be selected with a single key, and all the single key
      prefixes.  When you press the key for a single-letter entry, it is selected.
      When you press a prefix key, the commands (and maybe further prefixes)
      under this key will be shown and offered for selection.
    
      TITLE will be placed over the selection in the temporary buffer,
      PROMPT will be used when prompting for a key.  SPECIALS is an
      alist with (\"key\" \"description\") entries.  When one of these
      is selected, only the bare key is returned."
        (save-window-excursion
          (let ((inhibit-quit t)
          (buffer (org-switch-to-buffer-other-window "*Org Select*"))
          (prompt (or prompt "Select: "))
          case-fold-search
          current)
            (unwind-protect
          (catch 'exit
            (while t
              (setq-local evil-normal-state-cursor (list nil))
              (erase-buffer)
              (insert title "\n\n")
              (let ((des-keys nil)
              (allowed-keys '("\C-g"))
              (tab-alternatives '("\s" "\t" "\r"))
              (cursor-type nil))
          ;; Populate allowed keys and descriptions keys
          ;; available with CURRENT selector.
          (let ((re (format "\\`%s\\(.\\)\\'"
                (if current (regexp-quote current) "")))
                (prefix (if current (concat current " ") "")))
            (dolist (entry table)
              (pcase entry
                ;; Description.
                (`(,(and key (pred (string-match re))) ,desc)
                 (let ((k (match-string 1 key)))
             (push k des-keys)
             ;; Keys ending in tab, space or RET are equivalent.
             (if (member k tab-alternatives)
                 (push "\t" allowed-keys)
               (push k allowed-keys))
             (insert (propertize prefix 'face 'font-lock-comment-face) (propertize k 'face 'bold) (propertize "โ€บ" 'face 'font-lock-comment-face) "  " desc "โ€ฆ" "\n")))
                ;; Usable entry.
                (`(,(and key (pred (string-match re))) ,desc . ,_)
                 (let ((k (match-string 1 key)))
             (insert (propertize prefix 'face 'font-lock-comment-face) (propertize k 'face 'bold) "   " desc "\n")
             (push k allowed-keys)))
                (_ nil))))
          ;; Insert special entries, if any.
          (when specials
            (insert "โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n")
            (pcase-dolist (`(,key ,description) specials)
              (insert (format "%s   %s\n" (propertize key 'face '(bold all-the-icons-red)) description))
              (push key allowed-keys)))
          ;; Display UI and let user select an entry or
          ;; a sub-level prefix.
          (goto-char (point-min))
          (unless (pos-visible-in-window-p (point-max))
            (org-fit-window-to-buffer))
          (let ((pressed (org--mks-read-key allowed-keys prompt)))
            (setq current (concat current pressed))
            (cond
             ((equal pressed "\C-g") (user-error "Abort"))
             ;; Selection is a prefix: open a new menu.
             ((member pressed des-keys))
             ;; Selection matches an association: return it.
             ((let ((entry (assoc current table)))
                (and entry (throw 'exit entry))))
             ;; Selection matches a special entry: return the
             ;; selection prefix.
             ((assoc current specials) (throw 'exit current))
             (t (error "No entry available")))))))
        (when buffer (kill-buffer buffer))))))
      (advice-add 'org-mks :override #'org-mks-pretty)
      (defun +doct-icon-declaration-to-icon (declaration)
        "Convert :icon declaration to icon"
        (let ((name (pop declaration))
              (set  (intern (concat "all-the-icons-" (plist-get declaration :set))))
              (face (intern (concat "all-the-icons-" (plist-get declaration :color))))
              (v-adjust (or (plist-get declaration :v-adjust) 0.01)))
          (apply set `(,name :face ,face :v-adjust ,v-adjust))))
    
      (defun +doct-iconify-capture-templates (groups)
        "Add declaration's :icon to each template group in GROUPS."
        (let ((templates (doct-flatten-lists-in groups)))
          (setq doct-templates (mapcar (lambda (template)
                                         (when-let* ((props (nthcdr (if (= (length template) 4) 2 5) template))
                                                     (spec (plist-get (plist-get props :doct) :icon)))
                                           (setf (nth 1 template) (concat (+doct-icon-declaration-to-icon spec)
                                                                          "\t"
                                                                          (nth 1 template))))
                                         template)
                                       templates))))
    
      (setq doct-after-conversion-functions '(+doct-iconify-capture-templates))
    
      (add-transient-hook! 'org-capture-select-template
        (setq org-capture-templates
              (doct `(("Personal todo" :keys "t"
                       :icon ("checklist" :set "octicon" :color "green")
                       :file +org-capture-todo-file
                       :prepend t
                       :headline "Inbox"
                       :type entry
                       :template ("* TODO %?"
                                  "%i %a"))
                      ("Personal note" :keys "n"
                       :icon ("sticky-note-o" :set "faicon" :color "green")
                       :file +org-capture-todo-file
                       :prepend t
                       :headline "Inbox"
                       :type entry
                       :template ("* %?"
                                  "%i %a"))
                      ("Email" :keys "e"
                       :icon ("envelope" :set "faicon" :color "blue")
                       :file +org-capture-todo-file
                       :prepend t
                       :headline "Inbox"
                       :type entry
                       :template ("* TODO %^{type|reply to|contact} %\\3 %? :email:"
                                  "Send an email %^{urgancy|soon|ASAP|anon|at some point|eventually} to %^{recipiant}"
                                  "about %^{topic}"
                                  "%U %i %a"))
                      ("Interesting" :keys "i"
                       :icon ("eye" :set "faicon" :color "lcyan")
                       :file +org-capture-todo-file
                       :prepend t
                       :headline "Interesting"
                       :type entry
                       :template ("* [ ] %{desc}%? :%{i-type}:"
                                  "%i %a")
                       :children (("Webpage" :keys "w"
                                   :icon ("globe" :set "faicon" :color "green")
                                   :desc "%(org-cliplink-capture) "
                                   :i-type "read:web"
                                   )
                                  ("Article" :keys "a"
                                   :icon ("file-text" :set "octicon" :color "yellow")
                                   :desc ""
                                   :i-type "read:reaserch"
                                   )
                                  ("\tRecipie" :keys "r"
                                   :icon ("spoon" :set "faicon" :color "dorange")
                                   :file +org-capture-recipies
                                   :headline "Unsorted"
                                   :template "%(org-chef-get-recipe-from-url)"
                                   )
                                  ("Information" :keys "i"
                                   :icon ("info-circle" :set "faicon" :color "blue")
                                   :desc ""
                                   :i-type "read:info"
                                   )
                                  ("Idea" :keys "I"
                                   :icon ("bubble_chart" :set "material" :color "silver")
                                   :desc ""
                                   :i-type "idea"
                                   )))
                      ("Tasks" :keys "k"
                       :icon ("inbox" :set "octicon" :color "yellow")
                       :file +org-capture-todo-file
                       :prepend t
                       :headline "Tasks"
                       :type entry
                       :template ("* TODO %? %^G%{extra}"
                                  "%i %a")
                       :children (("General Task" :keys "k"
                                   :icon ("inbox" :set "octicon" :color "yellow")
                                   :extra ""
                                   )
                                  ("Task with deadline" :keys "d"
                                   :icon ("timer" :set "material" :color "orange" :v-adjust -0.1)
                                   :extra "\nDEADLINE: %^{Deadline:}t"
                                   )
                                  ("Scheduled Task" :keys "s"
                                   :icon ("calendar" :set "octicon" :color "orange")
                                   :extra "\nSCHEDULED: %^{Start time:}t"
                                   )
                                  ))
                    ("Project" :keys "p"
                     :icon ("repo" :set "octicon" :color "silver")
                       :prepend t
                       :type entry
                       :headline "Inbox"
                       :template ("* %{time-or-todo} %?"
                                  "%i"
                                  "%a")
                       :file ""
                       :custom (:time-or-todo "")
                       :children (("Project-local todo" :keys "t"
                                   :icon ("checklist" :set "octicon" :color "green")
                                   :time-or-todo "TODO"
                                   :file +org-capture-project-todo-file)
                                  ("Project-local note" :keys "n"
                                   :icon ("sticky-note" :set "faicon" :color "yellow")
                                   :time-or-todo "%U"
                                   :file +org-capture-project-notes-file)
                                  ("Project-local changelog" :keys "c"
                                   :icon ("list" :set "faicon" :color "blue")
                                   :time-or-todo "%U"
                                   :heading "Unreleased"
                                   :file +org-capture-project-changelog-file))
                       )
                      ("\tCentralised project templates"
                       :keys "o"
                       :type entry
                       :prepend t
                       :template ("* %{time-or-todo} %?"
                                  "%i"
                                  "%a")
                       :children (("Project todo"
                                   :keys "t"
                                   :prepend nil
                                   :time-or-todo "TODO"
                                   :heading "Tasks"
                                   :file +org-capture-central-project-todo-file)
                                  ("Project note"
                                   :keys "n"
                                   :time-or-todo "%U"
                                   :heading "Notes"
                                   :file +org-capture-central-project-notes-file)
                                  ("Project changelog"
                                   :keys "c"
                                   :time-or-todo "%U"
                                   :heading "Unreleased"
                                   :file +org-capture-central-project-changelog-file))
                       ))))))

    Improve how the capture dialogue looks

    (defun org-capture-select-template-prettier (&optional keys)
      "Select a capture template, in a prettier way than default
    Lisp programs can force the template by setting KEYS to a string."
      (let ((org-capture-templates
             (or (org-contextualize-keys
                  (org-capture-upgrade-templates org-capture-templates)
                  org-capture-templates-contexts)
                 '(("t" "Task" entry (file+headline "" "Tasks")
                    "* TODO %?\n  %u\n  %a")))))
        (if keys
            (or (assoc keys org-capture-templates)
                (error "No capture template referred to by \"%s\" keys" keys))
          (org-mks org-capture-templates
                   "Select a capture template\nโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”"
                   "Template key: "
                   `(("q" ,(concat (all-the-icons-octicon "stop" :face 'all-the-icons-red :v-adjust 0.01) "\tAbort")))))))
    (advice-add 'org-capture-select-template :override #'org-capture-select-template-prettier)
    
    (defun org-mks-pretty (table title &optional prompt specials)
      "Select a member of an alist with multiple keys. Prettified.
    
    TABLE is the alist which should contain entries where the car is a string.
    There should be two types of entries.
    
    1. prefix descriptions like (\"a\" \"Description\")
       This indicates that `a' is a prefix key for multi-letter selection, and
       that there are entries following with keys like \"ab\", \"ax\"โ€ฆ
    
    2. Select-able members must have more than two elements, with the first
       being the string of keys that lead to selecting it, and the second a
       short description string of the item.
    
    The command will then make a temporary buffer listing all entries
    that can be selected with a single key, and all the single key
    prefixes.  When you press the key for a single-letter entry, it is selected.
    When you press a prefix key, the commands (and maybe further prefixes)
    under this key will be shown and offered for selection.
    
    TITLE will be placed over the selection in the temporary buffer,
    PROMPT will be used when prompting for a key.  SPECIALS is an
    alist with (\"key\" \"description\") entries.  When one of these
    is selected, only the bare key is returned."
      (save-window-excursion
        (let ((inhibit-quit t)
        (buffer (org-switch-to-buffer-other-window "*Org Select*"))
        (prompt (or prompt "Select: "))
        case-fold-search
        current)
          (unwind-protect
        (catch 'exit
          (while t
            (setq-local evil-normal-state-cursor (list nil))
            (erase-buffer)
            (insert title "\n\n")
            (let ((des-keys nil)
            (allowed-keys '("\C-g"))
            (tab-alternatives '("\s" "\t" "\r"))
            (cursor-type nil))
        ;; Populate allowed keys and descriptions keys
        ;; available with CURRENT selector.
        (let ((re (format "\\`%s\\(.\\)\\'"
              (if current (regexp-quote current) "")))
              (prefix (if current (concat current " ") "")))
          (dolist (entry table)
            (pcase entry
              ;; Description.
              (`(,(and key (pred (string-match re))) ,desc)
               (let ((k (match-string 1 key)))
           (push k des-keys)
           ;; Keys ending in tab, space or RET are equivalent.
           (if (member k tab-alternatives)
               (push "\t" allowed-keys)
             (push k allowed-keys))
           (insert (propertize prefix 'face 'font-lock-comment-face) (propertize k 'face 'bold) (propertize "โ€บ" 'face 'font-lock-comment-face) "  " desc "โ€ฆ" "\n")))
              ;; Usable entry.
              (`(,(and key (pred (string-match re))) ,desc . ,_)
               (let ((k (match-string 1 key)))
           (insert (propertize prefix 'face 'font-lock-comment-face) (propertize k 'face 'bold) "   " desc "\n")
           (push k allowed-keys)))
              (_ nil))))
        ;; Insert special entries, if any.
        (when specials
          (insert "โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n")
          (pcase-dolist (`(,key ,description) specials)
            (insert (format "%s   %s\n" (propertize key 'face '(bold all-the-icons-red)) description))
            (push key allowed-keys)))
        ;; Display UI and let user select an entry or
        ;; a sub-level prefix.
        (goto-char (point-min))
        (unless (pos-visible-in-window-p (point-max))
          (org-fit-window-to-buffer))
        (let ((pressed (org--mks-read-key allowed-keys prompt)))
          (setq current (concat current pressed))
          (cond
           ((equal pressed "\C-g") (user-error "Abort"))
           ;; Selection is a prefix: open a new menu.
           ((member pressed des-keys))
           ;; Selection matches an association: return it.
           ((let ((entry (assoc current table)))
              (and entry (throw 'exit entry))))
           ;; Selection matches a special entry: return the
           ;; selection prefix.
           ((assoc current specials) (throw 'exit current))
           (t (error "No entry available")))))))
      (when buffer (kill-buffer buffer))))))
    (advice-add 'org-mks :override #'org-mks-pretty)

    The org-capture bin is rather nice, but I'd be nicer with a smaller frame, and no modeline.

    (setf (alist-get 'height +org-capture-frame-parameters) 15)
          ;; (alist-get 'name +org-capture-frame-parameters) "โ– Capture") ;; ATM hardcoded in other places, so changing breaks stuff
    (setq +org-capture-fn
          (lambda ()
            (interactive)
            (set-window-parameter nil 'mode-line-format 'none)
            (org-capture)))
  5. Roam

    1. Registering roam protocol

      [Desktop Entry]
      Name=Org-Protocol
      Exec=emacsclient %u
      Icon=emacs-icon
      Type=Application
      Terminal=false
      MimeType=x-scheme-handler/org-protocol
      

      Associate org-protocol:// links with the desktop file

      xdg-mime default org-protocol.desktop x-scheme-handler/org-protocol
      
    2. Graph Behaviour

      By default, clicking on an org-protocol:// link messes with the svg view. To fix this we can use an iframe, however that requires shifting to an html file. Hence, we need to do a bit of overriding.

      <!DOCTYPE html>
      <html>
          <head>
              <meta charset="utf-8">
              <title>Roam Graph</title>
              <meta name="viewport" content="width=device-width">
              <style type="text/css">
               body {
                   background: white;
               }
      
               svg {
                   position: relative;
                   top: 50vh;
                   left: 50vw;
                   transform: translate(-50%, -50%);
                   width: 95vw;
               }
      
               a > polygon {
                   transition-duration: 200ms;
                   transition-property: fill;
               }
      
               a > polyline {
                   transition-duration: 400ms;
                   transition-property: stroke;
               }
      
               a:hover > polygon {
                   fill: #d4d4d4;
               }
               a:hover > polyline {
                   stroke: #888;
               }
              </style>
              <script type="text/javascript">
               function create_iframe (url) {
                   i = document.createElement('iframe');
                   i.setAttribute('src', url);
                   i.style.setProperty('display', 'none');
                   document.body.append(i);
               }
               function listen_on_all_a () {
                   document.querySelectorAll("svg a").forEach(elem => {
                       elem.addEventListener('click', (e) => {
                           e.preventDefault();
                           create_iframe(elem.href.baseVal);
                       });
                   });
               }
              </script>
          </head>
          <body onload="listen_on_all_a()">
      %s
          </body>
      </html>
      (after! org-roam
        (setq org-roam-graph-node-extra-config '(("shape"      . "underline")
                                                 ("style"      . "rounded,filled")
                                                 ("fillcolor"  . "#EEEEEE")
                                                 ("color"      . "#C9C9C9")
                                                 ("fontcolor"  . "#111111")
                                                 ("fontname"   . "Overpass")))
      
        (setq +org-roam-graph--html-template
              (replace-regexp-in-string "%\\([^s]\\)" "%%\\1"
                                        (f-read-text (concat doom-private-dir "misc/org-roam-template.html"))))
      
        (defadvice! +org-roam-graph--build-html (&optional node-query callback)
          "Generate a graph showing the relations between nodes in NODE-QUERY. HTML style."
          :override #'org-roam-graph--build
          (unless (stringp org-roam-graph-executable)
            (user-error "`org-roam-graph-executable' is not a string"))
          (unless (executable-find org-roam-graph-executable)
            (user-error (concat "Cannot find executable %s to generate the graph.  "
                                "Please adjust `org-roam-graph-executable'")
                        org-roam-graph-executable))
          (let* ((node-query (or node-query
                                 `[:select [file titles] :from titles
                                   ,@(org-roam-graph--expand-matcher 'file t)]))
                 (graph      (org-roam-graph--dot node-query))
                 (temp-dot   (make-temp-file "graph." nil ".dot" graph))
                 (temp-graph (make-temp-file "graph." nil ".svg"))
                 (temp-html  (make-temp-file "graph." nil ".html")))
            (org-roam-message "building graph")
            (make-process
             :name "*org-roam-graph--build-process*"
             :buffer "*org-roam-graph--build-process*"
             :command `(,org-roam-graph-executable ,temp-dot "-Tsvg" "-o" ,temp-graph)
             :sentinel (progn
                         (lambda (process _event)
                           (when (= 0 (process-exit-status process))
                             (write-region (format +org-roam-graph--html-template (f-read-text temp-graph)) nil temp-html)
                             (when callback
                               (funcall callback temp-html)))))))))
    3. Modeline file name

      All those numbers! It's messy. Let's adjust this in a similar way that I have in the 6.5.1.1.

      (defadvice! doom-modeline--reformat-roam (orig-fun)
        :around #'doom-modeline-buffer-file-name
        (message "Reformat?")
        (message (buffer-file-name))
        (if (s-contains-p org-roam-directory (or buffer-file-name ""))
            (replace-regexp-in-string
             "\\(?:^\\|.*/\\)\\([0-9]\\{4\\}\\)\\([0-9]\\{2\\}\\)\\([0-9]\\{2\\}\\)[0-9]*-"
             "๐Ÿข”(\\1-\\2-\\3) "
             (funcall orig-fun))
          (funcall orig-fun)))
  6. Nicer generated heading IDs

    (defvar org-heading-contraction-max-words 3
      "Maximum number of words in a heading")
    (defvar org-heading-contraction-max-length 35
      "Maximum length of resulting string")
    (defvar org-heading-contraction-stripped-words
      '("the" "on" "in" "off" "a" "for" "by" "of" "and" "is" "to")
      "Unnecesary words to be removed from a heading")
    
    (defun org-heading-contraction (heading-string)
      "Get a contracted form of HEADING-STRING that is onlu contains alphanumeric charachters.
    Strips 'joining' words in `org-heading-contraction-stripped-words',
    and then limits the result to the first `org-heading-contraction-max-words' words.
    If the total length is > `org-heading-contraction-max-length' then individual words are
    truncated to fit within the limit"
      (let ((heading-words
             (-filter (lambda (word)
                        (not (member word org-heading-contraction-stripped-words)))
                      (split-string
                       (->> heading-string
                            s-downcase
                            (replace-regexp-in-string "\\[\\[[^]]+\\]\\[\\([^]]+\\)\\]\\]" "\\1") ; get description from org-link
                            (replace-regexp-in-string "[-/ ]+" " ") ; replace seperator-type chars with space
                            (replace-regexp-in-string "[^a-z0-9 ]" "") ; strip chars which need %-encoding in a uri
                            ) " "))))
        (when (> (length heading-words)
                 org-heading-contraction-max-words)
          (setq heading-words
                (subseq heading-words 0 org-heading-contraction-max-words)))
    
        (when (> (+ (-sum (mapcar #'length heading-words))
                    (1- (length heading-words)))
                 org-heading-contraction-max-length)
          ;; trucate each word to a max word length determined by
          ;;   max length = \floor{ \frac{total length - chars for seperators - \sum_{word \leq average length} length(word) }{num(words) > average length} }
          (setq heading-words (let* ((total-length-budget (- org-heading-contraction-max-length  ; how many non-separator chars we can use
                                                             (1- (length heading-words))))
                                     (word-length-budget (/ total-length-budget                  ; max length of each word to keep within budget
                                                            org-heading-contraction-max-words))
                                     (num-overlong (-count (lambda (word)                             ; how many words exceed that budget
                                                             (> (length word) word-length-budget))
                                                           heading-words))
                                     (total-short-length (-sum (mapcar (lambda (word)                 ; total length of words under that budget
                                                                         (if (<= (length word) word-length-budget)
                                                                             (length word) 0))
                                                                       heading-words)))
                                     (max-length (/ (- total-length-budget total-short-length)   ; max(max-length) that we can have to fit within the budget
                                                    num-overlong)))
                                (mapcar (lambda (word)
                                          (if (<= (length word) max-length)
                                              word
                                            (substring word 0 max-length)))
                                        heading-words))))
        (string-join heading-words "-")))

    Now here's alphapapa's subtly tweaked mode.

    (define-minor-mode unpackaged/org-export-html-with-useful-ids-mode
      "Attempt to export Org as HTML with useful link IDs.
    Instead of random IDs like \"#orga1b2c3\", use heading titles,
    made unique when necessary."
      :global t
      (if unpackaged/org-export-html-with-useful-ids-mode
          (advice-add #'org-export-get-reference :override #'unpackaged/org-export-get-reference)
        (advice-remove #'org-export-get-reference #'unpackaged/org-export-get-reference)))
    
    (defun unpackaged/org-export-get-reference (datum info)
      "Like `org-export-get-reference', except uses heading titles instead of random numbers."
      (let ((cache (plist-get info :internal-references)))
        (or (car (rassq datum cache))
            (let* ((crossrefs (plist-get info :crossrefs))
                   (cells (org-export-search-cells datum))
                   ;; Preserve any pre-existing association between
                   ;; a search cell and a reference, i.e., when some
                   ;; previously published document referenced a location
                   ;; within current file (see
                   ;; `org-publish-resolve-external-link').
                   ;;
                   ;; However, there is no guarantee that search cells are
                   ;; unique, e.g., there might be duplicate custom ID or
                   ;; two headings with the same title in the file.
                   ;;
                   ;; As a consequence, before re-using any reference to
                   ;; an element or object, we check that it doesn't refer
                   ;; to a previous element or object.
                   (new (or (cl-some
                             (lambda (cell)
                               (let ((stored (cdr (assoc cell crossrefs))))
                                 (when stored
                                   (let ((old (org-export-format-reference stored)))
                                     (and (not (assoc old cache)) stored)))))
                             cells)
                            (when (org-element-property :raw-value datum)
                              ;; Heading with a title
                              (unpackaged/org-export-new-named-reference datum cache))
                            (when (member (car datum) '(src-block table example fixed-width property-drawer))
                              ;; Nameable elements
                              (unpackaged/org-export-new-named-reference datum cache))
                            ;; NOTE: This probably breaks some Org Export
                            ;; feature, but if it does what I need, fine.
                            (org-export-format-reference
                             (org-export-new-reference cache))))
                   (reference-string new))
              ;; Cache contains both data already associated to
              ;; a reference and in-use internal references, so as to make
              ;; unique references.
              (dolist (cell cells) (push (cons cell new) cache))
              ;; Retain a direct association between reference string and
              ;; DATUM since (1) not every object or element can be given
              ;; a search cell (2) it permits quick lookup.
              (push (cons reference-string datum) cache)
              (plist-put info :internal-references cache)
              reference-string))))
    
    (defun unpackaged/org-export-new-named-reference (datum cache)
      "Return new reference for DATUM that is unique in CACHE."
      (cl-macrolet ((inc-suffixf (place)
                                 `(progn
                                    (string-match (rx bos
                                                      (minimal-match (group (1+ anything)))
                                                      (optional "--" (group (1+ digit)))
                                                      eos)
                                                  ,place)
                                    ;; HACK: `s1' instead of a gensym.
                                    (-let* (((s1 suffix) (list (match-string 1 ,place)
                                                               (match-string 2 ,place)))
                                            (suffix (if suffix
                                                        (string-to-number suffix)
                                                      0)))
                                      (setf ,place (format "%s--%s" s1 (cl-incf suffix)))))))
        (let* ((headline-p (eq (car datum) 'headline))
               (title (if headline-p
                          (org-element-property :raw-value datum)
                        (or (org-element-property :name datum)
                            (concat (org-element-property :raw-value
                                     (org-element-property :parent
                                      (org-element-property :parent datum)))))))
               ;; get ascii-only form of title without needing percent-encoding
               (ref (concat (org-heading-contraction (substring-no-properties title))
                            (unless (or headline-p (org-element-property :name datum))
                              (concat ","
                                      (case (car datum)
                                        ('src-block "code")
                                        ('example "example")
                                        ('fixed-width "mono")
                                        ('property-drawer "properties")
                                        (t (symbol-name (car datum))))
                                      "--1"))))
               (parent (when headline-p (org-element-property :parent datum))))
          (while (--any (equal ref (car it))
                        cache)
            ;; Title not unique: make it so.
            (if parent
                ;; Append ancestor title.
                (setf title (concat (org-element-property :raw-value parent)
                                    "--" title)
                      ;; get ascii-only form of title without needing percent-encoding
                      ref (org-heading-contraction (substring-no-properties title))
                      parent (when headline-p (org-element-property :parent parent)))
              ;; No more ancestors: add and increment a number.
              (inc-suffixf ref)))
          ref)))
    
    (add-hook 'org-load-hook #'unpackaged/org-export-html-with-useful-ids-mode)
  7. Nicer org-return

    Once again, from unpackaged.el

    (after! org
      (defun unpackaged/org-element-descendant-of (type element)
        "Return non-nil if ELEMENT is a descendant of TYPE.
    TYPE should be an element type, like `item' or `paragraph'.
    ELEMENT should be a list like that returned by `org-element-context'."
        ;; MAYBE: Use `org-element-lineage'.
        (when-let* ((parent (org-element-property :parent element)))
          (or (eq type (car parent))
              (unpackaged/org-element-descendant-of type parent))))
    
    ;;;###autoload
      (defun unpackaged/org-return-dwim (&optional default)
        "A helpful replacement for `org-return-indent'.  With prefix, call `org-return-indent'.
    
    On headings, move point to position after entry content.  In
    lists, insert a new item or end the list, with checkbox if
    appropriate.  In tables, insert a new row or end the table."
        ;; Inspired by John Kitchin: http://kitchingroup.cheme.cmu.edu/blog/2017/04/09/A-better-return-in-org-mode/
        (interactive "P")
        (if default
            (org-return t)
          (cond
           ;; Act depending on context around point.
    
           ;; NOTE: I prefer RET to not follow links, but by uncommenting this block, links will be
           ;; followed.
    
           ((eq 'link (car (org-element-context)))
           ;;  ;; Link: Open it.
             (org-open-at-point-global))
    
           ((org-at-heading-p)
            ;; Heading: Move to position after entry content.
            ;; NOTE: This is probably the most interesting feature of this function.
            (let ((heading-start (org-entry-beginning-position)))
              (goto-char (org-entry-end-position))
              (cond ((and (org-at-heading-p)
                          (= heading-start (org-entry-beginning-position)))
                     ;; Entry ends on its heading; add newline after
                     (end-of-line)
                     (insert "\n\n"))
                    (t
                     ;; Entry ends after its heading; back up
                     (forward-line -1)
                     (end-of-line)
                     (when (org-at-heading-p)
                       ;; At the same heading
                       (forward-line)
                       (insert "\n")
                       (forward-line -1))
                     ;; FIXME: looking-back is supposed to be called with more arguments.
                     (while (not (looking-back (rx (repeat 3 (seq (optional blank) "\n")))))
                       (insert "\n"))
                     (forward-line -1)))))
    
           ((org-at-item-checkbox-p)
            ;; Checkbox: Insert new item with checkbox.
            (org-insert-todo-heading nil))
    
           ((org-in-item-p)
            ;; Plain list.  Yes, this gets a little complicated...
            (let ((context (org-element-context)))
              (if (or (eq 'plain-list (car context))  ; First item in list
                      (and (eq 'item (car context))
                           (not (eq (org-element-property :contents-begin context)
                                    (org-element-property :contents-end context))))
                      (unpackaged/org-element-descendant-of 'item context))  ; Element in list item, e.g. a link
                  ;; Non-empty item: Add new item.
                  (org-insert-item)
                ;; Empty item: Close the list.
                ;; TODO: Do this with org functions rather than operating on the text. Can't seem to find the right function.
                (delete-region (line-beginning-position) (line-end-position))
                (insert "\n"))))
    
           ((when (fboundp 'org-inlinetask-in-task-p)
              (org-inlinetask-in-task-p))
            ;; Inline task: Don't insert a new heading.
            (org-return t))
    
           ((org-at-table-p)
            (cond ((save-excursion
                     (beginning-of-line)
                     ;; See `org-table-next-field'.
                     (cl-loop with end = (line-end-position)
                              for cell = (org-element-table-cell-parser)
                              always (equal (org-element-property :contents-begin cell)
                                            (org-element-property :contents-end cell))
                              while (re-search-forward "|" end t)))
                   ;; Empty row: end the table.
                   (delete-region (line-beginning-position) (line-end-position))
                   (org-return t))
                  (t
                   ;; Non-empty row: call `org-return-indent'.
                   (org-return t))))
           (t
            ;; All other cases: call `org-return-indent'.
            (org-return t))))))
    
    (map!
     :after evil-org
     :map evil-org-mode-map
     :i [return] #'unpackaged/org-return-dwim)
  8. Snippet Helper

    For snippets which want to depend on the #+THING: on the current line. This is mostly source blocks, and property args, so let's get fancy with them.

    One-letter snippets are super-convenient, but for them to not be a pain everywhere else we'll need a nice condition function to use in yasnippet.

    By having this function give slightly more than a simple t or nil, we can use in a second function to get the most popular language without explicit global header args.

    (defun +yas/org-src-lang ()
      "Try to find the current language of the src/header at point.
    Return nil otherwise."
      (save-excursion
        (pcase
            (downcase
             (buffer-substring-no-properties
              (goto-char (line-beginning-position))
              (or (ignore-errors (1- (search-forward " " (line-end-position))))
                  (1+ (point)))))
          ("#+property:"
           (when (re-search-forward "header-args:")
             (buffer-substring-no-properties
              (point)
              (or (and (forward-word) (point))
                  (1+ (point))))))
          ("#+begin_src"
           (buffer-substring-no-properties
            (point)
            (or (and (forward-word) (point))
                (1+ (point)))))
          ("#+header:"
           (search-forward "#+begin_src")
           (+yas/org-src-lang))
          (t nil))))
    
    (defun +yas/org-most-common-no-property-lang ()
      "Find the lang with the most source blocks that has no global header-args, else nil."
      (let (src-langs header-langs)
        (save-excursion
          (goto-char (point-min))
          (while (search-forward "#+begin_src" nil t)
            (push (+yas/org-src-lang) src-langs))
          (goto-char (point-min))
          (while (re-search-forward "#\\+property: +header-args" nil t)
            (push (+yas/org-src-lang) header-langs)))
    
        (setq src-langs
              (mapcar #'car
                      ;; sort alist by frequency (desc.)
                      (sort
                       ;; generate alist with form (value . frequency)
                       (cl-loop for (n . m) in (seq-group-by #'identity src-langs)
                                collect (cons n (length m)))
                       (lambda (a b) (> (cdr a) (cdr b))))))
    
        (car (set-difference src-langs header-langs :test #'string=))))

Visuals

  1. Exporting to HTML

    I want to tweak a whole bunch of things. While I'll want my tweaks almost all the time, occasionally I may want to test how something turns out using a more default config. With that in mind, a global minor mode seems like the most appropriate architecture to use.

    (define-minor-mode org-fancy-html-export-mode
      "Toggle my fabulous org export tweaks. While this mode itself does a little bit,
    the vast majority of the change in behaviour comes from switch statements in:
     - `org-html-template-fancier'
     - `org-html--build-meta-info-extended'
     - `org-html-src-block-collapsable'
     - `org-html-block-collapsable'
     - `org-html-table-wrapped'
     - `org-html--format-toc-headline-colapseable'
     - `org-html--toc-text-stripped-leaves'
     - `org-export-html-headline-anchor'"
      :global t
      :init-value t
      (if org-fancy-html-export-mode
          (setq org-html-style-default org-html-style-fancy
                org-html-checkbox-type 'html-span)
        (setq org-html-style-default org-html-style-plain
              org-html-checkbox-type 'html)))
    1. Extra header content

      We want to tack on a few more bits to the start of the body. Unfortunately, there doesn't seem to be any nice variable or hook, so we'll just override the relevant function.

      This is done to allow me to add the date and author to the page header, implement a CSS-only light/dark theme toggle, and a sprinkle of Open Graph metadata.

      (defadvice! org-html-template-fancier (orig-fn contents info)
        "Return complete document string after HTML conversion.
      CONTENTS is the transcoded contents string.  INFO is a plist
      holding export options. Adds a few extra things to the body
      compared to the default implementation."
        :around #'org-html-template
        (if (not org-fancy-html-export-mode)
            (funcall orig-fn contents info)
          (concat
           (when (and (not (org-html-html5-p info)) (org-html-xhtml-p info))
             (let* ((xml-declaration (plist-get info :html-xml-declaration))
                    (decl (or (and (stringp xml-declaration) xml-declaration)
                              (cdr (assoc (plist-get info :html-extension)
                                          xml-declaration))
                              (cdr (assoc "html" xml-declaration))
                              "")))
               (when (not (or (not decl) (string= "" decl)))
                 (format "%s\n"
                         (format decl
                                 (or (and org-html-coding-system
                                          (fboundp 'coding-system-get)
                                          (coding-system-get org-html-coding-system 'mime-charset))
                                     "iso-8859-1"))))))
           (org-html-doctype info)
           "\n"
           (concat "<html"
                   (cond ((org-html-xhtml-p info)
                          (format
                           " xmlns=\"http://www.w3.org/1999/xhtml\" lang=\"%s\" xml:lang=\"%s\""
                           (plist-get info :language) (plist-get info :language)))
                         ((org-html-html5-p info)
                          (format " lang=\"%s\"" (plist-get info :language))))
                   ">\n")
           "<head>\n"
           (org-html--build-meta-info info)
           (org-html--build-head info)
           (org-html--build-mathjax-config info)
           "</head>\n"
           "<body>\n<input type='checkbox' id='theme-switch'><div id='page'><label id='switch-label' for='theme-switch'></label>"
           (let ((link-up (org-trim (plist-get info :html-link-up)))
                 (link-home (org-trim (plist-get info :html-link-home))))
             (unless (and (string= link-up "") (string= link-home ""))
               (format (plist-get info :html-home/up-format)
                       (or link-up link-home)
                       (or link-home link-up))))
           ;; Preamble.
           (org-html--build-pre/postamble 'preamble info)
           ;; Document contents.
           (let ((div (assq 'content (plist-get info :html-divs))))
             (format "<%s id=\"%s\">\n" (nth 1 div) (nth 2 div)))
           ;; Document title.
           (when (plist-get info :with-title)
             (let ((title (and (plist-get info :with-title)
                               (plist-get info :title)))
                   (subtitle (plist-get info :subtitle))
                   (html5-fancy (org-html--html5-fancy-p info)))
               (when title
                 (format
                  "<div class='page-header'><div class='page-meta'>%s, %s</div><h1 class=\"title\">%s%s</h1></div>\n"
                  (format-time-string "%Y-%m-%d %A %-I:%M%p")
                  (org-export-data (plist-get info :author) info)
                  (org-export-data title info)
                  (if subtitle
                      (format
                       (if html5-fancy
                           "<p class=\"subtitle\">%s</p>\n"
                         (concat "\n" (org-html-close-tag "br" nil info) "\n"
                                 "<span class=\"subtitle\">%s</span>\n"))
                       (org-export-data subtitle info))
                    "")))))
           contents
           (format "</%s>\n" (nth 1 (assq 'content (plist-get info :html-divs))))
           ;; Postamble.
           (org-html--build-pre/postamble 'postamble info)
           ;; Possibly use the Klipse library live code blocks.
           (when (plist-get info :html-klipsify-src)
             (concat "<script>" (plist-get info :html-klipse-selection-script)
                     "</script><script src=\""
                     org-html-klipse-js
                     "\"></script><link rel=\"stylesheet\" type=\"text/css\" href=\""
                     org-html-klipse-css "\"/>"))
           ;; Closing document.
           "</div>\n</body>\n</html>")))
      (defun org-html--build-meta-entry (label identity &optional content-format &rest content-formatters)
        "Construct <meta> tag with LABEL=\"IDENTITY\" and content from CONTENT-FORMAT and CONTENT-FORMATTER."
        (concat "<meta "
                (format "%s=\"%s" label identity)
                (when content-format
                  (concat "\" content=\""
                          (replace-regexp-in-string
                           "\"" "&quot;"
                           (org-html-encode-plain-text
                            (if content-formatters
                                (apply #'format content-format content-formatters)
                              content-format)))))
                "\" />\n"))
      
      (defadvice! org-html--build-meta-info-extended (info)
        "Return meta tags for exported document, with more meta than usual.
      INFO is a plist used as a communication channel."
        :override #'org-html--build-meta-info
        (let* ((protect-string
                (lambda (str)
                  (replace-regexp-in-string
                   "\"" "&quot;" (org-html-encode-plain-text str))))
               (title (org-export-data (plist-get info :title) info))
               ;; Set title to an invisible character instead of leaving it
               ;; empty, which is invalid.
               (title (if (org-string-nw-p title) title "&lrm;"))
               (subtitle (org-export-data (plist-get info :subtitle) info))
               (author (and (plist-get info :with-author)
                            (let ((auth (plist-get info :author)))
               ;; Return raw Org syntax.
                              (and auth (org-element-interpret-data auth)))))
               (description (plist-get info :description))
               (keywords (plist-get info :keywords))
               (charset (or (and org-html-coding-system
                                 (fboundp 'coding-system-get)
                                 (coding-system-get org-html-coding-system
                                                    'mime-charset))
                            "iso-8859-1")))
          (concat
           (when (plist-get info :time-stamp-file)
             (format-time-string
              (concat "<!-- "
                      (plist-get info :html-metadata-timestamp-format)
                      " -->\n")))
      
           (org-html--build-meta-entry "charset" charset)
      
           (let ((viewport-options
                  (cl-remove-if-not (lambda (cell) (org-string-nw-p (cadr cell)))
                                    (plist-get info :html-viewport))))
             (if viewport-options
                 (org-html--build-meta-entry "name" "viewport"
                                             (mapconcat
                                              (lambda (elm) (format "%s=%s" (car elm) (cadr elm)))
                                              viewport-options ", "))))
      
           (format "<title>%s</title>\n" title)
      
           (org-html--build-meta-entry "name" "generator" "Org Mode")
      
           (when (org-string-nw-p author)
             (org-html--build-meta-entry "name" "author" author))
      
           (when (org-string-nw-p description)
             (org-html--build-meta-entry "name" "description" description))
      
           (when (org-string-nw-p keywords)
             (org-html--build-meta-entry "name" "keywords" keywords))
      
           (when org-fancy-html-export-mode
             (concat
              (org-html--build-meta-entry "name" "theme-color" "#77aa99")
      
              (org-html--build-meta-entry "property" "og:title" title)
              (org-html--build-meta-entry "property" "og:type" "article")
              (org-html--build-meta-entry "property" "og:image" "https://tecosaur.com/resources/org/nib.png")
              (when (org-string-nw-p author)
                (org-html--build-meta-entry "property" "og:article:author:first_name" (car (s-split " " author))))
              (when (and (org-string-nw-p author) (s-contains-p " " author))
                (org-html--build-meta-entry "property" "og:article:author:first_name" (cdr (s-split-up-to " " author 2))))
              (org-html--build-meta-entry "property" "og:article:published_time" (format-time-string "%FT%T%z"))
              (when (org-string-nw-p subtitle)
                (org-html--build-meta-entry "property" "og:description" subtitle)))))))
    2. TODO Custom CSS/JS

      <link rel="preload" as="font" crossorigin="crossorigin" type="font/woff2" href="https://tecosaur.com/resources/org/etbookot-roman-webfont.woff2">
      <link rel="preload" as="font" crossorigin="crossorigin" type="font/woff2" href="https://tecosaur.com/resources/org/etbookot-italic-webfont.woff2">
      (after! org
        (setq org-html-style-fancy
              (concat (f-read-text (expand-file-name "misc/org-export-header.html" doom-private-dir))
                    "<script>\n"
                    (f-read-text (expand-file-name "misc/pile-css-theme/main.js" doom-private-dir))
                    "</script>\n<style>\n"
                    (f-read-text (expand-file-name "misc/pile-css-theme/main.css" doom-private-dir))
                    "</style>")
              org-html-style-plain org-html-style-default
              org-html-style-default  org-html-style-fancy
              org-html-htmlize-output-type 'css
              org-html-doctype "html5"
              org-html-html5-fancy t))
    3. Collapsable src and example blocks

      By wrapping the <pre> element in a <details> block, we can obtain collapsable blocks with no CSS, though we will toss a little in anyway to have this looking somewhat spiffy.

      We can take our modification a step further, and add a gutter on the side of the Src block containing both an anchor referencing the current block, and a button to copy the content of the block.

      (defadvice! org-html-src-block-collapsable (orig-fn src-block contents info)
        "Wrap the usual <pre> block in a <details>"
        :around #'org-html-src-block
        (if (not org-fancy-html-export-mode)
            (funcall orig-fn src-block contents info)
          (let* ((properties (cadr src-block))
                 (lang (mode-name-to-lang-name
                        (plist-get properties :language)))
                 (name (plist-get properties :name))
                 (ref (org-export-get-reference src-block info)))
            (format
             "<details id='%s' class='code'%s><summary%s>%s</summary>
      <div class='gutter'>
      <a href='#%s'>#</a>
      <button title='Copy to clipboard' onclick='copyPreToClipdord(this)'>โŽ˜</button>\
      </div>
      %s
      </details>"
             ref
             (if (member (org-export-read-attribute :attr_html src-block :collapsed)
                         '("y" "yes" "t" "true"))
                 "" " open")
             (if name " class='named'" "")
             (if (not name) (concat "<span class='lang'>" lang "</span>")
               (format "<span class='name'>%s</span><span class='lang'>%s</span>" name lang))
             ref
             (if name
                 (replace-regexp-in-string (format "<pre\\( class=\"[^\"]+\"\\)? id=\"%s\">" ref) "<pre\\1>"
                                           (funcall orig-fn src-block contents info))
               (funcall orig-fn src-block contents info))))))
      
      (defun mode-name-to-lang-name (mode)
        (or (cadr (assoc mode
                         '(("C" "C")
                           ("D" "D")
                           ("J" "J")
                           ("R" "R")
                           ("abc" "ABC")
                           ("ada" "Ada")
                           ("ash" "ash")
                           ("asm" "Assembler")
                           ("asymptote" "Asymptote")
                           ("awk" "Awk")
                           ("bash" "bash")
                           ("calc" "Emacs Calc")
                           ("caml" "Caml")
                           ("clojure" "Clojure")
                           ("conf" "Configuration File")
                           ("coq" "Coq")
                           ("cpp" "C++")
                           ("csh" "csh")
                           ("css" "CSS")
                           ("dash" "dash")
                           ("delphi" "Delphi")
                           ("ditaa" "ditaa")
                           ("dot" "Graphviz")
                           ("ebnf2ps" "ebfn2ps")
                           ("emacs-lisp" "Emacs Lisp")
                           ("forth" "Forth")
                           ("fortran" "Fortran")
                           ("gnuplot" "gnuplot")
                           ("groovy" "Groovy")
                           ("haskell" "Haskell")
                           ("hledger" "hledger")
                           ("html" "HTML")
                           ("idl" "IDL")
                           ("io" "IO")
                           ("java" "Java")
                           ("js" "Javascript")
                           ("ksh" "ksh")
                           ("latex" "LaTeX")
                           ("ledger" "Ledger")
                           ("lilypond" "Lilypond")
                           ("lisp" "Lisp")
                           ("lua" "Lua")
                           ("makefile" "Makefile")
                           ("matlab" "MATLAB")
                           ("maxima" "Maxima")
                           ("mercury" "Mercury")
                           ("metapost" "MetaPost")
                           ("mksh" "mksh")
                           ("modula-2" "Modula-2")
                           ("mscgen" "Mscgen")
                           ("nxml" "XML")
                           ("ocaml" "Objective Caml")
                           ("octave" "Octave")
                           ("org" "Org mode")
                           ("oz" "OZ")
                           ("pascal" "Pascal")
                           ("perl" "Perl")
                           ("picolisp" "Pico Lisp")
                           ("plain-tex" "TeX")
                           ("plantuml" "Plantuml")
                           ("posh" "posh")
                           ("processing" "Processing.js")
                           ("prolog" "Prolog")
                           ("ps" "PostScript")
                           ("python" "Python")
                           ("ruby" "Ruby")
                           ("sass" "Sass")
                           ("scala" "Scala")
                           ("scheme" "Scheme")
                           ("screen" "Gnu Screen")
                           ("sed" "Sed")
                           ("sh" "shell")
                           ("shell" "Shell Script")
                           ("simula" "Simula")
                           ("sql" "SQL")
                           ("sqlite" "SQLite")
                           ("tcl" "tcl")
                           ("tex" "LaTeX")
                           ("verilog" "Verilog")
                           ("vhdl" "VHDL")
                           ("xml" "XML"))))
            mode))
      (after! org
        (defun org-html-block-collapsable (orig-fn block contents info)
          "Wrap the usual block in a <details>"
          (if (not org-fancy-html-export-mode)
              (funcall orig-fn block contents info)
            (let ((ref (org-export-get-reference block info))
                  (type (case (car block)
                          ('property-drawer "Properties")))
                  (collapsed-default (case (car block)
                                       ('property-drawer t)
                                       (t nil)))
                  (collapsed-value (org-export-read-attribute :attr_html block :collapsed)))
              (format
               "<details id='%s' class='code'%s>
      <summary%s>%s</summary>
      <div class='gutter'>\
      <a href='#%s'>#</a>
      <button title='Copy to clipboard' onclick='copyPreToClipdord(this)'>โŽ˜</button>\
      </div>
      %s\n
      </details>"
               ref
               (if (or (and collapsed-value (member collapsed-value '("y" "yes" "t" "true")))
                       collapsed-default)
                   "" " open")
               (if type " class='named'" "")
               (if type (format "<span class='type'>%s</span>" type) "")
               ref
               (funcall orig-fn block contents info)))))
      
        (advice-add 'org-html-example-block   :around #'org-html-block-collapsable)
        (advice-add 'org-html-fixed-width     :around #'org-html-block-collapsable)
        (advice-add 'org-html-property-drawer :around #'org-html-block-collapsable))
    4. Handle table overflow

      In order to accommodate wide tables โ€”particularly on mobile devicesโ€” we want to set a maximum width and scroll overflow. Unfortunately, this cannot be applied directly to the table element, so we have to wrap it in a div.

      While we're at it, we can a link gutter, as we did with src blocks, and show the #+name, if one is given.

      (defadvice! org-html-table-wrapped (orig-fn table contents info)
        "Wrap the usual <table> in a <div>"
        :around #'org-html-table
        (if (not org-fancy-html-export-mode)
            (funcall orig-fn table contents info)
          (let* ((name (plist-get (cadr table) :name))
                 (ref (org-export-get-reference table info)))
            (format "<div id='%s' class='table'>
      <div class='gutter'><a href='#%s'>#</a></div>
      <div class='tabular'>
      %s
      </div>\
      </div>"
                    ref ref
                    (if name
                        (replace-regexp-in-string (format "<table id=\"%s\"" ref) "<table"
                                                  (funcall orig-fn table contents info))
                      (funcall orig-fn table contents info))))))
    5. TOC as a collapsable tree

      The TOC is much nicer to navigate as a collapsable tree. Unfortunately we cannot achieve this with CSS alone. Thankfully we can avoid JS though, by adapting the TOC generation code to use a label for each item, and a hidden checkbox to keep track of state.

      To add this, we need to change one line in org-htmlโ€“format-toc-headline.

      Since we can actually accomplish the desired effect by adding advice around the function, without overriding it โ€” let's do that to reduce the bug surface of this config a tad.

      (defadvice! org-html--format-toc-headline-colapseable (orig-fn headline info)
        "Add a label and checkbox to `org-html--format-toc-headline's usual output,
      to allow the TOC to be a collapseable tree."
        :around #'org-html--format-toc-headline
        (if (not org-fancy-html-export-mode)
            (funcall orig-fn headline info)
          (let ((id (or (org-element-property :CUSTOM_ID headline)
                        (org-export-get-reference headline info))))
            (format "<input type='checkbox' id='toc--%s'/><label for='toc--%s'>%s</label>"
                    id id (funcall orig-fn headline info)))))

      Now, leaves (headings with no children) shouldn't have the label item. The obvious way to achieve this is by including some if no childrenโ€ฆ logic in org-html--format-toc-headline-colapseable. Unfortunately, I can't my elisp isn't up to par to extract the number of child headings from the mountain of info that org provides.

      (defadvice! org-html--toc-text-stripped-leaves (orig-fn toc-entries)
        "Remove label"
        :around #'org-html--toc-text
        (if (not org-fancy-html-export-mode)
            (funcall orig-fn toc-entries)
          (replace-regexp-in-string "<input [^>]+><label [^>]+>\\(.+?\\)</label></li>" "\\1</li>"
                                    (funcall orig-fn toc-entries))))
    6. Make verbatim different to code

      Since we have verbatim and code, let's use verbatim for key strokes.

      (setq org-html-text-markup-alist
            '((bold . "<b>%s</b>")
              (code . "<code>%s</code>")
              (italic . "<i>%s</i>")
              (strike-through . "<del>%s</del>")
              (underline . "<span class=\"underline\">%s</span>")
              (verbatim . "<kbd>%s</kbd>")))
    7. Change checkbox type

      We also want to use HTML checkboxes, however we want to get a bit fancier than default

      (after! org
      (appendq! org-html-checkbox-types '((html-span .
            ((on . "<span class='checkbox'></span>")
            (off . "<span class='checkbox'></span>")
            (trans . "<span class='checkbox'></span>")))))
      (setq org-html-checkbox-type 'html-span))
      • [ ] I'm yet to do this
      • [-] Work in progress
      • [X] This is done
    8. Header anchors

      I want to add GitHub-style links on hover for headings.

      (after! org
        (defun org-export-html-headline-anchor (text backend info)
          (when (and (org-export-derived-backend-p backend 'html)
                     org-fancy-html-export-mode)
            (unless org-msg-currently-exporting
              (replace-regexp-in-string
               "<h\\([0-9]\\) id=\"\\([a-z0-9-]+\\)\">\\(.*[^ ]\\)<\\/h[0-9]>" ; this is quite restrictive, but due to `org-heading-contraction' I can do this
               "<h\\1 id=\"\\2\">\\3<a aria-hidden=\"true\" href=\"#\\2\">#</a> </h\\1>"
               text))))
      
        (add-to-list 'org-export-filter-headline-functions
                     'org-export-html-headline-anchor))

Babel

(setq org-babel-python-command "python3")
(defun tec-org-python ()
  (if (eq major-mode 'python-mode)
   (progn (anaconda-mode t)
          (company-mode t))))
(add-hook 'org-src-mode-hook 'tec-org-python)

Markdown

Let's use mixed pitch, because it's great

(add-hook! (gfm-mode markdown-mode) #'mixed-pitch-mode)

Most of the time when I write markdown, it's going into some app/website which will do it's own line wrapping, hence we only want to use visual line wrapping. No hard stuff.

(add-hook! (gfm-mode markdown-mode) #'visual-line-mode #'turn-off-auto-fill)

Since markdown is often seen as rendered HTML, let's try to somewhat mirror the style or markdown renderers.

Most markdown renders seem to make the first three headings levels larger than normal text, the first two much so. Then the fourth level tends to be the same as body text, while the fifth and sixth are (increasingly) smaller, with the sixth greyed out. Since the sixth level is so small, I'll turn up the boldness a notch.

(custom-set-faces!
  '(markdown-header-face-1 :height 1.25 :weight extra-bold :inherit markdown-header-face)
  '(markdown-header-face-2 :height 1.15 :weight bold       :inherit markdown-header-face)
  '(markdown-header-face-3 :height 1.08 :weight bold       :inherit markdown-header-face)
  '(markdown-header-face-4 :height 1.00 :weight bold       :inherit markdown-header-face)
  '(markdown-header-face-5 :height 0.90 :weight bold       :inherit markdown-header-face)
  '(markdown-header-face-6 :height 0.75 :weight extra-bold :inherit markdown-header-face))

Python

(after! lsp-python-ms
  (set-lsp-priority! 'mspyls 1))

hledger

(setq ledger-mode-should-check-version nil
      ledger-report-links-in-register nil
      ledger-binary-path "hledger")

Authinfo

I too like syntax highlighting

;;; authinfo-mode.el -*- lexical-binding: t; -*-
(setq authinfo-colour-keywords
      '(("^#.*" . font-lock-comment-face)
        ("^\\(machine\\)[ \t]+\\([^ \t\n]+\\)"
         (1 font-lock-variable-name-face)
         (2 font-lock-builtin-face))
        ("\\(login\\)[ \t]+\\([^ \t\n]+\\)"
         (1 font-lock-comment-delimiter-face)
         (2 font-lock-keyword-face))
        ("\\(password\\)[ \t]+\\([^ \t\n]+\\)"
         (1 font-lock-comment-delimiter-face)
         (2 font-lock-doc-face))
        ("\\(port\\)[ \t]+\\([^ \t\n]+\\)"
         (1 font-lock-comment-delimiter-face)
         (2 font-lock-type-face))
        ("\\([^ \t\n]+\\)[, \t]+\\([^ \t\n]+\\)"
         (1 font-lock-constant-face)
         (2 nil))))

(defun authinfo-colour--hide-passwords (start end)
  "Just `authinfo--hide-passwords' with a different colour face overlay."
  (save-excursion
    (save-restriction
      (narrow-to-region start end)
      (goto-char start)
      (while (re-search-forward "\\bpassword +\\([^\n\t ]+\\)"
                                nil t)
        (let ((overlay (make-overlay (match-beginning 1) (match-end 1))))
          (overlay-put overlay 'display (propertize "****"
                                                    'face 'font-lock-doc-face))
          (overlay-put overlay 'reveal-toggle-invisible
                       #'authinfo-colour--toggle-display))))))

(defun authinfo-colour--toggle-display (overlay hide)
  "Just `authinfo--toggle-display' with a different colour face overlay."
  (if hide
      (overlay-put overlay 'display (propertize "****" 'face 'font-lock-doc-face))
    (overlay-put overlay 'display nil)))

(defvar authinfo-hide-passwords t
  "Whether to hide passwords in authinfo.")

(define-derived-mode authinfo-colour-mode fundamental-mode "Authinfo"
  "Major mode for editing .authinfo files.

Like `fundamental-mode', just with colour and passoword hiding."
  (font-lock-add-keywords nil authinfo-colour-keywords)
  (setq-local comment-start "#")
  (setq-local comment-end "")
  (when authinfo-hide-passwords
    (authinfo-colour--hide-passwords (point-min) (point-max))
    (reveal-mode)))

(provide 'authinfo-colour-mode)

(use-package! authinfo-colour-mode
  :mode ("authinfo\\.gpg\\'" . authinfo-colour-mode)
  :config
  (advice-add 'authinfo-mode :override #'authinfo-colour-mode))

Global modes

(delete-selection-mode 1)                         ; Replace selection when inserting text

(display-time-mode 1)                             ; Enable time in the mode-line

(unless (equal "Battery status not available"
               (battery))
  (display-battery-mode 1))                       ; On laptops it's nice to know how much power you have

(global-subword-mode 1)                           ; Iterate through CamelCase words

(global-display-fill-column-indicator-mode 1)     ; Show me when I've gone too far

(global-auto-revert-mode 1)                       ; Load files for me please

(winner-mode +1)                                  ; ยฏ\_(ใƒ„)_/ยฏ

(setq global-org-pretty-table-mode t)             ; Pretty desu

(use-package! wakatime-mode
  :config (global-wakatime-mode))

Key Binding

;; Surely there is a way to do these with (map! ...)
(use-package! org :bind ("C-c l" . org-store-link))
(define-key winner-mode-map (kbd "<M-h>") #'winner-undo)
(define-key winner-mode-map (kbd "<M-l>") #'winner-redo)

Hooks

(add-hook 'org-babel-after-execute-hook 'org-redisplay-inline-images)

(add-hook! 'org-mode-hook #'+org-pretty-mode #'mixed-pitch-mode)

Exporting

(after! org
  (setq org-export-headline-levels 5)
  (require 'ox-gfm nil t))