Custom configuration for Emacs

1. Overview

Downloadable, tangled version here.

1.1. [2023-08-03 Thu]

For a while I've been using Emacs as my primary text editor and relying heavily on Org-mode as my personal organization system (as well as using it at work for writing notes and tracking administrative time).

I spent some time with vanilla Emacs when I first started using it, but it wasn't until I installed Spacemacs that I started using it heavily. Part of the reason I first tried it was because I had also tried Vim at around the same time, and after the initial learning curve I decided I liked modal editing. Spacemacs offered a relatively easy way to get started with modal editing in Emacs, along with its other quality of life features.

However, I recently decided that I wanted to try setting up my own Emacs config more or less from scratch. This was for a few reasons:

  1. I only use a fraction of Emacs's feature set, which is to be expected. On top of that, though, I was only using a fraction of what Spacemacs offered, which felt like a little much.
  2. I wanted to better understand the systems I am using, and getting hands on with their config felt like a good way to do that.
  3. One of the things I like about using Org-mode is that, unlike proprietary todo apps, I don't have any worries about being able to read old files and/or notes in the future. Spacemacs, althought it is a well-supported project, is not (as far as I can tell) as well supported as Emacs generally or Org-mode. By cutting it out, I can decrease the danger that software I'm using now will become unsupported in the future.
  4. Spacemacs is usually stable once you have it working, but I've run into my fair share of issues with customizing it over the years, and (minor complaint) it's pretty slow to start, even on my beefy home desktop. If I'm going to have to occasionally delve into the guts of this thing to fix it, I may as well spend that time working on a config that's closer to base Emacs!

I want to be clear–I mean no disrespect towards Spacemacs. A lot of people put in a lot of hard work on it, and it's a great project as a result. If you're just starting out with Emacs–especially if you're coming from Vim–you could do worse than to start out by using Spacemacs. It flattened my Emacs learning curve by including (for me) lots of useful features with good enough default keybindings that I could comfortably use it and sort of ease my way into Emacs. It's only after a long time spent with Spacemacs that I feel both comfortable enough with Emacs to start this project and like I know enough about what features are important to me.

As of right now, mostly what I've done is get the keybindings working more or less the same way in vanilla Emacs as they do in Spacemacs, and get my org-mode configuration ported over. As I need more functionality, (e.g. language support), I'll add it manually.

2. Package configuration

The package.el configuration sits at the top of the config, because I want package.el to load first and check to make sure that all the packages I want are actually installed.

This works pretty well–if I need a new MELPA package added to Emacs, I just add its name to the list here then reload the init.

;;
;; package configuration
;;
(require 'package)
;; enable melpa archive
(add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t)
(package-refresh-contents)
;; package order notes:
;; - hydra has to be installed before ivy/counsel (https://github.com/abo-abo/swiper/issues/464)
(setq package-selected-packages
      '(
        evil
        hydra
        counsel
        ivy-hydra
        which-key
        flyspell-correct-ivy
        highlight-indentation
        company
        olivetti
        twilight-theme
        centered-cursor-mode
        async
        magit
        mwim
        htmlize
        prescient
        ivy-prescient
        nov
        org-random-todo
        org-roam
        systemd
        syslog-mode
        org-contrib
        yaml-mode
        json-mode
        language-detection ;; required by html2org
        )
)
;; install selected packages if not present
(package-install-selected-packages t)

2.1. Package List

Some note about the choices above.

2.1.1. evil

[2023-08-03 Thu] - I love the Vim keybindings. I love modal editing. Emacs is great but the default keybindings are not for me.

2.1.2. hydra

[2023-08-03 Thu] - I have Hydra mainly as a prerequisite to ivy-hydra. It's explicitly declared because it looks like there may be load-order issues that can affect it if it ends up being loaded after ivy.

2.1.3. counsel, ivy-hydra

[2023-08-03 Thu] - The other thing I need to work comfortably in Emacs–second only to Vim keybindings–is completion. Previously I used Spacemacs with Helm for completion, so I decided to try the other popular completion framework for comparison's sake.

Ivy-Hydra has to be explicitly declared, as I found out after an embarrassing amount of troubleshooting.

2.1.4. which-key

[2023-08-03 Thu] - Not as essential here as it was in Spacemacs, where I used it not only to remember commands I used often but to discover new commands. In this config, almost all keybindings that will be easily discoverable are going to be ones I made, but it's nice to have the reminder of where things are in case I get lost anyway.

2.1.5. flyspell-correct-ivy

[2023-08-03 Thu] - Integrates ivy completion and minibuffer hydra setup into flyspell.

2.1.6. org-contrib

[2023-08-03 Thu] - Provides various bits and bobs for org-mode

2.1.7. org-roam

[2023-08-03 Thu] - I use org-roam to take interconnected notes using a rough version of the Zettelkasten method.

2.1.8. olivetti

[2023-08-03 Thu] - Olivetti-mode formats the Emacs window in a way that I find more suitable for long-form writing than the default.

2.1.9. twilight-theme

[2023-08-03 Thu] - A good-enough dark theme for Emacs.

2.1.10. centered-cursor-mode

[2023-08-03 Thu] - Allows me to toggle keeping the active line in the center of the window. I don't always want it active, but especially in long files where I'm doing a lot of scrolling it's nice to (1) know where my cursor is and (2) not deal with the stutter-stop paging behavior.

2.1.11. mwim

[2025-08-09 Sat] - Prerequisite for nodejs-mode.

2.1.12. nov

[2025-08-09 Sat] - Used for reading epubs in emacs; has some basic org-mode integration including supporting org links

3. Autocomplete configuration

Just the basic setup recommended in the Ivy user's guide; includes recent buffers in the ivy switch buffer screen, and shows both the index and the full count when scrolling through Ivy results.

[2025-08-09 Sat] - I've since installed/enabled Prescient, which shows my most recent selections when in an Ivy completion menu.

;; 
;; ivy/counsel/swipe configuration
;;
(setq ivy-use-virtual-buffers t)
(setq ivy-count-format "(%d/%d) ")
(ivy-mode 1)
(ivy-prescient-mode 1)
(prescient-persist-mode 1)

3.1. ivy-hydra configuration

Configure the bindings in the hydra menu for Ivy. Enables me to use modal keybindings in Ivy buffers.

(defhydra hydra-ivy (:hint nil :color pink)
  "
^ ^ ^ ^ ^ ^ | ^Call^      ^ ^  | ^Cancel^ | ^Options^ | Action _w_/_s_/_a_: %-14s(ivy-action-name)
^-^-^-^-^-^-+-^-^---------^-^--+-^-^------+-^-^-------+-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^---------------------------
^ ^ _k_ ^ ^ | _f_ollow occ_U_r | _i_nsert | _c_: calling %-5s(if ivy-calling \"on\" \"off\") _C_ase-fold: %-10`ivy-case-fold-search
_h_ ^+^ _l_ | _d_one      ^ ^  | _o_ops   | _M_: matcher ^^^^^^^^^^^^ _T_runcate: %-11`truncate-lines
^ ^ _j_ ^ ^ | _g_o        ^ ^  | _p_aste  | _<_/_>_: shrink/grow^^^^^^^^^^^^^^^^^^^^^^^^^^^^ _D_efinition of this menu
"
  ;; arrows
  ("h" ivy-beginning-of-buffer)
  ("j" ivy-next-line)
  ("k" ivy-previous-line)
  ("l" ivy-end-of-buffer)
  ;; history
  ("J" ivy-next-history-element)
  ("K" ivy-previous-history-element)
  ;; mark
  ("m" ivy-mark)
  ("u" ivy-unmark)
  ("DEL" ivy-unmark-backward)
  ("t" ivy-toggle-marks)
  ;; actions
  ("o" keyboard-escape-quit :exit t)
  ("r" ivy-dispatching-done :exit t)
  ("C-g" keyboard-escape-quit :exit t)
  ("i" nil)
  ("C-o" nil)
  ("f" ivy-alt-done :exit nil)
  ("C-j" ivy-alt-done :exit nil)
  ("d" ivy-done :exit t)
  ("g" ivy-call)
  ("C-m" ivy-done :exit t)
  ("c" ivy-toggle-calling)
  ("M" ivy-rotate-preferred-builders)
  (">" ivy-minibuffer-grow)
  ("<" ivy-minibuffer-shrink)
  ("w" ivy-prev-action)
  ("s" ivy-next-action)
  ("a" ivy-hydra--read-action)
  ("T" ivy-hydra--toggle-truncate-lines)
  ("C" ivy-toggle-case-fold)
  ("U" ivy-occur :exit t)
  ("D" ivy-hydra--find-definition :exit t)
  ("p" yank))

4. Keymappings

4.1. [2023-08-03 Thu]

This is where most of the sweat went–figuring out not just how to rebind keys in Emacs, but to bind nested keymaps, make sure they're labeled in which-key, and have them respect the proper Evil mode.

I really wanted to emulate the default feel of evil-Spacemacs here. SPC (in normal mode) is the gateway to almost all commands.

In the first version of this config, I used general.el. It worked well and with a minimum of fuss, but once I had the keybindings set up that way, I found that I had a better understanding of how keybindings in Emacs worked. Thanks to that–and to noctuid's Evil guide–I was able to translate that config from General to just using the built-in keybinding commands from Emacs and Evil. The second way looks a lot clunkier, but it also fits better with the ethos I'm trying to bring here of using the fewest number of external packages (consistent, of course, with me both having the features I need and actually getting to use Emacs at some point).

The thing it took me the longest to wrap my head around was creating nested keymaps, which is done with the following construction:

(defvar leader-map-name (make-sparse-keymap))
(define-key keymap-key-is-bound-to "<key>" (cons "name for leader key in which-key" leader-map-name))
  (define-key leader-map-name "<key>" 'bound-function)

This will create a leader key accessible from a given keymap, give it a readable name in which-key, and add one or more commands under it.

4.2. Evil-mode configuration

The evil-mode configuration I have is pretty minimal; I just have it set respect visual-line-mode globally, and have a few things set to make it so that most buffers will default to normal mode (as opposed to motion mode).

;;
;; evil configuration
;;
;; set evil to respect visual line mode
(setq evil-respect-visual-line-mode t)
;; load evil mode
(require 'evil)
;; enable evil mode when non-nil
(evil-mode 1)
;; make normal the default state
(setq evil-emacs-state-modes nil)
(setq evil-insert-state-modes nil)
(setq evil-motion-state-modes nil)

4.3. Custom functions

I only have a couple custom functions I'm currently using (outside of the org-agenda).

Evil-org-special-o/O and evil-org-open-below are copied from evil-org-mode, and allow smart behavior when using "o" to start a new line in org-mode. On most lines, it will start a new line in Insert mode as normal–but on a list or a table, it will insert a new list item (preserving the line spacing between items) or table row.

;;
;; custom functions
;;

;; evil-org-open-below
;; copied from evil-org

(defcustom evil-org-special-o/O '(table-row item)
  "When o and O should be special.
This makes them continue item lists and table rows.
By default, o and O are bound to ‘evil-org-open-above’ and ‘evil-org-open-below’."
  :group 'evil-org
  :type '(set (const table-row) (const item)))

(defun evil-org-open-below (count)
  "Clever insertion of org item.
Argument COUNT number of lines to insert.
The behavior in items and tables can be controlled using ‘evil-org-special-o/O’.
Passing in any prefix argument, executes the command without special behavior."
  (interactive "P")
  (cond ((and (memq 'table-row evil-org-special-o/O) (org-at-table-p))
         (org-table-insert-row '(4))
         (evil-insert nil))
        ((and (memq 'item evil-org-special-o/O) (org-at-item-p)
              (progn (end-of-visible-line)
                     (org-insert-item (org-at-item-checkbox-p))))
         (evil-insert nil))
        ((evil-open-below count))))

4.4. Which-key configuration

Very simple–just load which-key and enable it by default.

;;
;; which-key configuration
;;
(require 'which-key)
;; enable which-key by default
(which-key-mode)

4.5. Global keybindings, leader-key mapped

;;
;; keybindings ;;
;;
;; set leader key
(defvar my-leader-map (make-sparse-keymap))
(define-key evil-normal-state-map (kbd "SPC") my-leader-map)
(define-key evil-motion-state-map (kbd "SPC") my-leader-map)

;; set global keymap
;; 
;; standalones
(define-key my-leader-map (kbd "SPC") 'counsel-M-x)
(define-key my-leader-map (kbd "TAB") (lambda () (interactive) (switch-to-buffer (other-buffer))))
;; buffers ;;
(defvar leader-buffer-map (make-sparse-keymap))
(define-key my-leader-map "b" (cons "buffers" leader-buffer-map))
;;
  (define-key leader-buffer-map "b" 'ivy-switch-buffer)
  (define-key leader-buffer-map "k" 'kill-buffer)
  (define-key leader-buffer-map "s" (lambda () (interactive) (switch-to-buffer "*scratch*")))
;;
;; calendar show
(define-key my-leader-map "C" 'calendar)
;; eval
(defvar leader-eval-map (make-sparse-keymap))
(define-key my-leader-map "e" (cons "eval" leader-eval-map))
;;
  (define-key leader-eval-map "b" 'eval-buffer)
  (define-key leader-eval-map "e" 'eval-expression)
  (define-key leader-eval-map "l" 'eval-last-sexp)
  (define-key leader-eval-map "r" 'eval-region)
;;
;; file ;;
(defvar leader-file-map (make-sparse-keymap))
(define-key my-leader-map "f" (cons "file" leader-file-map))
;;
  (define-key leader-file-map "f" 'counsel-find-file)
  (define-key leader-file-map "s" 'save-buffer)
  (define-key leader-file-map "S" 'evil-write-all)
  (define-key leader-file-map "w" 'write-file)
;;
;; git ;;
(define-key my-leader-map "g" (cons "git" 'magit-status))
;;
;; help ;;
(defvar leader-help-map (make-sparse-keymap))
(define-key my-leader-map "h" (cons "file" leader-help-map))
;;
  (define-key leader-help-map "c" 'describe-command)
  (define-key leader-help-map "f" 'counsel-describe-function)
  (define-key leader-help-map "k" 'describe-key)
  (define-key leader-help-map "m" 'describe-keymap)
  (define-key leader-help-map "o" 'describe-mode)
  (define-key leader-help-map "v" 'counsel-describe-variable)
;;
;; kill ring ;;
(define-key my-leader-map "k" 'counsel-yank-pop)
;;
(defvar leader-mode-map (make-sparse-keymap))
(define-key my-leader-map "m" (cons "mode toggles" leader-mode-map))
;;
  (define-key leader-mode-map "o" 'olivetti-mode)
  (define-key leader-mode-map "r" 'org-mode)
;;
;; mu4e ;;
(define-key my-leader-map "M" 'mu4e)
;; org ;;
(defvar leader-org-map (make-sparse-keymap))
(define-key my-leader-map "o" (cons "org" leader-org-map))
;;
  (define-key leader-org-map "a" 'org-agenda)
  (define-key leader-org-map "c" 'org-capture)
  (define-key leader-org-map "e" 'org-export-dispatch)
  (define-key leader-org-map "l" 'org-store-link)
  (define-key leader-org-map "o" 'org-todo)
  (define-key leader-org-map "R" 'org-random-todo-goto-new)
  (define-key leader-org-map "s" 'org-sort)
  (define-key leader-org-map "t" 'org-time-stamp-inactive)
  (define-key leader-org-map "T" 'org-time-stamp)
  ;; org-babel;;
  (defvar leader-org-babel-map (make-sparse-keymap))
  (define-key leader-org-map "b" (cons "babel" leader-org-babel-map))
    (define-key leader-org-babel-map "t" 'org-babel-tangle)
  ;; org-clock ;;
  (defvar leader-org-clock-map (make-sparse-keymap))
  (define-key leader-org-map "C" (cons "clock" leader-org-clock-map))
    (define-key leader-org-clock-map "i" 'org-clock-in)
    (define-key leader-org-clock-map "I" 'org-clock-in-last)
    (define-key leader-org-clock-map "o" 'org-clock-out)
    (define-key leader-org-clock-map "x" 'org-clock-cancel)
  ;; org date;;
  (defvar leader-org-date-map (make-sparse-keymap))
  (define-key leader-org-map "d" (cons "date" leader-org-date-map))
    (define-key leader-org-date-map "d" 'org-deadline)
    (define-key leader-org-date-map "s" 'org-schedule)
  ;; org-insert ;;
  (defvar leader-org-insert-map (make-sparse-keymap))
  (define-key leader-org-map "i" (cons "insert" leader-org-insert-map))
    (define-key leader-org-insert-map "b" 'org-insert-structure-template)
    (define-key leader-org-insert-map "d" 'org-download-clipboard)
    (define-key leader-org-insert-map "f" 'org-footnote-action)
    (define-key leader-org-insert-map "l" 'org-insert-link)
  (defvar leader-org-roam-map (make-sparse-keymap))
  (define-key leader-org-map "r" (cons "roam" leader-org-roam-map))
    (define-key leader-org-roam-map "c" 'org-roam-capture)
    (define-key leader-org-roam-map "f" 'org-roam-node-find)
    (define-key leader-org-roam-map "i" 'org-roam-node-insert)
;;
;; quit
(defvar leader-quit-map (make-sparse-keymap))
(define-key my-leader-map "q" (cons "quit" leader-quit-map))
;;
  (define-key leader-quit-map "Q" 'kill-emacs)
;;
;; automatic checks
(defvar leader-check-map (make-sparse-keymap))
(define-key my-leader-map "F" (cons "checks" leader-check-map))
  (define-key leader-check-map "w" '("spellcheck whole document" . flyspell-correct-wrapper))
  (define-key leader-check-map "s" '("spellcheck at point" . ispell-word))
  (define-key leader-check-map "p" 'check-parens)
;; toggle
(defvar leader-toggle-map (make-sparse-keymap))
(define-key my-leader-map "t" (cons "toggle" leader-toggle-map))
;;
  (define-key leader-toggle-map "c" 'centered-cursor-mode)
  (define-key leader-toggle-map "n" 'display-line-numbers-mode)
  (define-key leader-toggle-map "t" 'toggle-truncate-lines)
  (define-key leader-toggle-map "v" 'visual-line-mode)
;;
;; window
(defvar leader-window-map (make-sparse-keymap))
(define-key my-leader-map "w" (cons "window" leader-window-map))
;;
  (define-key leader-window-map "d" 'delete-window)
  (define-key leader-window-map "F" 'make-frame)
  (define-key leader-window-map "h" 'windmove-left)
  (define-key leader-window-map "j" 'windmove-down)
  (define-key leader-window-map "k" 'windmove-up)
  (define-key leader-window-map "l" 'windmove-right)
  (define-key leader-window-map "m" 'delete-other-windows)
  (define-key leader-window-map "-" 'split-window-below)
  (define-key leader-window-map "/" 'split-window-right)

4.6. Global keybindings, non-leader-key mapped

;;
;; non-leader mapped keys
;;
(evil-define-key '(normal motion) 'global "/" 'swiper-isearch)
(evil-define-key '(normal motion) 'global "\\" 'swiper-isearch-thing-at-point)
(evil-define-key '(normal motion visual) 'global "J" 'evil-scroll-page-down)
(evil-define-key '(normal motion visual) 'global "K" 'evil-scroll-page-up)
(evil-define-key '(normal motion visual) 'global "n" 'evil-search-previous)
(evil-define-key '(normal motion visual) 'global "N" 'evil-search-next)
(evil-define-key '(normal motion visual) 'global "I" 'string-insert-rectangle)
(evil-define-key '(normal motion visual) 'global "U" 'undo-redo)

4.7. Org-mode local leader bindings

(defvar org-leader-map (make-sparse-keymap)) 
(evil-define-key '(normal motion) org-mode-map
  ","
  org-leader-map)
  (define-key org-leader-map "," 'org-ctrl-c-ctrl-c)
  (define-key org-leader-map "R" 'org-refile)

4.8. Org-mode non-local-leader bindings

;;;; org-mode bindings
(evil-define-key '(normal motion) org-mode-map
  (kbd "<RET>")
  'org-return
)
(evil-define-key '(normal motion) org-mode-map
  "o"
  'evil-org-open-below
)
(evil-define-key '(normal motion) org-mode-map
  (kbd "<tab>") 
  'org-cycle
)

4.9. Org-agenda local leader

(defvar org-agenda-leader-map (make-sparse-keymap)) 
(evil-define-key '(normal motion) org-agenda-mode-map
  ","
  org-agenda-leader-map)
  (define-key org-agenda-leader-map "," 'org-agenda-ctrl-c-ctrl-c)
  ;; org-clock
  (defvar org-agenda-clock-map (make-sparse-keymap))
  (define-key org-agenda-leader-map "C" (cons "clock" org-agenda-clock-map)) 
    (define-key org-agenda-clock-map "i" 'org-agenda-clock-in)

4.10. Org-agenda non-local-leader

(evil-set-initial-state 'org-agenda-mode 'motion)
(evil-define-key '(normal motion) org-agenda-mode-map (kbd "<tab>") 'org-agenda-goto)
(evil-define-key '(normal motion) org-agenda-mode-map "j" 'org-agenda-next-line)
(evil-define-key '(normal motion) org-agenda-mode-map "k" 'org-agenda-previous-line)
(evil-define-key '(normal motion) org-agenda-mode-map "H" 'org-agenda-priority-down)
(evil-define-key '(normal motion) org-agenda-mode-map "L" 'org-agenda-priority-up)
(evil-define-key '(normal motion) org-agenda-mode-map "u" 'org-agenda-undo)
(evil-define-key '(normal motion) org-agenda-mode-map "o" 'org-agenda-todo) 
(evil-define-key '(normal motion) org-agenda-mode-map "p" 'org-agenda-priority) 
(evil-define-key '(normal motion) org-agenda-mode-map "r" 'org-agenda-redo) 
(evil-define-key '(normal motion) org-agenda-mode-map "R" 'org-agenda-refile) 
(evil-define-key '(normal motion) org-agenda-mode-map "f" 'org-agenda-later) 
(evil-define-key '(normal motion) org-agenda-mode-map "b" 'org-agenda-earlier) 

4.11. Org-capture-mode bindings

(evil-define-key '(normal motion) org-capture-mode-map ",," 'org-capture-finalize)
(evil-define-key '(normal motion) org-capture-mode-map ",r" 'org-capture-refile)
(evil-define-key '(normal motion) org-capture-mode-map ",k" 'org-capture-kill)

4.12. Org-src-mode bindings

(evil-define-key '(normal motion) org-mode-map ",'" 'org-edit-special)
(evil-define-key '(normal motion) org-src-mode-map ",'" 'org-edit-src-exit)
(evil-define-key '(normal motion) org-src-mode-map ",k" 'org-edit-src-abort)

4.13. Magit-status-mode-map keybindings

(evil-define-key '(normal motion) magit-status-mode-map "s" 'magit-stage-file)
(evil-define-key '(normal motion) magit-status-mode-map "u" 'magit-unstage-file)
(evil-define-key '(normal motion) magit-status-mode-map "p" 'magit-pull)
(evil-define-key '(normal motion) magit-status-mode-map "P" 'magit-push)
(evil-define-key '(normal motion) magit-status-mode-map "c" 'magit-commit)
(evil-define-key '(normal motion) magit-status-mode-map "d" 'magit-diff)

4.14. With-editor-mode-map keybindings

(evil-define-key '(normal motion) with-editor-mode-map ",," 'with-editor-finish)

4.15. Minibuffer keybindings

(define-key ivy-minibuffer-map (kbd "ESC") 'hydra-ivy/body)

Have to explicitly require swiper here otherwise the keymap is not loaded at start.

(require 'swiper)
(define-key swiper-isearch-map (kbd "ESC") 'hydra-ivy/body)

4.16. mu4e keybindings

;; mu4e-headers-mode-map
(evil-define-key '(normal motion) mu4e-headers-mode-map (kbd "RET") 'mu4e-headers-view-message)
(evil-define-key '(normal motion) mu4e-headers-mode-map "C" 'mu4e-compose-new)
(evil-define-key '(normal motion) mu4e-headers-mode-map "R" 'mu4e-compose-edit)
(evil-define-key '(normal motion) mu4e-headers-mode-map "o" 'mu4e-compose-forward)
(evil-define-key '(normal motion) mu4e-headers-mode-map "e" 'mu4e-compose-reply)
(evil-define-key '(normal motion) mu4e-headers-mode-map "d" 'mu4e-headers-mark-for-delete)
(evil-define-key '(normal motion) mu4e-headers-mode-map "f" 'mu4e-headers-mark-for-flag)
(evil-define-key '(normal motion) mu4e-headers-mode-map "F" 'mu4e-headers-mark-for-unflag)
(evil-define-key '(normal motion) mu4e-headers-mode-map "u" 'mu4e-headers-mark-for-read)
(evil-define-key '(normal motion) mu4e-headers-mode-map "U" 'mu4e-headers-mark-for-unread)
(evil-define-key '(normal motion) mu4e-headers-mode-map "t" 'mu4e-headers-mark-for-trash)
(evil-define-key '(normal motion) mu4e-headers-mode-map "T" 'mu4e-headers-mark-for-untrash)
(evil-define-key '(normal motion) mu4e-headers-mode-map "P" 'mu4e-headers-mark-pattern)
(evil-define-key '(normal motion) mu4e-headers-mode-map "M" 'mu4e-mark-resolve-deferred-marks)
(evil-define-key '(normal motion) mu4e-headers-mode-map "m" 'mu4e-headers-mark-for-unmark)

;; mu4e-compose-mode-map
(defvar mu4e-local-leader-map (make-sparse-keymap))
(evil-define-key '(normal motion) mu4e-compose-mode-map "," mu4e-local-leader-map)
  (define-key mu4e-local-leader-map "," 'message-send-and-exit)
  (define-key mu4e-local-leader-map "c" 'message-dont-send)
  (define-key mu4e-local-leader-map "k" 'message-kill-buffer)
  (define-key mu4e-local-leader-map "a" 'mml-attach-file)

4.17. olivetti-mode keybindings

(evil-define-key '(normal motion) olivetti-mode-map "{" 'olivetti-expand)
(evil-define-key '(normal motion) olivetti-mode-map "}" 'olivetti-shrink)

4.18. nov-mode keybindings

(evil-define-key '(normal motion) nov-mode-map "o" 'nov-previous-document)
(evil-define-key '(normal motion) nov-mode-map "p" 'nov-next-document)
(evil-define-key '(normal motion) nov-mode-map "r" 'nov-render-document)
(evil-define-key '(normal motion) nov-mode-map "t" 'nov-goto-toc)

5. Org configuration

This is the largest (or second largest, I haven't actually compared it to my keybindings) part of my emacs config, just because org is the thing I use emacs for the most.

Some of this could do with a clean-up; I haven't done a full overhaul of my org-mode config.

;;
;; org config
;;

5.1. misc config

Most of what's here is self explanatory. The only thing I think deserves a note is that I found that the best way to standardize my refile list across the various devices I use Emacs on was just to load the list from a file that is, itself, stored in the repo that I synchronize across my devices using Git.

;;+misc+;;

;; load additional org modules
(require 'org-checklist)
(require 'org-habit)

;; some display config
(setq org-pretty-entities t)
(setq org-pretty-entities-include-sub-superscripts t)

;; org-habit configuration
(setq org-habit-show-habits t)
(setq org-habit-show-all-today t)

;; set org directory and default refile destination
(setq org-directory "~/org")
(setq org-default-notes-file "~/org/refile.org")

;; load agenda files from an external file
(load "~/org/agenda_files.el")

;; create file association for .archorg files
(add-to-list 'auto-mode-alist
             '("\\.archorg\\'" . org-mode))

;;defines the TODO states available globally, the hotkey for the state, and whether switching to the state triggers a timestamp (!) and/or a note w/ timestamp (@)
(setq org-todo-keywords
  (quote ((sequence "TODO(t)" "NEXT(n)" "|" "DONE(d)")
    (sequence "WAIT(w@/!)" "HOLD(h@/!)" "|" "CANC(c@/!)" "INTR(i)")
    (sequence "NOTE(n)")
    (sequence "READ(r)" "BOOK(b)" "REREAD(R)" "|" "FINSH(f)" "ABDN(a)"))))

;; defines the appearance of the globally available TODO states
(setq org-todo-keyword-faces
  (quote (("TODO" :foreground "blue" :weight bold)
    ("NEXT" :foreground "red" :weight bold)
    ("WAIT" :foreground "magenta" :weight bold)
    ("HOLD" :foreground "grey" :weight bold)
    ("CANC" :foreground "sea green" :weight bold)
    ("DONE" :foreground "forest green" :weight bold)
    ("INTR" :foreground "white" :weight bold)
    ("BOOK" :foreground "light blue" :weight bold)
    ("READ" :foreground "pink" :weight bold)
    ("REREAD" :foreground "salmon" :weight bold)
    ("FINSH" :foreground "teal" :weight bold)
    ("ABDN" :foreground "dark cyan" :weight bold)
)))

;; defines todo state triggers; adds and removes state-related tags when changing states
(setq org-todo-state-tags-triggers
  (quote (("CANC" ("CANC" . t) ("HOLD") ("WAIT"))
    ("WAIT" ("WAIT" . t) ("HOLD"))
    ("HOLD" ("HOLD" . t))
    (done ("WAIT") ("HOLD"))
    ("DONE" ("CANC"))
    ("TODO" ("CANC") ("HOLD") ("WAIT"))
    ("NEXT" ("CANC") ("HOLD") ("WAIT"))
    ("BOOK" ("BOOK" . t))
    ("READ" ("BOOK" . t))
)))

;; enables fast todo selection
(setq org-use-fast-todo-selection t)

;; sets org to open links using <RET>
(setq org-return-follows-link t)

;; sets org-mode to display inline images by default
(setq org-startup-with-inline-images t)

5.1.1. Todos

5.1.1.1. TODO org-store-link -> org-insert-link doesn't work properly when I have inline code in the heading

5.2. org-capture config

At one point this was heavily copied from Bernt Hansen's Emacs config, but it's been stripped down a lot since then. Bernt has different capture templates for phone calls and meetings, both of which stop the current timeclock and start a clock on the new heading. I don't actually use the timeclocks on my personal configuration, but I do at work, and while I started off with the two different capture templates I've found that I really only need one–for my purposes it's less important whether the thing that I'm starting the clock on is a meeting or phone call than it is that it needs to be timed separately.

;;+org-capture+;;

;; capture templates
;; todo (t) - creates heading w/ TODO state; refiles to ~/org/refile.org:todos
;; urgent task (u) - creates heading w/ NEXT state; adds deadline for EOD; refiles to ~/org/refile.org:todos
;; note (n) - creates heading w/ the NOTE keyword; refiles to ~/org/refile.org:notes
;; interruption (i) - creates heading w/ INTR state; stops current timeclock and start clock on created heading; refiles to ~/org/refile.org:interruptions
(setq org-capture-templates
  (quote
    (("t" "todo" entry (file "~/org/refile.org") "* TODO %?")
    ("u" "urgent task" entry (file "~/org/refile.org") "* NEXT %?\nDEADLINE: %t")
    ("n" "note" entry (file "~/org/refile.org") "* NOTE %?")
    ("b" "bookmark" entry (file "~/org/bookmarks.org") "* NOTE %?")
    ("i" "interruption" entry (file "~/org/refile.org") "* INTR %? " :clock-in t :clock-resume t)
    ("o" "book" entry (file "~/org/reading_list.org") "* BOOK %?\n:PROPERTIES:\n:Title:\n:Author:\n:Year:\n:IdentType:\n:Identifier:\n:Score:\n**** Summary\n**** Notes\n**** Review")
)))

;; defines refile targets
;; includes all agenda files (to 9 headings deep), the current file (to 9 headings deep)
(setq org-refile-targets (quote ((nil :maxlevel . 9)
  (org-agenda-files :maxlevel . 9))))

;; use filename + full outline paths for refile targets
(setq org-refile-use-outline-path 'file)

;; allow refile to create parent tasks w/ confirmation
(setq org-refile-allow-creating-parent-nodes (quote confirm))

;; exclude DONE state tasks from refile targets
(defun jg/verify-refile-target ()
  "Exclude todo keywords with a done state from refile targets"
  (not (member (nth 2 (org-heading-components)) org-done-keywords))
  (setq org-refile-target-verify-function 'jg/verify-refile-target)
  )

5.2.1. Make capture window fullscreen

From this reddit thread

(defun stag-misanthropic-capture (&rest r)
  (delete-other-windows))

(advice-add  #'org-capture-place-template :after 'stag-misanthropic-capture)

5.3. org-roam config

I have two org-roam templates–one is the default which just prompts for a title (for when I just need an entry that corresponds to a general subject), and the other is for taking notes on a particular work (so it prompts me for some basic bibliographic information).

;;+org-roam+;;

;; set the org-roam directory
(setq org-roam-directory "~/org/org-roam")

;; set org-roam to autosync the database
(org-roam-db-autosync-mode)

;; activate org-roam-protocol
(require 'org-roam-protocol)

;; define the org-roam capture templates--
;; one for default/permanent notes
;; one for bibliographical notes
(setq org-roam-capture-templates
  '(
    ("d" "default" plain
      "%?"
      :if-new (file+head "%<%Y%m%d%H%M%S>-${slug}.org" "#+title: ${title}\n")
      :unnarrowed t)
    ("b" "biblio notes" plain
      "%^g\n* Source\n\nTitle: ${title}\nAuthor: %^{Author}\nType: %^{Type}\nYear: %^{Year}\nDate: %^{Date}\nLocation: \n* Notes\n\n%?"
      :if-new (file+head "%<%Y%m%d%H%M%S>-${slug}.org" "#+title: ${title}\n"))
))

5.4. org-agenda config

I currently only use the one org-agenda view, which lists the following:

  • Tasks that need to be refiled
  • Active tasks with no scheduling item
  • Scheduled tasks
  • Shopping list
  • Queued tasks (denoted by the "q" tag; a quick a dirty way for me to denote tasks I have "on my plate", or mark a task as a next todo if I have a thought about it.)
  • Project tasks (defined by a "project" tag, usually set at the file level. all of my "projects" get their own files in a subdirectory of my main org directory.)
  • Tasks in a "pending" state
;;+org-agenda+;;

;; supposed to disable orgmode's built in stuck project definition
(setq org-stuck-projects (quote ("" nil nil "")))

;; Do not dim blocked tasks
(setq org-agenda-dim-blocked-tasks nil)

;; Allows the tags-todo search to use the org-agenda-todo-ignore-* commands
(setq org-agenda-tags-todo-honor-ignore-options t)

;; Custom agenda command definitions
;; consider adding org-agenda-sorting-strategy function to sort items w/in agenda blocks
(setq org-agenda-custom-commands
  (quote (("o" "Agenda"
    ((agenda "")
      (tags "REFILE"
        ((org-agenda-overriding-header "tasks to refile")
        (org-tags-match-list-sublevels t)))
      (tags-todo "-REFILE-CANC-WAIT-HOLD-BOOK-shoppinglist-project-habit-ongoing/!"
        ((org-agenda-overriding-header "standalone tasks")
        (org-agenda-skip-function '(org-agenda-skip-entry-if 'timestamp))
      ))
      (tags-todo "-CANC-WAIT-HOLD-BOOK+q/!"
        ((org-agenda-overriding-header "queued tasks")
      ))
      (tags-todo "-REFILE-CANC-WAIT-HOLD-BOOK-shoppinglist-recurring-habit/!"
        ((org-agenda-overriding-header "scheduled tasks")
        (org-agenda-skip-function '(org-agenda-skip-entry-if 'nottimestamp))
      ))
      (tags-todo "+ongoing/!"
        ((org-agenda-overriding-header "ongoing")
        (org-agenda-skip-function '(org-agenda-skip-entry-if 'timestamp))
      ))
      (tags-todo "+shoppinglist"
        ((org-agenda-overriding-header "shopping list")
      ))
      (tags-todo "-REFILE-CANC-WAIT-HOLD-BOOK-shoppinglist+project/!"
        ((org-agenda-overriding-header "project tasks")
        (org-agenda-skip-function '(org-agenda-skip-entry-if 'timestamp))
      ))
      (tags-todo "-CANC+WAIT|HOLD/!"
        ((org-agenda-overriding-header "waiting & postponed tasks")
        (org-tags-match-list-sublevels nil)
      ))
    ))
    ("n" "notes" todo "NOTE"
      ((org-agenda-overriding-header "notes")
        (org-tags-match-list-sublevels t)))
)))

5.4.1. DONE Be able to lauch an emacs client directly into fullscreen org-agenda

Set up a script to store in the i3 directory and bound it to the hotkeys:

#!/bin/bash

  if [ $1 == "t" ]; then
      emacsclient -c -a '' -e '(org-capture nil "t")';
  elif [ $1 == "o" ]; then
      emacsclient -c -a '' -e '(org-agenda nil "o")' '(delete-other-windows)';
  else
      exit 1;
  fi

5.5. org-download config

(when (file-directory-p "~/.emacs.d/org-download/")
  (add-to-list 'load-path "~/.emacs.d/org-download/")
  (require 'org-download)
  (setq-default org-download-image-dir "./.org-download")
)

5.6. org-publish config

This section defines the org-publish project for my website.

(require 'ox-publish)
(setq org-publish-project-alist
      '(
        ("pubtext"
         :base-directory "~/org/neocities/content"
         :base-extension "org"
         :publishing-directory "~/org/neocities/html"
         :recursive t
         :publishing-function org-html-publish-to-html
         :headline-levels 4
         :auto-preamble t
        )
        ("pubstatic"
         :base-directory "~/org/neocities/content"
         :base-extension "css\\|js\\|png\\|jpg\\|gif\\|pdf\\|mp3\\|ogg\\|swf"
         :publishing-directory "~/org/neocities/html"
         :recursive t
         :publishing-function org-publish-attachment
        )
        ("neocities" :components ("pubtext" "pubstatic"))
       )
)

5.7. org-random-todo config

I really only want this for the randomizer function, not for the constant alerts, so I need to modify the default config. I also have it set to ignore certain TODO types.

(setq org-random-todo-how-often nil)
(setq org-random-todo-skip-keywords '("BOOK" "READ"))

5.8. canvas-mode config

(when (file-exists-p "~/.emacs.d/svg.el")
      (load "~/.emacs.d/svg.el")
      (require 'svg)
)

6. Display configuration

Sets the theme, font, and other display options. I like a little line spacing by default.

;;
;; font/text display configuration
;;
;; disable menu and toolbar
(menu-bar-mode -1)
(tool-bar-mode -1)
(load-theme 'twilight t)
;; enable column numbers in modeline
(setq column-number-mode t)
;; set default font
(setq-default line-spacing 3)
(add-to-list 'default-frame-alist
  '(font . "Ubuntu Mono-14"))
;; fix display of italics, underline
(set-face-attribute 'italic nil
                    :slant 'italic
                    :underline nil)
(set-face-attribute 'underline nil
                    :underline t)
;;
;; prettify org-mode
;;
;; hides emphasis markers
(setq org-hide-emphasis-markers t)
;; makes headlines bigger
(custom-theme-set-faces
  'user 
  ;; 'user specifies the pseudo-theme created by user settings in Customize
  `(org-level-1 ((t (:inherit outline-1 :weight bold :height 1.5))))
  ;; 'org-level-1' is the /face/; list can be found w/ 'list-faces-display'
  ;; 't' is the /display/, which defines which terminals the settings affect; in this case, all
  ;; the list after the display is the list of settings for the given face
  `(org-level-2 ((t (:inherit outline-2 :weight bold :height 1.25))))
  `(org-level-3 ((t (:inherit outline-3 :weight bold :height 1.1))))
  `(org-level-4 ((t (:inherit outline-4 :weight bold :height 1.05))))
)

(setq ccm-recenter-at-end-of-file t)

7. Other package configuration

7.1. mu4e configuration

(when (file-exists-p "~/.emacs.d/mu4e-config.el")
  (require 'mu4e-config)
)
(require 'mu4e)

;; use mu4e for e-mail in emacs
(setq mail-user-agent 'mu4e-user-agent)

(setq mu4e-drafts-folder "/jules-at-jwguidry-dot-online/Drafts")
(setq mu4e-sent-folder   "/jules-at-jwguidry-dot-online/Sent")
(setq mu4e-trash-folder  "/jules-at-jwguidry-dot-online/Trash")

;; don't save message to Sent Messages, Gmail/IMAP takes care of this
(setq mu4e-sent-messages-behavior 'delete)

;; (See the documentation for `mu4e-sent-messages-behavior' if you have
;; additional non-Gmail addresses and want assign them different
;; behavior.)

;; setup some handy shortcuts
;; you can quickly switch to your Inbox -- press ``ji''
;; then, when you want archive some messages, move them to
;; the 'All Mail' folder by pressing ``ma''.

(setq mu4e-maildir-shortcuts
      '( (:maildir "/jules-at-jwguidry-dot-online/INBOX"              :key ?i)
         (:maildir "/jules-at-jwguidry-dot-online/Sent"  :key ?s)
         (:maildir "/jules-at-jwguidry-dot-online/Trash"      :key ?t)
         (:maildir "/jules-at-jwguidry-dot-online/All Mail"   :key ?a)))

(add-to-list 'mu4e-bookmarks
      ;; ':favorite t' i.e, use this one for the modeline
   '(:query "maildir:/inbox" :name "Inbox" :key ?i :favorite t))

;; allow for updating mail using 'U' in the main view:
(setq mu4e-get-mail-command "offlineimap")

;; something about ourselves
(setq
   user-mail-address "jules@jwguidry.online"
   user-full-name  "Jules Guidry"
   message-signature
      (concat
        "Jules Guidry\n"
      ))

;; sending mail -- replace username with your gmail username
;; also, make sure the gnutls command line utils are installed
;; package 'gnutls-bin' in debian/ubuntu

(require 'smtpmail)
(setq message-send-mail-function 'smtpmail-send-it
   starttls-use-gnutls t
   smtpmail-default-smtp-server "127.0.0.1"
   smtpmail-smtp-server "127.0.0.1"
   smtpmail-smtp-service 1025)

;; alternatively, for emacs-24 you can use:
;;(setq message-send-mail-function 'smtpmail-send-it
;;     smtpmail-stream-type 'starttls
;;     smtpmail-default-smtp-server "smtp.gmail.com"
;;     smtpmail-smtp-server "smtp.gmail.com"
;;     smtpmail-smtp-service 587)

;; don't keep message buffers around
(setq message-kill-buffer-on-exit t)

;; set initial state to insert
;;(setq evil-set-initial-state mu4e-main-mode insert)
;;(setq evil-set-initial-state mu4e-headers-mode insert)

7.2. Flyspell configuration

Sets up flyspell/ivy integration, and enables flyspell by default in text mode files.

;;
;; flyspell configuration
;;
(require 'flyspell-correct-ivy)
;; list of modes that call flyspell
(dolist (hook '(text-mode-hook))
  (add-hook hook (lambda () (flyspell-mode 1))))
;; list of excluded modes
(dolist (hook '(change-log-mode-hook log-edit-mode-hook emacs-list-mode-hook))
  (add-hook hook (lambda () (flyspell-mode -1))))

7.3. Misc packages

Allow EPG to ask for the secret key password.

(setq epa-pinentry-mode 'loopback)

Enable company-mode globally.

(add-hook 'after-init-hook 'global-company-mode)

Set the path of the nov.el save place file.

(setq nov-save-place-file "~/org/.nov-places")

Automatically calls nov-mode when opening .epub files

;; nov.el config
(add-to-list 'auto-mode-alist '("\\.epub\\'" . nov-mode))

8. Misc configuration

Other small bits and bobs that need to be set.

Globally turn on auto-revert-mode.

(global-auto-revert-mode t)
(setq auto-revert-use-notify nil)

Set safe local variable values.

(setq safe-local-variable-values
     '((buffer-save-without-query . 1)
       (buffer-save-without-query . t)))

9. Local package configuration

This section is for packages that I've downloaded outside of MELPA, assuming they don't need to be loaded earlier in the config.

9.1. rgbds-mode configuration

Major mode for editing Game Boy assembly.

(when (file-directory-p "~/.emacs.d/rgbds-mode/")
  (add-to-list 'load-path "~/.emacs.d/rgbds-mode/")
  (require 'rgbds-mode)
)

(evil-define-key '(normal visual) rgbds-mode-map (kbd "RET") 'newline-and-indent)
(evil-define-key 'normal rgbds-mode-map (kbd "$") 'mwim-end-of-code-or-line)
(evil-define-key 'normal rgbds-mode-map (kbd "0") 'mwim-beginning-of-code-or-line)

9.2. nodejs-mode configuration

Major mode for editing Node.js files

(when (file-exists-p "~/.emacs.d/nodejs-repl.el")
  (add-to-list 'load-path "~/.emacs.d/nodejs-repl.el")
  (require 'nodejs-repl)
  (add-hook 'js-mode-hook #'nodejs-repl-minor-mode)
)

9.3. bbcode-mode configuration

(when (file-directory-p "~/.emacs.d/bbcode-mode/")
  (add-to-list 'load-path "~/.emacs.d/bbcode-mode/")
  (require 'bbcode-mode)
)

(defvar bbcode-mode-leader-map (make-sparse-keymap)) 
(evil-define-key '(normal motion) bbcode-mode-map
  ","
  bbcode-mode-leader-map)
  (define-key bbcode-mode-leader-map "b" 'bbcode/insert-tag-b)
  (define-key bbcode-mode-leader-map "i" 'bbcode/insert-tag-i)
  (define-key bbcode-mode-leader-map "u" 'bbcode/insert-tag-u)
  (define-key bbcode-mode-leader-map "l" 'bbcode/insert-tag-list)
  (define-key bbcode-mode-leader-map "*" 'bbcode/insert-tag-*)
  (define-key bbcode-mode-leader-map "c" 'bbcode/insert-tag-color)
  (define-key bbcode-mode-leader-map "I" 'bbcode/insert-tag-img)
  (define-key bbcode-mode-leader-map "U" 'bbcode/insert-tag-url)
  (define-key bbcode-mode-leader-map "C" 'bbcode/insert-tag-center)
  (define-key bbcode-mode-leader-map "n" 'bbcode/insert-tag-indent)
  (define-key bbcode-mode-leader-map "h" 'bbcode/insert-tag-hider)
  (define-key bbcode-mode-leader-map "1" 'bbcode/insert-tag-h1)
  (define-key bbcode-mode-leader-map "2" 'bbcode/insert-tag-h2)
  (define-key bbcode-mode-leader-map "3" 'bbcode/insert-tag-h3)
  (define-key bbcode-mode-leader-map "r" 'bbcode/insert-tag-hr)

9.4. html2org configuration

(when (file-directory-p "~/.emacs.d/html2org/")
  (add-to-list 'load-path "~/.emacs.d/html2org/")
  (require 'html2org)
)

Author: jules

Created: 2025-08-09 Sat 19:45

Validate