Skip to content

Instantly share code, notes, and snippets.

@mtnygard
Created June 6, 2024 16:22
Show Gist options
  • Save mtnygard/a78c3f40a1affeb87339c4ff55f433c8 to your computer and use it in GitHub Desktop.
Save mtnygard/a78c3f40a1affeb87339c4ff55f433c8 to your computer and use it in GitHub Desktop.
My literate Emacs config, as of June 2024. This is meant to be self-describing so future-me understands what the heck past-me was thinking.

Emacs in Org Configuration

This is a file in org-mode. It contains a mix (a tangle) of explanatory text and source blocks. The source blocks aren’t used directly here, but calling the following function will emit the file

The `header-args` property on line 2 defines where to write the “tangled” output file. The header args are injected into every code block. (Though they can be overridden per block by just specifying a different value for the argument.)

To do, to consider

Packages to look into:

  • gptel - another form of GPT integration
  • auth source - credential management without keeping things in elisp variables
  • Copilot.el - Github copilot library for Emacs
  • ChatGPT shell - would replace org-ai. Also includes DALL-E shell

Initial configuration

Very early config

Turn off some GUI chrome in early-init so the toolbar doesn’t appear and disappear during startup.

(scroll-bar-mode -1)
(tool-bar-mode -1)
(tooltip-mode -1)
(set-fringe-mode 10)
(menu-bar-mode 0)

Inspiration and a warning

First, we set up the top of the file, with a warning to Future Mike not to change this file directly. Also note the use of the file-local variable to force lexical binding. This is needed for some of the org-roam customization later.

;; -*- lexical-binding: t; -*-  
;; 
;; init.el
;;
;; DO NOT EDIT THIS FILE
;; Generated from emacs.org
;;
;; "Emacs outshines all other editing software in approximately the
;; same way that the noonday sun does the stars.  It is not just
;; bigger and brighter; it simply makes everything else vanish."
;; -Neal Stephenson, "In the Beginning was the Command Line"

Startup performance

Startup tends to trigger a lot of garbage collection. Setting the threshold high for startup will reduce the number of collections.

;; The default is 800 kilobytes.  Measured in bytes.
(setq gc-cons-threshold (* 80 1000 1000))

Startup time tends to get degrade over time. Profiling it will help me keep it under control.

(defun mtn/display-startup-time ()
  (message "Emacs loaded in %s with %d garbage collections."
           (format "%.2f seconds"
                   (float-time
                    (time-subtract after-init-time before-init-time)))
           gcs-done))

(add-hook 'emacs-startup-hook #'mtn/display-startup-time)

Know what system we’re on

Most of this configuration is generic. It should work on Linux, Mac, or Windows. A few things related to external paths and some items about I/O devices need to know which one we’re on

(defconst *is-a-mac* (eq system-type 'darwin))
(defconst *is-a-linux* (eq system-type 'gnu/linux))
(defconst *is-windows* (eq system-type 'windows-nt))

Package system setup

I’ve migrated to using `use-package` which is built-in in Emacs 29.

;; Emacs minimum vesion: 29e

I want to make sure to enable connection security before I download packages.

(setq tls-checktrust t)
(setq gnutls-verify-error t)

Setting up the package archives and package manager

(require 'package)

;; Pick my package sources
(setq package-archives '(("melpa" . "https://melpa.org/packages/")
                         ("elpa" . "https://elpa.gnu.org/packages/")))

(package-initialize)
(unless package-archive-contents
  (package-refresh-contents))

(require 'use-package-ensure)
(setq use-package-always-ensure t)  ;; force download of packages
(setq use-package-verbose t)        ;; record messages for init debugging

(use-package diminish)              ;; enables ':diminish t' in use-package macros later

If using macOS, and launched from the .app icon

Quoting from the `exec-path-from-shell` package:

On OS X (and perhaps elsewhere) the $PATH environment variable and `exec-path’ used by a windowed Emacs instance will usually be the system-wide default path, rather than that seen in a terminal window.

So, use that package

(when *is-a-mac*
  (use-package exec-path-from-shell
    :init
    (exec-path-from-shell-initialize)))

Cleaning and staying clean

Keep buffers clean

Delete trailing whitespace

(use-package ws-butler
  :diminish
  :hook
  ((prog-mode . ws-butler-mode)))

Keep folders clean

Tell Emacs to keep it’s extra files all in one place so I don’t have to see them in the working directories.

(use-package no-littering)

And keep the backup files all in one place so I don’t have to see them everywhere

(setq backup-directory-alist '(("" . "~/.emacs.d/var/backup")))

Keep customized vars out of init.el

Without this, any use of `customize` features junks up init.el, which in turn causes spurious diffs and conflicts with chezmoi (manager of my dotfiles)

(setq custom-file (locate-user-emacs-file "custom-vars.el"))
(load custom-file 'noerror 'nomessage)

Basic UI Configuration

These are some global tweaks. (Meaning, they are not specific to any particular language environment.)

(setq inhibit-startup-message t
      transient-mark-mode t
      color-theme-is-global t
      shift-select-mode nil
      echo-keystrokes 0.1
      font-lock-maximum-decoration t
      mouse-yank-at-point t
      require-final-newline t
      xterm-mouse-mode t
      save-place-file (concat user-emacs-directory "places"))

(prefer-coding-system 'utf-8)
(set-default-coding-systems 'utf-8)
(set-terminal-coding-system 'utf-8)
(set-keyboard-coding-system 'utf-8)

I like line numbers everywhere

(global-display-line-numbers-mode)
(setq display-line-numbers-width 4)

But some modes should not have line numbers (either due to performance degradation or my own UI preferences.)

;; Don't display line numbers for some modes
(dolist (mode '(org-mode-hook
                term-mode-hook
                shell-mode-hook
                treemacs-mode-hook
                eshell-mode-hook
                cider-repl-mode-hook
                deft-mode-hook))
  (add-hook mode (lambda () (display-line-numbers-mode 0))))

Bell

Don’t flash the screen or make an audible bell. Instead, politely blink the mode line.

(setq visible-bell nil)
(setq ring-bell-function
      (lambda ()
        (invert-face 'mode-line)
        (run-with-timer 0.1 nil 'invert-face 'mode-line)))

Modifier keys

On macOS, use alt for meta. The default (command key) conflicts with a bunch of macOS screen navigation hotkeys.

;;; Use alt for Meta rather than command
(setq mac-option-modifier 'meta)
(setq mac-command-modifier 'hyper)

But on X, use either:

;;; Use both alt and command for Meta when running via X
(setq x-alt-keysym 'meta)

Window switching

This allows the Shift-up, Shift-down, Shift-left, and Shift-right chords to move between panes in a frame

(windmove-default-keybindings)

Fonts

(when *is-a-mac*
  (set-frame-font "Inconsolata 18"))

(when *is-windows*
  (set-frame-font "Consolas 16"))

(defvar mtn/fixed-width-font "JetBrains Mono")
(defvar mtn/variable-width-font "Iosevka Aile")

;; Some face settings from https://systemcrafters.net/emacs-tips/presentations-with-org-present/
(set-face-attribute 'default nil :font mtn/fixed-width-font :weight 'light :height 140)
(set-face-attribute 'fixed-pitch nil :font mtn/fixed-width-font :weight 'light :height 140)
(set-face-attribute 'variable-pitch nil :font mtn/variable-width-font :weight 'light :height 140)

More advanced UI Configuration

Assistance with command panels

From the which-key github repo:

`which-key` is a minor mode for Emacs that displays the key bindings following your currently entered incomplete command (a prefix) in a popup. For example, after enabling the minor mode if you enter C-x and wait for the default of 1 second the minibuffer will expand with all of the available key bindings that follow C-x (or as many as space allows given your settings)

I prefer 0.3 seconds idle delay.

(use-package which-key
  :init (which-key-mode)
  :diminish which-key-mode)

Paren highlighting and colors

(use-package highlight-parentheses :defer t)

(use-package rainbow-delimiters
  :defer t
  :hook
  ((clojure-mode . rainbow-delimiters-mode)
   (scheme-mode . rainbow-delimiters-mode)
   (lisp-mode . rainbow-delimiters-mode)
   (emacs-lisp . rainbow-delimiters-mode)))

And the indispensible paredit. Make sure I use this for all the lisp-y things.

(use-package paredit
  :defer t
  :hook
  ((clojure-mode . paredit-mode)
   (scheme-mode . paredit-mode)
   (lisp-mode . paredit-mode)
   (emacs-lisp-mode . paredit-mode)))

(show-paren-mode t)

Folder browsing in a sidebar

Neotree

(use-package neotree :defer t)

Set up a theme

This is redundant with mtnygard/theme.el until I finish migration

;;; Define my "default" theme
(use-package all-the-icons
  :if
  (display-graphic-p))

(use-package doom-themes
  :config
  (setq doom-themes-enable-bold t
        doom-themes-enable-italic t)
  (load-theme 'doom-one-light t)
  (doom-themes-visual-bell-config)
  (doom-themes-neotree-config)
  (doom-themes-org-config))

;; A little alpha for personality
(set-frame-parameter (selected-frame) 'alpha '(97 . 100))
(add-to-list 'default-frame-alist '(alpha . (97 . 100)))

Snippets

Using good old `yasnippet`

(use-package yasnippet
  :init (yas-global-mode 1)
  :custom
  (yas-snippet-dirs (list (expand-file-name "snippets" user-emacs-directory))))

Suppress native compilation warnings buffer

I don’t need to see this buffer all the time. But in case I ever do work on elisp packages, I don’t want to completely suppress the warnings. I just don’t want to see the buffer.

;; Log native compilation warnings but don't pop up the buffer.
(setq native-comp-async-report-warnings-errors 'silent)

Multiple cursor support

I use Magnar Sveen’s multiple-cursors for the Emacs equivalent of a multiball feature on a pinball machine. When it works it’s glorious, but it’s hard to keep track of the moving parts and easy to screw it all up.

(use-package multiple-cursors
  :bind
  ("C-S-c C-S-c" . mc/edit-lines)
  ("C->" . mc/mark-next-like-this)
  ("C-<" . mc/mark-previous-like-this)
  ("C-c C-<" . mc/mark-all-like-this))

Sophisticated undo

Undo-tree is a package that visualizes, well, the tree of undo opreations that accumulate as you edit.

(use-package undo-tree
  :config
  (global-undo-tree-mode)
  (setq undo-tree-auto-save-history nil))

Move text

This is simple package to let me move text lines or regions without kill/yank combos.

(use-package drag-stuff
  :config
  (drag-stuff-global-mode 1)
  :custom
  (drag-stuff-except-modes '(clojure-mode))
  :bind
  ("M-<up>" . drag-stuff-up)
  ("M-<down>" . drag-stuff-down))

Holdovers from the Emacs Starter Kit

Key Bindings

These are key bindings I’ve gotten accustomed to. Most of them come from the Emacs Starter Kit, of which there are vestiges scattered about my config.

I’ve abandoned ido and helm for the time being. I will reintroduce one of them later once I have migrated more of my config to `use-package`

;; Home/End
(global-set-key (kbd "<home>") 'beginning-of-line)
(global-set-key (kbd "<end>") 'end-of-line)

;; You know, like Readline.
(global-set-key (kbd "C-M-h") 'backward-kill-word)

;; Align your code in a pretty way.
(global-set-key (kbd "C-x \\") 'align-regexp)

;; Perform general cleanup.
;; (global-set-key (kbd "C-c n") 'cleanup-buffer)

;; Turn on the menu bar for exploring new modes
(global-set-key (kbd "C-<f10>") 'menu-bar-mode)

;; Font size
(define-key global-map (kbd "C-+") 'text-scale-increase)
(define-key global-map (kbd "C--") 'text-scale-decrease)

;; Use regex searches by default.
(global-set-key (kbd "C-s") 'isearch-forward-regexp)
(global-set-key (kbd "\C-r") 'isearch-backward-regexp)

;; Start eshell or switch to it if it's active.
(global-set-key (kbd "C-x m") 'eshell)

;; Start a new eshell even if one is active.
(global-set-key (kbd "C-x M") (lambda () (interactive) (eshell t)))

;; Start a regular shell if you prefer that.
(global-set-key (kbd "C-x M-m") 'shell)

;; Help should search more than just commands
(global-set-key (kbd "C-h a") 'apropos)

(defun message-point ()
  (interactive)
  (message "%s" (point)))

;; For debugging Emacs modes
;; (global-set-key (kbd "C-c p") 'message-point)

;; So good!
;;(global-set-key (kbd "C-c q") 'join-line)

Affordances for all programming modes

Build up a bunch of switches that will all go into a hook

;; idle-highlight-mode highlights occurrences of the symbol
;; under the point, during idle time.
(use-package idle-highlight-mode
  :defer t)

;; We have a number of turn-on-* functions since it's advised that lambda
;; functions not go in hooks. Repeatedly evaling an add-to-list with a
;; hook value will repeatedly add it since there's no way to ensure
;; that a lambda doesn't already exist in the list.

(defun local-column-number-mode ()
  (make-local-variable 'column-number-mode)
  (column-number-mode t))

(defun local-comment-auto-fill ()
  (set (make-local-variable 'comment-auto-fill-only-comments) t)
  (auto-fill-mode t))

(defun turn-on-hl-line-mode ()
  (when (> (display-color-cells) 8) (hl-line-mode t)))

(defun turn-on-save-place-mode ()
  (setq save-place t))

(defun turn-on-whitespace ()
  (whitespace-mode t))

(defun turn-on-paredit ()
  (paredit-mode t))

(defun turn-off-tool-bar ()
  (tool-bar-mode -1))

(defun turn-on-idle-highlight ()
  (idle-highlight-mode t))

(defun add-watchwords ()
  (font-lock-add-keywords
   nil '(("\\<\\(FIX\\|TODO\\|FIXME\\|HACK\\|REFACTOR\\):"
          1 font-lock-warning-face t))))

(add-hook 'prog-mode-hook 'local-column-number-mode)
(add-hook 'prog-mode-hook 'local-comment-auto-fill)
(add-hook 'prog-mode-hook 'turn-on-hl-line-mode)
(add-hook 'prog-mode-hook 'turn-on-save-place-mode)
;;(add-hook 'prog-mode-hook 'pretty-lambdas)
(add-hook 'prog-mode-hook 'add-watchwords)
(add-hook 'prog-mode-hook 'turn-on-idle-highlight)

(defun run-prog-mode-hook ()
  "Enable things that are convenient across all coding buffers."
  (run-hooks 'prog-mode-hook))

Tabs versus spaces

Spaces.

(set-default 'indent-tabs-mode nil)
(set-default 'indicate-empty-lines t)

The `untabify` function is nice, but sometimes you’d like to just hit the whole buffer with it:

(defun untabify-buffer ()
  (interactive)
  (untabify (point-min) (point-max)))

Some buffer operations

The `untabify` function is nice, but sometimes you’d like to just hit the whole buffer with it:

(defun untabify-buffer ()
  (interactive)
  (untabify (point-min) (point-max)))

Why isn’t there a whole-buffer version of `indent`? Probably because this is all it takes:

(defun indent-buffer ()
  (interactive)
  (indent-region (point-min) (point-max)))

Combine these, plus deleting trailing whitespace into one operation:

(defun cleanup-buffer ()
  "Perform a bunch of operations on the whitespace content of a buffer."
  (interactive)
  (indent-buffer)
  (untabify-buffer)
  (delete-trailing-whitespace))

Eval a form and replace it with it’s value

(defun eval-and-replace ()
  "Replace the preceding sexp with its value."
  (interactive)
  (backward-kill-sexp)
  (condition-case nil
      (prin1 (eval (read (current-kill 0)))
             (current-buffer))
    (error (message "Invalid expression")
           (insert (current-kill 0)))))

;; Should be able to eval-and-replace anywhere.
(global-set-key (kbd "C-c e") 'eval-and-replace)

Elisp affordances

Always allow a file to declare that it uses lexical bindings

(add-to-list 'safe-local-variable-values '(lexical-binding . t))

Credentials & Security

There are some things I keep in my password manager instead of github.

(load-file "~/.passwords.el")

Knowledge Management

I’m using a combination of org-mode, org-roam, Zotero, and citar. Org-mode is for GTD and PARA. Org-roam is for Zettlekasten. Zotero and Citar are for bibliographic references.

Org-mode does double-duty at managing my emacs config, using this very file plus Babel and auto-tangling.

Org-mode is also used (occasionally) for presentations.

Sources and inspirations:

Org-mode for GTD and PARA

GTD is “Getting Things Done”, the classic productivity system from David K. Allen.

PARA is “Projects, Areas, Resources, Archives” from Tiago Forte.

I use GTD for tasks, with PARA for organizing files and artifacts related to each of those categories.

All these assets go in Dropbox

(setq mtn/org-directory "~/Dropbox/org")
(setq mtn/org-roam-directory "~/Dropbox/org-roam")
(setq mtn/bibliography-file (expand-file-name "biblio.bib" mtn/org-directory))

(defun mtn/ensure-directory (d)
  (when (not (file-directory-p d))
    (make-directory d)))

(mtn/ensure-directory mtn/org-directory)
(mtn/ensure-directory mtn/org-roam-directory)

Some of my repeating tasks have checkboxes in them. When I mark these as DONE (and org automatically updates the scheduled date and resets them to TODO) I want to clear the checkboxes so they’re empty on the next occurrence. To do that, I use some code from org-checklist. (I like this ability, but most of org-checklist is about export and printing which I don’t need.) This fragment is courtesy of Yasushi Shoji’s stackoverflow answer.

However, because these are defined before org-mode is (lazily) loaded, I actually add the hook later in the use-package block for org-mode itself.

(defun mtn/org-reset-checkbox-state-maybe ()
  "Reset all checkboxes in an entry if the `RESET_CHECK_BOXES' property is set"
  (interactive "*")
  (if (org-entry-get (point) "RESET_CHECK_BOXES")
      (org-reset-checkbox-state-subtree)))

(defun mtn/org-reset-checkbox-when-done ()
  (when (member org-state org-done-keywords) ;; org-state dynamically bound in org.el/org-todo
    (mtn/org-reset-checkbox-state-maybe)))

Here are some helper functions from “Organize Your Life in Plain Text”. These are used in the custom agenda commands.

(defun mtn/find-project-task ()
  "Move point to the parent (project) task if any"
  (save-restriction
    (widen)
    (let ((parent-task (save-excursion (org-back-to-heading 'invisible-ok) (point))))
      (while (org-up-heading-safe)
        (when (member (nth 2 (org-heading-components)) org-todo-keywords-1)
          (setq parent-task (point))))
      (goto-char parent-task)
      parent-task)))

(defun mtn/is-project-p ()
  "Any task with a todo keyword subtask"
  (save-restriction
    (widen)
    (let ((has-subtask)
          (subtree-end (save-excursion (org-end-of-subtree t)))
          (is-a-task (member (nth 2 (org-heading-components)) org-todo-keywords-1)))
      (save-excursion
        (forward-line 1)
        (while (and (not has-subtask)
                    (< (point) subtree-end)
                    (re-search-forward "^\*+ " subtree-end t))
          (when (member (org-get-todo-state) org-todo-keywords-1)
            (setq has-subtask t))))
      (and is-a-task has-subtask))))

(defun mtn/is-project-subtree-p ()
  "Any task with a todo keyword that is in a project subtree.
  Callers of this function already widen the buffer view."
  (let ((task (save-excursion (org-back-to-heading 'invisible-ok)
                              (point))))
    (save-excursion
      (mtn/find-project-task)
      (if (equal (point) task)
          nil
        t))))

(defun mtn/is-task-p ()
  "Any task with a todo keyword and no subtask"
  (save-restriction
    (widen)
    (let ((has-subtask)
          (subtree-end (save-excursion (org-end-of-subtree t)))
          (is-a-task (member (nth 2 (org-heading-components)) org-todo-keywords-1)))
      (save-excursion
        (forward-line 1)
        (while (and (not has-subtask)
                    (< (point) subtree-end)
                    (re-search-forward "^\*+ " subtree-end t))
          (when (member (org-get-todo-state) org-todo-keywords-1)
            (setq has-subtask t))))
      (and is-a-task (not has-subtask)))))

(defun mtn/is-subproject-p ()
  "Any task which is a subtask of another project"
  (let ((is-subproject)
        (is-a-task (member (nth 2 (org-heading-components)) org-todo-keywords-1)))
    (save-excursion
      (while (and (not is-subproject) (org-up-heading-safe))
        (when (member (nth 2 (org-heading-components)) org-todo-keywords-1)
          (setq is-subproject t))))
    (and is-a-task is-subproject)))

(defun mtn/list-sublevels-for-projects-indented ()
  "Set org-tags-match-list-sublevels so when restricted to a subtree we list all subtasks.
    This is normally used by skipping functions where this variable is already local to the agenda."
  (if (marker-buffer org-agenda-restrict-begin)
      (setq org-tags-match-list-sublevels 'indented)
    (setq org-tags-match-list-sublevels nil))
  nil)

(defun mtn/list-sublevels-for-projects ()
  "Set org-tags-match-list-sublevels so when restricted to a subtree we list all subtasks.
    This is normally used by skipping functions where this variable is already local to the agenda."
  (if (marker-buffer org-agenda-restrict-begin)
      (setq org-tags-match-list-sublevels t)
    (setq org-tags-match-list-sublevels nil))
  nil)

(defvar mtn/hide-scheduled-and-waiting-next-tasks t)

(defun mtn/toggle-next-task-display ()
  (interactive)
  (setq mtn/hide-scheduled-and-waiting-next-tasks (not mtn/hide-scheduled-and-waiting-next-tasks))
  (when  (equal major-mode 'org-agenda-mode)
    (org-agenda-redo))
  (message "%s WAITING and SCHEDULED NEXT Tasks" (if mtn/hide-scheduled-and-waiting-next-tasks "Hide" "Show")))

(defun mtn/skip-stuck-projects ()
  "Skip trees that are not stuck projects"
  (save-restriction
    (widen)
    (let ((next-headline (save-excursion (or (outline-next-heading) (point-max)))))
      (if (mtn/is-project-p)
          (let* ((subtree-end (save-excursion (org-end-of-subtree t)))
                 (has-next ))
            (save-excursion
              (forward-line 1)
              (while (and (not has-next) (< (point) subtree-end) (re-search-forward "^\\*+ NEXT " subtree-end t))
                (unless (member "WAITING" (org-get-tags-at))
                  (setq has-next t))))
            (if has-next
                nil
              next-headline)) ; a stuck project, has subtasks but no next task
        nil))))

(defun mtn/skip-non-stuck-projects ()
  "Skip trees that are not stuck projects"
  ;; (mtn/list-sublevels-for-projects-indented)
  (save-restriction
    (widen)
    (let ((next-headline (save-excursion (or (outline-next-heading) (point-max)))))
      (if (mtn/is-project-p)
          (let* ((subtree-end (save-excursion (org-end-of-subtree t)))
                 (has-next ))
            (save-excursion
              (forward-line 1)
              (while (and (not has-next) (< (point) subtree-end) (re-search-forward "^\\*+ NEXT " subtree-end t))
                (unless (member "WAITING" (org-get-tags-at))
                  (setq has-next t))))
            (if has-next
                next-headline
              nil)) ; a stuck project, has subtasks but no next task
        next-headline))))

(defun mtn/skip-non-projects ()
  "Skip trees that are not projects"
  ;; (mtn/list-sublevels-for-projects-indented)
  (if (save-excursion (mtn/skip-non-stuck-projects))
      (save-restriction
        (widen)
        (let ((subtree-end (save-excursion (org-end-of-subtree t))))
          (cond
           ((mtn/is-project-p)
            nil)
           ((and (mtn/is-project-subtree-p) (not (mtn/is-task-p)))
            nil)
           (t
            subtree-end))))
    (save-excursion (org-end-of-subtree t))))

(defun mtn/skip-non-tasks ()
  "Show non-project tasks.
  Skip project and sub-project tasks, habits, and project related tasks."
  (save-restriction
    (widen)
    (let ((next-headline (save-excursion (or (outline-next-heading) (point-max)))))
      (cond
       ((mtn/is-task-p)
        nil)
       (t
        next-headline)))))

(defun mtn/skip-project-trees-and-habits ()
  "Skip trees that are projects"
  (save-restriction
    (widen)
    (let ((subtree-end (save-excursion (org-end-of-subtree t))))
      (cond
       ((mtn/is-project-p)
        subtree-end)
       ((org-is-habit-p)
        subtree-end)
       (t
        nil)))))

(defun mtn/skip-projects-and-habits-and-single-tasks ()
  "Skip trees that are projects, tasks that are habits, single non-project tasks"
  (save-restriction
    (widen)
    (let ((next-headline (save-excursion (or (outline-next-heading) (point-max)))))
      (cond
       ((org-is-habit-p)
        next-headline)
       ((and mtn/hide-scheduled-and-waiting-next-tasks
             (member "WAITING" (org-get-tags-at)))
        next-headline)
       ((mtn/is-project-p)
        next-headline)
       ((and (mtn/is-task-p) (not (mtn/is-project-subtree-p)))
        next-headline)
       (t
        nil)))))

(defun mtn/skip-project-tasks-maybe ()
  "Show tasks related to the current restriction.
  When restricted to a project, skip project and sub project tasks, habits, NEXT tasks, and loose tasks.
  When not restricted, skip project and sub-project tasks, habits, and project related tasks."
  (save-restriction
    (widen)
    (let* ((subtree-end (save-excursion (org-end-of-subtree t)))
           (next-headline (save-excursion (or (outline-next-heading) (point-max))))
           (limit-to-project (marker-buffer org-agenda-restrict-begin)))
      (cond
       ((mtn/is-project-p)
        next-headline)
       ((org-is-habit-p)
        subtree-end)
       ((and (not limit-to-project)
             (mtn/is-project-subtree-p))
        subtree-end)
       ((and limit-to-project
             (mtn/is-project-subtree-p)
             (member (org-get-todo-state) (list "NEXT")))
        subtree-end)
       (t
        nil)))))

(defun mtn/skip-project-tasks ()
  "Show non-project tasks.
  Skip project and sub-project tasks, habits, and project related tasks."
  (save-restriction
    (widen)
    (let* ((subtree-end (save-excursion (org-end-of-subtree t))))
      (cond
       ((mtn/is-project-p)
        subtree-end)
       ((org-is-habit-p)
        subtree-end)
       ((mtn/is-project-subtree-p)
        subtree-end)
       (t
        nil)))))

(defun mtn/skip-non-project-tasks ()
  "Show project tasks.
  Skip project and sub-project tasks, habits, and loose non-project tasks."
  (save-restriction
    (widen)
    (let* ((subtree-end (save-excursion (org-end-of-subtree t)))
           (next-headline (save-excursion (or (outline-next-heading) (point-max)))))
      (cond
       ((mtn/is-project-p)
        next-headline)
       ((org-is-habit-p)
        subtree-end)
       ((and (mtn/is-project-subtree-p)
             (member (org-get-todo-state) (list "NEXT")))
        subtree-end)
       ((not (mtn/is-project-subtree-p))
        subtree-end)
       (t
        nil)))))

(defun mtn/skip-projects-and-habits ()
  "Skip trees that are projects and tasks that are habits"
  (save-restriction
    (widen)
    (let ((subtree-end (save-excursion (org-end-of-subtree t))))
      (cond
       ((mtn/is-project-p)
        subtree-end)
       ((org-is-habit-p)
        subtree-end)
       (t
        nil)))))

(defun mtn/skip-non-subprojects ()
  "Skip trees that are not projects"
  (let ((next-headline (save-excursion (outline-next-heading))))
    (if (mtn/is-subproject-p)
        nil
      next-headline)))

(defun mtn/skip-non-archivable-tasks ()
  "Skip trees that are not available for archiving"
  (save-restriction
    (widen)
    ;; Consider only tasks with done todo headings as archivable candidates
    (let ((next-headline (save-excursion (or (outline-next-heading) (point-max))))
          (subtree-end (save-excursion (org-end-of-subtree t))))
      (if (member (org-get-todo-state) org-todo-keywords-1)
          (if (member (org-get-todo-state) org-done-keywords)
              (let* ((daynr (string-to-number (format-time-string "%d" (current-time))))
                     (a-month-ago (* 60 60 24 (+ daynr 1)))
                     (last-month (format-time-string "%Y-%m-" (time-subtract (current-time) (seconds-to-time a-month-ago))))
                     (this-month (format-time-string "%Y-%m-" (current-time)))
                     (subtree-is-current (save-excursion
                                           (forward-line 1)
                                           (and (< (point) subtree-end)
                                                (re-search-forward (concat last-month "\\|" this-month) subtree-end t)))))
                (if subtree-is-current
                    subtree-end ; Has a date in this month or last month, skip it
                  nil))  ; available to archive
            (or subtree-end (point-max)))
        next-headline))))

Configure org-mode itself. This all goes inside one giant use-package, which gets kind of unwieldy.

I use the folloring agenda files:

  • projects.org for active projects. TODOs in here should appear in my agenda
  • areas.org for ongoing areas of concern. TODOs in here are habits and should appear in my agenda.
  • resources.org for external or third-party resources I haver saved. There should be no tasks in here, so nothing should appear in my agenda. This is a refile target.
  • archives.org for completed or canceled projects. TODOs in here are defunct and should not appear in my agenda. This is a refile target.
  • inbox.org for braindump items. Nothing should stay in here for long.
(use-package org
  :defer t
  :commands
  (org-agenda org-capture org-cdlatex-mode)

  :config
  (require 'org-mouse)

  (require 'org-habit)
  (add-to-list 'org-modules 'org-habit)

  (org-babel-do-load-languages
   'org-babel-load-languages
   '((emacs-lisp . t)
     (dot . t)
     (ditaa . t)
     (ruby . t)
     (gnuplot . t)
     (org . t)
     (plantuml . t)
     (latex . t)))

  (add-hook 'org-babel-after-execute-hook 'org-display-inline-images)
  (add-hook 'org-mode-hook 'org-display-inline-images)
  (add-hook 'org-after-todo-state-change-hook 'mtn/org-reset-checkbox-when-done)

  ;; Resize Org headings
  (dolist (face '((org-level-1 . 1.2)
                  (org-level-2 . 1.1)
                  (org-level-3 . 1.05)
                  (org-level-4 . 1.0)
                  (org-level-5 . 1.1)
                  (org-level-6 . 1.1)
                  (org-level-7 . 1.1)
                  (org-level-8 . 1.1)))
    (set-face-attribute (car face) nil :font mtn/variable-width-font :weight 'medium :height (cdr face)))

  ;; Make sure certain org faces use the fixed-pitch face when variable-pitch-mode is on
  (set-face-attribute 'org-block nil :foreground nil :inherit 'fixed-pitch)
  (set-face-attribute 'org-quote nil :height 240 :extend t :foreground "#333333" :inherit '(shadow variable-pitch))
  (set-face-attribute 'org-table nil :inherit 'fixed-pitch)
  (set-face-attribute 'org-formula nil :inherit 'fixed-pitch)
  (set-face-attribute 'org-code nil :inherit '(shadow fixed-pitch))
  (set-face-attribute 'org-verbatim nil :inherit '(shadow fixed-pitch))
  (set-face-attribute 'org-special-keyword nil :inherit '(font-lock-comment-face fixed-pitch))
  (set-face-attribute 'org-meta-line nil :inherit '(font-lock-comment-face fixed-pitch))
  (set-face-attribute 'org-checkbox nil :inherit 'fixed-pitch)

  ;; Options
  :custom
  (org-habit-graph-column 50)
  (org-confirm-babel-evaluate nil)
  (org-startup-indented nil)
  (org-pretty-entities t)
  (org-startup-with-inline-images t)
  (org-ellipsis "")
  (org-export-preserve-breaks t)
  (org-highlight-latex-and-related '(native))
  (org-src-fontify-natively t)
  (org-fontify-quote-and-verse-blocks t)
  (org-startup-folded t)
  (org-cycle-separator-lines 2)
  (org-catch-invisible-edits 'error)
  (org-ctrl-k-protect-subtree t)
  (org-image-actual-width nil)
  (org-return-follows-link t)
  (org-hide-emphasis-markers t)
  (org-hide-leading-stars t)
  (org-cycle-separator-lines 2)
  (org-log-repeat nil)
  (org-log-done nil)
  (org-latex-src-block-backend 'minted)
  (org-latex-packages-alist '(("" "minted")))
  (org-latex-tables-centered t)
  (org-insert-heading-respect-content t)

  (org-directory  mtn/org-directory)
  (org-default-notes-file (expand-file-name "inbox.org" mtn/org-directory))

  (org-capture-templates
   '(("i" "Inbox" entry (file "~/Dropbox/org/inbox.org"))
     ("t" "todo" entry (file "~/Dropbox/org/inbox.org")
      "* TODO %?\n  %i\n  %a\n")))

  (org-refile-targets '(("archive.org" :maxlevel . 2)
                        ("resources.org" :maxlevel . 3)
                        (org-agenda-files :maxlevel . 3)))
  (org-refile-allow-creating-parent-nodes 'confirm)

  (org-agenda-current-time-string "← now ─────────")
  (org-agenda-files '("projects.org" "areas.org" "inbox.org"))
  (org-agenda-tags-todo-honor-ignore-options t)
  (org-agenda-show-inherited-tags t)
  (org-agenda-remove-tags nil)
  (org-agenda-dim-blocked-tasks nil)
  (org-agenda-compact-blocks nil)
  (org-agenda-block-separator ?—)
  (org-agenda-show-all-dates t)
  (org-agenda-start-on-weekday 0)
  (org-agenda-time-grid '((daily today remove-match)
                          (0900 1100 1300 1500 1700)
                          "......"
                          "-----------------"))
  (org-agenda-format-date (lambda (date) (concat "\n" (org-agenda-format-date-aligned date))))
  (org-todo-keywords '((sequence "TODO(t)" "NEXT(n)" "DELEGATED(D)" "HOLD(h)" "WAIT(w)" "|" "DONE(d)" "CANCELED(C)")
                       (sequence "BACKLOG(l)" "WAIT(w)" "REVIEW(v)" "PLAN(p)" "READY(r)" "ACTIVE(a)" "|" "FINISHED(f)" "KILLED(k)" )))

  (org-todo-state-tags-triggers
   '(("CANCELED" ("CANCELED" . t))
     ("WAIT" ("WAITING" . t))
     ("HOLD" ("WAITING") ("HOLD" . t))
     ("DELEGATED" ("WAITING" . t) ("HOLD"))
     (done ("WAITING") ("HOLD"))
     ("TODO" ("WAITING") ("CANCELED") ("HOLD"))
     ("NEXT" ("WAITING") ("CANCELED") ("HOLD"))
     ("DONE" ("WAITING") ("CANCELED") ("HOLD"))))

  (org-agenda-custom-commands
   '((" " "Agenda"
      ((agenda "" nil)
       (tags "REFILE"
             ((org-agenda-overriding-header "Tasks to Refile")
              (org-agenda-todo-keyword-format "%-12s")
              (org-tags-match-list-sublevels nil)))
       (tags-todo "-CANCELED/!NEXT"
                  ((org-agenda-overriding-header (concat "Project Next Tasks"
                                                         (if mtn/hide-scheduled-and-waiting-next-tasks
                                                             ""
                                                           " (including WAITING and SCHEDULED tasks)")))
                   (org-agenda-skip-function 'mtn/skip-projects-and-habits-single-tasks)
                   (org-agenda-todo-keyword-format "%-12s")
                   (org-tags-match-list-sublevels t)
                   (org-agenda-todo-ignore-scheduled mtn/hide-scheduled-and-waiting-next-tasks)
                   (org-agenda-todo-ignore-deadlines mtn/hide-scheduled-and-waiting-next-tasks)
                   (org-agenda-todo-ignore-with-date mtn/hide-scheduled-and-waiting-next-tasks)
                   (org-agenda-sorting-strategy '(todo-state-down effort-up category-keep))))
       (tags-todo "-CANCELED/!"
                  ((org-agenda-overriding-header "Stuck Projects")
                   (org-agenda-skip-function 'mtn/skip-non-stuck-projects)
                   (org-agenda-todo-keyword-format "%-12s")
                   (org-agenda-sorting-strategy '(category-keep))))
       (tags-todo "-HOLD-CANCELED/!"
                  ((org-agenda-overriding-header "Projects")
                   (org-agenda-skip-function 'mtn/skip-non-projects)
                   (org-tags-match-list-sublevels t)
                   (org-agenda-todo-keyword-format "%-12s")
                   (org-agenda-sorting-strategy '(category-keep))))
       (tags-todo "-REFILE-CANCELED-WAITING-HOLD/!"
                  ((org-agenda-overriding-header (concat "Project Subtasks"
                                                         (if mtn/hide-scheduled-and-waiting-next-tasks
                                                             ""
                                                           " (including WAITING and SCHEDULED tasks)")))
                   (org-agenda-skip-function 'mtn/skip-non-project-tasks)
                   (org-agenda-todo-ignore-scheduled mtn/hide-scheduled-and-waiting-next-tasks)
                   (org-agenda-todo-ignore-deadlines mtn/hide-scheduled-and-waiting-next-tasks)
                   (org-agenda-todo-ignore-with-date mtn/hide-scheduled-and-waiting-next-tasks)
                   (org-agenda-todo-keyword-format "%-12s")
                   (org-agenda-sorting-strategy '(category-keep))))
       (tags-todo "-REFILE-CANCELLED-WAITING-HOLD/!"
                  ((org-agenda-overriding-header (concat "Standalone Tasks"
                                                         (if mtn/hide-scheduled-and-waiting-next-tasks
                                                             ""
                                                           " (including WAITING and SCHEDULED tasks)")))
                   (org-agenda-skip-function 'mtn/skip-project-tasks)
                   (org-agenda-todo-ignore-scheduled mtn/hide-scheduled-and-waiting-next-tasks)
                   (org-agenda-todo-ignore-deadlines mtn/hide-scheduled-and-waiting-next-tasks)
                   (org-agenda-todo-ignore-with-date mtn/hide-scheduled-and-waiting-next-tasks)
                   (org-agenda-todo-keyword-format "%-12s")
                   (org-agenda-sorting-strategy '(category-keep))))
       (tags-todo "-CANCELLED+WAITING|HOLD/!"
                  ((org-agenda-overriding-header (concat "Waiting Tasks"
                                                         (if mtn/hide-scheduled-and-waiting-next-tasks
                                                             ""
                                                           " (including WAITING and SCHEDULED tasks)")))
                   (org-agenda-skip-function 'mtn/skip-non-tasks)
                   (org-tags-match-list-sublevels nil)
                   (org-agenda-todo-keyword-format "%-12s")
                   (org-agenda-todo-ignore-scheduled mtn/hide-scheduled-and-waiting-next-tasks)
                   (org-agenda-todo-ignore-deadlines mtn/hide-scheduled-and-waiting-next-tasks)))
       (tags "-REFILE/"
             ((org-agenda-overriding-header "Tasks to Archive")
              (org-agenda-skip-function 'mtn/skip-non-archivable-tasks)
              (org-agenda-todo-keyword-format "%-12s")
              (org-tags-match-list-sublevels nil))))
      nil)

     ("W" "Weekly Review"
      ((agenda "" ((org-agenda-span 7))); review upcoming deadlines and appointments
                                        ; type "l" in the agenda to review logged items 
       (stuck "")                       ; review stuck projects as designated by org-stuck-projects
       (todo "PROJECT")                 ; review all projects (assuming you use todo keywords to designate projects)
       (todo "MAYBE")                   ; review someday/maybe items
       (todo "WAIT")))

     ("d" "Dashboard"
      ((agenda "" ((org-deadline-warning-days 7)))
       (todo "NEXT"
             ((org-agenda-overriding-header "Next Tasks")))
       (tags-todo "agenda/ACTIVE" ((org-agenda-overriding-header "Active Projects")))))
     ("n" "Next Tasks"
      ((todo "NEXT"
             ((org-agenda-overriding-header "Next Tasks")))))

     ("w" "Project Workflow Status"
      ((todo "WAIT"
             ((org-agenda-overriding-header "Waiting on External")
              (org-agenda-files org-agenda-files)))
       (todo "REVIEW"
             ((org-agenda-overriding-header "In Review")
              (org-agenda-files org-agenda-files)))
       (todo "PLAN"
             ((org-agenda-overriding-header "In Planning")
              (org-agenda-files org-agenda-files)))
       (todo "BACKLOG"
             ((org-agenda-overriding-header "Project Backlog")
              (org-agenda-files org-agenda-files)))
       (todo "READY"
             ((org-agenda-overriding-header "Ready for Work")
              (org-agenda-files org-agenda-files)))
       (todo "ACTIVE"
             ((org-agenda-overriding-header "Active Projects")
              (org-agenda-files org-agenda-files)))
       (todo "COMPLETED"
             ((org-agenda-overriding-header "Completed Projects")
              (org-agenda-files org-agenda-files)))
       (todo "CANCELED"
             ((org-agenda-overriding-header "Canceled Projects")
              (org-agenda-files org-agenda-files)))))))


  (org-archive-mark-done nil)
  (org-archive-location "%s_archive::* Archived Tasks")

  (org-tags-column -118)
  (org-fast-tag-selection-single-key 'expert)
  (org-tag-alist '((:startgroup)
                   ; mutually exclusive tags go here
                   (:endgroup)
                   ("@errand" . ?E)
                   ("@work" . ?W)
                   ("@home" . ?H)
                   ("agenda" . ?a)
                   ("planning" . ?p)
                   ("note" . ?n)
                   ("idea" . ?i)))

  :bind*
  (:map org-mode-map
        ("C-<return>" . org-meta-return)
        ("C-c h" . consult-org-heading)
        ("C-<tab>" . hippie-expand)
        ("C-c e" . org-latex-export-to-pdf)
        ("C-c C-<up>" . org-promote-subtree)
        ("C-c C-<down>" . org-demote-subtree)
        ("C-c 1" . org-toggle-narrow-to-subtree))
  (:map global-map
        ("C-c n n" . org-capture)
        ("C-c n a" . org-agenda)
        ("C-c S" . org-store-link)))

Org-mode appearance

I was using org-modern here. But after I had to disable indentation, entity hiding, and keyword fontification… it just wasn’t worth it. Axed it.

Org-roam for Zettelkasten

I haven’t fully adopted this yet. Most of my work is still in Roam Research. I’m trying this out though and might migrate.

My org-roam nodes are in a separate directory from agenda files. I’m keeping these separate org agendas are fast.

I have a bibliography file that Zotero+BetterBibTex keeps up to date. Citar lets me access it and reference citations in Emacs.

(use-package citar
  :after org-roam
  :custom
  (org-cite-global-bibliography (list mtn/bibliography-file))
  (org-cite-insert-processor 'citar)
  (org-cite-follow-processor 'citar)
  (org-cite-activate-processor 'citar)
  (citar-bibliography org-cite-global-bibliography)
  :bind
  (:map org-mode-map :package org ("C-c b" . #'org-cite-insert)))

Here is a convenience from System Crafters’ Build a Second Brain in Emacs. It allows me to quickly create a new note for a topic I mention without going to that buffer.

(defun org-roam-node-insert-immediate (arg &rest args)
  (interactive "P")
  (let ((args (cons arg args))
        (org-roam-capture-templates (list (append (car org-roam-capture-templates)
                                                  '(:immediate-finish t)))))
    (apply #'org-roam-node-insert args)))

And here is a nice function to create an org-roam node from a Citar citation (as managed by Zotero!). This also comes from How I Take Notes in Org-roam.

  (defun mtn/org-roam-node-from-cite (citekey)
      (interactive (list (citar-select-ref)))
      (let ((title (citar-format--entry "${author editor} :: ${title}" citekey)))
        (org-roam-capture- :templates
                           '(("r" "reference" plain "%?" :if-new
                              (file+head "reference/${citekey}.org"
                                         ":PROPERTIES:
:ROAM_REFS: [cite:@${citekey}]
:END:
#+title: ${title}\n")
                              :immediate-finish t
                              :unnarrowed t))
                           :info (list :citekey citekey)
                           :node (org-roam-node-create :title title)
                           :props '(:finalize find-file))))

Now configure org-roam itself.

(use-package org-roam
  :commands (org-roam-node-find org-roam-capture org-roam-dailies-goto-today)
  :custom
  (org-roam-v2-ack t)
  (org-roam-node-display-template
   (concat "${title:*} "
           (propertize "${tags:10}" 'face 'org-tag)))
  (org-roam-directory mtn/org-roam-directory)
  (org-roam-completion-everywhere t)

  (org-roam-capture-templates
   '(("m" "main" plain
      "%?"
      :if-new (file+head "main/${slug}.org" "#+options: _:{}\n#+options: ^:{}\n#+startup: latexpreview\n#+startup: entitiespretty\n#+startup: inlineimages\n#+title: ${title}\n\n* Metadata\n** Tags")
      :immediate-finish t
      :unnarrowed t)
     ("p" "person" plain "%?"
      :if-new (file+head "people/${slug}.org"  "#+options: _:{}\n#+options: ^:{}\n#+startup: latexpreview\n#+startup: entitiespretty\n#+startup: inlineimages\n#+title: ${title}\n\n* Metadata\n** Tags")
      :immediate-finish t
      :unnarrowed t)
     ("r" "reference" plain "%?"
      :if-new
      (file+head "reference/${title}.org" "#+title: ${title}\n")
      :immediate-finish t
      :unnarrowed t)
     ("a" "article" plain "%?"
      :if-new
      (file+head "articles/${title}.org" "#+title: ${title}\n#+filetags: :article:\n")
      :immediate-finish t
      :unnarrowed t)))

  (org-roam-dailies-capture-templates
    '(("d" "default" entry "* %?"
       :if-new (file+head+olp "%<%Y-%m-%d>.org" "#+title: %<%Y-%m-%d>\n\n* Action Items\n\n* Meetings\n\n* Scratch\n" ("Scratch"))
       :unnarrowed t)))

  (org-roam-node-display-template
   (concat "${type:15} ${title:*} " (propertize "${tags:10}" 'face 'org-tag)))

  :bind (("C-c n l" . org-roam-buffer-toggle)
         ("C-c n f" . org-roam-node-find)
         ("C-c n i" . org-roam-node-insert)
         ("C-c n I" . org-roam-node-insert-immediate)
         ("C-c n r" . mtn/org-roam-node-from-cite)
         :map org-mode-map
         ("C-M-i" . completion-at-point)
         :map org-roam-dailies-map
         ("Y" . org-roam-dailies-capture-yesterday)
         ("T" . org-roam-dailies-capture-tomorrow)
         ("C-c n g" . org-roam-graph)
         ("C-c n c" . org-roam-capture))
  :bind-keymap
  ("C-c n d" . org-roam-dailies-map)

  :config
  (require 'org-roam-dailies) ;; Ensure the keymap is available
  (org-roam-db-autosync-mode)

  ;; Use the directory name as the node "type" for display.
  (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 ""))))

Searching org-roam notes interactively and quickly is surprisingly difficult. I’ve tried Deft and NotDeft… Deft was too slow and NotDeft was too difficult to set up. For the time being, I’m using Xeft, but its setup isn’t great either. It requires a dynamic library that has to be built by hand outside of Emacs.

On first configuration, I will have to compile the Xeft module myself with something like this:

On macOS

brew install xapian
cd ~/.emacs.d/elpa/xeft-3.3
make PREFIX=/opt/homebrew

I’m not sure what the Linux equivalent is. I’ll work that out once I’m home from travel.

Once that configuration is done, this block will no longer give errors:

(use-package xeft
  :commands (xeft)
  :custom
  (xeft-directory mtn/org-roam-directory)
  (xeft-recursive t)
  (xeft-ignore-extension '("png" "pdf" "jpg" "mp3" "mp4"))
  :bind ("C-c a" . xeft))

Presenting from org-mode

Org-present does a nice job, but I want to customize some of the appearance. The start function sets up the presentation appearance: centered, larger text, visual wrapping, no line numbers, and some font customizations. The `-quit` function restores the original appearance.

Note that there could be some conflict with the theme loaded earlier.

(defun mtn/org-present-start ()
  ;; Center the presentation and wrap lines
  (setq visual-fill-column-center-text 1)
  (visual-fill-column-mode 1)
  (visual-line-mode 1)

  ;; Turn off line numbers
  (display-line-numbers-mode 0)

  ;; Make fonts more readable
  (setq-local face-remapping-alist '((default (:height 1.5) variable-pitch)
                                     (header-line (:height 4.0) variable-pitch)
                                     (org-document-title (:height 1.75) org-document-title)
                                     (org-code (:height 1.55) org-code)
                                     (org-verbatim (:height 1.55) org-verbatim)
                                     (org-block (:height 1.25) org-block)
                                     (org-block-begin-line (:height 0.7) org-block)))

  ;; Turn off the header glyphs
  (org-modern-mode 0)

  ;; Adjust some text elements, hide formatting markers
  (setq org-hide-emphasis-markers t)
  (setq header-line-format " ")
  (org-display-inline-images)

  ;; Hide some UI elements
  (menu-bar-mode 0)
  (scroll-bar-mode 0)

  ;; Don't distract the viewer with a blinking cursor
  (blink-cursor-mode 0))

(defun mtn/org-present-quit ()
  ;; Stop centering
  (setq visual-fill-column-center-text 0)
  (visual-fill-column-mode 0)
  (visual-line-mode 0)

  ;; Turn on line numbers
  (display-line-numbers-mode 1)

  (org-modern-mode 0)

  ;; Restore default fonts
  (setq-local face-remapping-alist '())

  ;; Restore cursor blinking
  (blink-cursor-mode 1)

  (setq header-line-format nil)

  ;; Restore menu and scroll
  (menu-bar-mode 1)
  (scroll-bar-mode 1))

The other customization is to start each slide with subheadings folded. This makes for a nice step-by-step presentation where I can unfold each subhead one at a time.

(defun mtn/org-present-prepare-slide (buffer-name heading)
  ;; Show top-level headings
  (org-overview)

  ;; Unfold current entry
  (org-show-entry)

  ;; Show only direct subheadings but don't expand
  (org-show-children))

And now load the packages for centering and presenting

(use-package visual-fill-column
  :commands (visual-fill-column-mode)
  :custom
  (visual-fill-column-center-text t)
  (visual-fill-column-width 110))

(use-package org-present
  :commands org-present
  :hook
  (org-present-mode . mtn/org-present-start)
  (org-present-mode-quit . mtn/org-present-quit)
  :config
  (add-hook 'org-present-after-navigate-functions 'mtn/org-present-prepare-slide))

Configure Babel Languages

To execute or export code in org-mode code blocks, you’ll need to set up org-babel-load-languages for each language you’d like to use. This page documents all of the languages that you can use with org-babel.

(with-eval-after-load 'org
  (org-babel-do-load-languages
   'org-babel-load-languages
   '((emacs-lisp . t))))

Structure Templates

Org Mode’s structure templates feature enables you to quickly insert code blocks into your Org files in combination with org-tempo by typing < followed by the template name like el or py and then press TAB. For example, to insert an empty emacs-lisp block below, you can type <el and press TAB to expand into such a block.

You can add more src block templates below by copying one of the lines and changing the two strings at the end, the first to be the template name and the second to contain the name of the language as it is known by Org Babel.

(with-eval-after-load 'org
  ;; This is needed as of Org 9.2
  (require 'org-tempo)

  (add-to-list 'org-structure-template-alist '("sh" . "src shell"))
  (add-to-list 'org-structure-template-alist '("el" . "src emacs-lisp"))
  (add-to-list 'org-structure-template-alist '("ai" . "ai"))
  (add-to-list 'org-structure-template-alist '("img" . "ai :image"))
  )

Auto-tangle Configuration Files

This snippet adds a hook to org-mode buffers so that efs/org-babel-tangle-config gets executed each time such a buffer gets saved. This function checks to see if the file being saved is the Emacs.org file you’re looking at right now, and if so, automatically exports the configuration here to the associated output files.

;; Automatically tangle our Emacs.org config file when we save it
(defun efs/org-babel-tangle-config ()
  (when (string-equal (file-name-directory (buffer-file-name))
                      (expand-file-name user-emacs-directory))
    ;; Dynamic scoping to the rescue
    (let ((org-confirm-babel-evaluate nil))
      (org-babel-tangle))))

(add-hook 'org-mode-hook (lambda () (add-hook 'after-save-hook #'efs/org-babel-tangle-config)))

Attachments and links

This package allows me to insert an org-mode link to a URL that is in the system clipboard. Small thing, but useful.

(use-package org-cliplink
  :after org
  :bind
  ("C-x p i" . org-cliplink))

I can use org-download to make org files into drag-and-drop targets. This will make it easier to take notes, since I can screenshot presentations or zoom videos and drop the screenshots directly into org instead of fiddling with file locations and manually inserting image links.

At least as of <2023-04-21 Fri> the drag-and-drop feature doesn’t work on my main linux desktop ‘recoil’. I get an elisp error from x-dnd-something-or-other complaining about a broken XDS implementation.

Taking a screenshot with C-M-y does work on ‘recoil’. It probably won’t work on macOS, but I will try that some other day.

(defvar mtn/org-attach-screenshot-command-line (if *is-a-mac* "/opt/homebrew/bin/pngpaste %f" "import %f"))

(defun mtn/org-screenshot-insert (linkfilename)
    (insert (concat "[[attachment:" (file-name-nondirectory linkfilename) "]]")))

  (use-package org-attach-screenshot
    :after org
    :bind
    ("C-M-y" . org-attach-screenshot)
    :custom
    (org-attach-screenshot-auto-refresh 'always)
    (org-attach-screenshot-insertfunction 'mtn/org-screenshot-insert)
    (org-attach-screenshot-command-line mtn/org-attach-screenshot-command-line)
    :config
    (setq org-attach-screenshot-dirfunction
          (lambda ()
            (progn (cl-assert (buffer-file-name))
                   (concat (file-name-sans-extension (buffer-file-name))
                           "-att")))))

Invoke GPT AI from org-mode

Org-ai lets me create #+begin_ai and #+end_ai blocks that will send prompts to GPT and insert the results. It can also summarize a region of text and refactor code.

It does require a secret API token, which I don’t want to put into my dotfiles, so I’ll read it from somewhere else.

(if (file-exists-p "~/.org-ai-gpt-token")
    (load "~/.org-ai-gpt-token"))
(use-package org-ai
  :commands (org-ai-mode)
  :custom
  (org-ai-openai-api-token mtn/org-ai-gpt-token)
  :init
  (add-hook 'org-mode-hook #'org-ai-mode)
  :config
  (org-ai-install-yasnippets)
  (setq org-ai-default-chat-model "gpt-4")

  (use-package greader :ensure)
  (require 'whisper)
  (setq org-ai-talk-say-words-per-minute 210)
  (setq org-ai-talk-say-voice "Karen")
  )

Use Whisper for speech-to-text inside Emacs

Whisper uses OpenAI’s speech to text model to convert audio into a transcript. Before this will work, I must run some commands:

cd ~/.emacs.d/ && git clone git@github.com:natrys/whisper.el.git

In order for Whisper to work on a Mac we have to do two things. First is some funny business with ffmpeg to figure out which audio input to consume. The following is borrowed from this gist.

    (defun rk/get-ffmpeg-device ()
      "Gets the list of devices available to ffmpeg.
    The output of the ffmpeg command is pretty messy, e.g.
      [AVFoundation indev @ 0x7f867f004580] AVFoundation video devices:
      [AVFoundation indev @ 0x7f867f004580] [0] FaceTime HD Camera (Built-in)
      [AVFoundation indev @ 0x7f867f004580] AVFoundation audio devices:
      [AVFoundation indev @ 0x7f867f004580] [0] Cam Link 4K
      [AVFoundation indev @ 0x7f867f004580] [1] MacBook Pro Microphone
    so we need to parse it to get the list of devices.
    The return value contains two lists, one for video devices and one for audio devices.
    Each list contains a list of cons cells, where the car is the device number and the cdr is the device name."
      (unless *is-a-mac*
        (error "This function is currently only supported on macOS"))

      (let ((lines (string-split (shell-command-to-string "ffmpeg -list_devices true -f avfoundation -i dummy || true") "\n")))
        (cl-loop with at-video-devices = nil
                 with at-audio-devices = nil
                 with video-devices = nil
                 with audio-devices = nil
                 for line in lines
                 when (string-match "AVFoundation video devices:" line)
                 do (setq at-video-devices t
                          at-audio-devices nil)
                 when (string-match "AVFoundation audio devices:" line)
                 do (setq at-audio-devices t
                          at-video-devices nil)
                 when (and at-video-devices
                           (string-match "\\[\\([0-9]+\\)\\] \\(.+\\)" line))
                 do (push (cons (string-to-number (match-string 1 line)) (match-string 2 line)) video-devices)
                 when (and at-audio-devices
                           (string-match "\\[\\([0-9]+\\)\\] \\(.+\\)" line))
                 do (push (cons (string-to-number (match-string 1 line)) (match-string 2 line)) audio-devices)
                 finally return (list (nreverse video-devices) (nreverse audio-devices)))))

    (defun rk/find-device-matching (string type)
    "Get the devices from `rk/get-ffmpeg-device' and look for a device
  matching `STRING'. `TYPE' can be :video or :audio."
    (let* ((devices (rk/get-ffmpeg-device))
           (device-list (if (eq type :video)
                            (car devices)
                          (cadr devices))))
      (cl-loop for device in device-list
               when (string-match-p string (cdr device))
               return (car device))))

  (defcustom rk/default-audio-device nil
    "The default audio device to use for whisper.el and outher audio processes."
    :type 'string)

(defun rk/select-default-audio-device (&optional device-name)
  "Interactively select an audio device to use for whisper.el and other audio processes.
If `DEVICE-NAME' is provided, it will be used instead of prompting the user."
  (interactive)
  (let* ((audio-devices (cadr (rk/get-ffmpeg-device)))
         (indexes (mapcar #'car audio-devices))
         (names (mapcar #'cdr audio-devices))
         (name (or device-name (completing-read "Select audio device: " names nil t))))
    (setq rk/default-audio-device (rk/find-device-matching name :audio))
    (when (boundp 'whisper--ffmpeg-input-device)
      (setq whisper--ffmpeg-input-device (format ":%s" rk/default-audio-device)))))

The second thing we have to do on a Mac is let macOS know that Emacs needs permission to use the microphone. As of <2024-02-14 Wed>, on Ventura 13.6.4, adding this snippet to /opt/homebrew/Cellar/emacs-plus@29/29.2/Emacs.app/Contents/Info.plist was enough to have macOS prompt me that Emacs wanted permission to use the microphone. It went right in the same section as the mention of NSCameraUsageDescription.

<key>NSMicrophoneUsageDescription</key>
<string>Emacs needs permission to access the microphone.</string>  

Then the following block will load Whisper and bind it to a keystroke

(use-package whisper
  :load-path "~/.emacs.d/whisper.el"
  :bind ("C-H-r" . whisper-run)
  :config
  (setq whisper-install-directory "~/bin/whisper"
        whisper-model "base"
        whisper-language "en"
        whisper-translate nil)
  (when *is-a-mac*
    (rk/select-default-audio-device "Macbook Pro Microphone")
    (when rk/default-audio-device
      (setq whisper--ffmpeg-input-device (format ":%s" rk/default-audio-device)))))

Google Calendar Integration

Google calendar integration with org-mode is kind of tricky, and has a tendency to break when Google changes their security systems.

I’m using the MELPA release of org-gcal:

(use-package org-gcal
  :after org-roam
  :custom
  (org-gcal-client-id mtn/org-gcal-client-id)
  (org-gcal-client-secret mtn/org-gcal-client-secret)
  (org-gcal-up-days 14)
  (org-gcal-down-days 60)
  (org-gcal-notify-p nil)
  (org-gcal-remove-api-cancelled-events t)
  (plstore-cache-passphrase-for-symmetric-encryption t)

  ;; personal calendar and family (shared) calendar
  (org-gcal-fetch-file-alist '((mtn/personal-calendar . "~/Dropbox/org/mtnygard_gmail_com.org")
                               (mtn/nygard-family-calendar . "~/Dropbox/org/nygard_family_calendar.org")))

  :bind (("C-c s-g p" . org-gcal-post-at-point)
         ("C-c s-g s" . org-gcal-sync)
         ("C-c s-g f" . org-gcal-fetch)
         ("C-c s-g d" . org-gcal-delete-at-point)
         ("C-c s-g b s" . org-gcal-sync-buffer)
         ("C-c s-g b f" . org-gcal-sync-buffer)))

Completion Framework

Vertico

VERTical Interactive COmpletion is a completion UI.

(use-package vertico
  :init
  (vertico-mode))

And it works well with the Orderless package which lets me type in space-separated words in the prompt. These will match any candidate that matches all of the components in any order.

(use-package orderless
  :after vertico
  :custom
  (completion-styles '(orderless basic))
  (completion-category-overrides '((file (styles basic partial-completion)))))

Marginalia adds some extra info in the right-hand side of completion buffers.

(use-package marginalia
:bind (("M-A" . marginalia-cycle)
       :map minibuffer-local-map
       ("M-A" . marginalia-cycle))

:init
(marginalia-mode))

Consult

Consult provides completion commands for many scenarios. It brings a ton of built-in commands but doesn’t do any keybindings. The following configuration comes from Consult’s readme on github.

  ;; Example configuration for Consult
  (use-package consult
    ;; Replace bindings. Lazily loaded due by `use-package'.
    :bind (;; C-c bindings (mode-specific-map)
           ("C-c M-x" . consult-mode-command)
;;           ("C-c h" . consult-history) ;; overridden by consult-org
           ("C-c k" . consult-kmacro)
           ("C-c m" . consult-man)
           ("C-c i" . consult-info)
           ([remap Info-search] . consult-info)
           ;; C-x bindings (ctl-x-map)
           ("C-x M-:" . consult-complex-command)     ;; orig. repeat-complex-command
           ("C-x b" . consult-buffer)                ;; orig. switch-to-buffer
           ("C-x 4 b" . consult-buffer-other-window) ;; orig. switch-to-buffer-other-window
           ("C-x 5 b" . consult-buffer-other-frame)  ;; orig. switch-to-buffer-other-frame
           ("C-x r b" . consult-bookmark)            ;; orig. bookmark-jump
           ("C-x p b" . consult-project-buffer)      ;; orig. project-switch-to-buffer
           ;; Custom M-# bindings for fast register access
           ("M-#" . consult-register-load)
           ("M-'" . consult-register-store)          ;; orig. abbrev-prefix-mark (unrelated)
           ("C-M-#" . consult-register)
           ;; Other custom bindings
           ("M-y" . consult-yank-pop)                ;; orig. yank-pop
           ;; M-g bindings (goto-map)
           ("M-g e" . consult-compile-error)
           ("M-g f" . consult-flymake)               ;; Alternative: consult-flycheck
           ("M-g g" . consult-goto-line)             ;; orig. goto-line
           ("M-g M-g" . consult-goto-line)           ;; orig. goto-line
           ("M-g o" . consult-outline)               ;; Alternative: consult-org-heading
           ("M-g m" . consult-mark)
           ("M-g k" . consult-global-mark)
           ("M-g i" . consult-imenu)
           ("M-g I" . consult-imenu-multi)
           ;; M-s bindings (search-map)
           ("M-s d" . consult-find)
           ("M-s D" . consult-locate)
           ("M-s g" . consult-grep)
           ("M-s G" . consult-git-grep)
           ("M-s r" . consult-ripgrep)
           ("M-s l" . consult-line)
           ("M-s L" . consult-line-multi)
           ("M-s k" . consult-keep-lines)
           ("M-s u" . consult-focus-lines)
           ;; Isearch integration
           ("M-s e" . consult-isearch-history)
           :map isearch-mode-map
           ("M-e" . consult-isearch-history)         ;; orig. isearch-edit-string
           ("M-s e" . consult-isearch-history)       ;; orig. isearch-edit-string
           ("M-s l" . consult-line)                  ;; needed by consult-line to detect isearch
           ("M-s L" . consult-line-multi)            ;; needed by consult-line to detect isearch
           ;; Minibuffer history
           :map minibuffer-local-map
           ("M-s" . consult-history)                 ;; orig. next-matching-history-element
           ("M-r" . consult-history))                ;; orig. previous-matching-history-element

    ;; Enable automatic preview at point in the *Completions* buffer. This is
    ;; relevant when you use the default completion UI.
    :hook (completion-list-mode . consult-preview-at-point-mode)

    ;; The :init configuration is always executed (Not lazy)
    :init

    ;; Optionally configure the register formatting. This improves the register
    ;; preview for `consult-register', `consult-register-load',
    ;; `consult-register-store' and the Emacs built-ins.
    (setq register-preview-delay 0.5
          register-preview-function #'consult-register-format)

    ;; Optionally tweak the register preview window.
    ;; This adds thin lines, sorting and hides the mode line of the window.
    (advice-add #'register-preview :override #'consult-register-window)

    ;; Use Consult to select xref locations with preview
    (setq xref-show-xrefs-function #'consult-xref
          xref-show-definitions-function #'consult-xref)

    ;; Configure other variables and modes in the :config section,
    ;; after lazily loading the package.
    :config

    ;; Optionally configure preview. The default value
    ;; is 'any, such that any key triggers the preview.
    ;; (setq consult-preview-key 'any)
    ;; (setq consult-preview-key "M-.")
    ;; (setq consult-preview-key '("S-<down>" "S-<up>"))
    ;; For some commands and buffer sources it is useful to configure the
    ;; :preview-key on a per-command basis using the `consult-customize' macro.
    (consult-customize
     consult-theme :preview-key '(:debounce 0.2 any)
     consult-ripgrep consult-git-grep consult-grep
     consult-bookmark consult-recent-file consult-xref
     consult--source-bookmark consult--source-file-register
     consult--source-recent-file consult--source-project-recent-file
     ;; :preview-key "M-."
     :preview-key '(:debounce 0.4 any))

    ;; Optionally configure the narrowing key.
    ;; Both < and C-+ work reasonably well.
    (setq consult-narrow-key "<") ;; "C-+"

    ;; Optionally make narrowing help available in the minibuffer.
    ;; You may want to use `embark-prefix-help-command' or which-key instead.
    ;; (define-key consult-narrow-map (vconcat consult-narrow-key "?") #'consult-narrow-help)

    ;; By default `consult-project-function' uses `project-root' from project.el.
    ;; Optionally configure a different project root function.
    ;;;; 1. project.el (the default)
    ;; (setq consult-project-function #'consult--default-project--function)
    ;;;; 2. vc.el (vc-root-dir)
    ;; (setq consult-project-function (lambda (_) (vc-root-dir)))
    ;;;; 3. locate-dominating-file
    ;; (setq consult-project-function (lambda (_) (locate-dominating-file "." ".git")))
    ;;;; 4. projectile.el (projectile-project-root)
    ;; (autoload 'projectile-project-root "projectile")
    ;; (setq consult-project-function (lambda (_) (projectile-project-root)))
    ;;;; 5. No project support
    ;; (setq consult-project-function nil)
  )
  

Company

I’m using `company` for in-buffer completion.

(use-package company
  :diminish company-mode
  :config
  (add-hook 'after-init-hook 'global-company-mode)
  :custom
  (company-minimum-prefix-length 1)
  (company-idle-delay 0.3)
  (company-dabbrev-downcase nil)

  :bind
  (:map company-active-map
        (("C-d" . company-show-doc-buffer)
         ("C-l" . company-show-location)
         ("C-n" . company-select-next)
         ("C-p" . company-select-previous)
         ("C-t" . company-select-next)
         ("C-s" . company-select-previous)
         ("TAB" . company-complete))))

Development Tools

Before setting up any specific programming modes, enable IDE features with Emacs’ `lsp-mode`. LSP keybindings will be mapped to “C-c l” in any buffer that uses LSP.

(defun mtn/lsp-mode-setup ()
  (setq lsp-header-line-breadcrumb-segments '(path-up-to-project file symbols))
  (lsp-headerline-breadcrumb-mode))

(use-package lsp-mode
  :init
  (setq lsp-keymap-prefix "C-c l")
  :commands (lsp lsp-deferred)
  :hook
  (dart-mode . lsp)
  (rust-mode . lsp)
  (python-mode . lsp)
  (zig-mode . lsp)
  (go-mode . lsp)
  (typescript-mode . lsp)
  (lsp-mode . mtn/lsp-mode-setup)
  (lsp-mode . lsp-enable-which-key-integration)
  :config
  (setq lsp-semantic-tokens-enable t
        lsp-idle-delay 0.2))

lsp-ui adds some more UI affordances (peek, sideline.)

(use-package lsp-ui
  :after lsp-mode
  :commands lsp-ui-mode
  :config
  (setq lsp-ui-doc-enable nil
        lsp-ui-peek-enable nil
        lsp-lens-enable nil))

And lsp-treemacs shows symbols in a file in a treemacs buffer:

(use-package lsp-treemacs
  :commands lsp-treemacs-errors-list)

Now consult-lsp adds some integration for searching by program symbols.

(use-package consult-lsp
  :commands consult-lsp-symbols)

Clojure

Configure Clojure, CIDER, Cljrefactor

(defun live-transpose-words-with-hyphens (arg)
  "Treat hyphens as a word character when transposing words"
  (interactive "*p")
  (with-syntax-table clojure-mode-with-hyphens-as-word-sep-syntax-table
    (transpose-words arg)))

(defun mtn/setup-clojure-mode ()
  (cljr-add-keybindings-with-prefix "C-c C-a"))

(use-package clojure-mode
  :mode
  ("\\.edn$" . clojure-mode)
  ("\\.fern$" . clojure-mode)
  :hook
  (clojure-mode . clj-refactor-mode)
  (clojure-mode . highlight-parentheses-mode)
  (clojure-mode . paredit-mode)
  (clojure-mode . cider-mode)
  (clojure-mode . clj-refactor-mode)
  mtn/setup-clojure-mode
  :bind
  (:map clojure-mode-map
        ("M-t" . live-transpose-words-with-hyphens))
  :config
  (setq clojure-indent-style 'align-arguments)
  
  :custom
  (clojure-align-forms-automatically t)
  (clojure-indent-style :always-indent))

(use-package cider
  :after clojure-mode

  :config
  (setq cider-show-error-buffer 'only-in-repl)

  :custom
  (cider-show-error-buffer nil)
  (cider-popup-stacktraces nil)
  (cider-popup-stacktraces-in-repl nil)
  (cider-repl-print-length 100)
  (cider-repl-pop-to-buffer-on-connect nil)
  (cider-save-file-on-load t)
  (cider-prompt-for-symbol nil)
  (cider-repl-display-help-banner nil))

(use-package clj-refactor
  :after clojure-mode
  :custom
  (cljr-favor-prefix-notation nil))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;
;;; Cider mode - IDE for Clojure
;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(add-hook 'cider-repl-mode-hook
          (lambda ()
            (eldoc-mode)
            (company-mode)))

(add-hook 'cider-mode-hook
          (lambda ()
            (eldoc-mode)
            (company-mode)))

(add-to-list 'same-window-buffer-names "*cider*")

I’m experimenting with REBL from CIDER:

;; Similar to C-x C-e, but sends to REBL
(defun rebl-eval-last-sexp ()
  (interactive)
  (let* ((bounds (cider-last-sexp 'bounds))
         (s (cider-last-sexp))
         (reblized (concat "(cognitect.rebl/inspect " s ")")))
    (cider-interactive-eval reblized nil bounds (cider--nrepl-print-request-map))))

;; Similar to C-M-x, but sends to REBL
(defun rebl-eval-defun-at-point ()
  (interactive)
  (let* ((bounds (cider-defun-at-point 'bounds))
         (s (cider-defun-at-point))
         (reblized (concat "(cognitect.rebl/inspect " s ")")))
    (cider-interactive-eval reblized nil bounds (cider--nrepl-print-request-map))))

;; C-S-x send defun to rebl
;; C-x C-r send last sexp to rebl (Normally bound to "find-file-read-only"... Who actually uses that though?)
(add-hook 'cider-mode-hook
          (lambda ()
            (local-set-key (kbd "C-S-x") #'rebl-eval-defun-at-point)
            (local-set-key (kbd "C-x C-r") #'rebl-eval-last-sexp)))

CMake

(use-package cmake-mode
  :defer t)

(use-package cmake-project
  :defer t)

CSV files

(use-package csv-mode
  :defer t)

Dart

(use-package dart-mode
  :defer t)

Dhall

(use-package dhall-mode :defer t)

EBNF grammar files

(use-package ebnf-mode
  :defer t)

Go

(defun mtn/lsp-go-clean-before-save ()
  (add-hook 'before-save-hook #'lsp-format-buffer t t)
  (add-hook 'before-save-hook #'lsp-organize-imports t t))

(use-package go-mode
  :defer t
  :mode ("\\.go\\'" . go-mode)
  :hook (go-mode . mtn/lsp-go-clean-before-save)
  :bind
  (:map go-mode-map ("M-." . godef-jump))
  :custom
  (lsp-before-save-edits t))

(use-package go-stacktracer
  :after go-mode)

(use-package go-add-tags
  :after go-mode)

(use-package go-gopath
  :after go-mode)

(use-package gotest
  :after go-mode)

Graphviz

(use-package graphviz-dot-mode :defer t)

JSON

(use-package json-mode :defer t)
(use-package json-reformat :defer t)

Markdown

(use-package markdown-mode
  :defer t)

Plant UML

Automatically download plantuml.jar if it’s not already installed

(let ((plantuml-directory (concat user-emacs-directory "extra/"))
      (plantuml-link "https://github.com/plantuml/plantuml/releases/download/v1.2023.1/plantuml-1.2023.1.jar"))
  (when (not (file-directory-p plantuml-directory))
    (make-directory plantuml-directory t))
  (let ((plantuml-target (concat plantuml-directory "plantuml.jar")))
    (if (not (file-exists-p plantuml-target))
        (progn (message "Downloading plantuml.jar")
               (shell-command
                (mapconcat 'identity (list "wget" plantuml-link "-O" plantuml-target) " "))
               (kill-buffer "*Shell Command Output*")))
    (setq org-plantuml-jar-path plantuml-target)))

And use PlantUML mode for the files:

(use-package plantuml-mode
    :defer t
    :custom
    (plantuml-jar-path (concat user-emacs-directory "extra/plantuml.jar"))
    (plantuml-default-exec-mode 'jar))

Projectile

Projectile provides “project” navigation and management. A “project” is determined by a folder with a “project definition” file in it. This might be a build.xml, project.clj, CMakeLists.txt, or some other known project definition.

This configuration needs migration to `use-package`.

(use-package projectile
  :diminish projectile-mode
  :config (projectile-mode)
  :bind-keymap
  ("C-c p" . projectile-command-map)
  :custom
  (projectile-project-search-path '("~/work" "~/src"))
  (projectile-use-git-grep t)
  (projectile-switch-project-action #'projectile-dired))

(use-package consult-projectile
  :after (consult projectile))

(use-package projectile-codesearch
  :after projectile)
(use-package projectile-variable
  :after projectile)

REST Client

(use-package restclient
  :defer t
  :mode ("\\.restclient\\'" . restclient-mode))

(use-package company-restclient
  :after restclient
  :config (add-to-list 'company-backends 'company-restclient))

(use-package ob-restclient
  :after restclient)

Rust

(use-package rust-mode
  :defer t)

Typescript

(use-package typescript-mode
  :defer t)

Version Control

Git

(use-package magit
  :commands magit-get-top-dir
  :bind (("C-c g" . magit-status)
         ("C-c C-g l" . magit-file-log)
         ("C-c f" . magit-grep))
  :custom
  (magit-push-always-verify nil))

YAML

(use-package yaml-mode
  :defer t
  :config
  (add-hook 'yaml-mode-hook 'flycheck-mode)
  (add-hook 'yaml-mode-hook 'flyspell-mode))

(use-package flycheck-yamllint
  :after yaml-mode
  :init
  (progn
    (eval-after-load 'flycheck
      '(add-hook 'flycheck-mode-hook 'flycheck-yamllint-setup))))

(use-package highlight-indentation
  :after yaml-mode
  :config
  (set-face-background 'highlight-indentation-face "#8B6090")
  (add-hook 'yaml-mode-hook 'highlight-indentation-mode))

RTags

RTags provides quick navigation (similar to etags or ctags). See RTags repo for details

(use-package rtags
  :commands rtags-find-symbol-at-point
  :custom
  (rtags-rc-binary-name "rtags-rc")
  (rtags-rdm-binary-name "rtags-rdm")
  :config
  (rtags-enable-standard-keybindings))

Zig

Zig mode is full featured with good defaults. All I need to do is load it.

(setq lsp-zig-zls-executable "/opt/zig/zls")

(use-package zig-mode
  :defer t
  :custom
  (zig-zig-bin "/opt/zig/zig"))

Writing Tools

Asciidoc

(use-package adoc-mode
  :defer t)

LaTeX

In some ways, LaTeX is like Emacs itself… ancient, a bit out of step with modern conventions, and there’s still nothing better.

This configuration needs to be migrated to `use-package`.

(require 'flymake)

;; (defun flymake-get-tex-args
;;   (file-name)
;;   (list "pdflatex"
;;         (list "-file-line-error" "-draftmode" "-interaction=nonstopmode" file-name)))

(add-hook 'LaTeX-mode-hook 'flymake-mode)

(setq ispell-program-name "/usr/bin/aspell")
(setq ispell-dictionary "english")

(add-hook 'LaTeX-mode-hook 'flyspell-mode)
(add-hook 'LaTeX-mode-hook 'flyspell-buffer)

(defun turn-on-outline-minor-mode () (outline-minor-mode 1))

(add-hook 'LaTeX-mode-hook 'turn-on-outline-minor-mode)
(add-hook 'latex-mode-hook 'turn-on-outline-minor-mode)
(setq outline-minor-mode-prefix "\C-c\C-o")

(require 'reftex)
(autoload 'reftex-mode "reftex" "RefTeX Minor Mode" t)
(autoload 'turn-on-reftex "reftex" "RefTeX Minor Mode" nil)
(autoload 'reftex-citation "reftex-cite" "Make citation" nil)
(autoload 'reftex-index-phrase-mode "reftex-index" "Phrase Mode"
  t)
(add-hook 'latex-mode-hook 'turn-on-reftex) ; with Emacs latex mode
(add-hook 'reftex-load-hook 'imenu-add-menubar-index)
(add-hook 'LaTeX-mode-hook 'turn-on-reftex)

(setq LaTeX-eqnarray-label "eq"
      LaTeX-equation-label "eq"
      LaTeX-figure-label "fig"
      LaTeX-table-label "tab"
      LaTeX-myChapter-label "chap"
      TeX-auto-save t
      TeX-newline-function 'reindent-then-newline-and-indent
      TeX-parse-self t
      TeX-style-path '("style/" "auto/" "~/.emacs.d/elpa/auctex-11.86/style/")
      LaTeX-section-hook
      '(LaTeX-section-heading
        LaTeX-section-title
        LaTeX-section-toc
        LaTeX-section-section
        LaTeX-section-label))

Pandoc

Recently migrated from a local install of pandoc-mode v0.1.8 to using it from MELPA. There might be some bindings needed.

(use-package pandoc-mode
  :defer t)

Emacs Writing Studio

Emacs Writing Studio (EWS) is a minimalist configuration that helps authors to research, write and publish articles books and websites.

The full configuration is on github. This is just some selected bits that I want to try out.

;; Distraction-free writing
(defun ews-distraction-free ()
  "Distraction-free writing environment using Olivetti package."
  (interactive)
  (if (equal olivetti-mode nil)
      (progn
        (window-configuration-to-register 1)
        (delete-other-windows)
        (text-scale-set 2)
        (olivetti-mode t))
    (progn
      (if (eq (length (window-list)) 1)
          (jump-to-register 1))
      (olivetti-mode 0)
      (text-scale-set 0))))

(use-package olivetti
  :demand t
  :bind
  (("<f9>" . ews-distraction-free)))

Social media and communication

ERC

ERC is the Emacs Internet Relay Chat client. I don’t use this often, but want to keep it around in case there’s a revival someday.

;; Passwords go in ~/.ercpass as
;;   (setq network-nick-pass "apassword")
;;
;; Update the nicks and references to passwords in
;;   erc-nickserv-passwords and erc-grove below
;;
;; Sets up the following features
;;  - Quick functions for Freenode and Grove
;;    M-x erc-freenode
;;    M-x erc-grove
;;  - Auto-ident for known networks
;;  - Growl notify when my name is mentioned

;; (autoload 'erc "erc" "" t)
;; (if (file-exists-p "~/.ercpass")
;;     (load "~/.ercpass"))
;; (require 'erc-services)
;; (erc-services-mode 1)

;; (setq erc-prompt-for-nickserv-password nil)

;; (setq erc-nickserv-passwords
;;       `((freenode     ((,freenode-nick-one . ,freenode-nick-one-pass)
;;                        (,freenode-nick-two . ,freenode-nick-two-pass)))))

;; (defmacro de-erc-connect (command server port nick)
;;   "Create interactive command `command', for connecting to an IRC server. The
;;    command uses interactive mode if passed an argument."
;;   (fset command
;;         `(lambda (arg)
;;            (interactive "p")
;;            (if (not (= 1 arg))
;;                (call-interactively 'erc)
;;              (erc :server ,server :port ,port :nick ,nick)))))

I pretty much only used Freenode, though I did have a couple of nicks for different purposes.

;; M-x erc-freenode
;; (de-erc-connect erc-freenode "irc.freenode.net" 6667 freenode-nick-two)
;; (de-erc-connect erc-me       "irc.freenode.net" 6667 freenode-nick-one)

ERC buffers can get pretty big, slowing down other work because they use too much memory. This config limits their max size.

;; Truncate buffers so they don't hog core.
;; (setq erc-max-buffer-size 20000)
;; (defvar erc-insert-post-hook)
;; (add-hook 'erc-insert-post-hook 'erc-truncate-buffer)
;; (setq erc-truncate-buffer-on-save t)

RSS Feeds

Using GitHub - skeeto/elfeed: An Emacs web feeds client

;; Configure Elfeed
(use-package elfeed
  :ensure t
  :config
  (setq elfeed-db-directory "~/Dropbox/elfeed/"
        elfeed-show-entry-switch 'display-buffer)
  :bind
  ("C-x w" . elfeed))

Since everything else is org-mode, might as well use org to manage my subscriptions

;; Configure Elfeed with org mode
(use-package elfeed-org
  :ensure t
  :config
  (elfeed-org)
  (setq rmh-elfeed-org-files '("~/Dropbox/elfeed/subscriptions.org")))

Employer-specific sections

Nubank

Nubank has some custom tools for Emacs (mostly related to Clojure work). But these will not be on any other personal machine, so I must check if the directory exists. That will be the signal this is on a Nubank computer.

(let ((nudev-emacs-path "~/dev/nu/nudev/ides/emacs/"))
  (when (file-directory-p nudev-emacs-path)
    (add-to-list 'load-path nudev-emacs-path)
    (require 'nu nil t)))

Finalization

Late GC tuning

After startup, reduce the GC threshold (but not all the way back to default.) This will reduce GC pause time later.

;; The default is 800 kilobytes.  Measured in bytes.
(setq gc-cons-threshold (* 2 1000 1000))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment