A Modest Proposal: That the Labours of Nine Thousand Lines of Emacs Configuration Shall Render All Other Text Editors Obsolete and Provide a Most Nutritious Meal for Gentlemen Programmers

Intro

Howdy. This is my personal doom emacs config - I lifted quite a bit from tecosaur’s Doom Emacs Configuration - but there’s a lot of additions that I use for publishing / org-roam / elfeed, etc. and I culled some of the more generic explanatory things / LaTex stuff since i don’t really do much publishing.

Rudimentary configuration

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

;;; config.el -*- lexical-binding: t; -*-
#!/usr/bin/env bash

Personal Information

It’s useful to have some basic personal information.1

(setq user-full-name "Justin"
      user-mail-address "[email protected]"
      calendar-location-name "Villa Hills, KY"
      calendar-time-zone-rule "EST"
      calendar-standard-time-zone-name "EST"
      calendar-daylight-time-zone-name "EDT")

Apparently this is used by GPG, and all sorts of other things.

Speaking of GPG, I want to use ~/.authinfo.gpg instead of the default in ~/.emacs.d. Why? Because my home directory is already cluttered, so this won’t make a difference, and I don’t want to accidentally purge this file (I have done rm -rf~/.emac.d~ before. I also want to cache as much as possible, as my home machine is pretty safe, and my laptop is shutdown a lot.

(setq auth-sources '("~/.authinfo.gpg")
      auth-source-cache-expiry nil) ; default is 7200 (2h)

Diary functions

Some useful things for the diary. This allows sunrise and sunset to be put into separate times for agenda viewing.

(setq diary-file "~/.org/diary")
(defun bv-calendar-dst-starts (year) "Daylight Savings Start"
  (calendar-nth-named-day -1 0 3 year))
 
 
(defun bv-calendar-dst-ends (year) "Daylight Savings End"
  (calendar-nth-named-day -1 0 10 year))
 
(setq calendar-daylight-savings-starts '(bv-calendar-dst-starts year))
(setq calendar-daylight-savings-ends '(bv-calendar-dst-ends year))

Better defaults

Simple settings

Browsing the web and seeing angrybacon/dotemacs and comparing with the values shown by SPC h v and selecting what I thought looks good, I’ve ended up adding the following:

 
(setq exec-path (append exec-path '("~/.local/bin/")))
 
(setq-default
 delete-by-moving-to-trash t                      ; Delete files to trash
 window-combination-resize t                      ; take new window space from all other windows (not just current)
 x-stretch-cursor t)                              ; Stretch cursor to the glyph width
 
(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 lose work, I certainly don't
      show-paren-mode t                           ; Show the matching parenthesis - something is disabling to being explicit
      truncate-string-ellipsis ""                ; Unicode ellispis are nicer than "...", and also save /precious/ space
      password-cache-expiry nil                   ; I can trust my computers ... can't I?
      ;; scroll-preserve-screen-position 'always     ; Don't have `point' jump around
      scroll-margin 2)                            ; It's nice to maintain a little margin
 
(display-time-mode 1)                             ; Enable time in the mode-line
 
(global-subword-mode 1)                           ; Iterate through CamelCase words
 
(global-set-key
        [remap dabbrev-expand] 'hippie-expand)    ; Replace dabbrev with hippie-expand

Frame sizing

It’s nice to control the size of new frames, when launching Emacs that can be done with emacs -geometry 160x48. After the font size adjustment during initialization this works out to be 102x31.

Thanks to hotkeys, it’s easy for me to expand a frame to half/full-screen, so it makes sense to be conservative with the sizing of new frames.

Then, for creating new frames within the same Emacs instance, we’ll just set the default to be something roughly 80% of that size.

(add-to-list 'default-frame-alist '(height . 24))
(add-to-list 'default-frame-alist '(width . 80))

Auto-customization’s

By default changes made via a customization interface are added to init.el. I prefer the idea of using a separate file for this. We just need to change a setting, and load it if it exists.

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

Windows

I find it rather handy to be asked which buffer I want to see after splitting the window. Let’s make that happen.

First, we’ll enter the new window

(setq evil-vsplit-window-right t
      evil-split-window-below t)

Then, we’ll pull up a buffer prompt.

(defadvice! prompt-for-buffer (&rest _)
  :after '(evil-window-split evil-window-vsplit)
  (consult-buffer))

Window rotation is nice, and can be found under SPC w r and SPC w R. Layout rotation is also nice though. Let’s stash this under SPC w SPC, inspired by Tmux’s use of C-b SPC to rotate windows.

We could also do with adding the missing arrow-key variants of the window navigation/swapping commands.

(map! :map evil-window-map
      "SPC" #'rotate-layout
      ;; Navigation
      "<left>"     #'evil-window-left
      "<down>"     #'evil-window-down
      "<up>"       #'evil-window-up
      "<right>"    #'evil-window-right
      ;; Swapping windows
      "C-<left>"       #'+evil/window-move-left
      "C-<down>"       #'+evil/window-move-down
      "C-<up>"         #'+evil/window-move-up
      "C-<right>"      #'+evil/window-move-right)

Buffer defaults

I’d much rather have my new buffers in org-mode~ than fundamental-mode, hence

;; (setq-default major-mode 'org-mode)

For some reason this + the mixed pitch hook causes issues with hydra and so I’ll just need to resort to SPC b o for now.

Doom configuration

Modules

Doom has this lovely modular configuration base that takes a lot of work out of configuring Emacs. Each module (when enabled) can provide a list of packages to install (on doom sync) and configuration to be applied. The modules can also have flags applied to tweak their behavior.

;;; init.el -*- lexical-binding: t; -*-
 
;; This file controls what Doom modules are enabled and what order they load in.
;; Press 'K' on a module to view its documentation, and 'gd' to browse its directory.
 
(doom! :completion
       <<doom-completion>>
 
       :ui
       <<doom-ui>>
 
       :editor
       <<doom-editor>>
 
       :emacs
       <<doom-emacs>>
 
       :term
       <<doom-term>>
 
       :checkers
       <<doom-checkers>>
 
       :tools
       <<doom-tools>>
 
       :os
       <<doom-os>>
 
       :lang
       <<doom-lang>>
 
       :email
       <<doom-email>>
 
       :app
       <<doom-app>>
 
       :config
       <<doom-config>>
       )
Structure

As you may have noticed by this point, this is a literate configuration. Doom has good support for this which we access though the literate module.

While we’re in the :config section, we’ll use Dooms nicer defaults, along with the bindings and smart-parens behavior (the flags aren’t documented, but they exist).

literate
(default +bindings +smartparens)
Interface

There’s a lot that can be done to enhance Emacs’ capabilities. I reckon enabling half the modules Doom provides should do it.

(company                     ; the ultimate code completion backend
 +childframe)                ; ... when your children are better than you
;;helm                       ; the *other* search engine for love and life
;;ido                        ; the other *other* search engine...
;; (ivy                      ; a search engine for love and life
;;  +icons                   ; ... icons are nice
;;  +prescient)              ; ... I know what I want(ed)
(vertico +icons)             ; the search engine of the future

deft                         ; notational velocity for Emacs
doom                         ; what makes DOOM look the way it does
doom-dashboard               ; a nifty splash screen for Emacs
doom-quit                    ; DOOM quit-message prompts when you quit Emacs
(emoji +unicode)             ; 🙂
hl-todo                      ; highlight TODO/FIXME/NOTE/DEPRECATED/HACK/REVIEW
;;hydra                      ; quick documentation for related commands
;;indent-guides              ; highlighted indent columns, notoriously slow
(ligatures +extra)           ; ligatures and symbols to make your code pretty again
;;minimap                    ; show a map of the code on the side
modeline                     ; snazzy, Atom-inspired modeline, plus API
nav-flash                    ; blink the current line after jumping
;;neotree                    ; a project drawer, like NERDTree for vim
ophints                      ; highlight the region an operation acts on
(popup                       ; tame sudden yet inevitable temporary windows
 +all                        ; catch all popups that start with an asterix
 +defaults)                  ; default popup rules
;;(tabs                      ; an tab bar for Emacs
;;  +centaur-tabs)           ; ... with prettier tabs
treemacs                     ; a project drawer, like neotree but cooler
;;unicode                    ; extended unicode support for various languages
vc-gutter                    ; vcs diff in the fringe
vi-tilde-fringe              ; fringe tildes to mark beyond EOB
(window-select +numbers)     ; visually switch windows
workspaces                   ; tab emulation, persistence & separate workspaces
zen                          ; distraction-free coding or writing

(evil +everywhere)           ; come to the dark side, we have cookies
file-templates               ; auto-snippets for empty files
fold                         ; (nigh) universal code folding
(format)                     ; automated prettiness
;;god                        ; run Emacs commands without modifier keys
;;lispy                      ; vim for lisp, for people who don't like vim
multiple-cursors             ; editing in many places at once
;;objed                      ; text object editing for the innocent
(parinfer +rust)             ; turn lisp into python, sort of
rotate-text                  ; cycle region at point between text candidates
snippets                     ; my elves. They type so I don't have to
word-wrap                  ; soft wrapping with language-aware indent

(dired +icons)               ; making dired pretty [functional]
electric                     ; smarter, keyword-based electric-indent
(ibuffer +icons)             ; interactive buffer management
undo                         ; persistent, smarter undo for your inevitable mistakes
vc                           ; version-control and Emacs, sitting in a tree

;;eshell                     ; the elisp shell that works everywhere
;;shell                      ; simple shell REPL for Emacs
;;term                       ; basic terminal emulator for Emacs
vterm                        ; the best terminal emulation in Emacs

syntax                       ; tasing you for every semicolon you forget
spell                        ; tasing you for misspelling mispelling
grammar                      ; tasing grammar mistake every you make

ansible                      ; a crucible for infrastructure as code
biblio
;;debugger                   ; FIXME stepping through code, to help you add bugs
;;direnv                     ; be direct about your environment
docker                       ; port everything to containers
;;editorconfig               ; let someone else argue about tabs vs spaces
;;ein                        ; tame Jupyter notebooks with emacs
(eval +overlay)              ; run code, run (also, repls)
;;gist                       ; interacting with github gists
(lookup                      ; helps you navigate your code and documentation
 +dictionary                 ; dictionary/thesaurus is nice
 +docsets)                   ; ...or in Dash docsets locally
(lsp                          ; Language Server Protocol
 +peek)
;;macos                      ; MacOS-specific commands
(magit                       ; a git porcelain for Emacs
 +forge)                     ; interface with git forges
make                         ; run make tasks from Emacs
;;pass                       ; password manager for nerds
pdf                          ; pdf enhancements
;;prodigy                    ; FIXME managing external services & code builders
;;rgb                        ; creating color strings
;;taskrunner                 ; taskrunner for all your projects
;;terraform                  ; infrastructure as code
;;tmux                       ; an API for interacting with tmux
tree-sitter                  ; syntax and parsing, sitting in a tree...
upload                       ; map local to remote projects via ssh/ftp

tty                          ; improve the terminal Emacs experience
Language support

We can be rather liberal with enabling support for languages as the associated packages/configuration are (usually) only loaded when first opening an associated file.

;;agda                       ; types of types of types of types...
;;beancount                  ; mind the GAAP
;;cc                         ; C/C++/Obj-C madness
;;clojure                    ; java with a lisp
;;common-lisp                ; if you've seen one lisp, you've seen them all
;;coq                        ; proofs-as-programs
;;crystal                    ; ruby at the speed of c
;;csharp                     ; unity, .NET, and mono shenanigans
data                         ; config/data formats
;;(dart +flutter)            ; paint ui and not much else
dhall                        ; JSON with FP sprinkles
;;elixir                     ; erlang done right
;;elm                        ; care for a cup of TEA?
emacs-lisp                   ; drown in parentheses
;;erlang                     ; an elegant language for a more civilized age
ess                          ; emacs speaks statistics
;;faust                      ; dsp, but you get to keep your soul
;;fsharp                     ; ML stands for Microsoft's Language
;;fstar                      ; (dependent) types and (monadic) effects and Z3
;;gdscript                   ; the language you waited for
;;(go +lsp)                  ; the hipster dialect
;;(haskell +lsp)             ; a language that's lazier than I am
hy                           ; readability of scheme w/ speed of python
;;idris                      ;
json                         ; At least it ain't XML
;;(java +meghanada)          ; the poster child for carpal tunnel syndrome
(javascript +lsp)            ; all(hope(abandon(ye(who(enter(here))))))
(julia +lsp)                 ; Python, R, and MATLAB in a blender
;;kotlin                     ; a better, slicker Java(Script)
(latex                       ; writing papers in Emacs has never been so fun
 +latexmk                    ; what else would you use?
 +cdlatex                    ; quick maths symbols
 +fold)                      ; fold the clutter away nicities
;;lean                       ; proof that mathematicians need help
;;factor                     ; for when scripts are stacked against you
;;ledger                     ; an accounting system in Emacs
lua                          ; one-based indices? one-based indices
markdown                     ; writing docs for people to ignore
;;nim                        ; python + lisp at the speed of c
;;nix                        ; I hereby declare "nix geht mehr!"
;;ocaml                      ; an objective camel
(org                         ; organize your plain life in plain text
 +dragndrop                  ; drag & drop files/images into org buffers
 +hugo                       ; use Emacs for hugo blogging
 +noter                      ; enhanced PDF notetaking
 +journal                    ; unleash your thoughts and dreams
 +jupyter                    ; ipython/jupyter support for babel
 +pandoc                     ; export-with-pandoc support
 +gnuplot                    ; who doesn't like pretty pictures
 ;;+pomodoro                 ; be fruitful with the tomato technique
 +present                    ; using org-mode for presentations
 +roam2)                     ; wander around notes
;;php                        ; perl's insecure younger brother
;;plantuml                   ; diagrams for confusing people more
;;purescript                 ; javascript, but functional
(python                      ; beautiful is better than ugly
 +lsp
 +pyright)
;;qt                         ; the 'cutest' gui framework ever
;;racket                     ; a DSL for DSLs
;;raku                       ; the artist formerly known as perl6
rest                         ; Emacs as a REST client
;;rst                        ; ReST in peace
;;(ruby +rails)              ; 1.step {|i| p "Ruby is #{i.even? ? 'love' : 'life'}"}
(rust +lsp)                  ; Fe2O3.unwrap().unwrap().unwrap().unwrap()
;;scala                      ; java, but good
scheme                       ; a fully conniving family of lisps
sh                           ; she sells {ba,z,fi}sh shells on the C xor
;;sml                        ; no, the /other/ ML
;;solidity                   ; do you need a blockchain? No.
;;swift                      ; who asked for emoji variables?
;;terra                      ; Earth and Moon in alignment for performance.
web                          ; the tubes
yaml                         ; JSON, but readable
zig                          ; C, but simpler
Everything in Emacs

It’s just too convenient being able to have everything in Emacs. I couldn’t resist the Email and Feed modules.

(:if (executable-find "mu") (mu4e +org +gmail))
;;notmuch
;;(wanderlust +gmail)

calendar                     ; A dated approach to timetabling
emms                         ; Multimedia in Emacs is music to my ears
everywhere                   ; *leave* Emacs!? You must be joking.
;; irc                       ; how neckbeards socialize
(rss +org)                   ; emacs as an RSS reader
;;twitter                    ; twitter client https://twitter.com/vnought

Visual Settings

Font Face

‘Fira Code’ is nice, and ‘Overpass’ makes for a nice sans companion. We just need to fiddle with the font sizes a tad so that they visually match. 🕵🏽

(setq doom-font (font-spec :family "FiraCode Nerd Font Mono" :size 16)
      doom-big-font (font-spec :family "FiraCode Nerd Font Mono" :size 20)
      doom-variable-pitch-font (font-spec :family "Overpass" :size 16)
      doom-unicode-font (font-spec :family "JuliaMono")
      doom-serif-font (font-spec :family "IBM Plex Mono" :weight 'light))

In addition to these fonts, Merriweather is used with nov.el, and Alegreya as a serif-ed proportional font used by mixed-pitch-mode for writeroom-mode with Org files.

Because we care about how things look let’s add a check to make sure we’re told if the system doesn’t have any of those fonts.

(defvar required-fonts '("FiraCode Nerd Font" "Overpass" "JuliaMono" "IBM Plex Mono" "Merriweather" "Alegreya"))
 
(defvar available-fonts
  (delete-dups (or (font-family-list)
                   (split-string (shell-command-to-string "fc-list : family")
                                 "[,\n]"))))
 
(defvar missing-fonts
  (delq nil (mapcar
             (lambda (font)
               (unless (delq nil (mapcar (lambda (f)
                                           (string-match-p (format "^%s$" font) f))
                                         available-fonts))
                 font))
             required-fonts)))
 
(if missing-fonts
    (pp-to-string
     `(unless noninteractive
        (add-hook! 'doom-init-ui-hook
          (run-at-time nil nil
                       (lambda ()
                         (message "%s missing the following fonts: %s"
                                  (propertize "Warning!" 'face '(bold warning))
                                  (mapconcat (lambda (font)
                                               (propertize font 'face 'font-lock-variable-name-face))
                                             ',missing-fonts
                                             ", "))
                         (sleep-for 0.5))))))
  ";; No missing fonts detected")
<<detect-missing-fonts()>>

This way whenever fonts are missing, after Doom’s UI has initialized, a warning listing the missing fonts should appear for at least half a second.

Theme and modeline
(setq doom-theme 'catppuccin)

However, by default red text is used in the modeline, so let’s make that orange so I don’t feel like something’s gone wrong when editing files.

(custom-set-faces!
  '(doom-modeline-buffer-modified :foreground "orange"))

While we’re modifying the modeline, LF UTF-8 is the default file encoding, and thus not worth noting in the modeline. So, let’s conditionally hide it.

(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 (and (memq (plist-get (coding-system-plist buffer-file-coding-system) :category)
                                 '(coding-category-undecided coding-category-utf-8))
                           (not (memq (coding-system-eol-type buffer-file-coding-system) '(1 2))))
                t)))
 
(add-hook 'after-change-major-mode-hook #'doom-modeline-conditional-buffer-encoding)
Miscellaneous

Relative line numbers are fantastic for knowing how far away line numbers are, then ESC 12 <UP> gets you exactly where you think.

(setq display-line-numbers-type 'relative)

I’d like some slightly nicer default buffer names

(setq doom-fallback-buffer-name "Doom Emacs"
      +doom-dashboard-name "Doom Emacs Dashboard")

Some helper macros

There are a few handy macros added by doom, namely

  • load! for loading external .el files relative to this one
  • use-package! for configuring packages
  • add-load-path! for adding directories to the load-path where Emacs looks when you load packages with require or use-package
  • map! for binding new keys

Allow babel execution in CLI actions

In this config I sometimes generate code to include in my config. This works nicely, but for it to work with doom sync et. al. I need to make sure that Org doesn’t try to confirm that I want to allow evaluation (I do!).

Thankfully Doom supports $DOOMDIR/cli.el file which is sourced every time a CLI command is run, so we can just enable evaluation by setting org-confirm-babel-evaluate to nil there. While we’re at it, we should silence org-babel-execute-src-block to avoid polluting the output.

;;; cli.el -*- lexical-binding: t; -*-
(setq org-confirm-babel-evaluate nil)
 
(defun doom-shut-up-a (orig-fn &rest args)
  (quiet! (apply orig-fn args)))
 
(advice-add 'org-babel-execute-src-block :around #'doom-shut-up-a)

Elisp REPL

I think an elisp REPL sounds like a fun idea, even if not a particularly useful one 😛. We can do this by adding a new command in cli.el.

(defcli! repl ((in-rlwrap-p ["--rl"] "For internal use only."))
  "Start an elisp REPL."
  (when (and (executable-find "rlwrap") (not in-rlwrap-p))
    ;; For autocomplete
    (setq autocomplete-file "/tmp/doom_elisp_repl_symbols")
    (unless (file-exists-p autocomplete-file)
      (princ "\e[0;33mInitialising autocomplete list...\e[0m\n")
      (with-temp-buffer
        (cl-do-all-symbols (s)
          (let ((sym (symbol-name s)))
            (when (string-match-p "\\`[[:ascii:]][[:ascii:]]+\\'" sym)
              (insert sym "\n"))))
        (write-region nil nil autocomplete-file)))
    (princ "\e[F")
    (throw 'exit (list "rlwrap" "-f" autocomplete-file
                       (concat doom-emacs-dir "bin/doom") "repl" "--rl")))
 
  (doom-initialize-packages)
  (require 'engrave-faces-ansi)
  (setq engrave-faces-ansi-color-mode '3-bit)
 
  ;; For some reason (require 'parent-mode) doesn't work :(
  (defun parent-mode-list (mode)
    "Return a list of MODE and all its parent modes.
 
The returned list starts with the parent-most mode and ends with MODE."
    (let ((result ()))
      (parent-mode--worker mode (lambda (mode)
                                  (push mode result)))
      result))
  (defun parent-mode--worker (mode func)
    "For MODE and all its parent modes, call FUNC.
 
FUNC is first called for MODE, then for its parent, then for the parent's
parent, and so on.
 
MODE shall be a symbol referring to a function.
FUNC shall be a function taking one argument."
    (funcall func mode)
    (when (not (fboundp mode))
      (signal 'void-function (list mode)))
    (let ((modefunc (symbol-function mode)))
      (if (symbolp modefunc)
          ;; Hande all the modes that use (defalias 'foo-parent-mode (stuff)) as
          ;; their parent
          (parent-mode--worker modefunc func)
        (let ((parentmode (get mode 'derived-mode-parent)))
          (when parentmode
            (parent-mode--worker parentmode func))))))
  (provide 'parent-mode)
  ;; Some extra highlighting (needs parent-mode)
  (require 'rainbow-delimiters)
  (require 'highlight-quoted)
  (require 'highlight-numbers)
  (setq emacs-lisp-mode-hook '(rainbow-delimiters-mode
                               highlight-quoted-mode
                               highlight-numbers-mode))
  ;; Pretty print
  (defun pp-sexp (sexp)
    (with-temp-buffer
      (cl-prettyprint sexp)
      (emacs-lisp-mode)
      (font-lock-ensure)
      (with-current-buffer (engrave-faces-ansi-buffer)
        (princ (string-trim (buffer-string)))
        (kill-buffer (current-buffer)))))
  ;; Now do the REPL
  (defvar accumulated-input nil)
  (while t
    (condition-case nil
        (let ((input (if accumulated-input
                         (read-string "\e[31m .\e[0m  ")
                       (read-string "\e[31mλ:\e[0m "))))
          (setq input (concat accumulated-input
                              (when accumulated-input "\n")
                              input))
          (cond
           ((string-match-p "\\`[[:space:]]*\\'" input)
            nil)
           ((string= input "exit")
            (princ "\n") (kill-emacs 0))
           (t
            (condition-case err
                (let ((input-sexp (car (read-from-string input))))
                  (setq accumulated-input nil)
                  (pp-sexp (eval input-sexp))
                  (princ "\n"))
              ;; Caused when sexp in unbalanced
              (end-of-file (setq accumulated-input input))
              (error
               (cl-destructuring-bind (backtrace &optional type data . _)
                   (cons (doom-cli--backtrace) err)
                 (princ (concat "\e[1;31mERROR:\e[0m " (get type 'error-message)))
                 (princ "\n       ")
                 (pp-sexp (cons type data))
                 (when backtrace
                   (print! (bold "Backtrace:"))
                   (print-group!
                    (dolist (frame (seq-take backtrace 10))
                      (print!
                       "%0.74s" (replace-regexp-in-string
                                 "[\n\r]" "\\\\n"
                                 (format "%S" frame))))))
                 (princ "\n")))))))
      ;; C-d causes an end-of-file error
      (end-of-file (princ "exit\n") (kill-emacs 0)))
    (unless accumulated-input (princ "\n"))))

Asynchronous config tangling

Doom adds an org-mode hook +literate-enable-recompile-h. This is a nice idea, but it’s too blocking for my taste. Since I trust my tangling to be fairly straightforward, I’ll just redefine it to a simpler, async, function.

(defvar +literate-tangle--proc nil)
(defvar +literate-tangle--proc-start-time nil)
 
(defadvice! +literate-tangle-async-h ()
  "A very simplified version of `+literate-tangle-h', but async."
  :override #'+literate-tangle-h
  (unless (getenv "__NOTANGLE")
    (let ((default-directory doom-private-dir))
      (when +literate-tangle--proc
        (message "Killing outdated tangle process...")
        (set-process-sentinel +literate-tangle--proc #'ignore)
        (kill-process +literate-tangle--proc)
        (sit-for 0.3)) ; ensure the message is seen for a bit
      (setq +literate-tangle--proc-start-time (float-time)
            +literate-tangle--proc
            (start-process "tangle-config"
                           (get-buffer-create " *tangle config*")
                           "emacs" "--batch" "--eval"
                           (format "(progn \
(require 'ox) \
(require 'ob-tangle) \
(setq org-confirm-babel-evaluate nil \
      org-inhibit-startup t \
      org-mode-hook nil \
      write-file-functions nil \
      before-save-hook nil \
      after-save-hook nil \
      vc-handled-backends nil \
      org-startup-folded nil \
      org-startup-indented nil) \
(org-babel-tangle-file \"%s\" \"%s\"))"
                                   +literate-config-file
                                   (expand-file-name (concat doom-module-config-file ".el")))))
      (set-process-sentinel +literate-tangle--proc #'+literate-tangle--sentinel)
      (run-at-time nil nil (lambda () (message "Tangling config.org"))) ; ensure shown after a save message
      "Tangling config.org...")))
 
(defun +literate-tangle--sentinel (process signal)
  (cond
   ((and (eq 'exit (process-status process))
         (= 0 (process-exit-status process)))
    (message "Tangled config.org sucessfully (took %.1fs)"
             (- (float-time) +literate-tangle--proc-start-time))
    (setq +literate-tangle--proc nil))
   ((memq (process-status process) (list 'exit 'signal))
    (pop-to-buffer (get-buffer " *tangle config*"))
    (message "Failed to tangle config.org (after %.1fs)"
             (- (float-time) +literate-tangle--proc-start-time))
    (setq +literate-tangle--proc nil))))
 
(defun +literate-tangle-check-finished ()
  (when (and (process-live-p +literate-tangle--proc)
             (yes-or-no-p "Config is currently retangling, would you please wait a few seconds?"))
    (switch-to-buffer " *tangle config*")
    (signal 'quit nil)))
(add-hook! 'kill-emacs-hook #'+literate-tangle-check-finished)

Htmlize command

Why not have a command to htmlize files? This is basically a little test of my engrave-faces package because it somehow seems to work without a GUI, while the htmlize package doesn’t.

(defcli! htmlize (file)
  "Export a FILE buffer to HTML."
 
  (print! "Htmlizing %s" file)
 
  (doom-initialize)
  (require 'highlight-numbers)
  (require 'highlight-quoted)
  (require 'rainbow-delimiters)
  (require 'engrave-faces-html)
 
  ;; Lighten org-mode
  (when (string= "org" (file-name-extension file))
    (setcdr (assoc 'org after-load-alist) nil)
    (setq org-load-hook nil)
    (require 'org)
    (setq org-mode-hook nil)
    (add-hook 'engrave-faces-before-hook
              (lambda () (if (eq major-mode 'org-mode)
                        (org-show-all)))))
 
  (engrave-faces-html-file file))

Dashboard quick actions

When using the dashboard, there are often a small number of actions I will take. As the dashboard is it’s own major mode, there is no need to suffer the tyranny of unnecessary keystrokes --- we can simply bind common actions to a single key!

(defun +doom-dashboard-setup-modified-keymap ()
  (setq +doom-dashboard-mode-map (make-sparse-keymap))
  (map! :map +doom-dashboard-mode-map
        :desc "Find file" :ne "f" #'find-file
        :desc "Recent files" :ne "r" #'consult-recent-file
        :desc "Config dir" :ne "C" #'doom/open-private-config
        :desc "Open config.org" :ne "c" (cmd! (find-file (expand-file-name "config.org" doom-private-dir)))
        :desc "Open elfeed summary" :ne "E" #'elfeed-summary
        :desc "Open elfeed" :ne "e" #'elfeed
        :desc "Open dotfile" :ne "." (cmd! (doom-project-find-file "~/.config/"))
        :desc "Notes (roam)" :ne "n" #'org-roam-node-find
        :desc "Search (roam)" :ne "N" #'justin/org-roam-rg-search
        :desc "Switch buffer" :ne "b" #'+vertico/switch-workspace-buffer
        :desc "Switch buffers (all)" :ne "B" #'consult-buffer
        :desc "IBuffer" :ne "i" #'ibuffer
        :desc "Agenda"  :ne "o" #'org-agenda
        :desc "Previous buffer" :ne "p" #'previous-buffer
        :desc "Set theme" :ne "t" #'consult-theme
        :desc "Quit" :ne "Q" #'save-buffers-kill-terminal
        :desc "Show keybindings" :ne "h" (cmd! (which-key-show-keymap '+doom-dashboard-mode-map))))
 
(add-transient-hook! #'+doom-dashboard-mode (+doom-dashboard-setup-modified-keymap))
(add-transient-hook! #'+doom-dashboard-mode :append (+doom-dashboard-setup-modified-keymap))
(add-hook! 'doom-init-ui-hook :append (+doom-dashboard-setup-modified-keymap))

Unfortunately the show keybindings help doesn’t currently work as intended, but this is still quite nice overall.

Now that the dashboard is so convenient, I’ll want to make it easier to get to.

(map! :leader :desc "Dashboard" "d" #'+doom-dashboard/open)

Other things

Editor interaction

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

Window title

I’d like to have just the buffer name, then if applicable the project folder

(setq frame-title-format
      '(""
        (:eval
         (if (s-contains-p org-roam-directory (or buffer-file-name ""))
             (replace-regexp-in-string
              ".*/[0-9]*-?" ""
              (subst-char-in-string ?_ ?  buffer-file-name))
           "%b"))
        (:eval
         (let ((project-name (projectile-project-name)))
           (unless (string= "-" project-name)
             (format (if (buffer-modified-p)  " ◉ %s" " - %s") project-name))))))

For example when I open my config file it the window will be titled config.org ● doom then as soon as I make a change it will become config.org ◉ doom.

Splash Images

I could probably just replicate the code here in entirety, but, effort - this is basically meant to pick a different splash image on the dashboard.

(package! random-splash-image
          :recipe (:host github :repo "kakakaya/random-splash-image"))
(use-package random-splash-image
  :ensure t
  :config
  (setq random-splash-image-dir (concat (getenv "HOME") "/.doom.d/misc/splash-images"))
  (unless (file-directory-p random-splash-image-dir)
  (make-directory random-splash-image-dir t))
  (random-splash-image-set))

Lastly, the doom dashboard “useful commands” are no longer useful to me. So, we’ll disable them and then for a particularly clean look disable the modeline and hl-line-mode, then also hide the cursor.

(remove-hook '+doom-dashboard-functions #'doom-dashboard-widget-shortmenu)
(add-hook! '+doom-dashboard-mode-hook (hide-mode-line-mode 1) (hl-line-mode -1))
(setq-hook! '+doom-dashboard-mode-hook evil-normal-state-cursor (list nil))

At the end, we have a minimal but rather nice splash screen.

Systemd daemon

For running a systemd service for a Emacs server I have the following

[Unit]
Description=Emacs server daemon
Documentation=info:emacs man:emacs(1) https://gnu.org/software/emacs/
 
[Service]
Type=forking
ExecStart=sh -c 'emacs --daemon && emacsclient -c --eval "(delete-frame)"'
ExecStop=/usr/bin/emacsclient --no-wait --eval "(progn (setq kill-emacs-hook nil) (kill emacs))"
Restart=on-failure
 
[Install]
WantedBy=default.target

which is then enabled by

systemctl --user enable emacs.service

For some reason if a frame isn’t opened early in the initialization process, the daemon doesn’t seem to like opening frames later --- hence the && emacsclient part of the ExecStart value.

It can now be nice to use this as a ‘default app’ for opening files. If we add an appropriate desktop entry, and enable it in the desktop environment.

[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="" --no-wait %F
Icon=emacs
Type=Application
Terminal=false
Categories=TextEditor;Utility;
StartupWMClass=Emacs
Keywords=Text;Editor;
X-KDE-StartupNotify=false

When the daemon is running, I almost always want to do a few particular things with it, so I may as well eat the load time at startup. We also want to keep mu4e running.

It would be good to start the IRC client (circe) too, but that seems to have issues when started in a non-graphical session.

Lastly, while I’m not sure quite why it happens, but after a bit it seems that new Emacsclient frames start on the *scratch* buffer instead of the dashboard. I prefer the dashboard, so let’s ensure that’s always switched to in new frames.

(defun greedily-do-daemon-setup ()
  (require 'org)
  (when (require 'mu4e nil t)
    (setq mu4e-confirm-quit t)
    (setq +mu4e-lock-greedy t)
    (setq +mu4e-lock-relaxed t)
    (+mu4e-lock-add-watcher)
    (when (+mu4e-lock-available t)
      (mu4e~start)))
  (when (require 'elfeed nil t)
    (run-at-time nil (* 8 60 60) #'elfeed-update)))
 
(when (daemonp)
  (add-hook 'emacs-startup-hook #'greedily-do-daemon-setup)
  (add-hook! 'server-after-make-frame-hook
    (unless (string-match-p "\\*draft\\|\\*stdin\\|emacs-everywhere" (buffer-name))
      (switch-to-buffer +doom-dashboard-name))))

Emacs client wrapper

I frequently want to make use of Emacs while in a terminal emulator. To make this easier, I can construct a few handy aliases.

However, a little convenience script in ~/.local/bin can have the same effect, be available beyond the specific shell I plop the alias in, then also allow me to add a few bells and whistles --- namely:

  • Accepting stdin by putting it in a temporary file and immediately opening it.
  • Guessing that the tty is a good idea when $DISPLAY is unset (relevant with SSH sessions, among other things).
  • With a whiff of 24-bit color support, sets TERM variable to a terminfo that (probably) announces 24-bit color support.
  • Changes GUI emacsclient instances to be non-blocking by default (--no-wait), and instead take a flag to suppress this behavior (-w).

I would use sh, but using arrays for argument manipulation is just too convenient, so I’ll raise the requirement to bash. Since arrays are the only ‘extra’ compared to sh, other shells like ksh etc. should work too.

#!/usr/bin/env bash
force_tty=false
force_wait=false
stdin_mode=""
 
args=()
 
while :; do
    case "$1" in
        -t | -nw | --tty)
            force_tty=true
            shift ;;
        -w | --wait)
            force_wait=true
            shift ;;
        -m | --mode)
            stdin_mode=" ($2-mode)"
            shift 2 ;;
        -h | --help)
            echo -e "\033[1mUsage: e [-t] [-m MODE] [OPTIONS] FILE [-]\033[0m
 
Emacs client convenience wrapper.
 
 
\033[1mOptions:\033[0m
\033[0;34m-h, --help\033[0m            Show this message
\033[0;34m-t, -nw, --tty\033[0m        Force terminal mode
\033[0;34m-w, --wait\033[0m            Don't supply \033[0;34m--no-wait\033[0m to graphical emacsclient
\033[0;34m-\033[0m                     Take \033[0;33mstdin\033[0m (when last argument)
\033[0;34m-m MODE, --mode MODE\033[0m  Mode to open \033[0;33mstdin\033[0m with
 
Run \033[0;32memacsclient --help\033[0m to see help for the emacsclient."
            exit 0 ;;
        --*=*)
            set -- "$@" "${1-*}"; else
            termstub="${TERM#*-}"; fi
        if infocmp "$termstub-direct" >/dev/null 2>&1; then
            TERM="$termstub-direct"; else
            TERM="xterm-direct"; fi # should be fairly safe
    fi
    emacsclient --tty -create-frame --alternate-editor="$ALTERNATE_EDITOR" "${args[@]}"
else
    if ! $force_wait; then
        args+=(--no-wait); fi
    emacsclient -create-frame --alternate-editor="$ALTERNATE_EDITOR" "${args[@]}"
fi

Now, to set an alias to use e with magit, and then for maximum laziness we can set aliases for the terminal-forced variants.

alias m='e --eval "(progn (magit-status) (delete-other-windows))"'
alias mt="m -t"
alias et="e -t"

Packages

Loading Instructions

This is where you install packages, by declaring them with the package! macro in packages.el, then running doom refresh on the command line. This file shouldn’t be byte compiled.

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

You’ll then 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.

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

Convenience

Avy

From the :config default module.

What a wonderful way to jump to buffer positions, and it uses the QWERTY home-row for jumping.

Casual

The casual series of packages are transient based porcelains for several popular Emacs functionalities that might be difficult to use.

Casual Dired
(package! casual-dired)
(use-package casual-dired
  :ensure t
  :config
  (map! :after dired
        :map dired-mode-map
        :leader
        :desc "Casual Dired Tmenu" "m T" #'casual-dired-tmenu
        :desc "Casual Dired Sort By Tmenu" "m s" #'casual-dired-sort-by-tmenu))
Casual Avy
(package! casual-avy)
(use-package casual-avy
  :ensure t
  :bind ("M-g" . casual-avy-tmenu))

Rotate (window management)

The rotate package just adds the ability to rotate window layouts, but that sounds nice to me.

(package! rotate :pin "4e9ac3ff800880bd9b705794ef0f7c99d72900a6")

Which-key

From the :core packages module.

Let’s make this popup a bit faster

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

I also think that having evil- appear in so many popups is a bit too verbose, let’s change that, and do a few other similar tweaks while we’re at it.

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

Tools

Abbrev

Thanks to use a single abbrev-table for multiple modes? - Emacs Stack Exchange I have the following.

(add-hook 'doom-first-buffer-hook
          (defun +abbrev-file-name ()
            (setq-default abbrev-mode t)
            (setq abbrev-file-name (expand-file-name "abbrev.el" doom-private-dir))))

Python Package Management

This lets me switch conda environments within python files. Since I primarily use them as a data scientist, I figure, can’t hurt.

(package! conda)
(package! pet)
(use-package! conda
  :init
  (setq conda-anaconda-home "/opt/mambaforge")
  (setq conda-env-home-directory "/opt/mambaforge/")
  (setq conda-env-executable-path "/opt/mambaforge/condabin/") ;; point directly to the condabin directory
  :config
  (conda-env-initialize-interactive-shells)
  (conda-env-initialize-eshell)
  (conda-env-autoactivate-mode t))
 
(add-to-list 'exec-path "/opt/mambaforge/condabin/")
(setenv "PATH" (concat "/opt/mambaforge/condabin/:" (getenv "PATH")))
(use-package! pet
  :config
  (add-hook 'python-base-mode-hook 'pet-mode -10))

Deft

This might require some config, but I’m not entirely sure how useful this is considering node-find for roam works. Overlapping / maybe use for org in general.

(use-package deft
:commands (deft)
:config
  (setq deft-directory "~/.org/")
  (setq deft-recursive t)
  (setq deft-strip-summary-regexp
  (concat "$$"
	  "^:.+:.*" ; any line with a :SOMETHING:
	  "\\|^#\\+.*\n" ; anyline starting with a #+
	  "\\|^\\*.+.*\n" ; anyline where an asterisk starts the line
	  "$$"))
     )
 
(advice-add 'deft-parse-title :override
    (lambda (file contents)
      (if deft-use-filename-as-title
	  (deft-base-filename file)
	(let* ((case-fold-search 't)
	       (begin (string-match "title: " contents))
	       (end-of-begin (match-end 0))
	       (end (string-match "\n" contents begin)))
	  (if begin
	      (substring contents end-of-begin end)
	    (format "%s" file))))))
 

Eros

From the :tools eval module.

This package enables the very nice inline evaluation with gr and gR. The prefix could be slightly nicer though.

(setq eros-eval-result-prefix "") ; default =>

EVIL

From the :editor evil module.

When I want to make a substitution, I want it to be global more often than not --- so let’s make that the default.

Now, EVIL cares a fair bit about keeping compatibility with Vim’s default behavior. I don’t. There are some particular settings that I’d rather be something else, so let’s change them.

(after! evil
  (setq evil-ex-substitute-global t     ; I like my s/../.. to by global by default
        evil-move-cursor-back nil       ; Don't move the block cursor when toggling insert mode
        evil-kill-on-visual-paste nil)) ; Don't put overwritten text in the kill ring

Sets the key-sequence “jk” for escape, allowing a quick hit of these to allow escaping insert mode. I might revisit this option, since technically anytime I want to say “lol jk” to someone, it would escape, but then I realized: “When the heck am I going to use that in emacs text”, so it seems fine.

; (package! evil-escape :disable t/) ; Disabling the package if needed
(setq-default evil-escape-key-sequence "jk")

Consult

From the :completion vertico module.

Since we’re using Marginalia too, the separation between buffers and files is already clear, and there’s no need for a different face.

(after! consult
  (set-face-attribute 'consult-file nil :inherit 'consult-buffer)
  (setf (plist-get (alist-get 'perl consult-async-split-styles-alist) :initial) ";"))

Large Language Models (LLM)

Large language model thingys! Can’t decide which I prefer.

GPTel

GitHub - karthink/gptel: A simple LLM client for Emacs - This is made by the same person who did the youtube-elfeed package, which is pretty neat. A lot of the power is in using gptel-request.

(package! gptel)
(package! gptel-quick :recipe (:host github :repo  "karthink/gptel-quick"))
(use-package! gptel
  :config
  (setq gptel-model "llama3:latest"
        gptel-org-branching-context t)
 
  (setq gptel-backend
        (gptel-make-ollama "Ollama"
                           :host "192.168.1.9:11434"
                           :stream t
                           :models '("llama3:latest"))))
 
(use-package! gptel-quick
  :after gptel
  :config
  ;; Add any gptel-quick specific configuration here
  )
 
;; Key bindings
(map! :leader
      (:prefix ("l" . "LLM")
       :desc "Quick GPTel" "q" #'gptel-quick
       :desc "Start GPTel" "s" #'gptel
       :desc "Send to GPTel" "S" #'gptel-send))
Ellama

Another LLM package using llm under the hood. Alows for switching LLMs more easily, I think. I’m keeping it around just to compare and contrast.

(package! ellama)
(setq my-api-key (getenv "OPENAI_API_KEY"))
 
(use-package ellama
  :init
  ;; setup key bindings
  (setopt ellama-keymap-prefix "C-c e")
  ;; language you want ellama to translate to
  (setopt ellama-language "Chinese")
  ;; could be llm-openai for example
  (require 'llm-ollama)
  (setopt ellama-provider
	  (make-llm-ollama
	   :chat-model "llama3:latest"
           :host "192.168.1.9"
           :port 11434
	   :embedding-model "nomic-embed-text"
	   :default-chat-non-standard-params '(("num_ctx" . 8192))))
  (setopt ellama-providers
	  '(("openai" . (make-llm-openai
                         :key my-api-key
	                 :chat-model "gpt-4o-mini"
	                 :embedding-model "nomic-embed-text")))))

Magit

From the :tools magit module.

Magit is great as-is, thanks for making such a lovely package Jonas!

Commit message templates

One little thing I want to add is some per-project commit message templates.

(defvar +magit-project-commit-templates-alist nil
  "Alist of toplevel dirs and template strings/functions.")
(after! magit
  (defun +magit-fill-in-commit-template ()
    "Insert template from `+magit-fill-in-commit-template' if applicable."
    (when-let ((template (and (save-excursion (goto-char (point-min)) (string-match-p "\\`\\s-*$" (thing-at-point 'line)))
                              (cdr (assoc (file-name-base (directory-file-name (magit-toplevel)))
                                          +magit-project-commit-templates-alist)))))
      (goto-char (point-min))
      (insert (if (stringp template) template (funcall template)))
      (goto-char (point-min))
      (end-of-line)))
  (add-hook 'git-commit-setup-hook #'+magit-fill-in-commit-template 90))

This is particularly useful when creating commits for Org, as they need to follow a certain format and sometimes I forget elements (oops!).

(after! magit
  (defun +org-commit-message-template ()
    "Create a skeleton for an Org commit message based on the staged diff."
    (let (change-data last-file file-changes temp-point)
      (with-temp-buffer
        (apply #'call-process magit-git-executable
               nil t nil
               (append
                magit-git-global-arguments
                (list "diff" "--cached")))
        (goto-char (point-min))
        (while (re-search-forward "^@@\\|^\\+\\+\\+ b/" nil t)
          (if (looking-back "^\\+\\+\\+ b/" (line-beginning-position))
              (progn
                (push (list last-file file-changes) change-data)
                (setq last-file (buffer-substring-no-properties (point) (line-end-position))
                      file-changes nil))
            (setq temp-point (line-beginning-position))
            (re-search-forward "^\\+\\|^-" nil t)
            (end-of-line)
            (cond
             ((string-match-p "\\.el$" last-file)
              (when (re-search-backward "^$?:[+-]? *\\|@@[ +-\\d,]+@@ $($?:cl-$?$?:defun\\|defvar\\|defmacro\\|defcustom$" temp-point t)
                (re-search-forward "$?:cl-$?$?:defun\\|defvar\\|defmacro\\|defcustom$ " nil t)
                (add-to-list 'file-changes (buffer-substring-no-properties (point) (forward-symbol 1)))))
             ((string-match-p "\\.org$" last-file)
              (when (re-search-backward "^[+-]\\*+ \\|^@@[ +-\\d,]+@@ \\*+ " temp-point t)
                (re-search-forward "@@ \\*+ " nil t)
                (add-to-list 'file-changes (buffer-substring-no-properties (point) (line-end-position)))))))))
      (push (list last-file file-changes) change-data)
      (setq change-data (delete '(nil nil) change-data))
      (concat
       (if (= 1 (length change-data))
           (replace-regexp-in-string "^.*/\\|.[a-z]+$" "" (caar change-data))
         "?")
       ": \n\n"
       (mapconcat
        (lambda (file-changes)
          (if (cadr file-changes)
              (format "* %s (%s): "
                      (car file-changes)
                      (mapconcat #'identity (cadr file-changes) ", "))
            (format "* %s: " (car file-changes))))
        change-data
        "\n\n"))))
 
  (add-to-list '+magit-project-commit-templates-alist (cons "org-mode" #'+org-commit-message-template)))

This relies on two small entries in the git config files which improves the hunk heading line selection for elisp and Org files.

[diff "lisp"]
  xfuncname = "^(((;;;+ )|$$|([ \t]+\\(((cl-|el-patch-)?def(un|var|macro|method|custom)|gb/))).*)$"
 
[diff "org"]
  xfuncname = "^(\\*+ +.*)$"

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 (which don’t get any syntax highlighting by default). This requires the delta binary. It’s packaged on some distributions, but most reliably installed through Rust’s package manager cargo.

cargo install git-delta

Now we can make use of the package for this.

;; (package! magit-delta :recipe (:host github :repo "dandavison/magit-delta") :pin "56cdffd377279589aa0cb1df99455c098f1848cf")

All that’s left is to hook it into magit

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

Unfortunately this currently seems to mess things up, which is something I’ll want to look into later.

Smerge

For repeated operations, a hydra would be helpful. But I prefer transient.

(defun smerge-repeatedly ()
  "Perform smerge actions again and again"
  (interactive)
  (smerge-mode 1)
  (smerge-transient))
(after! transient
  (transient-define-prefix smerge-transient ()
    [["Move"
      ("n" "next" (lambda () (interactive) (ignore-errors (smerge-next)) (smerge-repeatedly)))
      ("p" "previous" (lambda () (interactive) (ignore-errors (smerge-prev)) (smerge-repeatedly)))]
     ["Keep"
      ("b" "base" (lambda () (interactive) (ignore-errors (smerge-keep-base)) (smerge-repeatedly)))
      ("u" "upper" (lambda () (interactive) (ignore-errors (smerge-keep-upper)) (smerge-repeatedly)))
      ("l" "lower" (lambda () (interactive) (ignore-errors (smerge-keep-lower)) (smerge-repeatedly)))
      ("a" "all" (lambda () (interactive) (ignore-errors (smerge-keep-all)) (smerge-repeatedly)))
      ("RET" "current" (lambda () (interactive) (ignore-errors (smerge-keep-current)) (smerge-repeatedly)))]
     ["Diff"
      ("<" "upper/base" (lambda () (interactive) (ignore-errors (smerge-diff-base-upper)) (smerge-repeatedly)))
      ("=" "upper/lower" (lambda () (interactive) (ignore-errors (smerge-diff-upper-lower)) (smerge-repeatedly)))
      (">" "base/lower" (lambda () (interactive) (ignore-errors (smerge-diff-base-lower)) (smerge-repeatedly)))
      ("R" "refine" (lambda () (interactive) (ignore-errors (smerge-refine)) (smerge-repeatedly)))
      ("E" "ediff" (lambda () (interactive) (ignore-errors (smerge-ediff)) (smerge-repeatedly)))]
     ["Other"
      ("c" "combine" (lambda () (interactive) (ignore-errors (smerge-combine-with-next)) (smerge-repeatedly)))
      ("r" "resolve" (lambda () (interactive) (ignore-errors (smerge-resolve)) (smerge-repeatedly)))
      ("k" "kill current" (lambda () (interactive) (ignore-errors (smerge-kill-current)) (smerge-repeatedly)))
      ("q" "quit" (lambda () (interactive) (smerge-auto-leave)))]]))

Company

From the :completion company module.

It’s nice to have completions almost all the time, in my opinion. Key strokes are just waiting to be saved!

(after! company
  (setq company-idle-delay 1.0
        company-minimum-prefix-length 2
        company-box-doc-delay 1.5
        company-tooltip-maximum-width 50)
  (setq company-show-numbers t)
  (add-hook 'evil-normal-state-entry-hook #'company-abort)) ;; make aborting less annoying.

Now, the improvements from precedent are mostly from remembering history, so let’s improve that memory.

(setq-default history-length 1000)
(setq-default prescient-history-length 1000)

Patch for fixing company-box’s window floating off the side.

(defun company-box-doc--make-buffer-fixed (object)
     (let* ((buffer-list-update-hook nil)
            (inhibit-modification-hooks t)
            (string (cond ((stringp object) object)
                          ((bufferp object) (with-current-buffer object (buffer-string))))))
       (when (and string (> (length (string-trim string)) 0))
         (with-current-buffer (company-box--get-buffer "doc")
           (erase-buffer)
           (insert string)
           (setq mode-line-format nil
                 display-line-numbers nil
                 header-line-format nil
                 show-trailing-whitespace nil
                 cursor-in-non-selected-windows nil)
 
           (toggle-truncate-lines -1) ;; PATCHED HERE
 
           (current-buffer)))))
 
(advice-add 'company-box-doc--make-buffer :override #'company-box-doc--make-buffer-fixed)
ESS

company-dabbrev-code is nice. Let’s have it.

(set-company-backend! 'ess-r-mode '(company-R-args company-R-objects company-dabbrev-code :separate))

Projectile

From the :core packages module.

Looking at documentation via SPC h f and SPC h v and looking at the source can add package src directories to projectile. This isn’t desirable in my opinion.

(setq projectile-project-search-path '("~/code/"))
(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)))

TRAMP

Another lovely Emacs feature, TRAMP stands for Transparent Remote Access, Multiple Protocol. In brief, it’s a lovely way to wander around outside your local filesystem.

Prompt recognition

Unfortunately, when connecting to remote machines Tramp can be a wee pit picky with the prompt format. Let’s try to get Bash, and be a bit more permissive with prompt recognition.

(after! tramp
  (setenv "SHELL" "/bin/bash")
  (setq tramp-shell-prompt-pattern "\\(?:^\\|
$$[^]#$%>\n]*#?[]#$%>] *$$$[0-9;]*[a-zA-Z] *$*")) ;; default + 
Troubleshooting

In case the remote shell is misbehaving, here are some things to try

Zsh

There are some escape code you don’t want, let’s make it behave more considerately.

if [[ "$TERM" == "dumb" ]]; then
    unset zle_bracketed_paste
    unset zle
    PS1='$ '
    return
fi
Guix

Guix puts some binaries that TRAMP looks for in unexpected locations. That’s no problem though, we just need to help TRAMP find them.

(after! tramp
  (appendq! tramp-remote-path
            '("~/.guix-profile/bin" "~/.guix-profile/sbin"
              "/run/current-system/profile/bin"
              "/run/current-system/profile/sbin")))

Auto activating snippets

Sometimes pressing TAB is just too much.

(package! aas :recipe (:host github :repo "ymarco/auto-activating-snippets")
  :pin "566944e3b336c29d3ac11cd739a954c9d112f3fb")
(use-package! aas
  :commands aas-mode)

Screenshot

This makes it a breeze to take lovely screenshots.

(package! screenshot :recipe (:local-repo "lisp/screenshot"))

Some light configuring is all we need, so we can make use of the 0x0 wrapper file uploading script (which I’ve renamed to upload).

(use-package! screenshot
  :defer t
  :config (setq screenshot-upload-fn "upload %s 2>/dev/null"))

YASnippet

From the :editor snippets module.

Nested snippets are good, so let’s enable that.

(setq yas-triggers-in-field t)

String inflection

For when you want to change the case pattern for a symbol.

(package! string-inflection :pin "fd7926ac17293e9124b31f706a4e8f38f6a9b855")
(use-package! string-inflection
  :commands (string-inflection-all-cycle
             string-inflection-toggle
             string-inflection-camelcase
             string-inflection-lower-camelcase
             string-inflection-kebab-case
             string-inflection-underscore
             string-inflection-capital-underscore
             string-inflection-upcase)
  :init
  (map! :leader :prefix ("c~" . "naming convention")
        :desc "cycle" "~" #'string-inflection-all-cycle
        :desc "toggle" "t" #'string-inflection-toggle
        :desc "CamelCase" "c" #'string-inflection-camelcase
        :desc "downCase" "d" #'string-inflection-lower-camelcase
        :desc "kebab-case" "k" #'string-inflection-kebab-case
        :desc "under_score" "_" #'string-inflection-underscore
        :desc "Upper_Score" "u" #'string-inflection-capital-underscore
        :desc "UP_CASE" "U" #'string-inflection-upcase)
  (after! evil
    (evil-define-operator evil-operator-string-inflection (beg end _type)
      "Define a new evil operator that cycles symbol casing."
      :move-point nil
      (interactive "<R>")
      (string-inflection-all-cycle)
      (setq evil-repeat-info '([?g ?~])))
    (define-key evil-normal-state-map (kbd "g~") 'evil-operator-string-inflection)))

Smart parentheses

From the :core packages module.

(sp-local-pair
 '(org-mode)
 "<<" ">>"
 :actions '(insert))

Visuals

Themes

Proteolas did a lovely job with the Modus themes, so much so that they were welcomed into Emacs 28. However, he is also rather attentive with updates, and so I’d like to make sure we have a recent version.

(package! catppuccin-theme)

Theme magic

With all our fancy Emacs themes, my terminal is missing out!

(package! theme-magic :pin "844c4311bd26ebafd4b6a1d72ddcc65d87f074e3")

This operates using pywal, which is present in some repositories, but most reliably installed with pip.

sudo python3 -m pip install pywal

Theme magic takes a look at a number of faces, the saturation levels, and colour differences to try to cleverly pick eight colours to use. However, it uses the same colours for the light variants, and doesn’t always make the best picks. Since we’re using doom-themes, our life is a little easier and we can use the colour utilities from Doom themes to easily grab sensible colours and generate lightened versions --- let’s do that.

(use-package! theme-magic
  :commands theme-magic-from-emacs
  :config
  (defadvice! theme-magic--auto-extract-16-doom-colors ()
    :override #'theme-magic--auto-extract-16-colors
    (list
     (face-attribute 'default :background)
     (doom-color 'error)
     (doom-color 'success)
     (doom-color 'type)
     (doom-color 'keywords)
     (doom-color 'constants)
     (doom-color 'functions)
     (face-attribute 'default :foreground)
     (face-attribute 'shadow :foreground)
     (doom-blend 'base8 'error 0.1)
     (doom-blend 'base8 'success 0.1)
     (doom-blend 'base8 'type 0.1)
     (doom-blend 'base8 'keywords 0.1)
     (doom-blend 'base8 'constants 0.1)
     (doom-blend 'base8 'functions 0.1)
     (face-attribute 'default :foreground))))

Emojify

From the :ui emoji module.

For starters, twitter’s emojis look nicer than emoji-one. Other than that, this is pretty great OOTB 😀.

(setq emojify-emoji-set "twemoji-v2")

One minor annoyance is the use of emojis over the default character when the default is actually preferred. This occurs with overlay symbols I use in Org mode, such as checkbox state, and a few other miscellaneous cases.

We can accommodate our preferences by deleting those entries from the emoji hash table

(defvar emojify-disabled-emojis
  '(;; Org
    "" "" "" "" "" "" "" "" ""
    ;; Terminal powerline
    ""
    ;; Box drawing
    "" ""
    ;; I just want to see this as text
    "©" "")
  "Characters that should never be affected by `emojify-mode'.")
 
(defadvice! emojify-delete-from-data ()
  "Ensure `emojify-disabled-emojis' don't appear in `emojify-emojis'."
  :after #'emojify-set-emoji-data
  (dolist (emoji emojify-disabled-emojis)
    (remhash emoji emojify-emojis)))

Now, it would be good to have a minor mode which allowed you to type ascii/gh emojis and get them converted to unicode. Let’s make one.

(defun emojify--replace-text-with-emoji (orig-fn emoji text buffer start end &optional target)
  "Modify `emojify--propertize-text-for-emoji' to replace ascii/github emoticons with unicode emojis, on the fly."
  (if (or (not emoticon-to-emoji) (= 1 (length text)))
      (funcall orig-fn emoji text buffer start end target)
    (delete-region start end)
    (insert (ht-get emoji "unicode"))))
 
(define-minor-mode emoticon-to-emoji
  "Write ascii/gh emojis, and have them converted to unicode live."
  :global nil
  :init-value nil
  (if emoticon-to-emoji
      (progn
        (setq-local emojify-emoji-styles '(ascii github unicode))
        (advice-add 'emojify--propertize-text-for-emoji :around #'emojify--replace-text-with-emoji)
        (unless emojify-mode
          (emojify-turn-on-emojify-mode)))
    (setq-local emojify-emoji-styles (default-value 'emojify-emoji-styles))
    (advice-remove 'emojify--propertize-text-for-emoji #'emojify--replace-text-with-emoji)))

This new minor mode of ours will be nice for messages, so let’s hook it in for Email and IRC.

(add-hook! '(mu4e-compose-mode org-msg-edit-mode circe-channel-mode) (emoticon-to-emoji 1))

Keycast

For some reason, I find myself demoing Emacs every now and then. Showing what keyboard stuff I’m doing on-screen seems helpful. While screenkey does exist, having something that doesn’t cover up screen content is nice.

(package! keycast :pin "72d9add8ba16e0cae8cfcff7fc050fa75e493b4e")

Let’s just make sure this is lazy-loaded appropriately.

(use-package! keycast
  :commands keycast-mode
  :config
  (define-minor-mode keycast-mode
    "Show current command and its key binding in the mode line."
    :global t
    (if keycast-mode
        (progn
          (add-hook 'pre-command-hook 'keycast--update t)
          (add-to-list 'global-mode-string '("" mode-line-keycast " ")))
      (remove-hook 'pre-command-hook 'keycast--update)
      (setq global-mode-string (remove '("" mode-line-keycast " ") global-mode-string))))
  (custom-set-faces!
    '(keycast-command :inherit doom-modeline-debug
                      :height 0.9)
    '(keycast-key :inherit custom-modified
                  :height 1.1
                  :weight bold)))

Screencast

In a similar manner to Keycast, gif-screencast may come in handy.

(package! gif-screencast :pin "5517a557a17d8016c9e26b0acb74197550f829b9")

We can lazy load this using the start/stop commands.

I initially installed scrot for this, since it was the default capture program. However it raised glib error: Saving to file ... failed each time it was run. Google didn’t reveal any easy fixed, so I switched to maim. We now need to pass it the window ID. This doesn’t change throughout the lifetime of an emacs instance, so as long as a single window is used xdotool getactivewindow will give a satisfactory result.

It seems that when new colours appear, that tends to make gifsicle introduce artefacts. To avoid this we pre-populate the colour map using the current doom theme.

(use-package! gif-screencast
  :commands gif-screencast-mode
  :config
  (map! :map gif-screencast-mode-map
        :g "<f8>" #'gif-screencast-toggle-pause
        :g "<f9>" #'gif-screencast-stop)
  (setq gif-screencast-program "maim"
        gif-screencast-args `("--quality" "3" "-i" ,(string-trim-right
                                                     (shell-command-to-string
                                                      "xdotool getactivewindow")))
        gif-screencast-optimize-args '("--batch" "--optimize=3" "--usecolormap=/tmp/doom-color-theme"))
  (defun gif-screencast-write-colormap ()
    (f-write-text
     (replace-regexp-in-string
      "\n+" "\n"
      (mapconcat (lambda (c) (if (listp (cdr c))
                                 (cadr c))) doom-themes--colors "\n"))
     'utf-8
     "/tmp/doom-color-theme" ))
  (gif-screencast-write-colormap)
  (add-hook 'doom-load-theme-hook #'gif-screencast-write-colormap))

Mixed pitch

From the :ui zen module.

We’d like to use mixed pitch in certain modes. If we simply add a hook, when directly opening a file with (a new) Emacs mixed-pitch-mode runs before UI initialization, which is problematic. To resolve this, we create a hook that runs after UI initialization and both

  • conditionally enables mixed-pitch-mode
  • sets up the mixed pitch hooks
(defvar mixed-pitch-modes '(org-mode LaTeX-mode markdown-mode gfm-mode Info-mode)
  "Modes that `mixed-pitch-mode' should be enabled in, but only after UI initialisation.")
(defun init-mixed-pitch-h ()
  "Hook `mixed-pitch-mode' into each mode in `mixed-pitch-modes'.
Also immediately enables `mixed-pitch-modes' if currently in one of the modes."
  (when (memq major-mode mixed-pitch-modes)
    (mixed-pitch-mode 1))
  (dolist (hook mixed-pitch-modes)
    (add-hook (intern (concat (symbol-name hook) "-hook")) #'mixed-pitch-mode)))
(add-hook 'doom-init-ui-hook #'init-mixed-pitch-h)

As mixed pitch uses the variable mixed-pitch-face, we can create a new function to apply mixed pitch with a serif face instead of the default. This was created for writeroom mode.

(autoload #'mixed-pitch-serif-mode "mixed-pitch"
  "Change the default face of the current buffer to a serifed variable pitch, while keeping some faces fixed pitch." t)
 
(after! mixed-pitch
  (defface variable-pitch-serif
    '((t (:family "serif")))
    "A variable-pitch face with serifs."
    :group 'basic-faces)
  (setq mixed-pitch-set-height t)
  (setq variable-pitch-serif-font (font-spec :family "Alegreya" :size 27))
  (set-face-attribute 'variable-pitch-serif nil :font variable-pitch-serif-font)
  (defun mixed-pitch-serif-mode (&optional arg)
    "Change the default face of the current buffer to a serifed variable pitch, while keeping some faces fixed pitch."
    (interactive)
    (let ((mixed-pitch-face 'variable-pitch-serif))
      (mixed-pitch-mode (or arg 'toggle)))))

Now, as Harfbuzz is currently used in Emacs, we’ll be missing out on the following Alegreya ligatures:

ff ff ffi ffi ffj ffj ffl ffl fft fft fi fi fj fj ft ft Th Th

Thankfully, it isn’t to hard to add these to the composition-function-table.

(set-char-table-range composition-function-table ?f '(["$?:ff?[fijlt]$" 0 font-shape-gstring]))
(set-char-table-range composition-function-table ?T '(["$?:Th$" 0 font-shape-gstring]))

Marginalia

Part of the :completion vertico module.

Marginalia is nice, but the file metadata annotations are a little too plain. Specifically, I have these gripes

  • File attributes would be nicer if colored
  • I don’t care about the user/group information if the user/group is me
  • When a file time is recent, a relative age (e.g. 2h ago) is more useful than the date
  • An indication of file fatness would be nice

Thanks to the marginalia-annotator-registry, we don’t have to advise, we can just add a new file annotator.

Another small thing is the face used for docstrings. At the moment it’s (italic shadow), but I don’t like that.

(after! marginalia
  (setq marginalia-censor-variables nil)
 
  (defadvice! +marginalia--anotate-local-file-colorful (cand)
    "Just a more colourful version of `marginalia--anotate-local-file'."
    :override #'marginalia--annotate-local-file
    (when-let (attrs (file-attributes (substitute-in-file-name
                                       (marginalia--full-candidate cand))
                                      'integer))
      (marginalia--fields
       ((marginalia--file-owner attrs)
        :width 12 :face 'marginalia-file-owner)
       ((marginalia--file-modes attrs))
       ((+marginalia-file-size-colorful (file-attribute-size attrs))
        :width 7)
       ((+marginalia--time-colorful (file-attribute-modification-time attrs))
        :width 12))))
 
  (defun +marginalia--time-colorful (time)
    (let* ((seconds (float-time (time-subtract (current-time) time)))
           (color (doom-blend
                   (face-attribute 'marginalia-date :foreground nil t)
                   (face-attribute 'marginalia-documentation :foreground nil t)
                   (/ 1.0 (log (+ 3 (/ (+ 1 seconds) 345600.0)))))))
      ;; 1 - log(3 + 1/(days + 1)) % grey
      (propertize (marginalia--time time) 'face (list :foreground color))))
 
  (defun +marginalia-file-size-colorful (size)
    (let* ((size-index (/ (log10 (+ 1 size)) 7.0))
           (color (if (< size-index 10000000) ; 10m
                      (doom-blend 'orange 'green size-index)
                    (doom-blend 'red 'orange (- size-index 1)))))
      (propertize (file-size-human-readable size) 'face (list :foreground color)))))

All the icons

From the :core packages module.

all-the-icons does a generally great job giving file names icons. One minor niggle I have is that when I open a .m file, it’s much more likely to be Matlab than Objective-C. As such, it’ll be switching the icon associated with .m.

(after! all-the-icons
  (setcdr (assoc "m" all-the-icons-extension-icon-alist)
          (cdr (assoc "matlab" all-the-icons-extension-icon-alist))))

Prettier page breaks

In some files, ^L appears as a page break character. This isn’t that visually appealing, and Steve Purcell has been nice enough to make a package to display these as horizontal rules.

(package! page-break-lines :recipe (:host github :repo "purcell/page-break-lines")
  :pin "cc283621c64e4f1133a63e0945658a4abecf42ef")
(use-package! page-break-lines
  :commands page-break-lines-mode
  :init
  (autoload 'turn-on-page-break-lines-mode "page-break-lines")
  :config
  (setq page-break-lines-max-width fill-column)
  (map! :prefix "g"
        :desc "Prev page break" :nv "[" #'backward-page
        :desc "Next page break" :nv "]" #'forward-page))

Writeroom

From the :ui zen module.

For starters, I think Doom is a bit over-zealous when zooming in

(setq +zen-text-scale 0.8)

Then, when using Org it would be nice to make a number of other aesthetic tweaks. Namely:

  • Use a serifed variable-pitch font
  • Hiding headline leading stars
  • Using fleurons as headline bullets
  • Hiding line numbers
  • Removing outline indentation
  • Centring the text
(defvar +zen-serif-p t
  "Whether to use a serifed font with `mixed-pitch-mode'.")
(defvar +zen-org-starhide t
  "The value `org-modern-hide-stars' is set to.")
 
(after! writeroom-mode
  (defvar-local +zen--original-org-indent-mode-p nil)
  (defvar-local +zen--original-mixed-pitch-mode-p nil)
  (defun +zen-enable-mixed-pitch-mode-h ()
    "Enable `mixed-pitch-mode' when in `+zen-mixed-pitch-modes'."
    (when (apply #'derived-mode-p +zen-mixed-pitch-modes)
      (if writeroom-mode
          (progn
            (setq +zen--original-mixed-pitch-mode-p mixed-pitch-mode)
            (funcall (if +zen-serif-p #'mixed-pitch-serif-mode #'mixed-pitch-mode) 1))
        (funcall #'mixed-pitch-mode (if +zen--original-mixed-pitch-mode-p 1 -1)))))
  (pushnew! writeroom--local-variables
            'display-line-numbers
            'visual-fill-column-width
            'org-adapt-indentation
            'org-modern-mode
            'org-modern-star
            'org-modern-hide-stars)
  (add-hook 'writeroom-mode-enable-hook
            (defun +zen-prose-org-h ()
              "Reformat the current Org buffer appearance for prose."
              (when (eq major-mode 'org-mode)
                (setq display-line-numbers nil
                      visual-fill-column-width 60
                      org-adapt-indentation nil)
                (when (modulep 'org-modern)
                  (setq-local org-modern-star '("🙘" "🙙" "🙚" "🙛")
                              ;; org-modern-star '("🙐" "🙑" "🙒" "🙓" "🙔" "🙕" "🙖" "🙗")
                              org-modern-hide-stars +zen-org-starhide)
                  (org-modern-mode -1)
                  (org-modern-mode 1))
                (setq
                 +zen--original-org-indent-mode-p org-indent-mode
                 (org-indent-mode -1))))
            (add-hook 'writeroom-mode-disable-hook
                      (defun +zen-nonprose-org-h ()
                        "Reverse the effect of `+zen-prose-org'."
                        (when (eq major-mode 'org-mode)
                          (when (bound-and-true-p org-modern-mode)
                            (org-modern-mode -1)
                            (org-modern-mode 1))
                          (when +zen--original-org-indent-mode-p (org-indent-mode 1)))))))

Treemacs

From the :ui treemacs module.

Quite often there are superfluous files I’m not that interested in. There’s no good reason for them to take up space. Let’s add a mechanism to ignore them.

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

Now, we just identify the files in question.

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

Frivolities

Wttrin

Hey, let’s get the weather in here while we’re at it. Unfortunately this seems slightly unmaintained (few open bugfix PRs) so let’s roll our own version.

(package! wttrin :recipe (:local-repo "lisp/wttrin"))
(use-package! wttrin
  :commands wttrin)

Elcord

What’s even the point of using Emacs unless you’re constantly telling everyone about it?

(package! elcord :pin "70fd716e673b724b30b921f4cfa0033f9c02d0f2")
(use-package! elcord
  :commands elcord-mode
  :config
  (setq elcord-use-major-mode-as-main-icon t))

Smudge

Spotify! Need to set this up in such a way later that I don’t expose my keys. Commented out for now.

File types

Authinfo

My patch giving my patch giving authinfo-mode syntax highlighting is only available in Emacs28+. For older versions, I’ve got a package I can use.

(package! authinfo-color-mode
  :recipe (:local-repo "lisp/authinfo-color-mode"))

Now we just need to load it appropriately.

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

Systemd

For editing systemd unit files

(package! systemd :pin "b6ae63a236605b1c5e1069f7d3afe06ae32a7bae")
(use-package! systemd
  :defer t)

Applications

Bookmarking

There are bookmarking services that are still popular. There’s a myriad of options like Pocket, but I’m a fan of OSS solutions. Ideally, I’d just log everything in org-mode but I’ve had trouble with org-capture (as of writing, maybe I’ll fix it and forget to change this) - also it’d be nice to have it easily go to the phone.

Pocket

The software, Pocket is Mozilla’s link sharing thingy-ma-jig. It’s not FOSS, but easy online access + a good emacs package make it a decent choice for someone who doesn’t want to deal with the rigamarole around self-hosting internet exposed apps. (if you add a link at home, then want to read it at work, etc.)

(package! pocket-reader)
(use-package! pocket-reader
  :after elfeed
  :demand t
  :config
  (message "pocket-reader loaded"))
(map! :map pocket-reader-mode-map
      :after pocket-reader
      :nm "d" #'pocket-reader-delete
      :nm "a" #'pocket-reader-toggle-archived
      :nm "TAB" #'pocket-reader-open-url
      :nm "b" #'pocket-reader-open-in-external-browser
      :nm "tr" #'pocket-reader-remove-tags
      :nm "ta" #'pocket-reader-add-tags
      :nm "gr" #'pocket-reader-refresh
      :nm "p" #'pocket-reader-search
      :nm "y" #'pocket-reader-copy-url)

Calculator

Emacs includes the venerable calc, which is a pretty impressive RPN (Reverse Polish Notation) calculator. However, we can do a bit to improve the experience.

Defaults

Any sane person prefers radians and exact values.

(setq calc-angle-mode 'rad  ; radians are rad
      calc-symbolic-mode t) ; keeps expressions like \sqrt{2} irrational for as long as possible

CalcTeX

Everybody knows that mathematical expressions look best with LaTeX, so calc’s ability to create LaTeX representations of its expressions provides a lovely opportunity which takes advantage of in the CalcTeX package.

(package! calctex :recipe (:host github :repo "johnbcoughlin/calctex"
                           :files ("*.el" "calctex/*.el" "calctex-contrib/*.el" "org-calctex/*.el" "vendor"))
  :pin "67a2e76847a9ea9eff1f8e4eb37607f84b380ebb")

We’d like to use CalcTeX too, so let’s set that up, and fix some glaring inadequacies --- why on earth would you commit a hard-coded path to an executable that only works on your local machine, consequently breaking the package for everyone else!?

(use-package! calctex
  :commands calctex-mode
  :init
  (add-hook 'calc-mode-hook #'calctex-mode)
  :config
  (setq calctex-additional-latex-packages "
\\usepackage[usenames]{xcolor}
\\usepackage{soul}
\\usepackage{adjustbox}
\\usepackage{amsmath}
\\usepackage{amssymb}
\\usepackage{siunitx}
\\usepackage{cancel}
\\usepackage{mathtools}
\\usepackage{mathalpha}
\\usepackage{xparse}
\\usepackage{arevmath}"
        calctex-additional-latex-macros
        (concat calctex-additional-latex-macros
                "\n\\let\\evalto\\Rightarrow"))
  (defadvice! no-messaging-a (orig-fn &rest args)
    :around #'calctex-default-dispatching-render-process
    (let ((inhibit-message t) message-log-max)
      (apply orig-fn args)))
  ;; Fix hardcoded dvichop path (whyyyyyyy)
  (let ((vendor-folder (concat (file-truename doom-local-dir)
                               "straight/"
                               (format "build-%s" emacs-version)
                               "/calctex/vendor/")))
    (setq calctex-dvichop-sty (concat vendor-folder "texd/dvichop")
          calctex-dvichop-bin (concat vendor-folder "texd/dvichop")))
  (unless (file-exists-p calctex-dvichop-bin)
    (message "CalcTeX: Building dvichop binary")
    (let ((default-directory (file-name-directory calctex-dvichop-bin)))
      (call-process "make" nil nil nil))))

Embedded calc

Embedded calc is a lovely feature which let’s us use calc to operate on LaTeX math expressions. The standard keybinding is a bit janky however (C-x * e), so we’ll add a localleader-based alternative.

(map! :map calc-mode-map
      :after calc
      :localleader
      :desc "Embedded calc (toggle)" "e" #'calc-embedded)
(map! :map org-mode-map
      :after org
      :localleader
      :desc "Embedded calc (toggle)" "E" #'calc-embedded)
(map! :map latex-mode-map
      :after latex
      :localleader
      :desc "Embedded calc (toggle)" "e" #'calc-embedded)

Unfortunately this operates without the (rather informative) calculator and trail buffers, but we can advice it that we would rather like those in a side panel.

(defvar calc-embedded-trail-window nil)
(defvar calc-embedded-calculator-window nil)
 
(defadvice! calc-embedded-with-side-pannel (&rest _)
  :after #'calc-do-embedded
  (when calc-embedded-trail-window
    (ignore-errors
      (delete-window calc-embedded-trail-window))
    (setq calc-embedded-trail-window nil))
  (when calc-embedded-calculator-window
    (ignore-errors
      (delete-window calc-embedded-calculator-window))
    (setq calc-embedded-calculator-window nil))
  (when (and calc-embedded-info
             (> (* (window-width) (window-height)) 1200))
    (let ((main-window (selected-window))
          (vertical-p (> (window-width) 80)))
      (select-window
       (setq calc-embedded-trail-window
             (if vertical-p
                 (split-window-horizontally (- (max 30 (/ (window-width) 3))))
               (split-window-vertically (- (max 8 (/ (window-height) 4)))))))
      (switch-to-buffer "*Calc Trail*")
      (select-window
       (setq calc-embedded-calculator-window
             (if vertical-p
                 (split-window-vertically -6)
               (split-window-horizontally (- (/ (window-width) 2))))))
      (switch-to-buffer "*Calculator*")
      (select-window main-window))))

Ebooks

For managing my ebooks, I’ll hook into the well-established ebook library manager calibre. A number of Emacs clients for this exist, but this seems like a good option.

(package! calibredb :pin "2f2cfc38f2d1c705134b692127c3008ac1382482")

Then for reading them, the only currently viable options seems to be nov.el.

(package! nov :pin "8f5b42e9d9f304b422c1a7918b43ee323a7d3532")

Together these should give me a rather good experience reading ebooks.

calibredb lets us use calibre through Emacs, because who wouldn’t want to use something through Emacs?

(use-package! calibredb
  :commands calibredb
  :config
  (setq calibredb-root-dir "~/Documents/Library"
        calibredb-db-dir (expand-file-name "metadata.db" calibredb-root-dir))
  (map! :map calibredb-show-mode-map
        :ne "?" #'calibredb-entry-dispatch
        :ne "o" #'calibredb-find-file
        :ne "O" #'calibredb-find-file-other-frame
        :ne "V" #'calibredb-open-file-with-default-tool
        :ne "s" #'calibredb-set-metadata-dispatch
        :ne "e" #'calibredb-export-dispatch
        :ne "q" #'calibredb-entry-quit
        :ne "." #'calibredb-open-dired
        :ne [tab] #'calibredb-toggle-view-at-point
        :ne "M-t" #'calibredb-set-metadata--tags
        :ne "M-a" #'calibredb-set-metadata--author_sort
        :ne "M-A" #'calibredb-set-metadata--authors
        :ne "M-T" #'calibredb-set-metadata--title
        :ne "M-c" #'calibredb-set-metadata--comments)
  (map! :map calibredb-search-mode-map
        :ne [mouse-3] #'calibredb-search-mouse
        :ne "RET" #'calibredb-find-file
        :ne "?" #'calibredb-dispatch
        :ne "a" #'calibredb-add
        :ne "A" #'calibredb-add-dir
        :ne "c" #'calibredb-clone
        :ne "d" #'calibredb-remove
        :ne "D" #'calibredb-remove-marked-items
        :ne "j" #'calibredb-next-entry
        :ne "k" #'calibredb-previous-entry
        :ne "l" #'calibredb-virtual-library-list
        :ne "L" #'calibredb-library-list
        :ne "n" #'calibredb-virtual-library-next
        :ne "N" #'calibredb-library-next
        :ne "p" #'calibredb-virtual-library-previous
        :ne "P" #'calibredb-library-previous
        :ne "s" #'calibredb-set-metadata-dispatch
        :ne "S" #'calibredb-switch-library
        :ne "o" #'calibredb-find-file
        :ne "O" #'calibredb-find-file-other-frame
        :ne "v" #'calibredb-view
        :ne "V" #'calibredb-open-file-with-default-tool
        :ne "." #'calibredb-open-dired
        :ne "b" #'calibredb-catalog-bib-dispatch
        :ne "e" #'calibredb-export-dispatch
        :ne "r" #'calibredb-search-refresh-and-clear-filter
        :ne "R" #'calibredb-search-clear-filter
        :ne "q" #'calibredb-search-quit
        :ne "m" #'calibredb-mark-and-forward
        :ne "f" #'calibredb-toggle-favorite-at-point
        :ne "x" #'calibredb-toggle-archive-at-point
        :ne "h" #'calibredb-toggle-highlight-at-point
        :ne "u" #'calibredb-unmark-and-forward
        :ne "i" #'calibredb-edit-annotation
        :ne "DEL" #'calibredb-unmark-and-backward
        :ne [backtab] #'calibredb-toggle-view
        :ne [tab] #'calibredb-toggle-view-at-point
        :ne "M-n" #'calibredb-show-next-entry
        :ne "M-p" #'calibredb-show-previous-entry
        :ne "/" #'calibredb-search-live-filter
        :ne "M-t" #'calibredb-set-metadata--tags
        :ne "M-a" #'calibredb-set-metadata--author_sort
        :ne "M-A" #'calibredb-set-metadata--authors
        :ne "M-T" #'calibredb-set-metadata--title
        :ne "M-c" #'calibredb-set-metadata--comments))

Then, to actually read the ebooks we use nov.

(use-package! nov
  :mode ("\\.epub\\'" . nov-mode)
  :config
  (map! :map nov-mode-map
        :n "RET" #'nov-scroll-up)
 
  (defun doom-modeline-segment--nov-info ()
    (concat
     " "
     (propertize
      (cdr (assoc 'creator nov-metadata))
      'face 'doom-modeline-project-parent-dir)
     " "
     (cdr (assoc 'title nov-metadata))
     " "
     (propertize
      (format "%d/%d"
              (1+ nov-documents-index)
              (length nov-documents))
      'face 'doom-modeline-info)))
 
  (advice-add 'nov-render-title :override #'ignore)
 
  (defun +nov-mode-setup ()
    (face-remap-add-relative 'variable-pitch
                             :family "Merriweather"
                             :height 1.4
                             :width 'semi-expanded)
    (face-remap-add-relative 'default :height 1.3)
    (setq-local line-spacing 0.2
                next-screen-context-lines 4
                shr-use-colors nil)
    (require 'visual-fill-column nil t)
    (setq-local visual-fill-column-center-text t
                visual-fill-column-width 81
                nov-text-width 80)
    (visual-fill-column-mode 1)
    (hl-line-mode -1)
 
    (add-to-list '+lookup-definition-functions #'+lookup/dictionary-definition)
 
    (setq-local mode-line-format
                `((:eval
                   (doom-modeline-segment--workspace-name))
                  (:eval
                   (doom-modeline-segment--window-number))
                  (:eval
                   (doom-modeline-segment--nov-info))
                  ,(propertize
                    " %P "
                    'face 'doom-modeline-buffer-minor-mode)
                  ,(propertize
                    " "
                    'face (if (doom-modeline--active) 'mode-line 'mode-line-inactive)
                    'display `((space
                                :align-to
                                (- (+ right right-fringe right-margin)
                                   ,(* (let ((width (doom-modeline--font-width)))
                                         (or (and (= width 1) 1)
                                             (/ width (frame-char-width) 1.0)))
                                       (string-width
                                        (format-mode-line (cons "" '(:eval (doom-modeline-segment--major-mode))))))))))
                  (:eval (doom-modeline-segment--major-mode)))))
 
  (add-hook 'nov-mode-hook #'+nov-mode-setup))

Newsfeed

RSS feeds are still a thing. Why not make use of them with elfeed. I really like what fuxialexander has going on, but I don’t think I need a custom module. Let’s just try to patch on the main things I like the look of.

Setup

By default it uses the .emacs.d folder, I prefer keeping the configs in the doom folder with my literate config.

(setq rmh-elfeed-org-files (list "~/.doom.d/elfeed.org"))

New package for integrating youtube into elfeed, pretty neat.

(package! elfeed-tube :recipe (:host github :repo  "karthink/elfeed-tube"))
(package! aio)
(use-package! elfeed-tube
  :after elfeed
  :demand t
  :config
  ;; (setq elfeed-tube-auto-save-p nil) ;; t is auto-save (not default)
   (setq elfeed-tube-auto-fetch-p t) ;;  t is auto-fetch (default)
  (elfeed-tube-setup)
 
  :bind (:map elfeed-show-mode-map
         ("F" . elfeed-tube-fetch)
         ([remap save-buffer] . elfeed-tube-save)
         :map elfeed-search-mode-map
         ("F" . elfeed-tube-fetch)
         ([remap save-buffer] . elfeed-tube-save)))

Scoring

Using elfeed-score to sort my newsfeed by a point value that I determine.

(package! elfeed-score)

Another case of moving the config file to the doom folder for ease of tracking. It’s simply a plain text with a elisp s-expression as noted here: https://www.unwoundstack.com/doc/elfeed-score/curr#The-Score-File

(after! elfeed
(require 'elfeed-score)
(elfeed-score-enable)
(setq elfeed-score-serde-score-file "~/.doom.d/elfeed.score"))

Summary

Adding a dashboard style summary into the newsfeed.

(package! elfeed-summary)

TODO: need to clean this up, using the template config from the git repo.

(setq elfeed-summary-settings
      '(
        (group (:title . "Blogs [People]")
               (:elements
                (query . (and blogs people (not emacs)))
                (group (:title . "Emacs")
                       (:elements
                        (query . (and blogs people emacs))))))
        (group (:title . "Link Aggregators")
               (:elements
                (query . agg)))
        (group (:title . "Blogs")
               (:elements
                (group
                 (:title . "Religion")
                 (:elements
                  (query . religion)))
                (group
                 (:title . "Substack")
                 (:elements
                  (query . substack)))
                (group
                 (:title . "Other")
                 (:elements
                  (query . (and blog (not substack religion)))))))
                ;; ...
 
        (group (:title . "Videos")
               (:elements
                (query . youtube)))
 
        (group (:title . "Podcasts")
               (:elements
                (query . podcast)))
 
        (group (:title . "News")
               (:elements
                (query . news)))
        (group (:title . "Webcomics")
               (:elements
                (query . comics)))
        (group (:title . "Literary")
               (:elements
                (query . literary)))
        ;; ...
        (group (:title . "Miscellaneous")
               (:elements
                (group
                 (:title . "Searches")
                 (:elements
                  (search
                   (:filter . "@6-months-ago sqrtminusone")
                   (:title . "About me"))
                  (search
                   (:filter . "+later")
                   (:title . "Check later"))))
                (group
                 (:title . "Ungrouped")
                 (:elements :misc))))))

Keybindings

Elfeed keybindings. Easier to push in one place.

(map! :map elfeed-search-mode-map
      :after elfeed-search
      [remap kill-this-buffer] "q"
      [remap kill-buffer] "q"
      :n doom-leader-key nil
      :n "a" #'pocket-reader-elfeed-search-add-link
      :n "q" #'+rss/quit
      :n "e" #'elfeed-update
      :n "r" #'elfeed-search-untag-all-unread
      :n "u" #'elfeed-search-tag-all-unread
      :n "s" #'elfeed-search-live-filter
      :n "RET" #'elfeed-search-show-entry
      :n "p" #'elfeed-show-pdf
      :n "+" #'elfeed-search-tag-all
      :n "-" #'elfeed-search-untag-all
      :n "S" #'elfeed-search-set-filter
      :n "b" #'elfeed-search-browse-url
      :n "y" #'elfeed-search-yank)
(map! :map elfeed-show-mode-map
      :after elfeed-show
      [remap kill-this-buffer] "q"
      [remap kill-buffer] "q"
      :n doom-leader-key nil
      :nm "q" #'+rss/delete-pane
      :nm "o" #'ace-link-elfeed
      :nm "RET" #'org-ref-elfeed-add
      :nm "n" #'elfeed-show-next
      :nm "N" #'elfeed-show-prev
      :nm "p" #'elfeed-show-pdf
      :nm "+" #'elfeed-show-tag
      :nm "-" #'elfeed-show-untag
      :nm "s" #'elfeed-show-new-live-search
      :nm "y" #'elfeed-show-yank)

Usability enhancements

(after! elfeed-search
  (set-evil-initial-state! 'elfeed-search-mode 'normal))
(after! elfeed-show-mode
  (set-evil-initial-state! 'elfeed-show-mode   'normal))
 
(after! evil-snipe
  (push 'elfeed-show-mode   evil-snipe-disabled-modes)
  (push 'elfeed-search-mode evil-snipe-disabled-modes))

Visual enhancements

(after! elfeed
 
  (elfeed-org)
  (use-package! elfeed-link)
 
  (setq elfeed-search-filter "@3-days-ago +unread -agg -freq -notes"
        elfeed-search-print-entry-function '+rss/elfeed-search-print-entry
        ;elfeed-search-title-min-width 60
        elfeed-show-entry-switch #'pop-to-buffer
        elfeed-show-entry-delete #'+rss/delete-pane
        elfeed-show-refresh-function #'+rss/elfeed-show-refresh--better-style
        shr-max-image-proportion 0.6)
 
  (add-hook! 'elfeed-show-mode-hook (hide-mode-line-mode 1))
  (add-hook! 'elfeed-search-update-hook #'hide-mode-line-mode)
 
  (defface elfeed-show-title-face '((t (:weight ultrabold :slant italic :height 1.5)))
    "title face in elfeed show buffer"
    :group 'elfeed)
  (defface elfeed-show-author-face `((t (:weight light)))
    "title face in elfeed show buffer"
    :group 'elfeed)
  (set-face-attribute 'elfeed-search-title-face nil
                      :foreground 'nil
                      :weight 'light)
 
  (defadvice! +rss-elfeed-wrap-h-nicer ()
    "Enhances an elfeed entry's readability by wrapping it to a width of
`fill-column' and centering it with `visual-fill-column-mode'."
    :override #'+rss-elfeed-wrap-h
    (setq-local truncate-lines nil
                shr-width 120
                visual-fill-column-center-text t
                default-text-properties '(line-height 1.1))
    (let ((inhibit-read-only t)
          (inhibit-modification-hooks t))
      (visual-fill-column-mode)
      ;; (setq-local shr-current-font '(:family "Merriweather" :height 1.2))
      (set-buffer-modified-p nil)))
 
  (defvar elfeed-goodies/date-column-width 12)
  (defun +rss/elfeed-search-print-entry (entry)
   "Print ENTRY to the buffer."
   (let* ((elfeed-goodies/tag-column-width 15)
          (elfeed-goodies/feed-source-column-width 20)
          (date (format-time-string "%Y-%m-%d"
                  (seconds-to-time (elfeed-entry-date entry))))
          (date-column (elfeed-format-column
                        date (elfeed-clamp elfeed-goodies/date-column-width
                                            elfeed-goodies/date-column-width
                                            elfeed-goodies/date-column-width)
                        :left))
          (title (or (elfeed-meta entry :title) (elfeed-entry-title entry) ""))
          (title-faces (elfeed-search--faces (elfeed-entry-tags entry)))
          (feed (elfeed-entry-feed entry))
          (feed-title
           (when feed
               (or (elfeed-meta feed :title) (elfeed-feed-title feed))))
          (tags (mapcar #'symbol-name (elfeed-entry-tags entry)))
          (tags-str (concat (mapconcat 'identity tags ",")))
          (title-width (- (window-width)
                          elfeed-goodies/feed-source-column-width
                          elfeed-goodies/tag-column-width
                          elfeed-goodies/date-column-width
                          6))  ; Adjust the 6 if you're adding more/less space
 
          (tag-column (elfeed-format-column
                       tags-str (elfeed-clamp (length tags-str)
                                              elfeed-goodies/tag-column-width
                                              elfeed-goodies/tag-column-width)
                       :left))
          (feed-column (elfeed-format-column
                        feed-title (elfeed-clamp elfeed-goodies/feed-source-column-width
                                                    elfeed-goodies/feed-source-column-width
                                                    elfeed-goodies/feed-source-column-width)
                        :left)))
 
       (insert (propertize date-column 'face 'elfeed-search-date-face) " ")
       (insert (propertize feed-column 'face 'elfeed-search-feed-face) " ")
       (insert (propertize title       'face title-faces 'kbd-help title) " ")
       (insert (propertize tag-column  'face 'elfeed-search-tag-face) " ")
       (setq-local line-spacing 0.2)))
 
  (defun +rss/elfeed-show-refresh--better-style ()
    "Update the buffer to match the selected entry, using a mail-style."
    (interactive)
    (let* ((inhibit-read-only t)
           (title (elfeed-entry-title elfeed-show-entry))
           (date (seconds-to-time (elfeed-entry-date elfeed-show-entry)))
           (author (elfeed-meta elfeed-show-entry :author))
           (link (elfeed-entry-link elfeed-show-entry))
           (tags (elfeed-entry-tags elfeed-show-entry))
           (tagsstr (mapconcat #'symbol-name tags ", "))
           (nicedate (format-time-string "%a, %e %b %Y %T %Z" date))
           (content (elfeed-deref (elfeed-entry-content elfeed-show-entry)))
           (type (elfeed-entry-content-type elfeed-show-entry))
           (feed (elfeed-entry-feed elfeed-show-entry))
           (feed-title (elfeed-feed-title feed))
           (base (and feed (elfeed-compute-base (elfeed-feed-url feed)))))
      (erase-buffer)
      (insert "\n")
      (insert (format "%s\n\n" (propertize title 'face 'elfeed-show-title-face)))
      (insert (format "%s\t" (propertize feed-title 'face 'elfeed-search-feed-face)))
      (when (and author elfeed-show-entry-author)
        (insert (format "%s\n" (propertize author 'face 'elfeed-show-author-face))))
      (insert (format "%s\n\n" (propertize nicedate 'face 'elfeed-log-date-face)))
      (when tags
        (insert (format "%s\n"
                        (propertize tagsstr 'face 'elfeed-search-tag-face))))
      ;; (insert (propertize "Link: " 'face 'message-header-name))
      ;; (elfeed-insert-link link link)
      ;; (insert "\n")
      (cl-loop for enclosure in (elfeed-entry-enclosures elfeed-show-entry)
               do (insert (propertize "Enclosure: " 'face 'message-header-name))
               do (elfeed-insert-link (car enclosure))
               do (insert "\n"))
      (insert "\n")
      (if content
          (if (eq type 'html)
              (elfeed-insert-html content base)
            (insert content))
        (insert (propertize "(empty)\n" 'face 'italic)))
      (goto-char (point-min)))))
 
 

Functionality enhancements

(after! elfeed-show
  (require 'url)
 
  (defvar elfeed-pdf-dir
    (expand-file-name "pdfs/"
                      (file-name-directory (directory-file-name elfeed-enclosure-default-dir))))
 
  (defvar elfeed-link-pdfs
    '(("https://www.jstatsoft.org/index.php/jss/article/view/v0$[^/]+$" . "https://www.jstatsoft.org/index.php/jss/article/view/v0\\1/v\\1.pdf")
      ("http://arxiv.org/abs/$[^/]+$" . "https://arxiv.org/pdf/\\1.pdf"))
    "List of alists of the form (REGEX-FOR-LINK . FORM-FOR-PDF)")
 
  (defun elfeed-show-pdf (entry)
    (interactive
     (list (or elfeed-show-entry (elfeed-search-selected :ignore-region))))
    (let ((link (elfeed-entry-link entry))
          (feed-name (plist-get (elfeed-feed-meta (elfeed-entry-feed entry)) :title))
          (title (elfeed-entry-title entry))
          (file-view-function
           (lambda (f)
             (when elfeed-show-entry
               (elfeed-kill-buffer))
             (pop-to-buffer (find-file-noselect f))))
          pdf)
 
      (let ((file (expand-file-name
                   (concat (subst-char-in-string ?/ ?, title) ".pdf")
                   (expand-file-name (subst-char-in-string ?/ ?, feed-name)
                                     elfeed-pdf-dir))))
        (if (file-exists-p file)
            (funcall file-view-function file)
          (dolist (link-pdf elfeed-link-pdfs)
            (when (and (string-match-p (car link-pdf) link)
                       (not pdf))
              (setq pdf (replace-regexp-in-string (car link-pdf) (cdr link-pdf) link))))
          (if (not pdf)
              (message "No associated PDF for entry")
            (message "Fetching %s" pdf)
            (unless (file-exists-p (file-name-directory file))
              (make-directory (file-name-directory file) t))
            (url-copy-file pdf file)
            (funcall file-view-function file))))))
 
  )

Language configuration

General

File Templates

For some file types, we overwrite defaults in the snippets directory, others need to have a template assigned.

(set-file-template! "\\.tex$" :trigger "__" :mode 'latex-mode)
(set-file-template! "\\.org$" :trigger "__" :mode 'org-mode)
(set-file-template! "/LICEN[CS]E$" :trigger '+file-templates/insert-license)

Org

Finally, because this section is expensive to initialize, we’ll wrap it in an (after! ...) block.

(after! org
  <<org-conf>>
)

System config

Misc Config

Miscellaneous things I’ve noticed I’ve had to change (for some reason this defaulted to org - unknown why)

(setq org-id-locations-file "~/.org/.orgids")
Mime types

Org mode isn’t recognized as it’s own mime type by default, but that can easily be changed with the following file. For system-wide changes try /usr/share/mime/packages/org.xml.

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

What’s nice is that Papirus now has an icon for text/org. One simply needs to refresh their mime database

update-mime-database ~/.local/share/mime

Then set Emacs as the default editor

xdg-mime default emacs.desktop text/org
Git diffs

Protesilaos wrote a very helpful article in which he explains how to change the git diff chunk heading to something more useful than just the immediate line above the hunk --- like the parent heading.

This can be achieved by first adding a new diff mode to git in ~/.config/git/attributes

*.org   diff=org

Then adding a regex for it to ~/.config/git/config

[diff "org"]
  xfuncname = "^(\\*+ +.*)$"

Packages

Visuals
Org Modern

Fontifying org-mode buffers to be as pretty as possible is of paramount importance, and Minad’s lovely org-modern goes a long way in this regard.

(package! org-modern)

…with a touch of configuration…

(use-package! org-modern
  :hook (org-mode . org-modern-mode)
  :config
  (setq org-modern-star ["" "" "" "" "" "" "" ""]
        org-modern-table-vertical 1
        org-modern-table-horizontal 0.2
        org-modern-list '((43 . "")
                          (45 . "")
                          (42 . ""))
        org-modern-todo-faces
        '(("TODO" :inverse-video t :inherit org-todo)
          ("PROJ" :inverse-video t :inherit +org-todo-project)
          ("STRT" :inverse-video t :inherit +org-todo-active)
          ("[-]"  :inverse-video t :inherit +org-todo-active)
          ("HOLD" :inverse-video t :inherit +org-todo-onhold)
          ("WAIT" :inverse-video t :inherit +org-todo-onhold)
          ("[?]"  :inverse-video t :inherit +org-todo-onhold)
          ("KILL" :inverse-video t :inherit +org-todo-cancel)
          ("NO"   :inverse-video t :inherit +org-todo-cancel))
        org-modern-footnote
        (cons nil (cadr org-script-display))
  )
  (custom-set-faces! '(org-modern-statistics :inherit org-checkbox-statistics-todo)))
 
(setq org-modern-priority t)

Since org-modern’s tag face supplants Org’s tag face, we need to adjust the spell-check face ignore list

(after! spell-fu
  (cl-pushnew 'org-modern-tag (alist-get 'org-mode +spell-excluded-faces-alist)))
Emphasis markers

While org-hide-emphasis-markers is nice, it can sometimes make edits which occur at the border a bit more fiddly. We can improve this situation without sacrificing visual amenities with the org-appear package.

(package! org-appear :recipe (:host github :repo "awth13/org-appear")
  :pin "60ba267c5da336e75e603f8c7ab3f44e6f4e4dac")
(use-package! org-appear
  :hook (org-mode . org-appear-mode)
  :config
  (setq org-appear-autoemphasis t
        org-appear-autosubmarkers t
        org-appear-autolinks nil)
  ;; for proper first-time setup, `org-appear--set-elements'
  ;; needs to be run after other hooks have acted.
  (run-at-time nil nil #'org-appear--set-elements))
Heading structure

Speaking of headlines, a nice package for viewing and managing the heading structure has come to my attention.

(package! org-ol-tree :recipe (:host github :repo "Townk/org-ol-tree")
  :pin "207c748aa5fea8626be619e8c55bdb1c16118c25")

We’ll bind this to O on the org-mode localleader, and manually apply a PR recognizing the pgtk window system.

(use-package! org-ol-tree
  :commands org-ol-tree
  :config
(setq org-ol-tree-ui-icon-set
        (if (and (display-graphic-p)
                 (fboundp 'all-the-icons-material))
            'all-the-icons
          'unicode))
  (org-ol-tree-ui--update-icon-set))
(map! :map org-mode-map
      :after org
      :localleader
      :desc "Outline" "O" #'org-ol-tree)
Extra functionality
Julia support

ob-julia is currently a bit borked, but there’s an effort to improve this. Additionally, ox-pluto allows working with org files within Julia.

(package! ob-julia :recipe (:local-repo "lisp/ob-julia" :files ("*.el")))
(package! ox-pluto :recipe (:host github :repo "tecosaur/ox-pluto"))
(use-package! ob-julia
  :commands org-babel-execute:julia)
HTTP requests

I like the idea of being able to make HTTP requests with Babel.

(package! ob-http :pin "b1428ea2a63bcb510e7382a1bf5fe82b19c104a7")
(use-package! ob-http
  :commands org-babel-execute:http)
Transclusion

There’s a really cool package in development to transclude Org document content.

(package! org-transclusion :recipe (:host github :repo "nobiot/org-transclusion"))
(use-package! org-transclusion
  :after org
  :commands org-transclusion-mode
  :init
  (map! :map org-mode-map
        "<f12>" #'org-transclusion-mode))
 
(after! org-transclusion
  (set-face-attribute 'org-transclusion-fringe nil
                      :background "orange"
                      :foreground "black")
  (set-face-attribute 'org-transclusion-source-fringe nil
                      :background "purple"
                      :foreground "green"))
(fringe-mode '(15 . 15))  ; This sets both left and right fringes to 8 pixels
Restore pdf views
(package! pdf-view-restore)
(use-package! pdf-view-restore
  :after pdf-tools
  :config
  (add-hook 'pdf-view-mode-hook 'pdf-view-restore-mode))
Heading graph

Came across this and … it’s cool

(package! org-graph-view :recipe (:host github :repo "alphapapa/org-graph-view") :pin "233c6708c1f37fc60604de49ca192497aef39757")
Cooking recipes

I need this in my life. It take a URL to a recipe from a common site, and inserts an org-ified version at point. Isn’t that just great.

(package! org-chef :pin "6a786e77e67a715b3cd4f5128b59d501614928af")

Loading after org seems a bit premature. Let’s just load it when we try to use it, either by command or in a capture template.

(use-package! org-chef
  :commands (org-chef-insert-recipe org-chef-get-recipe-from-url))
Org-anki

I like anki. Especially for learning languages.

(package! org-anki)
(use-package! org-anki
  :after org
  :config
  (setq org-anki-default-deck "Default")
)
Org query language

Query org files like SQL! Useful for a big TODO list.

(package! org-ql)

Behavior

Tweaking defaults
(setq org-directory "~/.org"                      ; let's put files here
      org-log-into-drawer t                       ; changes of state into a LOGBOOK
      org-use-property-inheritance t              ; it's convenient to have properties inherited
      org-log-done 'time                          ; having the time a item is done sounds convenient
      org-list-allow-alphabetical t               ; have a. A. a) A) list bullets
      org-catch-invisible-edits 'smart            ; try not to accidently do weird stuff in invisible regions
      org-export-use-babel nil                    ; I don't want things to run automatically as I export
      org-export-with-sub-superscripts '{}        ; don't treat lone _ / ^ as sub/superscripts, require _{} / ^{}
      org-export-headline-levels 6
      org-export-with-todo-keywords t
      org-export-with-planning t
      org-export-with-priority t
      org-export-with-creator t
      org-export-with-properties nil
      org-export-with-tags t)

I also like the :comments header-argument, so let’s make that a default.

(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. However this messes with tables in Org-mode, and other plaintext files (e.g. markdown, \LaTeX) so I’ll turn it off for this, and manually enable it for more specific modes as desired.

(remove-hook 'text-mode-hook #'visual-line-mode)
(add-hook 'text-mode-hook #'auto-fill-mode)
(add-hook 'auto-save-hook 'org-save-all-org-buffers)

There also seem to be a few keybindings which use hjkl, but miss arrow key equivalents.

(map! :map evil-org-mode-map
      :after evil-org
      :n "g <up>" #'org-backward-heading-same-level
      :n "g <down>" #'org-forward-heading-same-level
      :n "g <left>" #'org-up-element
      :n "g <right>" #'org-down-element)
Extra functionality
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))
The utility of zero-width spaces

Occasionally in Org you run into annoyances where you want to have two separate blocks right together without a space. For example, to emphasize part of a word, or put a currency symbol immediately before an inline source block. There is a solution to this, it just sounds slightly hacky --- zero width spaces. Because this is Emacs, we can make this feel much less hacky by making a minor addition to the Org key map 🙂.

(map! :map org-mode-map
      :nie "M-SPC M-SPC" (cmd! (insert "\u200B")))

We then want to stop the space from being included in exports, which can be done with a little filter.

(defun +org-export-remove-zero-width-space (text _backend _info)
  "Remove zero width spaces from TEXT."
  (unless (org-export-derived-backend-p 'org)
    (replace-regexp-in-string "\u200B" "" text)))
 
(after! ox
  (add-to-list 'org-export-filter-final-output-functions #'+org-export-remove-zero-width-space t))
List bullet sequence

I think it makes sense to have list bullets change with depth

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

Occasionally I want to cite something, and org-ref is the package for that.

Unfortunately, it ignores the file = {...} .bib keys though. Let’s fix that. I separate files on ;, which may just be a Zotero/BetterBibLaTeX thing, but it’s a good idea in my case at least.

(use-package! org-ref
  ;; :after org
  :defer t
  :config
  (defadvice! org-ref-open-bibtex-pdf-a ()
    :override #'org-ref-open-bibtex-pdf
    (save-excursion
      (bibtex-beginning-of-entry)
      (let* ((bibtex-expand-strings t)
             (entry (bibtex-parse-entry t))
             (key (reftex-get-bib-field "=key=" entry))
             (pdf (or
                   (car (-filter (lambda (f) (string-match-p "\\.pdf$" f))
                                 (split-string (reftex-get-bib-field "file" entry) ";")))
                   (funcall org-ref-get-pdf-filename-function key))))
        (if (file-exists-p pdf)
            (org-open-file pdf)
          (ding)))))
  (defadvice! org-ref-open-pdf-at-point-a ()
    "Open the pdf for bibtex key under point if it exists."
    :override #'org-ref-open-pdf-at-point
    (interactive)
    (let* ((results (org-ref-get-bibtex-key-and-file))
           (key (car results))
           (pdf-file (funcall org-ref-get-pdf-filename-function key)))
      (with-current-buffer (find-file-noselect (cdr results))
        (save-excursion
          (bibtex-search-entry (car results))
          (org-ref-open-bibtex-pdf))))))

There’s also the new org-cite though. It would be nice to try that out.

To improve org-cite.

(package! citar)
(package! citeproc)
(package! org-cite-csl-activate :recipe (:host github :repo "andras-simonyi/org-cite-csl-activate"))
(use-package! citar
  :when (modulep! :completion vertico)
  :custom
  (org-cite-insert-processor 'citar)
  (org-cite-follow-processor 'citar)
  (org-cite-activate-processor 'citar)
  :config
  (setq citar-bibliography
        (let ((libfile-search-names '("biblio.json" "Biblio.json" "biblio.bib" "Biblio.bib"))
              (libfile-dir "~/.org/brain/")
              paths)
          (dolist (libfile libfile-search-names)
            (when (and (not paths)
                       (file-exists-p (expand-file-name libfile libfile-dir)))
              (setq paths (list (expand-file-name libfile libfile-dir)))))
          paths))
  :custom
  (citar-notes-paths '("~/.org/brain/references")) ; List of directories for reference nodes
  (citar-open-note-function 'orb-citar-edit-note)  ; Open notes in `org-roam'
  (citar-at-point-function 'embark-act))           ; Use `embark'
 
(use-package! citeproc
  :defer t)
 
;;; Org-Cite configuration
 
(map! :after org
      :map org-mode-map
      :localleader
      :desc "Insert citation" "@" #'org-cite-insert)
 
(use-package! oc
  :after org citar
  :config
  (require 'ox)
  (setq org-cite-global-bibliography
        (let ((paths (or citar-bibliography
                         (bound-and-true-p bibtex-completion-bibliography))))
          ;; Always return bibliography paths as list for org-cite.
          (if (stringp paths) (list paths) paths)))
  ;; setup export processor; default csl/citeproc-el, with biblatex for latex
  (setq org-cite-export-processors
        '((t csl))))
 
  ;;; Org-cite processors
(use-package! oc-biblatex
  :after oc)
 
(use-package! oc-csl
  :after oc
  :config
  (setq org-cite-csl-styles-dir "~/Zotero/styles"))
 
(use-package! oc-natbib
  :after oc)
 
(use-package! oc-csl-activate
  :after oc
  :config
  (setq org-cite-csl-activate-use-document-style t)
  (defun +org-cite-csl-activate/enable ()
    (interactive)
    (setq org-cite-activate-processor 'csl-activate)
    (add-hook! 'org-mode-hook '((lambda () (cursor-sensor-mode 1)) org-cite-csl-activate-render-all))
    (defadvice! +org-cite-csl-activate-render-all-silent (orig-fn)
      :around #'org-cite-csl-activate-render-all
      (with-silent-modifications (funcall orig-fn)))
    (when (eq major-mode 'org-mode)
      (with-silent-modifications
        (save-excursion
          (goto-char (point-min))
          (org-cite-activate (point-max)))
        (org-cite-csl-activate-render-all)))
    (fmakunbound #'+org-cite-csl-activate/enable)))
(setq citar-citeproc-csl-styles-dir "~/Zotero/styles")
cdlatex

It’s also nice to be able to use cdlatex.

(add-hook 'org-mode-hook 'turn-on-org-cdlatex)

It’s handy to be able to quickly insert environments with C-c }. I almost always want to edit them afterwards though, so let’s make that happen by default.

(defadvice! org-edit-latex-emv-after-insert ()
  :after #'org-cdlatex-environment-indent
  (org-edit-latex-environment))

At some point in the future it could be good to investigate splitting org blocks. Likewise this looks good for symbols.

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
  '("go" "python" "ipython" "bash" "sh"))
(dolist (lang org-babel-lang-list)
  (eval `(lsp-org-babel-enable ,lang)))
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.

(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.")
Journal

Just setting the directory for the journal. Didn’t want to mix and mesh with the roam dailies.

(setq org-journal-dir "~/.org/journal/")
Clock

Some settings for org-clock

;; Resume clocking task when emacs restarts.
(org-clock-persistence-insinuate)
;; Show lot of clocking history so it's easy to pick items off the C-F11 list
(setq org-clock-history-length 23)
;; Resume clocking task on clock-in if the clock is open
(setq org-clock-in-resume t)
;; Sometimes I change tasks I'm clocking quickly - this removes clocked tasks with 0:00 duration
(setq org-clock-out-remove-zero-time-clocks t)
;; Clock out when moving task to a done state
(setq org-clock-out-when-done t)
;; Save the running clock and all clock history when exiting Emacs, load it on startup
(setq org-clock-persist t)
;; Include current clocking task in clock reports
(setq org-clock-report-include-clocking-task t)
Habit

org-habit, I use this for repeating tasks (such as exercise) in my org-agenda view. This allows for collecting certain datapoints in variables / notes.

(require 'org-habit)
(defun get-habits-from-file ()
  (org-map-entries
   (lambda ()
     (when (string= (org-entry-get nil "STYLE") "habit")
       (cons (org-get-heading t t t t)
             (org-entry-get nil "LAST_REPEAT"))))
   "STYLE=\"habit\"" 'file))
 
(defun count-habit-completions (last-repeat)
  (let* ((now (current-time))
         (week-start (time-subtract now (days-to-time (nth 6 (decode-time now)))))
         (last-done (org-time-string-to-time last-repeat)))
    (if (time-less-p week-start last-done)
        1
      0)))
 
(defun parse-habit-data (habit-name last-repeat)
  (let ((data '())
        (max-streak 0)
        (current-streak 0))
    (org-map-entries
     (lambda ()
       (let ((state-changes (org-entry-get nil "LOGGING" t)))
         (when state-changes
           (dolist (change (split-string state-changes "\n"))
             (when (string-match "\\[$[0-9]+-[0-9]+-[0-9]+$.*$$ State \"DONE\"" change)
               (push (match-string 1 change) data))))))
     (concat "+STYLE=\"habit\"+" (regexp-quote habit-name))
     'file)
    (setq data (nreverse data))
    (let* ((now (current-time))
           (streak-end now)
           (day-sec 86400))
      (when last-repeat
        (push last-repeat data))
      (dolist (date data)
        (let ((date-time (org-time-string-to-time date)))
          (if (time-less-p
               (time-subtract streak-end (seconds-to-time day-sec))
               date-time)
              (setq current-streak (1+ current-streak))
            (setq max-streak (max max-streak current-streak))
            (setq current-streak 1))
          (setq streak-end date-time)))
      (setq max-streak (max max-streak current-streak)))
    (list current-streak max-streak)))
 
(defun calculate-trend (habit-name)
  (let* ((last-repeat (cdr (assoc habit-name (get-habits-from-file))))
         (this-week (count-habit-completions last-repeat))
         (last-week-time (time-subtract (current-time) (days-to-time 7)))
         (last-week (if (time-less-p (org-time-string-to-time last-repeat) last-week-time) 0 1)))
    (cond ((> this-week last-week) "")
          ((< this-week last-week) "")
          (t ""))))
 
(defun insert-ultra-fancy-habit-summary ()
  (interactive)
  (let ((habits (get-habits-from-file)))
    (insert "| Habit | This Week | Streak | Max Streak | Trend |\n|---|---|---|---|---|\n")
    (dolist (habit habits)
      (let* ((name (car habit))
             (last-repeat (cdr habit))
             (this-week (count-habit-completions last-repeat))
             (streak-data (parse-habit-data name last-repeat))
             (streak (car streak-data))
             (max-streak (cadr streak-data))
             (trend (calculate-trend name)))
        (insert (format "| %s | %d/7 | %d | %d | %s |\n"
                        name this-week streak max-streak trend))))
    (org-table-align)))
 
(global-set-key (kbd "C-c m") 'insert-ultra-fancy-habit-summary)
Hugo

I use hugo for my site, so these are some of the default settings.

(setq-default org-hugo-base-dir "~/Documents/Code/justin.vc"
              org-hugo-section "main"
              org-hugo-front-matter-format "yaml"
              org-hugo-auto-set-lastmod t)
Agenda

Adding the proper files to pull agenda from. This adds in the project-based files in addition, since each could/can have their own todo.org file.

(setq org-agenda-files (apply 'append
      (mapcar
       (lambda (directory)
         (directory-files-recursively
           directory org-agenda-file-regexp))
           '("~/.org/"))))

The agenda is nice, but a souped up version is nicer.

(package! org-super-agenda)
(eval-after-load 'org
  '(progn
     (setq org-agenda-start-day "-0d")
     (setq org-agenda-start-on-weekday nil)))
 
(use-package! org-super-agenda
  :after org-agenda
  :init
  (setq
        org-agenda-time-grid
        (quote
         ((daily today require-timed)
          (0700 0800 0900 1000 1100 1200 1300 1400 1500 1600 1700 1800 1900 2000 2100 2200)
          "......" "-----------------------------------------------------"))
        org-agenda-skip-scheduled-if-done t
        org-agenda-skip-deadline-if-done t
        org-agenda-include-deadlines t
        org-agenda-include-diary t
        org-agenda-block-separator nil
        org-agenda-compact-blocks t
        org-agenda-span 1
        org-agenda-start-with-log-mode t
        org-agenda-custom-commands
        '(("o" "Overview"
                    ((agenda "" ((org-agenda-span 'day)
                                 (org-super-agenda-groups
                                  '((:name "\nToday"
                                           :time-grid t
                                           :date today
                                           :todo "TODAY"
                                           :scheduled today
                                           :order 1)))))
                     (alltodo "" ((org-agenda-overriding-header "\nCategories")
                                  (org-super-agenda-groups
                                   '((:name "Next"
                                      :todo "NEXT"
                                      :order 2)
                                     (:name "Important"
                                      :tag "Important"
                                      :priority "A"
                                      :order 6)
                                     (:name "Due Today"
                                      :deadline today
                                      :order 3)
                                     (:name "Due Soon"
                                      :deadline future
                                      :order 8)
                                     (:name "Overdue"
                                      :deadline past
                                      :face error
                                      :order 7)
                                     (:name "To read"
                                      :tag "Read"
                                      :order 30)
                                     (:name "Waiting"
                                      :todo "WAITING"
                                      :order 20)
                                     (:name "Trivial"
                                            :priority<= "C"
                                            :tag ("Trivial" "Unimportant")
                                            :todo ("SOMEDAY")
                                            :order 90)
                                     (:discard (:anything t))))))))))
  :config
  (org-super-agenda-mode))
Capture

Let’s setup some org-capture templates, and make them visually nice to access.

doct (Declarative Org Capture Templates) seems to be a nicer way to set up org-capture.

(package! doct
  :recipe (:host github :repo "progfolio/doct")
  :pin "506c22f365b75f5423810c4933856802554df464")
(use-package! doct
  :commands doct)
(after! org-capture
  <<prettify-capture>>
  (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))
 
  (defvar +org-capture-recipes  "~/.org/recipes.org")
  (defvar +org-capture-slipbox  "~/.org/brain/slip-box.org")
 
  (defun jethro/org-capture-slipbox ()
     (interactive)
     (org-capture nil "s"))
 
  (defun set-org-capture-templates ()
    (setq org-capture-templates
          (doct `(("Inbox" :keys "t"
                   :file +org-capture-todo-file
                   :prepend t
                   :headline "Inbox"
                   :type entry
                   :template ("* TODO %?"
                              "%i %a"))
                  ("Slip-box" :keys "s"
                   :file +org-capture-slipbox
                   :prepend t
                   :headline "Inbox"
                   :type entry
                   :template ("* %?"))
                  ("Link" :keys "L"
                   :file +org-capture-slipbox
                   :prepend t
                   :headline "Inbox"
                   :type entry
                   :immediate-finish t
                   :template ("* IDEA [#C] %:description\n:PROPERTIES:\n:CREATED: %U\n:END:\n%:link\n%:initial\n"))
                  ("Interesting" :keys "i"
                   :file +org-capture-todo-file
                   :prepend t
                   :headline "Interesting"
                   :type entry
                   :template ("* [ ] %{desc}%? :%{i-type}:"
                              "%i %a")
                   :children (("Webpage" :keys "w"
                               :desc "%(org-cliplink-capture) "
                               :i-type "read:web")
                              ("Article" :keys "a"
                               :desc ""
                               :i-type "read:reaserch")
                              ("\tRecipe" :keys "r"
                               :file +org-capture-recipes
                               :headline "Unsorted"
                               :template "%(org-chef-get-recipe-from-url)")))
                  ("Tasks" :keys "k"
                   :file +org-capture-todo-file
                   :prepend t
                   :headline "Tasks"
                   :type entry
                   :template ("* TODO %? %^G%{extra}"
                              "%i %a")
                   :children (("General Task" :keys "k"
                               :extra "")
                              ("Task with deadline" :keys "d"
                               :extra "\nDEADLINE: %^{Deadline:}t")
                              ("Scheduled Task" :keys "s"
                               :extra "\nSCHEDULED: %^{Start time:}t")))
                  ("Project" :keys "p"
                   :prepend t
                   :type entry
                   :headline "Inbox"
                   :template ("* %{time-or-todo} %?"
                              "%i"
                              "%a")
                   :file ""
                   :custom (:time-or-todo "")
                   :children (("Project-local todo" :keys "t"
                               :time-or-todo "TODO"
                               :file +org-capture-project-todo-file)
                              ("Project-local note" :keys "n"
                               :time-or-todo "%U"
                               :file +org-capture-project-notes-file)
                              ("Project-local changelog" :keys "c"
                               :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)))))))
 
  (set-org-capture-templates)
  (unless (display-graphic-p)
    (add-hook 'server-after-make-frame-hook
              (defun org-capture-reinitialise-hook ()
                (when (display-graphic-p)
                  (set-org-capture-templates)
                  (remove-hook 'server-after-make-frame-hook
                               #'org-capture-reinitialise-hook))))))

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)))

Some functions from Jethro Kuan used to process the captured inbox

(defun jethro/org-process-inbox ()
  "Called in org-agenda-mode, processes all inbox items."
  (interactive)
  (org-agenda-bulk-mark-regexp "inbox:")
  (jethro/bulk-process-entries))
 
(defvar jethro/org-current-effort "1:00"
  "Current effort for agenda items.")
 
(defun jethro/my-org-agenda-set-effort (effort)
  "Set the effort property for the current headline."
  (interactive
   (list (read-string (format "Effort [%s]: " jethro/org-current-effort) nil nil jethro/org-current-effort)))
  (setq jethro/org-current-effort effort)
  (org-agenda-check-no-diary)
  (let* ((hdmarker (or (org-get-at-bol 'org-hd-marker)
                       (org-agenda-error)))
         (buffer (marker-buffer hdmarker))
         (pos (marker-position hdmarker))
         (inhibit-read-only t)
         newhead)
    (org-with-remote-undo buffer
      (with-current-buffer buffer
        (widen)
        (goto-char pos)
        (org-show-context 'agenda)
        (funcall-interactively 'org-set-effort nil jethro/org-current-effort)
        (end-of-line 1)
        (setq newhead (org-get-heading)))
      (org-agenda-change-all-lines newhead hdmarker))))
 
(defun jethro/org-agenda-process-inbox-item ()
  "Process a single item in the org-agenda."
  (org-with-wide-buffer
   (org-agenda-set-tags)
   (org-agenda-priority)
   (call-interactively 'jethro/my-org-agenda-set-effort)
   (org-agenda-refile nil nil t)))
 
(defun jethro/bulk-process-entries ()
  (let ())
  (if (not (null org-agenda-bulk-marked-entries))
      (let ((entries (reverse org-agenda-bulk-marked-entries))
            (processed 0)
            (skipped 0))
        (dolist (e entries)
          (let ((pos (text-property-any (point-min) (point-max) 'org-hd-marker e)))
            (if (not pos)
                (progn (message "Skipping removed entry at %s" e)
                       (cl-incf skipped))
              (goto-char pos)
              (let (org-loop-over-headlines-in-active-region) (funcall 'jethro/org-agenda-process-inbox-item))
              ;; `post-command-hook' is not run yet.  We make sure any
              ;; pending log note is processed.
              (when (or (memq 'org-add-log-note (default-value 'post-command-hook))
                        (memq 'org-add-log-note post-command-hook))
                (org-add-log-note))
              (cl-incf processed))))
        (org-agenda-redo)
        (unless org-agenda-persistent-marks (org-agenda-bulk-unmark-all))
        (message "Acted on %d entries%s%s"
                 processed
                 (if (= skipped 0)
                     ""
                   (format ", skipped %d (disappeared before their turn)"
                           skipped))
                 (if (not org-agenda-persistent-marks) "" " (kept marked)")))))
 
(defun jethro/org-inbox-capture ()
  (interactive)
  "Capture a task in agenda mode."
  (org-capture nil "t"))
 
(after! org-agenda (map! :map org-agenda-mode-map
      "i" #'org-agenda-clock-in
      "I" #'jethro/clock-in-and-advance
      "r" #'jethro/org-process-inbox
      "R" #'org-agenda-refile
      "C" #'jethro/org-inbox-capture))
 
(defun jethro/advance-todo ()
  (org-todo 'right)
  (remove-hook 'org-clock-in-hook #'jethro/advance-todo))
 
(defun jethro/clock-in-and-advance ()
  (interactive)
  (add-hook 'org-clock-in-hook 'jethro/advance-todo)
  (org-agenda-clock-in))
Protocol
(require 'org-protocol)
ox-hugo

For putting quotebacks in my org files - which then I export to my site

(defun justin/org-insert-indented-inline-html ()
  "Insert an indented inline HTML export block under the current org header.
   Uses clipboard content if it contains 'quotebacks', otherwise prompts for input.
   Cleans up the HTML by removing %20 from title and removing the script tag."
  (interactive)
  (let* ((element (org-element-at-point))
         (type (org-element-type element))
         (clipboard-content (with-temp-buffer
                              (clipboard-yank)
                              (buffer-string)))
         (html-content (if (s-contains? "quotebacks" clipboard-content)
                           clipboard-content
                         (read-string "Enter HTML content: ")))
         (content-no-script (replace-regexp-in-string "<script[^>]*>.*?</script>" "" html-content t t))
         (cleaned-content (replace-regexp-in-string
                           "data-title=\"$[^\"]*$\""
                           (lambda (match)
                             (let ((title (match-string 1 match)))
                               (format "data-title=\"%s\""
                                       (replace-regexp-in-string "%20" " " title))))
                           content-no-script t))
         (indented-content (format "<div class=\"org-indent\">\n%s\n</div>" cleaned-content)))
    (if (eq type 'headline)
        (progn
          (org-end-of-meta-data)
          (insert (format "\n#+BEGIN_EXPORT html\n%s\n#+END_EXPORT\n" indented-content)))
      (message "Not at a headline."))))
 
;; Bind this function to a key in org-mode-map
(define-key org-mode-map (kbd "C-c h") #'justin/org-insert-indented-inline-html)
Roam
Basic settings
(use-package org-roam
  :after org
  :init
  (setq org-roam-directory "~/.org/brain/"
        org-roam-db-location "~/.org/brain/org-roam.db")
   :custom
   (org-roam-database-connector 'sqlite-builtin))

That said, if the directory doesn’t exist we likely don’t want to be using roam. Since we don’t want to trigger errors (which will happen as soon as roam tries to initialize), let’s not load roam.

(package! org-roam :disable t)
Export, saving, and hook settings

Just some functions for exporting and hooks

(defun justin/my-org-hugo-export-function ()
  "Strips out the feed portion of a link for my website."
  (interactive)
  (let ((file (buffer-file-name)))
    (with-temp-buffer
      (insert-file-contents file)
      (org-mode)  ;; Set the mode to org-mode
      (goto-char (point-min))
           (while (re-search-forward
             (rx "/"
                 (and (or "feed" "rss" "rss " "atom.xml" "rss.xml" "blog-feed.xml" "feed.xml" "index.xml")
                      (zero-or-more "/") (or eos eol string-end))) nil t)
       (replace-match ""))
     (org-hugo-export-to-md))))

This will add a “stub” tag to org-roam (those that have IDs) org files when they’re < a certain buffer size. Could use a refactor, but it works.

(defun justin/org-roam-tag-stubs-on-save ()
  (when (and (eq major-mode 'org-mode)
             (org-entry-get (point) "ID")
             (not (string-prefix-p (expand-file-name "~/.org/brain/daily/")
                                   (buffer-file-name))))
    (if (< (buffer-size) 250)
        (unless (member "stub" (org-roam-node-tags (org-roam-node-at-point)))
          (org-roam-tag-add '("stub")))
      (when (and (> (buffer-size) 251)
                 (member "stub" (org-roam-node-tags (org-roam-node-at-point))))
        (org-roam-tag-remove '("stub"))))))
 
(add-hook 'after-save-hook #'justin/org-roam-tag-stubs-on-save)

This is for creating my ‘references’ page by transcluding tags.

 

testing out exporting to hugo

(defun export-org-roam-to-hugo ()
  "Export all Org-roam files to Hugo-compatible markdown, excluding those tagged with 'stub'."
  (interactive)
  (dolist (file (org-roam-list-files))
    (with-current-buffer (find-file-noselect file)
      (unless (member "stub" (org-get-tags))
        (let ((org-hugo-base-dir "~/Documents/Code/justin.vc")
              (org-hugo-section "main"))  ;
          (org-hugo-export-wim-to-md))))))
Searching

Just for searching my org-roam notes - for -SOME- reason the globs aren’t respected. Need to figure this out later.

(defun justin/org-roam-rg-search ()
  "Search org-roam directory using consult-ripgrep. With live-preview."
  (interactive)
    (let ((consult-ripgrep-command "rg --null --ignore-case --type org --line-buffered --color=always\
--max-columns=500 --no-heading --line-number . --glob=!biblio.bib --glob=!quoteback.json -e ARG OPTS"))
    (consult-ripgrep org-roam-directory)))
Roam Capture settings

These are settings related to the way I do work, initially inspired by Jethro Kuan’s org-roam guide and this post on issuecloser.com

(setq time-stamp-active t
      time-stamp-start "#\\+last_modified:[ \t]*"
      time-stamp-end "$"
      time-stamp-format "\[%Y-%02m-%02d %3a %02H:%02M\]")
(add-hook 'before-save-hook 'time-stamp nil)
 
(setq org-roam-capture-templates
      '(("m" "main" plain
         "%?"
         :if-new
         (file+head "main/${slug}.org"
          "#+title: ${title}
          #+hugo_tags: noexport
          #+HTML_CONTAINER: div
          #+HTML_CONTAINER_CLASS: jvc
          #+date: %U\n\n")
         :immediate-finish t
         :unnarrowed t)
        ("p" "person" plain
         "%?"
         :if-new
         (file+head "main/${slug}.org"
          "#+title: ${title}
           #+filetags: :person:
          #+date: %U\n\n")
         :immediate-finish t
         :unnarrowed t)
        ("w" "work" plain
         "%?"
         :if-new
         (file+head "work/${slug}.org"
          "#+title: ${title}
           #+filetags: :private:
          #+date: %U\n\n")
         :immediate-finish t
         :unnarrowed t)
        ("r" "reference" plain
         "%?"
         :if-new
         (file+head "reference/${title}.org"
          "#+title: ${title}
          #+date: %U\n\n")
         :immediate-finish t
         :unnarrowed t)
        ("a" "article" plain
         "%?"
         :if-new
         (file+head "articles/${title}.org"
                   "#+HUGO_BASE_DIR: ~/Documents/Code/justin.vc
                   #+HUGO_SECTION: ./posts
                   #+TITLE: ${title}
                   #+DATE: %U
                   #+HUGO_TAGS: draft
                   #+macro: sidenote @@html:{{%/* sidenote \"$1\" $2 */%}} $3 {{%/* /sidenote */%}}@@
                   #+HUGO_DRAFT: true\n")
         :immediate-finish t
         :unnarrowed t)))
 

Capture Templates for Daily

(setq org-roam-dailies-capture-templates
      '(("d" "daily" plain
         "* Agenda\n** Tasks\n- [ ] %?\n\n* Notes\n** Reading\n- \n\n* Journal\n"
         :target (file+head "%<%Y/%m/%Y-%m-%d>.org"
                            "#+title: %<%Y-%m-%d>
                            #+HTML_CONTAINER: div
                            #+HTML_CONTAINER_CLASS: jvc
                            #+date: %U
                            #+hugo_tags: noexport\n\n")
         :unnarrowed t
         :empty-lines 1
                )))
(after! org-roam
(cl-defmethod org-roam-node-type ((node org-roam-node))
  "Return the TYPE of NODE."
  (condition-case nil
      (file-name-nondirectory
       (directory-file-name
        (file-name-directory
         (file-relative-name
          (org-roam-node-file node)
          org-roam-directory))))
    (error ""))))
Modeline file name

All those numbers! It’s messy. Let’s adjust this in a similar way that I have in the Window title.

(defadvice! doom-modeline--buffer-file-name-roam-aware-a (orig-fun)
  :around #'doom-modeline-buffer-file-name ; takes no args
  (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) "
       (subst-char-in-string ?_ ?  buffer-file-name))
    (funcall orig-fun)))
Graph view

Org-roam is nice by itself, but there are so extra nice packages which integrate with it.

(package! org-roam-ui :recipe (:host github :repo "org-roam/org-roam-ui" :files ("*.el" "out")))
(package! websocket :pin "82b370602fa0158670b1c6c769f223159affce9b") ; dependency of `org-roam-ui'
(use-package! websocket
  :after org-roam)
 
(use-package! org-roam-ui
  :after org-roam
  :commands org-roam-ui-open
  :hook (org-roam . org-roam-ui-mode)
  :config
  (require 'org-roam) ; in case autoloaded
  (defun org-roam-ui-open ()
    "Ensure the server is active, then open the roam graph."
    (interactive)
    (unless org-roam-ui-mode (org-roam-ui-mode 1))
    (browse-url-xdg-open (format "http://localhost:%d" org-roam-ui-port))))
Macros

This is for adding new global macros for exporting from org-mode, primarily as a mechanism to add new shortcodes to ox-hugo.

(setq org-export-global-macros '(
  ("sidenote" . "@@html:{{%/* sidenote $1 $2 */%}} $3 {{%/* /sidenote */%}}@@")
        ))
Noter

Config for org-noter, I use a similar structure to Jethro Kuan’s notes, and like to set up my org notes for org-noter to match the folders.

(setq
        org-noter-notes-search-path '("~/.org/brain/references")
        org-noter-separate-notes-from-heading t
        org-noter-always-create-frame nil) ; keeps it in the same buffer
Reference

Jethro Kuan’s function for parsing references (+ adding stuff to make it easier for my notes).

 
(setq citar-bibliography '("~/.org/brain/biblio.bib"))
(setq  bibtex-completion-bibliograph "~/.org/brain/biblio.bib")
 
(defun justin/org-roam-node-from-cite (keys)
    (interactive (list (citar-select-ref)))
    (let
        ((title (citar-format--entry "${author editor} :: ${title}" (citar-get-entry keys))))
     (org-roam-capture- :templates
                        '(("r" "references" plain "%?" :if-new
                           (file+head "references/${citekey}.org"
                            ":PROPERTIES:\n:ROAM_REFS: [cite:@${citekey}]\n:END:
                             #+title: ${title}
                             #+hugo_tags: noexport
                             #+HTML_CONTAINER: div
                             #+HTML_CONTAINER_CLASS: jvc
                             #+HUGO_SECTION: ./references
                             #+date: %U\n\n")
                           :immediate-finish t
                           :unnarrowed t))
                        :info (list :citekey keys)
                        :node (org-roam-node-create :title title)
                        :props '(:finalize find-file))))
 
;; rebind citar-open-notes for convenience so I don't accidentally create notes in wrong spot
(fset 'citar-open-notes #'justin/org-roam-node-from-cite)
Nicer generated heading IDs

Thanks to alphapapa’s unpackaged.el.

By default, Org generated heading IDs like #org80fc2a5 which … works, but has two issues

  • It’s uninformative, I have no idea what’s referenced
  • If I export the same file, everything will change. Now, while without hardcoded values it’s impossible to set references in stone, it would be nice for there to be a decent chance of staying the same.

Both of these issues can be addressed by generating IDs like #language-configuration, which is what I’ll do here.

It’s worth noting that alphapapa’s use of url-hexify-string seemed to cause me some issues. Replacing that in a53899 resolved this for me. To go one step further, I create a function for producing nice short links, like an inferior version of reftex-label.

(defvar org-reference-contraction-max-words 3
  "Maximum number of words in a reference reference.")
(defvar org-reference-contraction-max-length 35
  "Maximum length of resulting reference reference, including joining characters.")
(defvar org-reference-contraction-stripped-words
  '("the" "on" "in" "off" "a" "for" "by" "of" "and" "is" "to")
  "Superfluous words to be removed from a reference.")
(defvar org-reference-contraction-joining-char "-"
  "Character used to join words in the reference reference.")
 
(defun org-reference-contraction-truncate-words (words)
  "Using `org-reference-contraction-max-length' as the total character 'budget' for the WORDS
and truncate individual words to conform to this budget.
 
To arrive at a budget that accounts for words undershooting their requisite average length,
the number of characters in the budget freed by short words is distributed among the words
exceeding the average length.  This adjusts the per-word budget to be the maximum feasable for
this particular situation, rather than the universal maximum average.
 
This budget-adjusted per-word maximum length is given by the mathematical expression below:
 
max length = \\floor{ \\frac{total length - chars for seperators - \\sum_{word \\leq average length} length(word) }{num(words) > average length} }"
  ;; trucate each word to a max word length determined by
  ;;
  (let* ((total-length-budget (- org-reference-contraction-max-length  ; how many non-separator chars we can use
                                 (1- (length words))))
         (word-length-budget (/ total-length-budget                      ; max length of each word to keep within budget
                                org-reference-contraction-max-words))
         (num-overlong (-count (lambda (word)                            ; how many words exceed that budget
                                 (> (length word) word-length-budget))
                               words))
         (total-short-length (-sum (mapcar (lambda (word)                ; total length of words under that budget
                                             (if (<= (length word) word-length-budget)
                                                 (length word) 0))
                                           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)))
            words)))
 
(defun org-reference-contraction (reference-string)
  "Give a contracted form of REFERENCE-STRING that is only contains alphanumeric characters.
Strips 'joining' words present in `org-reference-contraction-stripped-words',
and then limits the result to the first `org-reference-contraction-max-words' words.
If the total length is > `org-reference-contraction-max-length' then individual words are
truncated to fit within the limit using `org-reference-contraction-truncate-words'."
  (let ((reference-words
         (-filter (lambda (word)
                    (not (member word org-reference-contraction-stripped-words)))
                  (split-string
                   (->> reference-string
                        downcase
                        (replace-regexp-in-string "$$\\[[^]]+$$$$$[^]]+$$$\\]" "\\1") ; get description from org-link
                        (replace-regexp-in-string "[-/ ]+" " ") ; replace seperator-type chars with space
                        puny-encode-string
                        (replace-regexp-in-string "^xn--$.*?$ ?-?$[a-z0-9]+$$" "\\2 \\1") ; rearrange punycode
                        (replace-regexp-in-string "[^A-Za-z0-9 ]" "") ; strip chars which need %-encoding in a uri
                        ) " +"))))
    (when (> (length reference-words)
             org-reference-contraction-max-words)
      (setq reference-words
            (cl-subseq reference-words 0 org-reference-contraction-max-words)))
 
    (when (> (apply #'+ (1- (length reference-words))
                    (mapcar #'length reference-words))
             org-reference-contraction-max-length)
      (setq reference-words (org-reference-contraction-truncate-words reference-words)))
 
    (string-join reference-words org-reference-contraction-joining-char)))

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)))
(unpackaged/org-export-html-with-useful-ids-mode 1) ; ensure enabled, and advice run
 
(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-reference-contraction (substring-no-properties title))
                        (unless (or headline-p (org-element-property :name datum))
                          (concat ","
                                  (pcase (car datum)
                                    ('src-block "code")
                                    ('example "example")
                                    ('fixed-width "mono")
                                    ('property-drawer "properties")
                                    (_ (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-reference-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)

We also need to redefine (org-export-format-reference) as it now may be passed a string as well as a number.

(defadvice! org-export-format-reference-a (reference)
  "Format REFERENCE into a string.
 
REFERENCE is a either a number or a string representing a reference,
as returned by `org-export-new-reference'."
  :override #'org-export-format-reference
  (if (stringp reference) reference (format "org%07x" reference)))
Nicer org-return

Once again, from unpackaged.el

(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))
               (while (not (looking-back "$?:[[:blank:]]?\n$\\{3\\}" nil))
                 (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)
Snippet Helpers

I often want to set src-block headers, and it’s a pain to

  • type them out
  • remember what the accepted values are
  • oh, and specifying the same language again and again

We can solve this in three steps

  • having one-letter snippets, conditioned on (point) being within a src header
  • creating a nice prompt showing accepted values and the current default
  • pre-filling the src-block language with the last language used

For header args, the keys I’ll use are

  • r for :results
  • e for :exports
  • v for :eval
  • s for :session
  • d for :dir
(defun +yas/org-src-header-p ()
  "Determine whether `point' is within a src-block header or header-args."
  (pcase (org-element-type (org-element-context))
    ('src-block (< (point) ; before code part of the src-block
                   (save-excursion (goto-char (org-element-property :begin (org-element-context)))
                                   (forward-line 1)
                                   (point))))
    ('inline-src-block (< (point) ; before code part of the inline-src-block
                          (save-excursion (goto-char (org-element-property :begin (org-element-context)))
                                          (search-forward "]{")
                                          (point))))
    ('keyword (string-match-p "^header-args" (org-element-property :value (org-element-context))))))

Now let’s write a function we can reference in yasnippets to produce a nice interactive way to specify header args.

(defun +yas/org-prompt-header-arg (arg question values)
  "Prompt the user to set ARG header property to one of VALUES with QUESTION.
The default value is identified and indicated. If either default is selected,
or no selection is made: nil is returned."
  (let* ((src-block-p (not (looking-back "^#\\+property:[ \t]+header-args:.*" (line-beginning-position))))
         (default
           (or
            (cdr (assoc arg
                        (if src-block-p
                            (nth 2 (org-babel-get-src-block-info t))
                          (org-babel-merge-params
                           org-babel-default-header-args
                           (let ((lang-headers
                                  (intern (concat "org-babel-default-header-args:"
                                                  (+yas/org-src-lang)))))
                             (when (boundp lang-headers) (eval lang-headers t)))))))
            ""))
         default-value)
    (setq values (mapcar
                  (lambda (value)
                    (if (string-match-p (regexp-quote value) default)
                        (setq default-value
                              (concat value " "
                                      (propertize "(default)" 'face 'font-lock-doc-face)))
                      value))
                  values))
    (let ((selection (consult--read values :prompt question :default default-value)))
      (unless (or (string-match-p "(default)$" selection)
                  (string= "" selection))
        selection))))

Finally, we fetch the language information for new source blocks.

Since we’re getting this info, we might as well go a step further and also provide the ability to determine the most popular language in the buffer that doesn’t have any header-args set for it (with #+properties).

(defun +yas/org-src-lang ()
  "Try to find the current language of the src/header at `point'.
Return nil otherwise."
  (let ((context (org-element-context)))
    (pcase (org-element-type context)
      ('src-block (org-element-property :language context))
      ('inline-src-block (org-element-property :language context))
      ('keyword (when (string-match "^header-args:$[^ ]+$" (org-element-property :value context))
                  (match-string 1 (org-element-property :value context)))))))
 
(defun +yas/org-last-src-lang ()
  "Return the language of the last src-block, if it exists."
  (save-excursion
    (beginning-of-line)
    (when (re-search-backward "^[ \t]*#\\+begin_src" nil t)
      (org-element-property :language (org-element-context)))))
 
(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 (re-search-forward "^[ \t]*#\\+begin_src" nil t)
        (push (+yas/org-src-lang) src-langs))
      (goto-char (point-min))
      (while (re-search-forward "^[ \t]*#\\+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 (cl-set-difference src-langs header-langs :test #'string=))))
Translate capital keywords (old) to lower case (new)

Everyone used to use #+CAPITAL keywords. Then people realized that #+lowercase is actually both marginally easier and visually nicer, so now the capital version is just used in the manual.

Org is standardized on lower case. Uppercase is used in the manual as a poor man’s bold, and supported for historical reasons. --- Nicolas Goaziou on the Org ML

To avoid sometimes having to choose between the hassle out of updating old documents and using mixed syntax, I’ll whip up a basic transcode-y function. It likely misses some edge cases, but should mostly work.

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

The [[yt:...]] links preview nicely, but don’t export nicely. Thankfully, we can fix that.

(org-link-set-parameters "yt" :export #'+org-export-yt)
(defun +org-export-yt (path desc backend _com)
  (cond ((org-export-derived-backend-p backend 'html)
         (format "<iframe width='440' \
height='335' \
src='https://www.youtube.com/embed/%s' \
frameborder='0' \
allowfullscreen>%s</iframe>" path (or "" desc)))
        ((org-export-derived-backend-p backend 'latex)
         (format "\\href{https://youtu.be/%s}{%s}" path (or desc "youtube")))
        (t (format "https://youtu.be/%s" path))))

Trying out a thing to play youtube links in mpv.

(defun mpv-play-url (url &rest args)
  "Play the given URL in MPV."
  (interactive)
  (start-process "my-process" nil "mpv"
                        "--speed=2.0"
                        "--pause"
                        "--cache=yes"
                        "demuxer-max-bytes=5000M"
                        "demuxer-max-back-bytes=3000M" url))
 
(setq browse-url-handlers
      '(("youtu\\.?be.*\\.xml" . browse-url-default-browser)  ; Open YouTube RSS feeds in the browser
        ("youtu\\.?be" . mpv-play-url)))                      ; Use mpv-play-url for other YouTube URLs
Fix problematic hooks

When one of the org-mode-hook functions errors, it halts the hook execution. This is problematic, and there are two hooks in particular which cause issues. Let’s make their failure less eventful.

(defadvice! shut-up-org-problematic-hooks (orig-fn &rest args)
  :around #'org-fancy-priorities-mode
  (ignore-errors (apply orig-fn args)))

Visuals

Here I try to do two things: improve the styling of the various documents, via font changes etc, and also propagate colours from the current theme.

Font Display

Mixed pitch is great. As is +org-pretty-mode, let’s use them.

(add-hook 'org-mode-hook #'+org-pretty-mode)

Let’s make headings a bit bigger

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

And the same with the title.

(custom-set-faces!
  '(org-document-title :height 1.2))

It seems reasonable to have deadlines in the error face when they’re passed.

(setq org-agenda-deadline-faces
      '((1.001 . error)
        (1.0 . org-warning)
        (0.5 . org-upcoming-deadline)
        (0.0 . org-upcoming-distant-deadline)))

We can then have quote blocks stand out a bit more by making them italic.

(setq org-fontify-quote-and-verse-blocks t)

Org files can be rather nice to look at, particularly with some of the customization’s here. This comes at a cost however, expensive font-lock. Feeling like you’re typing through molasses in large files is no fun, but there is a way I can defer font-locking when typing to make the experience more responsive.

(defun locally-defer-font-lock ()
  "Set jit-lock defer and stealth, when buffer is over a certain size."
  (when (> (buffer-size) 50000)
    (setq-local jit-lock-defer-time 0.05
                jit-lock-stealth-time 1)))
 
(add-hook 'org-mode-hook #'locally-defer-font-lock)

Apparently this causes issues with some people, but I haven’t noticed anything problematic beyond the expected slight delay in some fontification, so until I do I’ll use the above.

Fontifying inline src blocks

Org does lovely things with #+begin_src blocks, like using font-lock for language’s major-mode behind the scenes and pulling out the lovely colorful results. By contrast, inline src_ blocks are somewhat neglected.

I am not the first person to feel this way, thankfully others have taken to stackexchange to voice their desire for inline src fontification. I was going to steal their work, but unfortunately they didn’t perform true source code fontification, but simply applied the org-code face to the content.

We can do better than that, and we shall! Using org-src-font-lock-fontify-block we can apply language-appropriate syntax highlighting. Then, continuing on to {{{results(...)}}} , it can have the org-block face applied to match, and then the value-surrounding constructs hidden by mimicking the behavior of prettify-symbols-mode.

This currently only highlights a single inline src block per line. I have no idea why it stops, but I’d rather it didn’t. If you have any idea what’s going on or how to fix this please get in touch.

(setq org-inline-src-prettify-results '("" . ""))

Doom theme’s extra fontification is more problematic than helpful.

(setq doom-themes-org-fontify-special-tags nil)
Symbols

It’s also nice to change the character used for collapsed items (by default ), I think is better for indicating ‘collapsed section’. and add an extra org-bullet to the default list of four.

(setq org-ellipsis ""
      org-hide-leading-stars t)

It’s also nice to make use of the prettify-symbols-mode for a few Org syntactic tokens which we’d like to prettify that aren’t covered by org-modern or any other settings.

(appendq! +ligatures-extra-symbols
          `(:list_property ""
            :em_dash       ""
            :ellipses      ""
            :arrow_right   ""
            :arrow_left    ""
            :arrow_lr      ""
            :properties    ""
            :end           ""))
(set-ligatures! 'org-mode
  :merge t
  :list_property "::"
  :em_dash       "---"
  :ellipsis      "..."
  :arrow_right   "->"
  :arrow_left    "<-"
  :arrow_lr      "<->"
  :properties    ":PROPERTIES:"
  :end           ":END:")
LaTeX Fragments
Prettier highlighting

First off, we want those fragments to look good.

(setq org-highlight-latex-and-related '(native script entities))

However, by using native highlighting the org-block face is added, and that doesn’t look too great --- particularly when the fragments are previewed.

Ideally org-src-font-lock-fontify-block wouldn’t add the org-block face, but we can avoid advising that entire function by just adding another face with :inherit default which will override the background colour.

Inspecting org-do-latex-and-related shows that "latex" is the language argument passed, and so we can override the background as discussed above.

(require 'org-src)
(add-to-list 'org-src-block-faces '("latex" (:inherit default :extend t)))
More eager rendering

What’s better than syntax-highlighted LaTeX is rendered LaTeX though, and we can have this be performed automatically with org-fragtog.

(package! org-fragtog :pin "680606189d5d28039e6f9301b55ec80517a24005")
(use-package! org-fragtog
  :hook (org-mode . org-fragtog-mode))
Prettier rendering

It’s nice to customize the look of LaTeX fragments so they fit better in the text --- like this . Let’s start by adding a sans font. I’d also like to use some of the functionality from bmc-maths, so we’ll load that too.

(setq org-format-latex-header "\\documentclass{article}
\\usepackage[usenames]{xcolor}
 
\\usepackage[T1]{fontenc}
 
\\usepackage{booktabs}
 
\\pagestyle{empty}             % do not remove
% The settings below are copied from fullpage.sty
\\setlength{\\textwidth}{\\paperwidth}
\\addtolength{\\textwidth}{-3cm}
\\setlength{\\oddsidemargin}{1.5cm}
\\addtolength{\\oddsidemargin}{-2.54cm}
\\setlength{\\evensidemargin}{\\oddsidemargin}
\\setlength{\\textheight}{\\paperheight}
\\addtolength{\\textheight}{-\\headheight}
\\addtolength{\\textheight}{-\\headsep}
\\addtolength{\\textheight}{-\\footskip}
\\addtolength{\\textheight}{-3cm}
\\setlength{\\topmargin}{1.5cm}
\\addtolength{\\topmargin}{-2.54cm}
% my custom stuff
\\usepackage[nofont,plaindd]{bmc-maths}
\\usepackage{arev}
")

Since we can, instead of making the background colour match the default face, let’s make it transparent.

(setq org-format-latex-options
      (plist-put org-format-latex-options :background "Transparent"))
Rendering speed tests

We can either render from a dvi or pdf file, so let’s benchmark latex and pdflatex.

latex timepdflatex time
135 ± 2 ms215 ± 3 ms

On the rendering side, there are two .dvi-to-image converters which I am interested in: dvipng and dvisvgm. Then with the a .pdf we have pdf2svg. For inline preview we care about speed, while for exporting we care about file size and prefer a vector graphic.

Using the above latex expression and benchmarking lead to the following results:

dvipng timedvisvgm timepdf2svg time
89 ± 2 ms178 ± 2 ms12 ± 2 ms

Now let’s combine this to see what’s best

Tool chainTotal timeResultant file size
latex + dvipng226 ± 2 ms7 KiB
latex + dvisvgm392 ± 4 ms8 KiB
pdflatex + pdf2svg230 ± 2 ms16 KiB

So, let’s use dvipng for previewing LaTeX fragments in-Emacs, but dvisvgm for .

Unfortunately, it seems that SVG sizing is annoying ATM, so let’s actually not do this right now.

Stolen from scimax (semi-working right now)

I want fragment justification

(defun scimax-org-latex-fragment-justify (justification)
  "Justify the latex fragment at point with JUSTIFICATION.
JUSTIFICATION is a symbol for 'left, 'center or 'right."
  (interactive
   (list (intern-soft
          (completing-read "Justification (left): " '(left center right)
                           nil t nil nil 'left))))
  (let* ((ov (ov-at))
         (beg (ov-beg ov))
         (end (ov-end ov))
         (shift (- beg (line-beginning-position)))
         (img (overlay-get ov 'display))
         (img (and (and img (consp img) (eq (car img) 'image)
                        (image-type-available-p (plist-get (cdr img) :type)))
                   img))
         space-left offset)
    (when (and img
               ;; This means the equation is at the start of the line
               (= beg (line-beginning-position))
               (or
                (string= "" (s-trim (buffer-substring end (line-end-position))))
                (eq 'latex-environment (car (org-element-context)))))
      (setq space-left (- (window-max-chars-per-line) (car (image-size img)))
            offset (floor (cond
                           ((eq justification 'center)
                            (- (/ space-left 2) shift))
                           ((eq justification 'right)
                            (- space-left shift))
                           (t
                            0))))
      (when (>= offset 0)
        (overlay-put ov 'before-string (make-string offset ?\ ))))))
 
(defun scimax-org-latex-fragment-justify-advice (beg end image imagetype)
  "After advice function to justify fragments."
  (scimax-org-latex-fragment-justify (or (plist-get org-format-latex-options :justify) 'left)))
 
 
(defun scimax-toggle-latex-fragment-justification ()
  "Toggle if LaTeX fragment justification options can be used."
  (interactive)
  (if (not (get 'scimax-org-latex-fragment-justify-advice 'enabled))
      (progn
        (advice-add 'org--format-latex-make-overlay :after 'scimax-org-latex-fragment-justify-advice)
        (put 'scimax-org-latex-fragment-justify-advice 'enabled t)
        (message "Latex fragment justification enabled"))
    (advice-remove 'org--format-latex-make-overlay 'scimax-org-latex-fragment-justify-advice)
    (put 'scimax-org-latex-fragment-justify-advice 'enabled nil)
    (message "Latex fragment justification disabled")))

There’s also this lovely equation numbering stuff I’ll nick

;; Numbered equations all have (1) as the number for fragments with vanilla
;; org-mode. This code injects the correct numbers into the previews so they
;; look good.
(defun scimax-org-renumber-environment (orig-func &rest args)
  "A function to inject numbers in LaTeX fragment previews."
  (let ((results '())
        (counter -1)
        (numberp))
    (setq results (cl-loop for (begin . env) in
                           (org-element-map (org-element-parse-buffer) 'latex-environment
                             (lambda (env)
                               (cons
                                (org-element-property :begin env)
                                (org-element-property :value env))))
                           collect
                           (cond
                            ((and (string-match "\\\\begin{equation}" env)
                                  (not (string-match "\\\\tag{" env)))
                             (cl-incf counter)
                             (cons begin counter))
                            ((string-match "\\\\begin{align}" env)
                             (prog2
                                 (cl-incf counter)
                                 (cons begin counter)
                               (with-temp-buffer
                                 (insert env)
                                 (goto-char (point-min))
                                 ;; \\ is used for a new line. Each one leads to a number
                                 (cl-incf counter (count-matches "\\\\$"))
                                 ;; unless there are nonumbers.
                                 (goto-char (point-min))
                                 (cl-decf counter (count-matches "\\nonumber")))))
                            (t
                             (cons begin nil)))))
 
    (when (setq numberp (cdr (assoc (point) results)))
      (setf (car args)
            (concat
             (format "\\setcounter{equation}{%s}\n" numberp)
             (car args)))))
 
  (apply orig-func args))
 
 
(defun scimax-toggle-latex-equation-numbering ()
  "Toggle whether LaTeX fragments are numbered."
  (interactive)
  (if (not (get 'scimax-org-renumber-environment 'enabled))
      (progn
        (advice-add 'org-create-formula-image :around #'scimax-org-renumber-environment)
        (put 'scimax-org-renumber-environment 'enabled t)
        (message "Latex numbering enabled"))
    (advice-remove 'org-create-formula-image #'scimax-org-renumber-environment)
    (put 'scimax-org-renumber-environment 'enabled nil)
    (message "Latex numbering disabled.")))
 
(advice-add 'org-create-formula-image :around #'scimax-org-renumber-environment)
(put 'scimax-org-renumber-environment 'enabled t)
Org Plot

We can use some of the variables in org-plot to use the current doom theme colours.

(defvar +org-plot-term-size '(1050 . 650)
  "The size of the GNUPlot terminal, in the form (WIDTH . HEIGHT).")
 
(after! org-plot
  (defun +org-plot-generate-theme (_type)
    "Use the current Doom theme colours to generate a GnuPlot preamble."
    (format "
fgt = \"textcolor rgb '%s'\" # foreground text
fgat = \"textcolor rgb '%s'\" # foreground alt text
fgl = \"linecolor rgb '%s'\" # foreground line
fgal = \"linecolor rgb '%s'\" # foreground alt line
 
# foreground colors
set border lc rgb '%s'
# change text colors of  tics
set xtics @fgt
set ytics @fgt
# change text colors of labels
set title @fgt
set xlabel @fgt
set ylabel @fgt
# change a text color of key
set key @fgt
 
# line styles
set linetype 1 lw 2 lc rgb '%s' # red
set linetype 2 lw 2 lc rgb '%s' # blue
set linetype 3 lw 2 lc rgb '%s' # green
set linetype 4 lw 2 lc rgb '%s' # magenta
set linetype 5 lw 2 lc rgb '%s' # orange
set linetype 6 lw 2 lc rgb '%s' # yellow
set linetype 7 lw 2 lc rgb '%s' # teal
set linetype 8 lw 2 lc rgb '%s' # violet
 
# border styles
set tics out nomirror
set border 3
 
# palette
set palette maxcolors 8
set palette defined ( 0 '%s',\
1 '%s',\
2 '%s',\
3 '%s',\
4 '%s',\
5 '%s',\
6 '%s',\
7 '%s' )
"
            (doom-color 'fg)
            (doom-color 'fg-alt)
            (doom-color 'fg)
            (doom-color 'fg-alt)
            (doom-color 'fg)
            ;; colours
            (doom-color 'red)
            (doom-color 'blue)
            (doom-color 'green)
            (doom-color 'magenta)
            (doom-color 'orange)
            (doom-color 'yellow)
            (doom-color 'teal)
            (doom-color 'violet)
            ;; duplicated
            (doom-color 'red)
            (doom-color 'blue)
            (doom-color 'green)
            (doom-color 'magenta)
            (doom-color 'orange)
            (doom-color 'yellow)
            (doom-color 'teal)
            (doom-color 'violet)))
 
  (defun +org-plot-gnuplot-term-properties (_type)
    (format "background rgb '%s' size %s,%s"
            (doom-color 'bg) (car +org-plot-term-size) (cdr +org-plot-term-size)))
 
  (setq org-plot/gnuplot-script-preamble #'+org-plot-generate-theme)
  (setq org-plot/gnuplot-term-extra #'+org-plot-gnuplot-term-properties))

Babel

Doom lazy-loads babel languages, with is lovely. It also pulls in ob-async, which is nice, but it would be even better if it was used by default.

There are two caveats to ob-async:

  1. It does not support :session
    • So, we don’t want :async used when :session is set
  2. It adds a fixed delay to execution
    • This is undesirable in a number of cases, for example it’s generally unwanted with emacs-lisp code
    • As such, I also introduce a async language blacklist to control when it’s automatically enabled

Due to the nuance in the desired behavior, instead of just adding :async to org-babel-default-header-args, I advice org-babel-get-src-block-info to add :async intelligently. As an escape hatch, it also recognizes :sync as an indication that :async should not be added.

I did originally have this enabled for everything except for emacs-lisp and LaTeX (there were weird issues), but this added a ~3s “startup” cost to every src block evaluation, which was a bit of a pain. Since :async can be added easily with #+properties, I’ve turned this behavior from a blacklist to a whitelist.

(add-transient-hook! #'org-babel-execute-src-block
  (require 'ob-async))
 
(defvar org-babel-auto-async-languages '()
  "Babel languages which should be executed asyncronously by default.")
 
(defadvice! org-babel-get-src-block-info-eager-async-a (orig-fn &optional light datum)
  "Eagarly add an :async parameter to the src information, unless it seems problematic.
This only acts o languages in `org-babel-auto-async-languages'.
Not added when either:
+ session is not \"none\"
+ :sync is set"
  :around #'org-babel-get-src-block-info
  (let ((result (funcall orig-fn light datum)))
    (when (and (string= "none" (cdr (assoc :session (caddr result))))
               (member (car result) org-babel-auto-async-languages)
               (not (assoc :async (caddr result))) ; don't duplicate
               (not (assoc :sync (caddr result))))
      (push '(:async) (caddr result)))
    result))

ESS

We don’t want R evaluation to hang the editor, hence

(setq ess-eval-visibly 'nowait)

Syntax highlighting is nice, so let’s turn all of that on

(setq ess-R-font-lock-keywords
      '((ess-R-fl-keyword:keywords . t)
        (ess-R-fl-keyword:constants . t)
        (ess-R-fl-keyword:modifiers . t)
        (ess-R-fl-keyword:fun-defs . t)
        (ess-R-fl-keyword:assign-ops . t)
        (ess-R-fl-keyword:%op% . t)
        (ess-fl-keyword:fun-calls . t)
        (ess-fl-keyword:numbers . t)
        (ess-fl-keyword:operators . t)
        (ess-fl-keyword:delimiters . t)
        (ess-fl-keyword:= . t)
        (ess-R-fl-keyword:F&T . t)))

Lastly, in the event that I use JAGS, it would be nice to be able to use jags as the language identifier, not ess-jags.

(add-to-list '+org-babel-mode-alist '(jags . ess-jags))

Python

; Gets rid of the annoying garbage collection messages - not specific to Python, but still
(setq-default garbage-collection-messages nil)

Jupyter

 
(org-babel-do-load-languages
 'org-babel-load-languages
 '((emacs-lisp . t)
   (julia . t)
   (python . t)
   (jupyter . t)))
 
(setq ob-async-no-async-languages-alist '("jupyter-python" "jupyter-julia"))
(setq org-babel-default-header-args:jupyter-python '((:async . "yes")
                                                     (:session . "py")
                                                     (:kernel . "base")))
 
(setq org-babel-default-header-args:jupyter-julia '((:async . "yes")
                                                    (:session . "jl")
                                                    (:kernel . "julia-1.7.3")))
 
(defun previous-org-jupyter-kernel ()
  (save-excursion
   (if (re-search-backward " :kernel $\\w+$" nil t)
       (substring-no-properties (match-string 1))
     "base")))

PDF

MuPDF

pdf-tools is nice, but a mupdf-based solution is nicer.

(package! paper :recipe (:host github :repo "ymarco/paper-mode"
                         :files ("*.el" ".so")
                         :pre-build ("make")))
 (use-package paper
  :mode ("\\.pdf\\'"  . paper-mode)
  :mode ("\\.epub\\'"  . paper-mode)
  :config
  (require 'evil-collection-paper)
  (evil-collection-paper-setup))

R

Editor Visuals

(after! ess-r-mode
  (appendq! +ligatures-extra-symbols
            '(:assign ""
              :multiply "×"))
  (set-ligatures! 'ess-r-mode
    ;; Functional
    :def "function"
    ;; Types
    :null "NULL"
    :true "TRUE"
    :false "FALSE"
    :int "int"
    :floar "float"
    :bool "bool"
    ;; Flow
    :not "!"
    :and "&&" :or "||"
    :for "for"
    :in "%in%"
    :return "return"
    ;; Other
    :assign "<-"
    :multiply "%*%"))

Julia

As mentioned in lsp-julia#35, lsp-mode seems to serve an invalid response to the Julia server. The pseudo-fix is rather simple at least

(after! julia-mode
  (add-hook 'julia-mode-hook #'rainbow-delimiters-mode-enable)
  (add-hook! 'julia-mode-hook
    (setq-local lsp-enable-folding t
                lsp-folding-range-limit 100)))

Graphviz

A nice method of visualizing simple graphs, based on plaintext .dot / .gv files.

(package! graphviz-dot-mode :pin "6e96a89762760935a7dff6b18393396f6498f976")
(use-package! graphviz-dot-mode
  :commands graphviz-dot-mode
  :mode ("\\.dot\\'" "\\.gz\\'")
  :init
  (after! org
    (setcdr (assoc "dot" org-src-lang-modes)
            'graphviz-dot)))
 
(use-package! company-graphviz-dot
  :after graphviz-dot-mode)

Markdown

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 grayed 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))

Footnotes

  1. No, this isn’t where I’m at, I just kinda picked a random place nearby.