Emacs Configuration

Read Me!

This file is the readme, but it also is the code. Neat! org-babel is used to 'tangle' the code blocks in this file and combine their contents to create the init.el file. Ain't that fancy :)

This is my current configuration for emacs, which I use for personal knowledge management and code development. There are a few places where I link into my knowledge graph, but I anticipate reading and tweaking it locally and frequently so I'm not going to make any special effort to fix or even indicate those links.

Set lexical binding

Org roam depends (somehow) on lexical binding. Having it set everywhere doesn't break anything, so I do.

;;; -*- lexical-binding: t; -*-

Package management

(require 'package)

(setq package-archives '(("melpa" . "https://melpa.org/packages/")
                         ("org" . "https://orgmode.org/elpa/")
                         ("elpa" . "https://elpa.gnu.org/packages/")))

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

;; Initialize use-package on non-Linux platforms
(unless (package-installed-p 'use-package)
  (package-install 'use-package))

(require 'use-package)
(setq use-package-always-ensure t)

System specific path settings

My personal computer is linux and launches emacs in ~. My work computer is windows and launches emacs in %APPDATA%\Roaming. I figure it's easier to just define my routes in an untracked file.

;; Set my/default-directory and my/org-file-path in this file.
;; They should generally be shaped like "~/Documents/git/" and "~/Documents/org/"
(load "~/.emacs.d/routes.el")

UI Configuration

;; Don't display the start page
(setq inhibit-startup-message t)
;; Disable visible scrollbar
(scroll-bar-mode -1)
;; Disable top toolbar
(tool-bar-mode -1)
;; Disable tooltips
(tooltip-mode -1)
;; Give space on the edges
(set-fringe-mode 10)
;; Disable menu bar
(menu-bar-mode -1)
;; When there is an alarm ping, flash the screen
(setq visible-bell nil)

(column-number-mode)
(global-display-line-numbers-mode t)

;; Disable line numbers for org, terminal, and shell modes
(dolist (mode '(org-mode-hook
                term-mode-hook
                shell-mode-hook
                eshell-mode-hook))
  (add-hook mode (lambda () (display-line-numbers-mode 0))))

Fonts

(defvar my/default-font-size 120)

(set-face-attribute 'default nil :family "Iosevka" :height my/default-font-size)
(set-face-attribute 'variable-pitch nil :family "Iosevka Aile" :height my/default-font-size)

Modeline

(use-package doom-modeline
  :init (doom-modeline-mode 1)
  :custom ((doom-modeline-height 15)))

Themes

(use-package modus-themes
  :config (modus-themes-select 'modus-vivendi-tinted))

Rainbow Delimiters

(use-package rainbow-delimiters
  :hook (prog-mode . rainbow-delimiters-mode))

Utility

Prompt with what command chords are available and what they will do when hesitating during command input.

(use-package which-key
  :init (which-key-mode)
  :diminish which-key-mode
  :config
  (setq which-key-idle-delay 1)
  :defer)

Improve the help pages for variables and functions.

(use-package helpful
  :custom
  (counsel-describe-function-function #'helpful-callable)
  (counsel-describe-variable-function #'helpful-variable)
  :bind
  ([remap describe-function] . counsel-describe-function)
  ([remap describe-command] . helpful-command)
  ([remap describe-variable] . counsel-describe-variable)
  ([remap describe-key] . helpful-key))

Sentence End

By default emacs expects sentences to end with a double space. I don't like that.

(setq sentence-end-double-space nil)

Autocompletion and Search Framework

I use ivy for search because David Wilson of System Crafters uses it in the "Emacs from Scratch" tutorial series. It's fine, I don't really know any advantages or disadvantages to different completion frameworks. But I live and die by counsel-switch-buffer and counsel is built on top of ivy so I can never change lmao

(use-package ivy
  ;; :diminish ; I'm not sure what this does, or why it's taken out.
  :bind (("C-s" . swiper)
         :map ivy-minibuffer-map
         ("TAB" . ivy-alt-done)
         ("C-l" . counsel-up-directory)
         ("C-n" . ivy-next-line)
         ("C-e" . ivy-previous-line)
         :map ivy-switch-buffer-map
         ("C-n" . ivy-next-line)
         ("C-e" . ivy-previous-line)
         ("C-l" . counsel-up-directory)
         ("C-d" . ivy-switch-buffer-kill)
         :map ivy-reverse-i-search-map
         ("C-n" . ivy-next-line)
         ("C-e" . ivy-previous-line)
         ("C-d" . ivy-reverse-i-search-kill))
  :config
  (ivy-mode 1))

(use-package ivy-rich
  :init (ivy-rich-mode 1)
  :after ivy)

(use-package counsel
  :bind (("M-x" . counsel-M-x)
         ("<apps>" . counsel-M-x)
         ("C-x b" . counsel-ibuffer)
         ("C-x C-f" . counsel-find-file)
         ("C-M-n" . counsel-switch-buffer)
         :map minibuffer-local-map
         ("C-r" . 'counsel-minibuffer-history)))

Evil

Add vim-style keybindings and modes.

(use-package evil
  :init
  (setq evil-want-integration t)
  (setq evil-want-keybinding nil)
  (setq evil-want-C-u-scroll t)
  (setq evil-want-C-d-scroll t)
  (setq evil-want-C-h-delete nil)
  (setq evil-want-C-i-jump nil)
  :config
  (evil-mode 1)
  (define-key evil-insert-state-map (kbd "C-g") 'evil-normal-state)

  ;; Use visual line motions even outside of visual-line-mode buffers
  (evil-global-set-key 'motion "j" 'evil-next-visual-line)
  (evil-global-set-key 'motion "k" 'evil-previous-visual-line)

  (evil-set-initial-state 'messages-buffer-mode 'normal)
  (evil-set-initial-state 'dashboard-mode 'normal))

Evil collection is a compatibility library for evil, to make sure things like magit work nicely with it.

(use-package evil-collection
    :ensure t
    :after evil
    :config
    (evil-collection-init))

(use-package evil-colemak-basics
  :config (global-evil-colemak-basics-mode))

Keybindings

All the keybindings which aren't set in package definitions and aren't map dependent.

(global-set-key (kbd "<escape>") 'keyboard-escape-quit)
(global-set-key (kbd "M-y") 'yank)
(global-set-key (kbd "C-c k") 'kill-region)
(global-set-key (kbd "S-C-<left>") 'shrink-window-horizontally)
(global-set-key (kbd "S-C-<right>") 'enlarge-window-horizontally)
(global-set-key (kbd "S-C-<down>") 'shrink-window)
(global-set-key (kbd "S-C-<up>") 'enlarge-window)

Development Tools

Magit

The best git tool I've ever used.

(use-package magit
  :custom
  (magit-display-buffer-function #'magit-display-buffer-same-window-except-diff-v1)
  :bind (("C-x g" . magit-status)))

(global-set-key (kbd "C-x x b") 'magit-blame)

Projectile

(use-package projectile
  :diminish projectile-mode
  :config (projectile-mode)
  :custom ((projectile-completion-system 'ivy))
  :bind-keymap
  ("C-c p" . projectile-command-map)
  :init
  ;; NOTE: Set this to the folder where you keep your Git repos!
  (when (file-directory-p my/default-directory)
    (setq projectile-project-search-path `(,my/default-directory)) ;; This may include multiple routes
    (setq projectile-switch-project-action #'projectile-dired)))

(use-package counsel-projectile
  :config (counsel-projectile-mode)
  :after projectile)

For searching within a project, projectile uses ripgrep. I set that up here.

(use-package ripgrep)
(use-package rg)

Paredit

(use-package paredit)

Languages

Language Servers

(use-package lsp-mode
  :commands (lsp lsp-deferred)
  :init
  (setq lsp-keymap-prefix "C-c l")
  :config
  (lsp-enable-which-key-integration t))

JavaScript/TypeScript

(add-hook 'javascript-mode-hook #'lsp-deferred)

(use-package typescript-mode
  :mode "\\.tsx?\\'" ;; .ts and .tsx files are typescript
  :hook (typescript-mode . lsp-deferred)
  :config
  (setq typescript-indent-level 2))

YAML

(use-package yaml-mode)

Gherkin

Gherkin is used in BDD to describe tests and expected behaviors.

(use-package feature-mode)

Python

(use-package lsp-pyright
  :ensure t
  :hook (python-mode . (lambda ()
                         (require 'lsp-pyright)
                         (lsp-deferred))))

When I open a python file it complains that it can't configure company-mode. I don't have company installed! Let's fix that.

(use-package company)

SQL

(use-package lsp-mssql)

GLSL

I'm experimenting with shader art. For when I want to edit shadertoy scripts in emacs, I have glsl-mode.

(use-package glsl-mode)

  • NEXT Add GLSL file types to the mode definition

Guile Scheme

(use-package geiser)

Website Development

HTTP Daemon

(use-package simple-httpd
  :ensure t)

File Management

Dired

Dired is great by default (when you know how to use it), but why not customize a good thing to make it better for me?

Bind dired-jump, and use H/L to move up/down directories and open files.

(use-package dired
  :ensure nil
  :commands (dired dired-jump)
  :bind (("C-x C-j" . dired-jump))
  :custom ((dired-listing-switches "-agho --group-directories-first"))
  :config
  (evil-collection-define-key 'normal 'dired-mode-map
    "h" 'dired-up-directory
    "l" 'dired-find-file))

Bindings

Some useful dired bindings to remember. C: copy R: rename/move % R: rename based on regular expressions

Org

Org is the bread and butter, the lifeblood of my emacs workflow. This configuration section is my baby, and if you hurt it I will hurt you <3

Org-Modern

Org modern streamlines making org files a bit more visually appealing. Unfortunately it also has dropped section highlighting on code blocks and some font issues have led me to not differentiate fonts between code and normal org, so code blocks are a bit harder to identify while using Modern.

(use-package org-modern :after org :config (setq org-auto-align-tags t org-tags-column 0 org-catch-invisible-edits 'show-and-error org-special-ctrl-a/e t org-insert-heading-respect-content t

org-hide-emphasis-markers t org-pretty-entities t org-ellipsis "…"

org-agenda-tags-column 100 org-agenda-block-separator ?─ org-agenda-time-grid '((daily today require-timed) (800 1000 1200 1400 1600 1800 2000) " ┄┄┄┄┄ " "┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄") org-agenda-current-time-string "> now ─────────────────────────────────────────────────")

(set-face-attribute 'org-modern-symbol nil :family "Iosevka")

(global-org-modern-mode))

I took out org modern - it caused problems. But I liked a lot of the little things it did, maybe I can bring those in?

(setq org-auto-align-tags t
      org-tags-column 0
      org-catch-invisible-edits 'show-and-error
      org-special-ctrl-a/e t
      org-insert-heading-respect-content t

      org-hide-emphasis-markers nil
      org-pretty-entities t
      org-ellipsis "…"

      org-agenda-tags-column 100
      org-agenda-block-separator ?─
      org-agenda-time-grid
      '((daily today require-timed)
        (800 1000 1200 1400 1600 1800 2000)
        " ┄┄┄┄┄ " "┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄")
      org-agenda-current-time-string
      "> now ─────────────────────────────────────────────────")

Setup

Before actually configuring org, create helper and prettifier functions that can be called later.

(setq org-use-sub-superscripts '{})

(defun my/org-mode-setup ()
  (org-indent-mode)
  (variable-pitch-mode 1)
  (visual-line-mode 1)
  (auto-fill-mode 0)
  (setq evil-auto-indent nil))

;; This should be made unnecessary by org-modern
(defun my/org-font-setup ()
  ;; Replace list hyphen with dot
  (font-lock-add-keywords 'org-mode
                          '(("^ *\\([-]\\) "
                             (0 (prog1 () (compose-region (match-beginning 1) (match-end 1) "•"))))))

  ;; Set faces for heading levels
  (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 "Cantarell" :weight 'regular :height (cdr face)))

  ;; Ensure that anything that should be fixed-pitch in Org files appears that way
  (set-face-attribute 'org-block nil :foreground nil :inherit 'fixed-pitch)
  (set-face-attribute 'org-code nil   :inherit '(shadow fixed-pitch))
  (set-face-attribute 'org-table 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))

Org main config

(use-package org
  :hook (org-mode . my/org-mode-setup)
  :bind (("C-c a" . org-agenda)
         :map org-agenda-keymap
         ("y" . org-agenda-todo-yesterday))

  :config
  (setq org-agenda-start-with-log-mode t)
  (setq org-log-done 'time)
  (setq org-log-into-drawer t)

  (setq org-deadline-warning-days 8)

  (setq org-todo-keywords
        ;; Tasks
        '((sequence "TODO(t)"
                    "NEXT(n)"
                    "|"
                    "DONE(d!)")
          ;; Projects
          (sequence "BACKLOG(b)"
                    "PLAN(p)"
                    "READY(r)"
                    "ACTIVE(a)"
                    "REVIEW(v)"
                    "WAIT(w@/!)"
                    "HOLD(h)"
                    "|"
                    "COMPLETED(c)"
                    "CANCELLED(k@)")

          ;; Reading

          (sequence "TO-READ(T)"
                    "READING(R)"
                    "ONGOING(O)"
                    "|"
                    "FINISHED(F)"
                    "DROPPED(D)")))

  ;; Configure custom agenda view
  (setq org-agenda-custom-commands
        '(("d" "Dashboard"
           ((todo "ACTIVE" ((org-agenda-overriding-header "Active Projects")))
            (todo "READING" ((org-agenda-overriding-header "Current Media")))
            (todo "NEXT" ((org-agenda-overriding-header "Next Tasks")))))

          ("w" "Work Tasks" tags-todo "+job")

          ;;Low-effort next actions
          ("e" "Low Effort" tags-todo "+TODO=\"NEXT\"+Effort<=15&+Effort>0"
           ((org-agenda-overriding-header "Low Effort Tasks")
            (org-agenda-max-todos 20)
            (org-agenda-files org-agenda-files)))

          ("h" "Urgent Habits"
           agenda "+Style=habit"
           ((org-agenda-overriding-header "Most Pressing Habits")
            (org-agenda-max-todos 10)))

          ("p" "Project 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-todo-list-sublevels nil)
                   (org-agenda-files org-agenda-files)))
            (todo "BACKLOG"
                  ((org-agenda-overriding-header "Project Backlog")
                   (org-agenda-todo-list-sublevels nil)
                   (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 "CANCELLED"
                  ((org-agenda-overriding-header "Cancelled Projects")
                   (org-agenda-files org-agenda-files)))))))

  ;; Save org buffers after refiling
  (advice-add 'org-refile :after 'org-save-all-org-buffers)

  ;; Scheduled TODOs with STYLE: HABIT will show a history chart in the agenda
  (require 'org-habit)
  (add-to-list 'org-modules 'org-habit)
  (setq org-habit-graph-column 60)
  (setq org-habit-preceding-days 15)
  (setq org-habit-following-days 5)

  ;(my/org-font-setup)

  (setq org-columns-default-format "%TODO %75ITEM %3PRIORITY %TAGS"))

The org-tag-alist is useful for managing predefined tags on TODOs, which I'm trying out using for work contexts.

(setq org-tag-alist
      '(;; Places
        ("@home" . ?H)
        ("@work" . ?W)
        ("@pharmacy" . ?X)
        ;; Tools
        ("@phone" . ?P)
        ("@computer" . ?C)
        ;; Activities
        ("@planning" . ?n)
        ("@programming" . ?p)
        ("@writing" . ?w)
        ("@email" . ?e)
        ("@calls" . ?a)
        ("@errands" . ?r)
        ("@creative" . ?c)
        ("@pkm" . ?k)
        ;; People
        ("@Quinn" . ?Q)))

Org Tempo helps with word expansion, e.g. using <el `<TAB>' as a shortcut for the emacs-lisp source blocks in this file.

(require 'org-tempo)

The org-structure-template-alist determines what can be auto expanded as a code block. Each expanded block begins with #+begin_ and continues with the second string in the relevant definition.

(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 '("py" . "src python"))
(add-to-list 'org-structure-template-alist '("mr" . "src mermaid :file temp.png"))

Center and pad org files

(defun my/org-mode-visual-fill ()
  (setq visual-fill-column-width 180
        visual-fill-column-center-text t)
  (visual-fill-column-mode 1))

(use-package visual-fill-column
  :hook (org-mode . my/org-mode-visual-fill))

Org Babel

Code block embedding, tangling, and evaluation. This is built into Org, so this is just configuration and not activation.

(org-babel-do-load-languages
 'org-babel-load-languages '((emacs-lisp . t)
                             (python . t)))
(setq org-confirm-babel-evaluate nil)

Auto-tangle this config file

Files which are intended to be narrated elisp config files can be specified here so that they automagically babel-tangle to their target files every time they are saved.

(defun my/org-babel-tangle-config ()
(when (string-equal (buffer-file-name)
                    (expand-file-name "~/.emacs.d/config.org")) ; This file
  (let ((org-confirm-babel-evaluate nil))
    (org-babel-tangle))))

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

Org Roam

A personal knowledge management tool based on Roam Research, sort of like a zettelkasten.

Main config

(use-package org-roam
  :ensure t
  :demand t  ;; Ensure org-roam is loaded by default
  :init
  (setq org-roam-v2-ack t)
  :custom
  (org-roam-directory (concat my/org-file-path "roam"))
  (org-roam-completion-everywhere t)
  :bind (("C-c n l" . org-roam-buffer-toggle)
         ("C-c n o" . org-roam-node-find)
         ("C-c n f" . org-roam-refile)
         ("C-c n i" . org-roam-node-insert)
         ("C-c n I" . org-roam-node-insert-immediate)
         ("C-c n p" . my/org-roam-find-active-project)
         ("C-c n a" . my/org-roam-find-area)
         ("C-c n r" . my/org-roam-find-resource)
         ("C-c n t" . my/org-roam-capture-task)
         ("C-c n e" . my/org-roam-find-rpg)
         ("C-c n b" . my/org-roam-capture-inbox)
         ("C-c n R" . org-roam-node-random)
         ("C-c n T a" . org-roam-tag-add)
         ("C-c n T r" . org-roam-tag-remove)
         ("C-c n A a" . org-roam-alias-add)
         ("C-c n A r" . org-roam-alias-remove)
         ("C-c n s" . org-roam-db-sync)
         :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))
  :bind-keymap
  ("C-c n d" . org-roam-dailies-map)
  :config
  (require 'org-roam-dailies) ;; Ensure the keymap is available
  (setq org-roam-db-autosync-mode t))

Templates

For the sake of organization I tangle templates into a separate templates.el file which is then loaded in my init.el. My hope is to use this convention for all templates, not just org-roam.

First, several core templates are defined which are broadly reused. I save them as variables so I don't have to repeat myself and to minimize future editing work.

(setq my/org-roam-project-template "* Purpose and Principles\n/You cannot know what a project should look like or how to create it if you don't understand *why* you're doing it. This includes both its 'purpose' - why this project needs to be produced - and you 'principles' - the standards and values you hold that impact how and what you produce/.\n\n** Purpose\n\n** Principles\n\n* Outcome Visioning\n/What will it be like when this project is out in the world? It's much easier to see how to do something once it's already done. So, envision your completion of the project so that you know what it might take to get there/.\n\n** What will the end ideally look like?\n\n** How will I ideally feel after completing this?\n\n** How will others ideally respond?\n\n** What else will result from the completion of this project?\n\n* Brainstorm\n/Write down *every* idea that occurs to you so that you don't have to hold *any* ideas in your head. Do not judge the ideas, aim for quantity not quality. Resist the urge to organize or analyze for now/.\n\n* Tasks\n/Look through the brainstorm output, and decide what bite-sized, well defined tasks can be started *now*. Then work on them/!\n\n** PLAN ${title}"
      my/org-roam-area-template "* Goals\n\n%?\n\n* Tasks\n\n** NEXT Add initial tasks\n\n* Dates\n\n"
      my/org-roam-biblio-template "* Source\n\nAuthor: %^{Author}\nTitle: ${title}\nYear: %^{Year}\n\n* Summary\n\n%?\n\n* Notes\n\n"
      my/org-roam-daily-template "#+title: %<%Y-%m-%d>\n#+filetags: Daily\n\n* High Impact Tasks\n** TODO\n** TODO\n** TODO\n* Day in Review\n** Today's Wins\n- \n* Notes")

org-roam-capture-templates defines the normal capture templates used when org-roam-node-find finds a nonexisting node. I slot the previously defined variables in where they are relevant.

(setq org-roam-capture-templates
      `(("d" "default" plain
         "%?"
         :if-new (file+head
                  "%<%Y%m%d%H%M%S>-${slug}.org" "#+title: ${title}\n\n")
         :unnarrowed t)

        ("p" "project" plain
         ,my/org-roam-project-template
         :if-new (file+head
                  "%<%Y%m%d%H%M%S>-${slug}.org" "#+title: ${title}\n#+category: ${title}\n#+filetags: Project\n\n")
         :unnarrowed t)

        ("a" "area" plain
         ,my/org-roam-area-template
         :if-new (file+head
                  "%<%Y%m%d%H%M%S>-${slug}.org" "#+title: ${title}\n#+category: ${title}\n#+filetags: Area\n\n")
         :unnarrowed t)

        ("b" "bibliography" plain
         ,my/org-roam-biblio-template
         :if-new (file+head
                  "%<%Y%m%d%H%M%S>-biblio-${slug}.org" "#+title: ${title}\n#+filetags: Biblio\n\n")
         :unnarrowed t)))

(setq org-roam-dailies-capture-templates
      `(("d" "default" entry
         "** %<%H:%M>: %?"
         :if-new (file+head+olp
                  "%<%Y-%m-%d>.org" ,my/org-roam-daily-template ("Notes")))))

Back in init.el, run the code from that other file.

(load "~/.emacs.d/templates.el")

Support Functions

  • Filtering

    There's a couple of filter modes I had to custom define to be able to implement PARA how I wanted to.

    Create a filtering lambda which accepts all nodes tagged with tag-name, a string.

    (defun my/org-roam-filter-by-tag (tag-name)
        "Takes TAG-NAME, a string, and creates a lambda which return t iff a provided org-roam node is tagged with TAG-NAME."
        (lambda (node)
          (member tag-name (org-roam-node-tags node))))
    
    

    Using that filtering lambda, create a list of all nodes with a provided tag.

    (defun my/org-roam-list-notes-by-tag (tag-name)
      "Create and return a list of all org-roam nodes which have TAG-NAME as one of their tags."
      (mapcar #'org-roam-node-file
              (seq-filter
               (my/org-roam-filter-by-tag tag-name)
               (org-roam-node-list))))
    
    (defun my/org-roam-list-notes-by-tags-exclude-archive (taglist)
      "Returns a list of all org-roam nodes which are tagged with TAGLIST and not tagged with 'Archive'"
      (mapcar #'org-roam-node-file
              (seq-filter
               (my/org-roam-filter-by-tags-exclude-archive taglist)
               (org-roam-node-list))))
    
    

    Create a filter lambda which accepts all nodes tagged with any element of taglist, a list of strings.

    (defun my/org-roam-filter-by-tags (taglist)
      "Create a lambda which returns t iff any string in TAGLIST is a tag on a provided org-roam node."
      (lambda (node)
        (setq check nil)
        (dolist (tag taglist)
          (if (member tag (org-roam-node-tags node))
              (setq check t)))
        check))
    
    

    Create a filtering lambda which accepts all nodes that are not tagged with any member of taglist, a list of strings.

    (defun my/org-roam-filter-by-tags-exclusive (taglist)
      "Create a filtering lambda which returns nil iff the provided roam node is tagged with any member of taglist, and returns t otherwise"
      (lambda (node)
        (setq check t)
        (dolist (tag taglist)
          (if (member tag (org-roam-node-tags node)) (setq check nil)))
        check))
    
    

    Create a filtering lambda which accepts all nodes which are tagged with any element of taglist, and are not tagged with "Archive".

    (defun my/org-roam-filter-by-tags-exclude-archive (taglist)
      "Does the same thing as my/org-roam-filter-by-tags, but will always return nil if \"Archive\" is a member of the node's tags."
      (lambda (node)
        (setq check nil)
        (dolist (tag taglist)
          (if (and (member tag (org-roam-node-tags node)) (not (member "Archive" (org-roam-node-tags node))))
              (setq check t)))
        check))
    
    
  • Node Link Insertion

    This initially seemed like a great idea, enabling something similar to Obsidian's \[\[link-to-an-unreal-note]] syntax. Since realizing a zettelkasten is not a wiki though it seems much less useful - How can I link to a proper note of my own thought that I haven't recorded yet? I think it is usually better to capture that thought as an inbox entry and a link to the note which prompted it. I am considering removing this function.

    (defun org-roam-node-insert-immediate (arg &rest args)
      "Insert a link to an org-roam node. If the node does not exist, create it but do not prompt for a template or contents."
      (interactive "P")
      (let ((args (push arg args))
            (org-roam-capture-templates (list (append (car org-roam-capture-templates)
                                                      '(:immediate-finish t)))))
        (apply #'org-roam-node-insert args)))
    
    
  • Org Agenda management

    I used to dynamically generate the agenda files list from tagged roam nodes, but I have since changed to a single dedicated file.

    (setq org-agenda-files `(,(concat org-roam-directory "/000-Agenda.org")))
    
    
  • Node Searching

    Searching for org-roam nodes is a little bit of a fiddly task, because I want to be able to search in heavily filtered ways, and have specific capture templates for each kind of filtered search I do.

    First of all, there is a hook to rebuild the agenda files list after a roam capture, to ensure the agenda is always up to date with newly created projects.

    (defun my/org-roam-project-finalize-hook ()
      "Refreshes `org-agenda-files' to ensure the captured node is added if the capture was not aborted."
      ;; When this hook is invoked, remove it from the hookpoint
      (remove-hook 'org-capture-after-finalize-hook #'my/org-roam-project-finalize-hook)
      (unless org-note-abort
        (my/org-roam-refresh-agenda-list)))
    
    (defun my/org-roam-refresh-agenda-list ()
      "Does nothing. Used to ensure a new project file was added to the agenda list.")
    
    

    Search for all nodes tagged with Project not tagged with Archive. If the selected node doesn't exist, capture a new node with only one available template - my project template.

    (defun my/org-roam-find-active-project ()
      "Find or create a node by title which has the tag \"Project\" and does not have the tag \"Archive\". If the target node does not exist, the creation process is identical to `my/org-roam-find-all-projects'."
      (interactive)
      (add-hook 'org-capture-after-finalize-hook #'my/org-roam-project-finalize-hook)
      (org-roam-node-find
       nil
       nil
       (my/org-roam-filter-by-tags-exclude-archive '("Project"))
       nil
       :templates `(("p" "project" plain
                     ,my/org-roam-project-template
                     :if-new (file+head
                              "%<%Y%m%d%H%M%S>-${slug}.org"
                              "#+title: ${title}\n#+category: ${title}\n#+filetags: Project\n\n")
                     :unnarrowed t))))
    
    

    Filtering out archived projects was a refinement of this original function as I finished setting up a PARA-ish roam graph, but I decided to keep the option to search among all projects including the archive.

    (defun my/org-roam-find-all-projects ()
      "Find or create an org node by title which has the tag \"Project\"."
      (interactive)
      (add-hook 'org-capture-after-finalize-hook #'my/org-roam-project-finalize-hook)
      (org-roam-node-find
       nil
       nil
       (my/org-roam-filter-by-tag "Project")
       nil
       :templates `(("p" "project" plain
                     ,my/org-roam-project-template
                     :if-new (file+head
                              "%<%Y%m%d%H%M%S>-${slug}.org"
                              "#+title: ${title}\n#+category: ${title}\n#+filetags: Project\n\n")
                     :unnarrowed t))))
    
    

    And after all, why not see only what's archived? So here's that search function.

    (defun my/org-roam-find-archive ()
      "Find or create an org node by title which has the tag \"Archive\"."
      (interactive)
      (add-hook 'org-capture-after-finalize-hook #'my/org-roam-project-finalize-hook)
      (org-roam-node-find
       nil
       nil
       (my/org-roam-filter-by-tag "Archive")
       nil)) ;; No template override is given, so defaults are used.
    
    

    Search for all roam nodes which are tagged Area.

    (defun my/org-roam-find-area ()
      "Find or create an org node by title which has the tag \"Area\"."
      (interactive)
      (add-hook 'org-capture-after-finalize-hook #'my/org-roam-project-finalize-hook)
      (org-roam-node-find
       nil
       nil
       (my/org-roam-filter-by-tags-exclude-archive '("Area"))
       nil
       :templates `(("a" "area" plain
                     ,my/org-roam-area-template
                     :if-new (file+head
                              "%<%Y%m%d%H%M%S>-${slug}.org"
                              "#+title: ${title}\n#+category: ${title}\n#+filetags: Area\n")
                     :unnarrowed t))))
    
    

    And finally the laziest part of my PARA implementation. Anything which is not a Project or Area (or the Inbox) must be a resource, archived or not.

    (defun my/org-roam-find-resource ()
      "Find an org node by title which is not tagged with \"Project\", \"Area\", or \"Inbox\", or \"Daily\"."
      (interactive)
      (org-roam-node-find
       nil nil
       (my/org-roam-filter-by-tags-exclusive '("Project" "Area" "Inbox" "Daily"))
       nil)) ;; Don't override default templates if creating a new file
    
    
    • TTRPG specific

      RPG resources are important for me to be able to find, so I'm adding an explicit section to find it in my PARA setup.

      (defun my/org-roam-find-rpg ()
        "Find an org node by title which is tagged with \"ttrpg\""
        (interactive)
        (org-roam-node-find
         nil nil
         (my/org-roam-filter-by-tag "ttrpg") nil)) ;; Don't override default templates
      
      
  • Capturing
    (defun my/org-roam-capture-inbox ()
      "Capture a bullet into the Inbox.org file."
      (interactive)
      (org-roam-capture- :node (org-roam-node-create)
                         :templates '(("i" "default" plain
                                       "* %?"
                                       :if-new (file+head
                                                "Inbox.org" "#+title: Inbox\n")))))
    
    

    I need to rewrite capture-task in order to direct the captures into the Agenda File on new projects and areas.

    (defun my/org-roam-capture-task ()
      (interactive)
      ;; Ensure that the project or area node is included in the org agenda after the capture is saved
      (add-hook 'org-capture-after-finalize-hook #'my/org-roam-project-finalize-hook)
    
      ;; Capture the new task, creating the project file if necessary
      (org-roam-capture- :node (org-roam-node-read
                                nil
                                (my/org-roam-filter-by-tags-exclude-archive '("Area" "Project")))
                         :templates '(("p" "project" plain
                                       "** TODO %?"
                                       :if-new (file+head+olp
                                                "%<%Y%m%d%H%M%S>-${slug}.org"
                                                "#+title: ${title}\n#+category: ${title}\n#+filetags: Project\n\n"
                                                ("Tasks")))
    
                                      ("a" "area" plain
                                       "** TODO %?"
                                       :if-new (file+head+olp
                                                "%<%Y%m%d%H%M%S>-${slug}.org"
                                                "#+title: ${title}\n#+category: ${title}\n#+filetags: Area\n"
                                                ("Tasks"))))))
    
    (defun my/org-roam-capture-event ()
      (interactive)
      (org-roam-capture- :node (org-roam-node-read
                                nil
                                (my/org-roam-filter-by-tags-exclude-archive '("Area" "Project")))
                         :templates '(("e" "event" plain
                                       "** %?\n%U\n%^T"
                                       :if-new (file+head+olp
                                                "%<%Y%m%d%H%M%S>-${slug}.org"
                                                "#+title: ${title}\n#+category: ${title}"
                                                ("Dates"))))))
    
    

Org Roam UI

(use-package org-roam-ui
  :after org-roam
  :bind (("C-c n u" . org-roam-ui-open))
  :config
  (setq org-roam-ui-sync-theme t
        org-roam-ui-follow t
        org-roam-ui-update-on-save t
        org-roam-ui-open-on-start t))

Mermaid

Mermaid is a tool for markdown descriptions to generate graph diagrams such as UML, flowcharts, Sequence diagrams, and burndown charts. Syntax information and docs can be found at https://mermaid.js.org/intro.

(use-package ob-mermaid)
(use-package mermaid-mode)

Communication

It's cool to use emacs for internet communication!

Matrix

Ement's main drawback is that it doesn't natively handle encrypted channels. That's quite important to me, so I'm not using it for now and will address this… someday.

;; (use-package ement)

Mastodon

mastodon-instance-url and mastodon-active-user are set elsewhere to keep my accounts out of my git history.

(use-package emojify)

(use-package mastodon
  :bind (("C-c m h" . mastodon-tl--get-home-timeline)
         ("C-c m l" . mastodon-tl--get-local-timeline)
         ("C-c m F" . mastodon-tl--get-federated-timeline)
         ("C-c m n" . mastodon-notifications-get)
         ("C-c m m" . mastodon)
         ("C-c m t" . mastodon-toot)
         ("C-c m T" . mastodon-tl--thread)
         ("C-c m f" . mastodon-toot--toggle-favourite)
         ("C-c m b" . mastodon-toot--toggle-boost)
         ("C-c m B" . mastodon-toot--toggle-bookmark)
         ("C-c m r" . mastodon-toot--reply)
         ("C-c m p" . mastodon-profile--my-profile)
         ("C-c m c w" . mastodon-tl--toggle-spoiler-text-in-toot)
         :map mastodon-mode-map
         ("S-SPC" . mastodon-tl--scroll-up-command)))

(load "~/.emacs.d/my-mastodon.el")

(defun my/mastodon-select-active-account (accounts index)
  (setq mastodon-active-user (nth 1 (nth index accounts)))
  (setq mastodon-instance-url (nth 2 (nth index accounts))))

Gemini

An alternative to HTTP.

Elpher is a browser

(use-package elpher
  :bind (("C-c e" . elpher))
  :config
  (setq elpher-default-url-type "gemini"))

Gemini mode and ox gemini are editing tools.

(use-package gemini-mode)
(use-package ox-gemini)

RSS

(use-package elfeed)

Dice

I want to write my own package for this as a learning exercise, but in the mean time it sure is handy to have an arbitrary dice roller.

(use-package decide
  :bind (("C-c d" . decide-roll-dice)))

Work Timer

I find it really helpful to have a work/rest cycle clock such as a pomodoro timer. Rather than keep using youtube videos for this, I can save internet traffic by just… running a timer locally. I guess I'll find out whether the benefit is from the timer or from watching other people work!

Pomm

pomm implements pomodoros and the 'third-time' system, which I quite like. It allows for configurable work/break ratios, but does not allow for custom and one-off timers.

(use-package pomm
  :bind (("C-c t t" . pomm-third-time)
         ("C-c t b" . pomm-third-time-switch)
         ("C-c t p" . pomm))
  :config
  (setq pomm-audio-player-executable (or pomm-audio-player-executable (executable-find "mpv"))
        pomm-audio-enabled t
        pomm-mode-line-mode t
        setq pomm-third-time-fraction "2/3"))

Egg-timer

egg-timer on the other hand is specifically made to let me set up one-off timers!

(use-package egg-timer
  :bind (("C-c t e" . egg-timer-schedule)))

Macros

I used to constantly open my daily agenda view before I changed my workflow. This sets up a shortcut to the view I preferred.

(fset 'mkr-agenda
 (kmacro-lambda-form [?\C-c ?a ?a ?d ?\C-x ?1] 0 "%d"))

(global-set-key (kbd "C-a") 'mkr-agenda)

Custom Set Variables

I probably don't want to manually set this! Doing this guarantees that package-selected-packages is always set to the same thing, even when emacs would change it. But if I don't include this, package-selected-packages will be wiped out every time I edit my config!

(custom-set-variables
 ;; custom-set-variables was added by Custom.
 ;; If you edit it by hand, you could mess it up, so be careful.
 ;; Your init file should contain only one such instance.
 ;; If there is more than one, they won't work right.
 '(package-selected-packages nil))
(custom-set-faces
 ;; custom-set-faces was added by Custom.
 ;; If you edit it by hand, you could mess it up, so be careful.
 ;; Your init file should contain only one such instance.
 ;; If there is more than one, they won't work right.
 )

Open a fresh daily note when opening emacs

(org-roam-dailies-goto-today)

Emacs 29.4 (Org mode 9.6.15)