Custom configuration for Emacs

1. Overview

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.

1.1. Housekeeping

This was originally written in fall of 2023; since then I've obviously update the config itself and added notes, but I haven't done a full revision, so if there are any inconsistencies in the commentary, that's why.

I'm offering this because looking at other people's Emacs configs has been the single most useful thing for me in developing my own. It works for me; hopefully it helps you.

1.2. A note about priorities

In this document, TODO items with the following priorities are:

[ A ] - Needs

Features that I need to have completed in order to accomplish certain tasks I want to do inside Emacs.

[ B ] - Wants

Features that would significantly enhance my quality of life working in Emacs.

[ C ] - Window Dressing

Minor cosmetic features, or minor tweaks that would be nice but are by no means important.

2. Package.el 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.

;;
;; 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
        flx
        hydra
        counsel
        ivy-hydra
        which-key
        flyspell-correct-ivy
        highlight-indentation
        yaml-mode
        org-contrib
        org-roam
        olivetti
        twilight-theme
        centered-cursor-mode
        async
        magit
        mwim
        htmlize
        )
)
;; install selected packages if not present
(package-install-selected-packages t)

2.1. Package List

2.1.1. evil

I love the Vim keybindings. I love modal editing. Emacs is great but the default keybindings are not for me.

2.1.2. flx

I installed this because I'm trying out ivy but I'm not a big fan of the results I get when I use it for autocompletion; I can't remember where, but someone recommended installing flx as a solution. I'll be honest, it hasn't made things any better for me.

2.1.3. hydra

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.4. counsel, ivy-hydra

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.5. which-key

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.6. flyspell-correct-ivy

Integrates ivy completion and minibuffer hydra setup into flyspell.

2.1.7. org-contrib

Installed in order to get org-checklist

2.1.8. org-roam

I use org-roam to take interconnected notes using a rough version of the Zettelkasten method.

2.1.9. olivetti

Olivetti-mode formats the Emacs window in a way that I find more suitable for long-form writing than the default.

2.1.10. twilight-theme

A good-enough dark theme for Emacs.

2.1.11. centered-cursor-mode

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.12. yaml-mode

Added because I use Emacs to edit YAML files for Ansible.

2.1.13. highlight-indentation

Self-explanatory.

2.1.14. mwim

Prerequisite for nodejs-mode.

2.2. To-dos

2.2.1. DONE Set up config to automatically install missing packages on first launch

This is handled by calling (package-install-selected-packages t).

3. Evil 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. Ivy/counsel/swipe 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.

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

4.1. Todos

4.1.1. TODO Look into ways to improve Ivy autocompletion

  • What I think I would really like is for the selections to mirror what Spacemacs did, which was (iirc) to display a list of my most recent selections.

5. Flyspell configuration

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

;;
;; flyspell configuration
;;
(require 'flyspell-correct-ivy)
(dolist (hook '(text-mode-hook))
  (add-hook hook (lambda () (flyspell-mode 1))))

5.1. Todos

5.1.1. TODO Disable flyspell in specific modes

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

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

8. ivy-hydra config

Configure the bindings in the hydra menu for Ivy.

(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        ^ ^  | ^ ^      | _<_/_>_: 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)
  ("ESC" 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))

9. Custom functions

I only have a few 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.

Jg/switch-to-prev-buffer is a quick function I map to SPC-TAB to enable me to quickly switch a window to the previously displayed buffer.

*

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

(defun jg/switch-to-prev-buffer ()
  (interactive ())
  (switch-to-buffer (other-buffer))
)
(defun jg/switch-to-scratch ()
  (interactive ())
  (switch-to-buffer "*scratch*")
)

10. Keymappings

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.

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") 'jg/switch-to-prev-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" 'jg/switch-to-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)
;;
;; 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 "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 "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))
;; 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)

Global keybindings, non-leader-key mapped:

;;
;; non-leader mapped keys
;;
(evil-define-key '(normal motion) 'global "/" 'swiper-isearch)
(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)

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)

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
)

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)

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) 

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)

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)

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)

With-editor-mode-map keybindings:

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

10.1. Todos

10.1.1. TODO Vim keybindings in minibuffers

In at least the minibuffers I use most often, I want to be able to

  1. Select results using Vim motion keys
  2. Paste using p

(1) is accomplished in Ivy buffers using Ivy-hydra already, so it should just be a matter of extending that to map p as well.

Question here–how do I get the active mode while inside the minibuffer?

10.1.2. DONE Map org-edit-special and org-edit-src-exit to ,-', org-edit-src-abort to ,-k

First needs to be done in org-mode-map, second and third in org-src-mode-map

Bound the keys, but inside the src-edit buffer, I have to hit ESC before it will recognize the keycombo. It seems like it loads into the language's mode (elisp in this case) initially, where the keycombo is not mapped; using ESC doesn't get me out of that mode, but it does something that makes the buffer recognize the keycombo (describe-key says it's bound to evil-force-normal-state).

10.1.3. DONE Bind correct RET behavior in org-mode

Right now, <RET> while in org-mode is bound to evil-ret, in the evil-motion-state-map.

Investigating shows that the command should be org-return, but that in turn will only do the expected behavior when org-return-follows-link is non-nil. So, under the org configuration we put:

(setq org-return-follows-link t)

then in the keymap section we add

(evil-define-key '(normal motion) org-mode-map
  (kbd "<RET>")
  'org-return
)

10.1.4. DONE Org-babel bindings - org-babel-tangle

Done by just creating the local leader map and binding this keybind to org-ctrl-c-ctrl-c.

11. Which-key config

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)

12. 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
;;

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

;; 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 "BOOK(b)" "READ(r)" "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)

12.1.1. Todos

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

12.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+headline "~/org/refile.org" "todos") "* TODO %?")
    ("u" "urgent task" entry (file+headline "~/org/refile.org" "todos") "* NEXT %?\nDEADLINE: %t")
    ("n" "note" entry (file+headline "~/org/refile.org" "notes") "* NOTE %?")
    ("b" "bookmark" entry (file "~/org/bookmarks.org") "* NOTE %?")
    ("i" "interruption" entry (file+headline "~/org/refile.org" "interruptions") "* INTR %? " :clock-in t :clock-resume t)
)))

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

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

12.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"))
))

12.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/!"
        ((org-agenda-overriding-header "standalone tasks")
        (org-agenda-skip-function '(org-agenda-skip-entry-if 'timestamp))
      ))
      (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 "+habit/!"
        ((org-agenda-overriding-header "habit")
        (org-agenda-skip-function '(org-agenda-skip-entry-if 'timestamp))
      ))
      (tags-todo "+shoppinglist"
        ((org-agenda-overriding-header "shopping list")
      ))
      (tags-todo "-CANC-WAIT-HOLD-BOOK+q/!"
        ((org-agenda-overriding-header "queued tasks")
      ))
      (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)))
)))

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

12.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")
)

12.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"))
       )
)

12.7. Todos

12.7.1. TODO Map a command to show the currently clocked-in item

This is mostly something I want to figure out for my work config.

12.7.2. DONE Be able to lauch an emacs client directly into fullscreen org-capture

Can launch into org-capture with emacsclient -c -a '' -e '(org-capture)', but I'd like to be able to have that window maximized as well.

https://old.reddit.com/r/orgmode/comments/14bx0v4/open_orgcapture_frame_maximized_to_the_window/

emacsclient -e '(org-capture nil "t")' --create-frame will let you launch directly into a template

Can use the meta-shift-t shortcut for the launcher in i3, set up a menu to choose between. Annoying to keep it update in two places, but…

Adding the following snippet does work:

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

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

13. Display config

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

13.1. DONE Fix displaying underline and italics

https://superuser.com/questions/439159/underline-instead-of-italic-in-org-mode https://www.gnu.org/software/emacs/manual/html_node/elisp/Face-Attributes.html https://emacs.stackexchange.com/questions/28940/how-to-overwrite-properly-a-face-for-a-particular-theme

Not sure what the deal with italics was, but underline was being overwritten by the Twilight theme. Need to put the changes after the point where the theme is loaded.

(set-face-attribute 'italic nil
                    :slant 'italic
                    :underline nil)

(set-face-attribute 'underline nil
                    :underline t)

Author: jules

Created: 2024-10-28 Mon 22:07

Validate