Skip to content

Latest commit

 

History

History
3733 lines (3141 loc) · 120 KB

config.org

File metadata and controls

3733 lines (3141 loc) · 120 KB

My GNU Emacs configuration

Enable lexical binding for Emacs Lisp

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

Package management

I’m specifying the Emacs I’m using via nixpkgs / my home-manager setup, but for package management I’m currently using the now-built-in use-package with integration for straight. Having the packages come from nixpkgs (marked as external in list-packages) is nice, but requires a nixos-rebuild and restarting Emacs after making some changes.

Straight.el

Straight.el is bootstrapped in init.el as first step of the setup. This needs to be done at the earliest possible time during startup, as I don’t want to use the built-in version of org, and org is needed to read this file and thus start the whole process.

Now set up use-package (which is nowadays built-in) in a way such that the :ensure keyword is unset by default, that is, use-package does not install packages on its own, as straight.el is responsible for doing so.

(require 'use-package)
(setq straight-use-package-by-default t)
(setq use-package-always-ensure nil)

Tangling this literate Emacs configuration

I like the systemcrafters idea of having a custom minor mode for my org mode configuration files which takes care of tangling (that is, transforming org to Emacs Lisp). I previously only tangled on startup, from my init.el file, which left me unable to better document this process in exchange for not having to deal with a one-off bootstrapping “problem”.

After defining this particular minor mode, we want to enable it in all the org files that reside in user-emacs-directory.

(define-minor-mode +literate-config-mode
  "A minor mode for interacting with org files belonging to my literate
Emacs configuration."
  :init-value nil
  :global nil
  :lighter " +litconf")

(defun +is-literate-config-file ()
  "Determine whether the current file is part of my literate Emacs
configuration."
  (when-let (file (buffer-file-name))
    (and (file-in-directory-p file user-emacs-directory)
         (string= "org" (file-name-extension file)))))

(add-hook
 'org-mode-hook
 (defun +possibly-enable-+literate-config-mode ()
   "Enable `+literate-config-mode' if this org file belongs to my literate
Emacs configuration."
   (when (+is-literate-config-file)
     (+literate-config-mode 1))))

This might not be the best way to tangle when saving (systemcrafters does it by installing some after-save-hook only when their respective minor mode is enabled), but here goes:

(add-hook
 'after-save-hook
 (defun +literate-config-after-save-hook ()
   "If `+literate-config-mode' is enabled, tangle the currently visited org
file."
   (when +literate-config-mode
     (+tangle-literate-config-file (buffer-file-name)))))

Profiling the startup time

For no real reason whatsoever I like it when my Emacs starts up quickly. The benchmark-init package nicely profiles where the time during startup went, and if a culprit is found, the “fix” is usually to add :defer to a use-package call.

Note that this block is not tangled; I only run this when I encounter weird behavior.

(use-package benchmark-init
  :demand
  :config (benchmark-init/activate)
  :hook (after-init . benchmark-init/deactivate))

Working around issues using fish as default shell

Sometimes having non-POSIX shells (like fish that I’m using most of the time) as $SHELL leads to hard-to-debug problems and subtle failures. To circumvent this we can point Emacs to any POSIX shell:

(setq shell-file-name (executable-find "bash"))

This isn’t so nice overall as now bash is used in term and vterm by default, but for the latter we can just set a different shell again, as vterm is only used interactively.

Emacs special file management (backup, auto save, file lock)

By default, Emacs creates lots of backup files while editing (which is nice and saved me a couple of times). Yet I can’t stand pressing C-x C-j and seeing all those greyed-out names seemingly cluttering my directory, getting copied when using scp or similar, etc. Yes, they’re globally ignored in my git configuration, but wouldn’t it be cool if they were to just live somewhere else?

(setq +backup-directory (file-name-concat user-emacs-directory ".autosave/"))

(setq backup-by-copying t)
(setq backup-directory-alist `(("." . ,+backup-directory)))
(setq auto-save-file-name-transforms `((".*" ,+backup-directory t)))
(setq lock-file-name-transforms auto-save-file-name-transforms)

Customized *scratch* buffer

I don’t know what lisp-interaction-mode is, but usually I just want to execute or try out some Emacs Lisp in a *scratch* buffer anyway.

(setq initial-scratch-message "")
(setq initial-major-mode #'emacs-lisp-mode)

Keybinding management

I use the general package to define keybindings in a more straightforward and declarative way. This is especially useful for Vim emulation with evil-mode, where keys have to be bound according to the current (Vim) mode.

(use-package general)

The concept of a prefix key (or leader key) is quite useful in my opinion: decide on a key that should hold all custom or built-in functionality (not specific to a major mode, where C-c <letter> would be the appropriate place to bind things to). With this mechanism you basically create your own custom mode-agnostic keymap, accessible almost everywhere.

I used to use C-t as my prefix key when not using evil. With Vim emulation SPC becomes is more attractive as leader key. (That’s why Spacemacs, Doom Emacs, Helix etc. are also using it by default.)

(general-create-definer leader
  :keymaps 'override
  :states '(normal insert emacs visual motion)
  :prefix "SPC"
  :non-normal-prefix "C-t")
(use-package emacs
  :bind
  ("C-h F" . describe-face)
  ("C-h M" . describe-keymap)
  ("C-h V" . set-variable)
  ("C-x C-o" . other-window)
  :config
  (leader
    "SPC" 'execute-extended-command
    "b b" 'consult-buffer
    "b k" 'kill-current-buffer
    "b i" 'ibuffer
    "b p" 'previous-buffer
    "b n" 'next-buffer
    "b r" 'revert-buffer-quick
    "b s" 'scratch-buffer
    "q k" 'save-buffers-kill-emacs
    "q r" 'restart-emacs))

The region

The command to narrow the view to the current region is disabled by default, as it might actually confuse people new to Emacs who invoke it by accident.

Narrowing is a concept used in many areas of Emacs, but I now mostly use it when writing org files like this one right here. Narrowing means restricting what can be seen of the current buffer to a smaller area; marking the whole buffer with C-x h or similar commands then only affect that restricted area. In org-mode there are built-in commands to narrow to a subtree/section, or blocks, for instance; but narrowing is also used by Emacs itself. An example would be capturing todo items (with org-capture), where you on see the item(s) you wish to add, but are actually editing your whole inbox .org file within a narrowed view.

(put 'narrow-to-region 'disabled nil)

Copying the current line

It’s awkward to have to do C-S-<backspace> or C-k C-k, followed by yanking, to “just” copy the line point is on to the kill ring. Note that when using evil, this command is obsolete, as Vim bindings make this quite easy.

(defun +copy-line-at-point ()
  "Copy the current line without killing it first."
  (interactive)
  (save-excursion
    (beginning-of-line)
    (let ((start (point)))
      (forward-line 1)
      (kill-ring-save start (point)))))

Loading additional ELisp configuration files

I do not want customizations done via customize to end up in this file. Use a separate file instead and load that one on startup.

(let ((my-custom-file (locate-user-emacs-file "custom.el")))
  (setq custom-file my-custom-file)
  (load custom-file 'no-error))

A quick way to jump to this file, and an accompanying keybinding, as I do this very often.

(defun +open-init-file ()
  "Open my config.org file."
  (interactive)
  (find-file (file-truename (locate-user-emacs-file "config.org"))))

(leader "e" #'+open-init-file)

“Menus” with hydra

TODO

(use-package hydra
  :defer)

“Error” navigation

Commands like next-error provide navigation for more than just errors in the strict sense of the word. For instance, they also work with occur and grep type buffers (this holds for the variants of those that I use as well: embark and ripgrep, with or without consult).

(defhydra hydra-navigate-errors (:hint nil)
  "\n
  _f_: first   _n_: next   _p_: previous\n
  "
  ("n" next-error)
  ("p" previous-error)
  ("f" first-error)
  ("q" nil "exit"))

(leader "n e" 'hydra-navigate-errors/body)

Custom Emacs look

Load themes and other improvements over the default Emacs look.

One thing to pay attention to is that nearly all those operations on “visuals” work slightly differently when starting Emacs as emacsclient, for instance via

emacsclient -a ''

Setting some things at this point often results in the changes not being applied correctly. It’s due to them happening in a technical, invisible Emacs frame. So you’ll often see (daemonp) being called, checking whether this instance of Emacs is using the daemon, then adding some initializer function to server-after-make-frame-hook if it is.

Cursor

The default cursor is black, which interferes with mostly using a dark theme. Brighten it up a bit.

(set-mouse-color "white")

Enable a forever-blinking cursor. I used to disable this mode but I found myself searching for the cursor a couple of times lately.

(setq blink-cursor-delay 0.5)
(setq blink-cursor-blinks -1)
(blink-cursor-mode 1)

Don’t show a cursor in inactive windows.

(setq-default cursor-in-non-selected-windows nil)

Fonts

An alist of my preferred font families, together with a plist of certain attributes that need to be applied when switching to the respective font.

(setq +font-alist
      '((pragmata-pro
         . (:family
            "Pragmata Pro Mono"))
        (ibm-vga-8x14
         . (:family
            "MxPlus IBM VGA 8x14"
            :default-height
            200))
        (ibm-vga-9x16
         . (:family
            "MxPlus IBM VGA 9x16"
            :default-height
            200))
        (dos-16
         . (:family
            "Mx437 DOS/V TWN16"
            :default-height
            200))
        (iosevka
         . (:family
            "Iosevka Term"))
        (iosevka-comfy
         . (:family
            "Iosevka Comfy"))
        (dejavu
         . (:family
            "DejaVu Sans Mono"))
        (ibm-plex
         . (:family
            "IBM Plex Mono"))
        (cascadia
         . (:family
            "Cascadia Code"))
        (source-code-pro
         . (:family
            "Source Code Pro"))
        (consolas
         . (:family
            "Consolas"
            :default-height
            180))
        (fira-code
         . (:family
            "Fira Code"
            :org-height
            0.8))
        (jetbrains-mono
         . (:family
            "JetBrains Mono"
            :org-height
            0.8))
        (julia-mono
         . (:family
            "JuliaMono"
            :org-height
            0.9))
        (courier-prime
         . (:family
            "Courier Prime"
            :org-height
            0.95))
        (fantasque-sans-mono
         . (:family
            "Fantasque Sans Mono"))
        (lucida-console
         . (:family
            "Lucida Console"
            :default-height
            166
            :org-height
            0.8))
        (inconsolata
         . (:family
            "Inconsolata"
            :default-height
            170
            :org-height
            0.9))
        (termingus
         . (:family
            "Termingus"
            :default-height
            200))
        (unifont
         . (:family
            "Unifont"
            :default-height
            200))
        (geist
         . (:family
            "Geist Mono"))
        (berkeley-mono
         . (:family
            "Berkeley Mono"))
        (pt-mono
         . (:family
            "PT Mono"))))

+current-font contains a symbol pointing to one of the fonts specified in +font-alist. Since I can now globally “set” and change a font I like for my system, Emacs should try to adapt to this (at startup) as well. The function +load-font-from-env does just that, as the global font – if it exists – can be read from an environment variable. If a setting for this font is found in Emacs, that is taken, unless there’s a +default-font set, which always “wins”.

If neither default font nor environment variable are set/found, I fall back to DejaVu Sans Mono.

(setq +default-font 'berkeley-mono)

(defun +load-font-from-env ()
  "See whether an environment variable specifying a 'system font' is
set, and translate that to one of the font symbols."
  (when-let ((f (getenv "KENRAN_DEFAULT_FONT"))
             (font (seq-find
                    (lambda (x)
                      (equal (plist-get (cdr x) :family)
                             f))
                    +font-alist)))
    (car font)))

(setq +current-font
      (or +default-font
          (+load-font-from-env)
          'dejavu))

For org-mode I (sometimes) like using a non-monospace font. This is it.

(defconst +variable-pitch-font "Cantarell")

The function I use to switch between the various fonts I like. It applies the attributes of its value in +font-alist.

(defun +switch-font (arg font)
  "Apply the attributes stored for FONT in `+font-alist'.  When
called with non-nil prefix argument ARG the current height is
reset to the default height for the selected font."
  (interactive
   (list current-prefix-arg
         (intern
          (completing-read
           "Font: "
           (mapcar #'car
                   (assoc-delete-all +current-font
                                     (copy-alist +font-alist)))))))
  (let* ((attrs (alist-get font +font-alist))
         (family (plist-get attrs :family))
         (default-height (or (plist-get attrs :default-height) 170))
         (height (or (and arg default-height)
                     (face-attribute 'default :height)))
         ;; `buffer-face-mode' is enabled when calling
         ;; `variable-pitch-mode'
         (org-height (if (bound-and-true-p buffer-face-mode)
                         (or (plist-get attrs :org-height) 0.9)
                       1.0))
         (weight (or (plist-get attrs :weight) 'regular)))
    (setq +current-font font)
    (setq +default-font-height default-height)
    (set-face-attribute
     'default nil
     :font family
     :weight weight
     :height height)
    (set-face-attribute
     'fixed-pitch nil
     :font family
     :height org-height)
    (set-face-attribute
     'variable-pitch nil
     :font +variable-pitch-font
     :height 1.0)
    (set-face-attribute
     'fixed-pitch-serif nil
     :font family
     :inherit 'fixed-pitch
     :height 1.0)
    (message "Switched to font: %s" family)))

Finally, set all the face attributes synchronously, or register a hook that makes sure that these also work when using the Emacs daemon together with emacsclient.

(if (daemonp)
    (add-hook 'server-after-make-frame-hook
              (defun +switch-to-current-font ()
                (+switch-font t +current-font)
                (remove-hook 'server-after-make-frame-hook
                             #'+switch-to-current-font)))
  (+switch-font t +current-font))

I find myself switching fonts all the time; I just seem to need that kind of visual refreshment. So let’s bind it to a “leader” key.

(leader "t f" #'+switch-font)

Color theme

Utilities

A command to switch themes interactively. Emacs’s load-theme applies all the loaded themes on top of each other; I like to only have one theme active at all times, so I use disable-theme on all themes in custom-enabled-themes before enabling the target theme.

(defun +available-themes ()
  "Get a list of the names of all available themes, excluding the
currently enabled one(s)."
  (mapcar #'symbol-name
          (seq-difference (custom-available-themes)
                          custom-enabled-themes)))

(defun +switch-theme (name)
  "Switch themes interactively. Similar to `load-theme' but also
disables all other enabled themes."
  (interactive
   (list (intern
          (completing-read
           "Theme: "
           (+available-themes)))))
  (progn
    (mapc #'disable-theme
          custom-enabled-themes)
    (princ name)
    (load-theme name t)))

(defun +enable-random-theme ()
  "Randomly choose and enable a theme."
  (interactive)
  (+switch-theme
   (intern
    (seq-random-elt (+available-themes)))))

(defhydra hydra-random-theme (:hint nil)
  "\n
  Choose a random theme

  [_n_]: next    [_q_]: exit\n
  "
  ("n" +enable-random-theme)
  ("q" nil))

When developing a theme, like I did with naga, it’s handy to be able to reload it on the fly.

(defun +reload-theme ()
  "Reload the currently active theme."
  (interactive)
  (let ((active-theme (car custom-enabled-themes)))
    (+switch-theme active-theme)))

As with fonts, I like changing visuals regularly, as in, multiple times a day usually. So keybindings for this come in useful:

(leader
  "t t" #'+switch-theme
  "t r" #'+reload-theme
  "t R" 'hydra-random-theme/body)

My favorite Emacs themes

Since I cannot ever decide which theme I like best, there are a few themes, or theme collections, loaded here.

Custom theme: naga

I usually use my own naga theme. It can be found on MELPA nowadays, though it’s still only “finished” for the parts I really use. Should there be enough interest, I could style some more parts, but it’s not anything I plan on doing for now.

I’m using a “mutable” path to the theme repository, assuming I’ve cloned my project repository to a fixed location. This allows for quick iteration by changing stuff in the theme, followed by +reload-theme.

(add-to-list
 'custom-theme-load-path
 (file-name-as-directory (expand-file-name "~/projects/emacs-naga-theme")))

Configure naga and naga-dimmed:

(setq naga-theme-use-lighter-org-block-background nil)
(setq naga-theme-modeline-style 'green-box)
(setq naga-theme-use-red-cursor t)
(setq naga-theme-surround-org-blocks t)
(setq naga-theme-use-lighter-org-block-background t)

Modus themes

This package by Protesilaos Stavrou is my first choice of “external” themes. I find myself going back to modus-vivendi in the evening, even though I keep saying that I don’t like that high of a contrast.

These two themes are very customizable and come with the most comprehensive and extensive documentation (same as with basically anything that Prot makes available).

(use-package modus-themes
  :straight (:type built-in)
  :defer
  :config
  (setq modus-themes-subtle-line-numbers t)
  (setq modus-themes-bold-constructs t)
  (setq modus-themes-italic-constructs nil)
  (setq modus-themes-syntax '(green-strings alt-syntax))
  (setq modus-themes-prompts '(background bold))
  (setq modus-themes-mode-line nil)
  (setq modus-themes-completions
        '((matches . (intense background))
          (selection . (intense accented))
          (popup . (intense accented))))
  (setq modus-themes-fringes nil)
  (setq modus-themes-paren-match '(bold intense))
  (setq modus-themes-region '(accented bg-only))
  ;; TODO: org agenda, mail citations
  (setq modus-themes-org-blocks nil))

Gruber darker

Whenever you want or need to channel your inner Tsoding, switch to Iosevka and turn on:

(use-package gruber-darker-theme
  :disabled
  :defer)

Doom themes

This package used to be my go-to source of different themes. It’s a megapack, started by the creator of Doom Emacs, Henrik Lissner, but over time it grew into an extensive collection of different themes.

It also comes with a DSL to create custom “doom themes”, that is, one specifies a relatively small number of faces / colors and the results are propagated to most faces of all the common packages. Without using (something like) this, it’s quite a bit of work to style lots of packages, as one might imagine. I’ll leave this here for posterity and also to from time to time enable it and check out newly added doom themes.

(use-package doom-themes
  :disabled
  :defer)

Srcery

I discovered this package by accident, while randomly selecting themes to try out via straight-use-package.

(use-package srcery-theme
  :disabled
  :defer)

Spacemacs themes

For nostalgic reasons I like to pretend I’m using Spacemacs from time to time.

(use-package spacemacs-theme
  :defer)

base16 themes

(use-package base16-theme
  :defer)

Set the current theme

(+switch-theme 'naga-dimmed)

Render color names/codes in their respective color

Rainbow-mode does what the (org) title says: whenever you see a color in text in Emacs, say, the nice orange #ff9000, then it will be rendered with the respective background color. The package will even pick a fitting light or dark foreground for you.

Note that I load this deferred, and it won’t be automatically started when a color is encountered; I’ll activate it with M-x rainbow-mode RET whenever I see fit.

(use-package rainbow-mode
  :defer)

Mode line

I could (and can) live with the default mode line just fine, but I sometimes (usually when sharing my screen) stupidly click on the modes and something annoying happens. So let’s try to fiddle with it to make it work the way I like:

  • No context/mouse menus
  • major mode separate from the list of minor modes I want to see
  • Render the major mode with its “real” (but shortened through stripping the always-present -mode ending) name, i.e., in this file it should just show org
  • Strip stuff away that I don’t look at anyway
(defmacro +with-active-face (face)
  "Return FACE if we're in the mode line of the active window, and
the `mode-line-inactive' face otherwise."
  `(if (mode-line-window-selected-p)
   ,face
     'mode-line-inactive)) 

(defcustom +evil-state-mode-line-format
  '(:eval
    (let ((fg (face-attribute 'default :foreground))
          (bg (face-attribute 'default :background))
          (error-fg (face-attribute 'error :foreground)))
      (cond
       ((eq evil-state 'insert)
        (propertize
         " INSERT "
         'face
         (+with-active-face `(:foreground ,bg :background ,error-fg))))
       ((eq evil-state 'normal)
        (propertize
         " NORMAL "
         'face
         (+with-active-face `(:foreground ,bg :background ,fg))))
       ((eq evil-state 'motion)
        (propertize
         " MOTION "
         'face
         (+with-active-face `(:foreground ,bg :background ,fg))))
       ((eq evil-state 'visual)
        (propertize
         " VISUAL "
         'face
         (+with-active-face `(:foreground ,bg :background ,(face-attribute 'font-lock-function-name-face :foreground)))))
       ((eq evil-state 'emacs)
        (propertize
         " EMACS "
         'face
         (+with-active-face `(:foreground ,bg :background ,(face-attribute 'font-lock-keyword-face :foreground)))))
       (t "        "))))
  "Specifies how to display the current `evil-state' in the mode
line."
  :risky t)

(defcustom +mode-line-compilation-format
  '(compilation-in-progress
    (:eval (propertize
	        "   [Compiling...]"
	        'face
	        (+with-active-face compilation-mode-line-run))))
  "How to display the indicator for a running compilation process in
the mode line."
  :risky t)

(defcustom +mode-line-flymake-format
  '(flymake-mode
    (:eval (when-let ((counters (format-mode-line 'flymake-mode-line-counters)))
	     `("   " ,counters))))
  "How to display the name of the current buffer in the mode line."
  :risky t)

(defcustom +major-mode-mode-line-format
  '("" (:eval (string-replace "-mode" "" (symbol-name major-mode))))
  "How to display the active major mode in the mode line."
  :risky t)

(defun +visible-minor-modes ()
  "Return `minor-mode-alist', but with certain modes I don't want to
see filtered out."
  (let ((hidden-modes '(gcmh-mode
                        yas-minor-mode
                        buffer-face-mode
                        eldoc-mode
                        evil-org-mode
                        evil-commentary-mode
                        company-mode
                        company-box-mode
                        global-company-mode
                        lsp-lens-mode
                        org-indent-mode
                        auto-revert-mode
                        auto-fill-function
                        dot-mode
                        editorconfig-mode
                        flymake-mode
                        evil-collection-unimpaired-mode
                        abbrev-mode)))
    (seq-difference minor-mode-alist
                    hidden-modes
                    (lambda (hidden cell)
                      (eq (car cell)
                          hidden)))))

(defcustom +minor-modes-mode-line-format
  '(:eval
    (let ((s (format-mode-line (+visible-minor-modes))))
      (if (string-empty-p s) ""
        (concat "(" (substring s 1) ")"))))
  "How to display the active minor modes in the mode line."
  :risky t)

(setq-default
 mode-line-format
 '(""
   +evil-state-mode-line-format
   +mode-line-flymake-format
   +mode-line-compilation-format
   (:propertize "   %b" face mode-line-buffer-id)
   ;; Always show current line and column, without checking `column-number-mode'
   ;; and `line-number-mode'
   ("   L%l C%c")
   ("   " +major-mode-mode-line-format)
   ("   " +minor-modes-mode-line-format)))

Disable the help display in the minibuffer when hovering over the mode line.

(setq-default mode-line-default-help-echo nil)

By default, Emacs has a couple of keybindings defined for interaction with the mode line (usually mouse bindings). These are tagged with the special <mode-line> “key”. Let’s remove all of them.

(keymap-global-unset "<mode-line>" t)

Ligature support

(let ((alist '((33 . ".\\(?:\\(?:==\\|!!\\)\\|[!=]\\)")
               (35 . ".\\(?:###\\|##\\|_(\\|[#(?[_{]\\)")
               (36 . ".\\(?:>\\)")
               (37 . ".\\(?:\\(?:%%\\)\\|%\\)")
               (38 . ".\\(?:\\(?:&&\\)\\|&\\)")
               (42 . ".\\(?:\\(?:\\*\\*/\\)\\|\\(?:\\*[*/]\\)\\|[*/>]\\)")
               (43 . ".\\(?:\\(?:\\+\\+\\)\\|[+>]\\)")
               (45 . ".\\(?:\\(?:-[>-]\\|<<\\|>>\\)\\|[<>}~-]\\)")
               (46 . ".\\(?:\\(?:\\.[.<]\\)\\|[.=-]\\)")
               (47 . ".\\(?:\\(?:\\*\\*\\|//\\|==\\)\\|[*/=>]\\)")
               (48 . ".\\(?:x[a-zA-Z]\\)")
               (58 . ".\\(?:::\\|[:=]\\)")
               (59 . ".\\(?:;;\\|;\\)")
               (60 . ".\\(?:\\(?:!--\\)\\|\\(?:~~\\|->\\|\\$>\\|\\*>\\|\\+>\\|--\\|<[<=-]\\|=[<=>]\\||>\\)\\|[*$+~/<=>|-]\\)")
               (61 . ".\\(?:\\(?:/=\\|:=\\|<<\\|=[=>]\\|>>\\)\\|[<=>~]\\)")
               (62 . ".\\(?:\\(?:=>\\|>[=>-]\\)\\|[=>-]\\)")
               (63 . ".\\(?:\\(\\?\\?\\)\\|[:=?]\\)")
               (91 . ".\\(?:]\\)")
               (92 . ".\\(?:\\(?:\\\\\\\\\\)\\|\\\\\\)")
               (94 . ".\\(?:=\\)")
               (119 . ".\\(?:ww\\)")
               (123 . ".\\(?:-\\)")
               (124 . ".\\(?:\\(?:|[=|]\\)\\|[=>|]\\)")
               (126 . ".\\(?:~>\\|~~\\|[>=@~-]\\)"))))
  (dolist (char-regexp alist)
    (set-char-table-range composition-function-table (car char-regexp)
                          `([,(cdr char-regexp) 0 font-shape-gstring]))))

Basic options

Startup

FIXME: Move some of the following to early-init.el instead. See Prot’s configuration for inspiration and give credit.

I wish to know how fast my Emacs is starting. I’m not sure how to make use of all that use-package has to offer in that regard yet, but I want to at least know when I’ve made things worse.

(add-hook
 'emacs-startup-hook
 (lambda ()
   (message
    "Emacs startup took %.2f seconds with %d garbage collection%s"
    (float-time (time-subtract after-init-time before-init-time))
    gcs-done
    (if (> gcs-done 1) "s" ""))))

Disable the graphical UI things like the tool and menu bars, the splash screen, and others.

(tool-bar-mode -1)
(menu-bar-mode -1)
(when (boundp 'scroll-bar-mode)
  (scroll-bar-mode -1))
(tooltip-mode -1)
(setq inhibit-splash-screen nil)
(setq inhibit-x-resources nil)

Resize proportionally after deleting windows

(setq window-combination-resize t)

Less annoying yes/no questions

The following setting enables answering those yes/no questions with just y or n.

(fset 'yes-or-no-p 'y-or-n-p)

No annoying bell sounds

If ring-bell-function is nil, Emacs will still make a sound on, for instance, C-g. Since this annoys me to no end, I disable this by customizing the function to essentially “do nothing”.

(setq ring-bell-function #'ignore)

Mode-sensitive completion for extended commands

Make commands shown with M-x depend on the active major mode. Note: this doesn’t work correctly yet, as (command-modes 'some-command) seems to return the modes in an unexpected format.

(setq read-extended-command-predicate
      #'command-completion-default-include-p)

Line and column numbers

To display line numbers, the aptly named display-line-numbers package is used. I prefer a hybrid mode for displaying line numbers. That is, line numbers are shown in a relative way, but the current line displays its absolute line number. In insert mode, line numbers should be disabled altogether. That’s what these two functions are used for.

(defun +switch-to-absolute-line-numbers ()
  "Enable absolute line numbers."
  (when (bound-and-true-p display-line-numbers-mode)
    (setq display-line-numbers t)))

(defun +switch-to-hybrid-line-numbers ()
  "Enable relative line numbers, but with the current line
showing its absolute line number."
  (when (bound-and-true-p display-line-numbers-mode)
    (setq display-line-numbers 'relative)
    (setq display-line-numbers-current-absolute t)))

(defun +toggle-line-numbers ()
  "Toggle `display-line-numbers-mode'.  Meant to be used in a
keybinding."
  (interactive)
  (display-line-numbers-mode 'toggle))

(use-package display-line-numbers
  :defer
  :hook ((evil-insert-state-entry . +switch-to-absolute-line-numbers)
         (evil-insert-state-exit . +switch-to-hybrid-line-numbers))
  :config
  (setq display-line-numbers-type 'relative)
  (setq display-line-numbers-current-absolute t))

(leader "t l" #'+toggle-line-numbers)

Insert a newline at the end of files

(setq require-final-newline t)
(setq mode-require-final-newline t)

Suppress warning from native compilation

When using Emacs HEAD (with the merged native-comp branch) a lot of warnings show up during startup and when changing modes. We could increase the minimum severity for logs to be shown by setting warning-minimum-level to :error, or just disable the warnings for native compilation entirely like this:

(setq native-comp-async-report-warnings-errors 'silent)

Spaces over tabs

(setq-default indent-tabs-mode nil)

If I have to use tabs, at least make them smaller

Looking at you, Go.

(setq-default tab-width 4)

File name searches should be case-insensitive

(setq read-file-name-completion-ignore-case t)

Yank (paste) at point with the mouse

The default Emacs behavior when yanking (in the Emacs sense of the word) things from the clipboard by clicking the middle mouse button is to insert those at the mouse cursor position. I wish to be able to carelessly click anywhere and have it insert at point, similar to how it’s done in most terminal emulators.

Of course there’s an existing Emacs options for this:

(setq mouse-yank-at-point t)

Breaking long lines

When writing prose I often use auto-fill-mode to automatically break long lines. Emacs uses the fill-column variable to determine when to break. Its default of 70 is a little low for my taste, though.

(setq-default fill-column 80)

Don’t require two spaces to end sentences

Controversial, I know, but I’ve gotten used to it in Doom and actually like not having to change my typing flow depending on the context anymore.

(setq sentence-end-double-space nil)

Automatically scroll compilation output

Emacs’ M-x compile command (and M-x project-compile, which I use much more often) create a new buffer that contains the compilation output. This buffer does not automatically follow the output if it reaches the bottom of the first page, so let’s change that.

(setq compilation-scroll-output t)

Vim emulation with evil

The evil package offers a very complete Vim experience inside of Emacs. I’ve borrowed some pieces of configuration from wasamasa, specifically the part where I default to emacs mode. The reason is that (sometimes due to evil, other times evil-collection) some buffers, like popups in special-mode, don’t behave the way I’d expect them to.

(use-package evil
  :init
  (setq evil-want-integration t)
  (setq evil-want-keybinding nil)
  (evil-mode 1)
  :config
  (setq evil-insert-state-cursor '(hbar . 6))
  (general-define-key
   :states 'normal
   "U" 'evil-redo)
  (general-define-key
   :keymaps 'special-mode-map
   :states '(normal motion)
   "q" #'quit-window)
  (add-to-list 'evil-emacs-state-modes 'sieve-manage-mode)
  :custom
  ((evil-want-C-u-scroll t)
   (evil-want-C-u-delete nil)
   (evil-want-C-w-delete t)
   (evil-want-Y-yank-to-eol t)
   (evil-undo-system 'undo-redo)
   (evil-symbol-word-search t)
   (evil-jumps-cross-buffers nil)
   (evil-mode-line-format nil))
  :bind
  (:map evil-window-map
        ("C-h" . evil-window-left)
        ("C-k" . evil-window-up)
        ("C-j" . evil-window-down)
        ("C-l" . evil-window-right)
        ("C-d" . evil-window-delete)))

In order for scrolling with C-u, C-d, C-f, C-b, and especially with z t and z b, to not leave point on the first or last line of the visible page, we can use the built-in scroll-margin variable.

(setq scroll-margin 2)

Interacting with “surrounding things”

The analogue of Tim Pope’s vim-surround plugin in Emacs. Now I can use things like ysiw) to surround an inner word with non-padded normal parentheses, ds] to delete surrounding brackets, or cs[{ to change surrounding brackets to curly braces with whitespace padding. Selected regions can be surround with e.g. S`.

(use-package evil-surround
  :after evil
  :config
  (global-evil-surround-mode))

Commenting code

By default. Emacs distinguishes between commenting a single line and commenting a region. Its built-in commands are C-x C-; and comment-or-uncomment-region. Using these with evil is in my opinion a little clunky. The evil-commentary packages aims to make this easier and comes with a couple more useful functions, like commenting out a selection while also copying it into a register. Let’s try it out and see whether it’s more useful than, say, just writing some ELisp to call the correct Emacs command depending on the visual selection.

(use-package evil-commentary
  :after evil
  :config
  (evil-commentary-mode))

Local leader key

A local leader key is something that can be used to bind situational commands to usually mode-specific ones. I used , for this in Vim; same here now.

(general-create-definer local-leader
  :states '(normal visual motion)
  :prefix ",")

Evil integration with other packages: evil-collection

This is a package I have a love/hate relationship with. evil-collection in principle is a great idea, but I’ve found it to be “slightly buggy” at times, and I also don’t need or like evil to be integrated everywhere. The most prominent example for this might be terminal-like things, but I might be coming around to that.

In the past, whenever I had any misbehavior after a package update, it felt like a 50:50 chance of evil-collection being the reason behind it. This is not meant to be a stab in their direction, as I think that this just lies in the nature of all things evil: the community will usually follow up with a solution, but there will be a period of time between underlying package changes and that solution where it just does not really work.

For these reasons I have (twice now) tried to live without this package, but that doesn’t seem to satisfy me either; the context switching between traditional C-n or C-p bindings (or n and p, which are often used in special modes) starts to be frustrating after a month or so. So here goes another try, this time selectively enabling packages instead of evilify everything.

(use-package evil-collection
  :after evil
  :config
  (evil-collection-init
   '(dired
     docker
     eldoc
     evil-mc
     git-timemachine
     grep
     help
     helpful
     ibuffer
     imenu
     magit
     markdown-mode
     mu4e
     mu4e-conversation
     (package-menu package)
     pass
     proced
     vterm ; let's try this once more
     xref
     )))

Built-in packages with extensions

Emacs Lisp

I like evaluating the top-level form I’m currently on by pressing C-c C-c, similar to how one compiles in SLY/SLIME.

(use-package emacs
  :bind
  (:map emacs-lisp-mode-map
        ("C-c C-c" . eval-defun)))

Display whitespace

Make whitespace symbols visible.

(use-package whitespace
  :defer
  :config
  (setq whitespace-line-column 100)
  (setq whitespace-global-modes
        '(not magit-status-mode
              org-mode))
  (setq whitespace-style
        '(face newline newline-mark missing-newline-at-eof
               trailing empty tabs tab-mark))
  (setq whitespace-display-mappings
        '((newline-mark 10
                        [9166 10])
          (tab-mark 9
                    [187 9]
                    [92 9]))))

Render manpages in Emacs

When the manpage to be opened has finished loading, I’d like it to be shown in a separate, selected window. One way to accomplish this is by configuring the notification method via Man-notify-method.

(use-package man
  :defer
  :config
  (setq Man-notify-method 'aggressive))

Automatically selecting some built-in “popups”

As with a lot of built-in popup-like functionality in Emacs, there’s a lot of different ways to configure them. I want the *Apropos* and *Help* buffers to be selected (i.e., focused) automatically, like the rest of the popups out there.

(defun +pop-to-current-buffer ()
  "Pop to the current buffer. This is supposed to be used in hooks
for modes/commands that spawn unfocused windows, like `apropos'."
  (pop-to-buffer (current-buffer)))

(use-package emacs
  :straight (:type built-in)
  :config
  (setq help-window-select t)
  (add-hook 'apropos-mode-hook #'+pop-to-current-buffer)
  (add-hook 'compilation-mode-hook #'+pop-to-current-buffer))

Isearch

For many things I use avy now, but can’t get around the de-facto standard isearch. I haven’t gotten around to configuring it a lot, but this will probably grow in the coming weeks or months.

(use-package isearch
  :straight (:type built-in)
  :config
  (setq-default isearch-lazy-count t))

Don’t trim ELisp evaluation results

(use-package simple
  :straight (:type built-in)
  :config
  (setq eval-expression-print-length nil)
  (setq eval-expression-print-level nil))

ElDoc

(use-package eldoc
  :config
  (advice-add 'eldoc-doc-buffer
              :after
              (defun +focus-eldoc-buffer ()
                (pop-to-buffer eldoc--doc-buffer))))

Directory editor

(use-package dired
  :straight (:type built-in)
  :defer
  :config
  (setq dired-kill-when-opening-new-dired-buffer t)
  (setq dired-create-destination-dirs 'ask)
  :custom
  ;; Sort directories to the top
  (dired-listing-switches "-la --group-directories-first"))

Beautify dired a bit.

(use-package diredfl
  :defer
  :after dired
  :hook (dired-mode . diredfl-mode))

Dired-narrow is a package containing functionality to enter a filter to narrow down the contents of a dired buffer interactively. The filter could be either some fixed string, with normal or fuzzy matching, or a regural expression. Bind those three functions to the local leader key to have easier access, as dired already has lots of keys bound.

(use-package dired-narrow
  :defer
  :after dired)

Ediff

Ediff is a great way to diff and/or merge files or buffers. By default it creates a new frame containing a “control buffer” used to navigate the diff and manipulate the output. Unfortunately for the longest time this behaved weirdly for me: whenever I’d tab to the frame containing the diff, do something, then tab back, the next navigational command from the control frame would work but drop me back in the diff frame. It’s possible to use ediff-setup-windows-plain as setup function, which makes ediff single-frame, circumventing the problem.

(use-package ediff-wind
  :defer
  :straight (:type built-in)
  :config
  (setq ediff-window-setup-function #'ediff-setup-windows-plain))

Auto-closing parens, braces and other pairs

I used to use smartparens to automatically insert closing parentheses and other pairs in non-lispy modes. One thing I was missing from Neovim, though, was the newlines and indentation that it inserted automatically when pressing RET with point between braces.

The built-in electric-pair-mode does just that (by default). I just realized that I don’t really need it after all (neither with evil nor without it).

(use-package emacs
  :straight (:type built-in)
  :init
  (electric-pair-mode -1) ; disabled
  :config
  (setq electric-pair-open-newline-between-pairs t))

Undo changes to window arrangements

Sometimes I accidentally mess up my window layout. Winner-mode comes with the winner-undo command (bound by default to C-<left>) that reverts such changes.

(use-package emacs
  :straight (:type built-in)
  :init (winner-mode))

Correct typos while typing with abbrev

Abbrev-mode is a nice built-in minor mode that silently replaces some things I type with other things. It is mostly used for correcting typos, though I haven’t really “trained” my self-made list of abbrevs – I’ve just started using it.

Since it doesn’t come with a global mode itself, I use setq-default to enable it everywhere.

(use-package emacs
  :straight (:type built-in)
  :init
  (setq-default abbrev-mode t)
  :config
  (setq save-abbrevs nil)
  (setq abbrev-file-name
        (locate-user-emacs-file "abbrev_defs")))

Incremental narrowing with vertico

I started with helm in spacemacs, then later switched to Doom Emacs where after a while I tried out ivy and loved it. Configuring Emacs from scratch was when I decided to try out some of the newer, more lightweight Emacs packages like selectrum and vertico. Those integrate very well with default Emacs functionality, so a lot of things can utilize them “implicitly”. I’ve stuck with vertico and I’ve been happy with it ever since.

(use-package vertico
  :straight (vertico :files (:defaults "extensions/*.el"))
  :init
  (vertico-mode)
  :custom
  (vertico-cycle t)
  (vertico-resize t)
  :bind
  (:map vertico-map
        ("C-;" . +vertico-select-randomly)))

Directory navigation in C-x d or C-x C-f is something else that I liked in Doom Emacs, as Doom had a notion of “directory name”, that is, DEL would delete one level in the directory hierarchy, including the slash symbol. The following extension to vertico does just that.

(use-package vertico-directory
  :straight nil
  :after vertico
  :bind (:map vertico-map
              ("DEL" . vertico-directory-delete-char)
              ("C-w" . vertico-directory-delete-word)
              ("RET" . vertico-directory-enter)))

Showing vertico in a centered frame

Let’s give vertico-posframe another try. This makes Emacs look a little similar to Neovim with something like Telescope, though I like my centered frame to be a little smaller.

(use-package vertico-posframe
  :after vertico
  :config
  (setq vertico-posframe-height 13)
  (setq vertico-posframe-border-width 3)
  (setq vertico-posframe-poshandler #'posframe-poshandler-frame-center)
  (vertico-posframe-mode))

Remembering command history

savehist-mode keeps a history of commands and inputs I’ve done in a context-sensitive way, and then shows those at the top when presented with possible results from vertico.

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

Orderless

orderless is a completion style that fits in very well with vertico (or selectrum, for that matter). Parts of a search string may match according to several matching styles. We want to be able to specify which matching style to use by appending a suffix so a search string. Therefore we define style dispatchers and use them to customize orderless-style-dispatchers.

Prepending an equals sign to a search term will search for literal matches of the preceding string.

(defun +literal-if-= (pattern _index _total)
  (when (string-prefix-p "=" pattern)
    `(orderless-literal . ,(substring pattern 1))))

A prepended bang discards everything that matches the preceding literal string.

(defun +without-if-! (pattern _index _total)
  (when (string-prefix-p "!" pattern)
    `(orderless-without-literal . ,(substring pattern 1))))

The tilde sign gives me a way to have “fuzzy” search, if needed.

(defun +flex-if-~ (pattern _index _total)
  (when (string-prefix-p "~" pattern)
    `(orderless-flex . ,(substring pattern 1))))
(use-package orderless
  :custom (completion-styles '(orderless))
  (orderless-style-dispatchers
   '(+literal-if-=
     +without-if-!
     +flex-if-~)))

Consult

The consult package is the analogue of counsel, which I used for quite some time, though not in any extent close to full. This defines some basic bindings mostly taken from an example in its readme.

(use-package consult
  :bind (("C-x b" . consult-buffer)
         ("C-x C-b" . consult-buffer)
         ("C-x 4 b" . consult-buffer-other-window)
         ("C-x 5 b" . consult-buffer-other-frame)
         ("M-g e" . consult-compile-error)
         ("M-g g" . consult-goto-line)
         ("M-g M-g" . consult-goto-line)
         ("M-g o" . consult-outline)
         ("M-g m" . consult-mark)
         ("M-g k" . consult-global-mark)
         ("M-g i" . consult-imenu)
         ("M-s f" . consult-find)
         ("M-s L" . consult-locate)
         ("M-s g" . consult-grep)
         ("M-s G" . consult-git-grep)
         ("M-s r" . consult-ripgrep)
         ("M-s l" . consult-line)
         ("M-s k" . consult-keep-lines)
         ("M-s u" . consult-focus-lines))
  :config
  (setq consult-project-root-function
        (lambda ()
          (when-let (project (project-current))
            (project-root project))))
  (setq consult-ripgrep-args
        (concat consult-ripgrep-args
                " --hidden"
                " -g \"!.git\"")))
;; TODO other isearch integration?
;; TODO :init narrowing, preview delay

Minibuffer actions

Embark

I haven’t really grokked Embark yet. It seems to be amazing, though! What I mostly use it for at the moment is its embark-act command in conjunction with embark-export. With this I often pull the results of some grep command into a separate buffer, where I can then utilize wgrep to bulk-modify the original buffers.

(use-package embark
  :bind (("C-," . embark-act)
         ("C-h B" . embark-bindings))
  :init
  (setq prefix-help-command #'embark-prefix-help-command))

Integrate embark with consult.

(use-package embark-consult
  :after (embark consult)
  :demand
  :hook (embark-collect-mode . embark-consult-preview-minor-mode))

Selecting a random candidate

I switch themes frequently, usually often in one single day, depending on lighting and mood. But I can never quite decide, and sometimes +enable-random-theme hits too many “negatives”. One thing I now like to do is the following:

  • Call +switch-theme
  • Narrow it down with vertico / orderless according to current taste, like base16 !light !metal
  • From the remaining entries, choose a random candidate

This is the function used to do so; it can be used in many contexts.

(defun +vertico-select-randomly ()
  "Select a random thing from the current (possibly narrowed) list of
candidates."
  (interactive)
  (unless (= vertico--total 0)
    (let ((index (random vertico--total)))
      (vertico--goto index)
      (vertico-exit))))

Jumping and sniping with avy

Try out avy to quickly jump to specific locations in the currently visible area of the buffer. This is similar to evil-snipe in Emacs, or (neo)vim plugins like vim-snipe, easymotion, leap.nvim, hop.nvim, lightspeed.nvim, etc.

One cool thing about avy is that it is well-integrated with evil, meaning that it’s possible to use avy operations in conjunction with Vim commands. For instance, deleting up until the next avy-goto-char-timer match can be done with ds (where I bind s to the avy operation below) and then sniping the correct result.

(use-package avy
  :defer
  :config
  (setq avy-timeout-seconds 0.3)
  (setq avy-all-windows nil)
  (general-define-key
   :states '(normal motion)
   "S" 'evil-avy-goto-char-timer)
  (general-define-key
   :states '(normal motion)
   "s" 'evil-avy-goto-char-in-line-timer)
  :bind
  ("C-'" . avy-goto-char-timer))

The command avy-goto-char-timer is the perfect solution for my jumping needs in almost every case. I still find myself trying to navigate to multi-character sequences in the current line quite often, and would like to have the same behavior there; that is:

  • Incrementally narrowing and highlighting the possible results
  • Immediately jumping to unique matches, so I can type until it’s unique and “be there”

The following snippet was handed to me on reddit.

(defun avy-goto-char-in-line-timer ()
  (interactive)
  (let ((avy-all-windows nil))
    (cl-letf (((symbol-function 'avy--find-visible-regions)
               (lambda (&rest args)
                 `((,(point-at-bol) . ,(point-at-eol))))))
      (call-interactively 'avy-goto-char-timer))))
(evil-define-avy-motion avy-goto-char-in-line-timer inclusive)

Org mode

I sometimes like using variable-pitch-mode, which makes it so only code, verbatim, and some other things are written with my current monospace / fixed-width font, and the rest uses a serif font more suitable for longer texts. but customizing these faces with set-face-attribute has the usual problems with the initial daemon startup, and doesn’t hold up when switching fonts or themes. that’s why i’ve put those changes into the following functions which i can call whenever these sorts of changes happen, either through hooks or manual trigger.

Note that some themes, like modus-{vivendi,operandi}, might set the :inherit attribute on a face, in which case a naive (set-face-attribute face nil :inherit 'fixed-pitch) overrides the theme settings. To circumvent this I’ve written the following function that appends a single new value to the current :inherit attribute value of a face.

(defun +inherit-fixed-pitch (face)
  "Append `fixed-pitch' to the `:inherit' attribute of FACE."
  (let* ((current (face-attribute face :inherit))
         (new (cond
               ((eq 'unspecified current)
                'fixed-pitch)
               ((listp current)
                (if (member 'fixed-pitch current)
                    current
                  (cons 'fixed-pitch current)))
               ((not (eq 'fixed-pitch current))
                (list 'fixed-pitch current)))))
    (set-face-attribute face nil :inherit new)))

(defun +org-font-setup ()
  "Set the face attributes for code, verbatim, and other markup
elements."
  (interactive)
  (+inherit-fixed-pitch 'org-block)
  (+inherit-fixed-pitch 'org-block-begin-line)
  (+inherit-fixed-pitch 'org-block-end-line)
  (+inherit-fixed-pitch 'org-document-info-keyword)
  (+inherit-fixed-pitch 'org-document-info)
  (+inherit-fixed-pitch 'org-code)
  (+inherit-fixed-pitch 'org-table)
  (+inherit-fixed-pitch 'org-verbatim)
  (+inherit-fixed-pitch 'org-checkbox)
  (+inherit-fixed-pitch 'org-meta-line)
  (+inherit-fixed-pitch 'org-special-keyword)
  (+inherit-fixed-pitch 'org-link)
  (+inherit-fixed-pitch 'org-todo)
  (+inherit-fixed-pitch 'org-done)
  (+inherit-fixed-pitch 'org-drawer)
  (+inherit-fixed-pitch 'org-property-value)
  (+inherit-fixed-pitch 'org-document-title))

(add-hook '+switch-theme-hook #'+org-font-setup)

I capture mostly TODO items, so it’s convenient to have a special shortcut for that.

(defun +capture-todo ()
  "Capture a TODO item with `org-capture'."
  (interactive)
  (org-capture nil "t"))

For navigation and other org-specific stuff I’m going to try out another hydra.

(defhydra hydra-org (:hint nil)
  "\n
  navigational commands
  ^^----------------------^^----------------------------
  visible header:  [_n_] / [_p_]
  sibling header:  [_N_] / [_P_]
  parent header:   [_k_]
  block:           [_b_] / [_B_]\n
  "
  ("n" org-next-visible-heading)
  ("p" org-previous-visible-heading)
  ("b" org-next-block)
  ("B" org-previous-block)
  ("N" org-forward-heading-same-level)
  ("P" org-backward-heading-same-level)
  ("k" org-up-element)
  ("q" nil "exit"))

When writing text with org, auto-fill-mode should be enabled to automatically break overly long lines into smaller pieces when typing. One may still use M-q to re-fill paragraphs when editing text. After loading org, a custom font setup might run to adjust the headers.

(use-package org
  ;; Use the built-in version of org (which is quite up-to-date as I'm
  ;; always using emacs HEAD).  This circumvents problems with
  ;; 'org-compat of the older version having been loaded.
  :straight (:type built-in)
  :hook
  ((org-mode . auto-fill-mode)
   (org-mode . +org-font-setup)
   ;; (org-mode . variable-pitch-mode)
   (org-trigger . save-buffer)
   ;; Inheriting fixed-pitch in +org-font-setup doesn't work; the
   ;; face is not yet known there, so use a hook.
   (org-indent-mode . (lambda ()
                        (+inherit-fixed-pitch 'org-indent)
                        (+inherit-fixed-pitch 'org-hide)))
   (org-capture-mode . evil-insert-state))
  :custom
  ((org-startup-indented t)
   (org-startup-folded 'content)
   (org-directory "~/org")
   (org-log-done t)
   (org-special-ctrl-a/e t)
   ;; If this has a value greater than 0, every RET press
   ;; keeps indenting the source block further and further.
   (org-edit-src-content-indentation 0)
   (org-default-notes-file "~/org/notes.org")
   (org-agenda-files '("~/org/inbox.org"
                       "~/org/gtd.org"))
   (org-agenda-restore-windows-after-quit t)
   (org-refile-targets `(("~/org/gtd.org" :maxlevel . 3)
                         ("~/org/someday.org" :level . 1)))
   (org-capture-templates '(("t" "Todo" entry
                             (file+headline "~/org/inbox.org" "Tasks")
                             "* TODO %i%?")
                            ("n" "Note" entry
                             (file+headline "~/org/notes.org" "Notes")
                             "* %?\n%a\nNote taken on %U")))
   (org-capture-bookmark nil)
   (org-bookmark-names-plist nil)
   (org-todo-keywords '((sequence
                         "TODO(t)"
                         "WAITING(w)"
                         "|"
                         "DONE(d)"
                         "CANCELLED(c)")))
   (org-html-htmlize-output-type 'css))
  :config
  (setq org-use-fast-todo-selection 'expert)
  (setq-default org-hide-emphasis-markers t)
  (advice-add 'org-refile
              :after (lambda (&rest _) (org-save-all-org-buffers)))
  (local-leader
    :keymaps 'org-mode-map
    "," 'hydra-org/body)
  (leader
    "o a" #'org-agenda
    "o t" #'+capture-todo
    "o c" #'org-capture
    "o l" #'org-store-link
    "o f" #'org-cycle-agenda-files)
  :bind
  (:map org-mode-map
        ("C-'" . nil)))

Integrating evil with org-mode

Some things don’t quite work when evil is enabled, like the header cycling. Evil-org fixes these small issue, and also adds some bonus functionality like o and O being slightly “smart”, for instance, adding new bullet points when inside lists. Additionally, it configures the org-agenda view to be more compatible with evil as well.

(use-package evil-org
  :defer t
  :hook (org-mode . evil-org-mode)
  :config
  (require 'evil-org-agenda)
  (evil-org-agenda-set-keys))

Giving org a more modern look&feel

Minad’s org-modern package looks very promising, so let’s try it out.

(use-package org-modern
  :hook
  (org-mode . org-modern-mode)
  :config
  (setq org-modern-star '("" "" "" "" "" "" "" "")
        org-modern-block-name '((t . t)
                                ("src" "»" "«")
                                ("example" "»–" "–«")
                                ("quote" "" "")
                                ("export" "" ""))))

Show emphasis markers depending on point

In my org configuration I’m setting org-hide-emphasis-markers to t, thus hiding certain markup elements around text. Unfortunately it seem to be currently impossible to switch this interactively, or I just don’t know how, which prevents me from simply adding a keybinding to toggle it.

Thankfully a new package has appeared recently: org-appear. It reacts to the position of point to automatically show surrounding markup.

(use-package org-appear
  :hook ((org-mode . org-appear-mode))
  :config
  (setq org-appear-autolinks t)
  (setq org-appear-autosubmarkers t)
  (setq org-appear-autoentities t)
  (setq org-appear-autokeywords t)
  (setq org-appear-trigger 'always))

Enable syntax highlighting when exporting to HTML

(use-package htmlize
  :defer
  :after ox)

Tiny presentations with org-present

Sometimes I need to showcase some stuff, often code, in a quick and textual manner. This is where org-present comes in handy, as it can present Org mode files/headers with a huge font size, looking a bit like slides.

(use-package org-present
  :hook
  ((org-present-mode
    . (lambda ()
        (org-present-big)
        (org-display-inline-images)
        (org-present-read-only)))
   (org-present-mode-quit
    . (lambda ()
        (org-present-small)
        (org-remove-inline-images)
        (org-present-read-write))))
  :config
  (local-leader
    :keymaps 'org-present-mode-keymap
    "n" #'org-present-next
    "p" #'org-present-prev
    "q" #'org-present-quit))

On-the-fly syntax checking (and other things): Flymake

(use-package flymake
  :straight (:type built-in)
  :defer
  :config
  (setq flymake-suppress-zero-counters nil)
  (setq flymake-fringe-indicator-position 'left-fringe)
  (setq flymake-no-changes-timeout 1.0)
  (setq flymake-mode-line-lighter ""))
(defun +flymake-next-error ()
  "Jump to the next flymake diagnostic that's at least of severity
`:error'."
  (interactive)
  (flymake-goto-next-error 1 '(:error) t))

(defun +flymake-prev-error ()
  "Jump to the previous flymake diagnostic that's at least of severity
`:error'."
  (interactive)
  (flymake-goto-prev-error 1 '(:error) t))

Static analysis of shell scripts

ShellCheck is a great little program providing feedback when writing shell scripts. The Emacs package flymake-shellcheck integrates ShellCheck with Flymake. We have to trigger flymake-shellcheck-load when loading shell scripts, and also enable Flymake itself, both done via hooks to sh-mode.

(use-package flymake-shellcheck
  :commands (flymake-shellcheck-load)
  :hook ((sh-mode . flymake-shellcheck-load)
         (sh-mode . flymake-mode)))

Auto-completion popups via corfu

This is another one of Daniel Mendler’s (aka minad’s) absolutely great Emacs packages! I’ve replaced company with corfu in the past, but back then it did not have the automatic mode (corfu-auto) yet. Without automatic completion it was a little more tedious to use in modes where TAB changes the level of indentation, like in haskell-mode for instance.

Now that this feature exists it’s time to give the package another try. The first impression was very positive, as corfu is using a child frame for the completion popup and thus does not clash with whitespace-mode the way company does.

(use-package corfu
  :disabled
  :straight (corfu :files (:defaults "extensions/*.el"))
  :init (global-corfu-mode)
  :hook (evil-insert-state-exit . corfu-quit)
  :config
  (setq corfu-cycle t)
  (setq corfu-auto t)
  (setq corfu-auto-delay 0.0)
  (setq corfu-exclude-modes
        '(erc-mode
          haskell-interactive-mode)))

Show documentation in a separate popup

I’ll have to figure out whether I like this or not. At the moment it seems great.

(use-package corfu-popupinfo
  :disabled
  :straight nil
  :after corfu
  :config
  (corfu-popupinfo-mode)
  (setq corfu-popupinfo-delay 0.5))

More completion-at-point backends via cape

(defun +ignore-elisp-keywords (cand)
  "Do not show Emacs Lisp keywords in completions in
'emacs-lisp-mode'."
  (or (not (keywordp cand))
      (eq (char-after (car completion-in-region--data)) ?:)))

(defun +setup-elisp-capfs ()
  "Uses 'cape-capf-super' to work around the problem that dabbrev
completions don't show up in 'emacs-lisp-mode' by default."
  (setq-local completion-at-point-functions
              `(,(cape-capf-super
                  (cape-capf-predicate
                   #'elisp-completion-at-point
                   #'+ignore-elisp-keywords)
                  #'cape-dabbrev)
                cape-file))
  (setq-local cape-dabbrev-min-length 4))

(defun +register-default-capfs ()
  "I use 'cape-dabbrev' and 'cape-file' everywhere as they are
generally useful. This function needs to be called in certain
mode hooks, as some modes fill the buffer-local capfs with
exclusive completion functions, so that the global ones don't get
called at all."
  (add-to-list 'completion-at-point-functions #'cape-dabbrev)
  (add-to-list 'completion-at-point-functions #'cape-file))

(use-package cape
  :hook ((emacs-lisp-mode . +setup-elisp-capfs)
         (haskell-mode . +register-default-capfs))
  :init
  (+register-default-capfs))

TODO: company?

(use-package company
  :init
  (global-company-mode)
  :hook
  (evil-insert-state-exit . company-abort))
(use-package company-box
  :hook (company-mode . company-box-mode))

E-mail configuration

There are different ways to “do e-mail in Emacs”. Over the last two years I’ve tried out notmuch, gnus, and mu4e. Some thoughts on each of those:

Notmuch

The Emacs integration for notmuch is great; it has the most intuitive and appealing UI from each of the options. Notmuch works by referencing incoming e-mail in a separate database only, not ever touching or modifying it. I really like this idea, and in practice it also felt great due to the quick und customizable searches. The usual approach is to use a tag-based system of categorizing your e-mail, but simply having lots of stored queries is a little bit more flexible.

But notmuch only handles this single aspect; this means that one needs to find solutions to the following:

  • Getting mail
  • Sending mail
  • Initial tagging + synchronization of tags between machines
  • Alternatively to tagging, using saved queries to “separate” mails

Due to the declarative e-mail account configuration from home-manager getting and sending mail are very simple, and I could also easily switch between different tools like isync or offlineimap. For sending mail I use msmtp.

The initial tagging can be done with a shell script using the well-documented notmuch CLI, or via afew, but I’ve since done away with tagging at all due to synchronization problems I’ll describe in the following section.

muchsync

Using muchsync looks great on paper but is very finicky with sent mail, which I’d also like to sync back via IMAP to my accounts. The client machine sends this and puts it into respective sent directories; muchsync synchronizes these directories as well, but I’ve had problems with mails appearing twice, or not appearing at all on the respective “other” machine, at least in the past. It looks or feels like my usage of muchsync --nonew on the clients was a potential problem: I’ve verified that after sending a mail and it having landed in the correct sent directory, a simple muchsync my-server didn’t lead to the mail appearing on my servers. It worked after executing notmuch new once, though, so I guess muchsync only synchronizes those mails that are part of the current notmuch database state.

One solution would be to make sure that whenever I’m polling from within Emacs, both muchsync my-server and notmuch new are executed. Since notmuch has deprecated the notmuch-poll-script variable in their Emacs client, I have to use the hooks it provides to make sure muchsync is executed. Putting muchsync --nonew into the preNew hook while having an unsynchronized sent mail on the client sounds correct on paper in order to not execute notmuch new twice, but it means that in the case of an unsynchronized sent mail, this mail won’t have been pushed to the server after the first call, if I am correct. So I’ll have to experiment and probably live with notmuch new being called twice (which is fine as it’s blazingly fast).

I’ve never managed to get it quite right, and debugging misbehavior has been a nightmare as I cannot reliably reproduce it. So when trying out notmuch once more, I’ll do so without any tagging at all, utilizing saved queries only.

Configuration

Now pull in and configure the actual notmuch package. Note that some of these options customize built-in functionality, but thematically they do belong here.

(use-package notmuch
  :defer
  :init
  (setq user-mail-address "johannes.maier@mailbox.org")
  (leader "m" 'notmuch)
  :custom
  ;; msmtp is registered as sendmail
  (message-send-mail-function 'message-send-mail-with-sendmail)
  (message-kill-buffer-on-exit t)
  ;; When replying to mail, choose the account to use
  ;; based on the recipient address
  (message-sendmail-envelope-from 'header)
  (mail-envelope-from 'header)
  (mail-user-agent 'message-user-agent)
  ;; Settings for notmuch itself
  (notmuch-show-all-multipart/alternative-parts nil)
  (notmuch-hello-sections
   '(notmuch-hello-insert-header
     notmuch-hello-insert-saved-searches
     notmuch-hello-insert-footer))
  (notmuch-show-empty-saved-searches t)
  (notmuch-always-prompt-for-sender t)
  (notmuch-search-oldest-first nil)
  (notmuch-maildir-use-notmuch-insert t)
  (notmuch-archive-tags nil)
  (notmuch-fcc-dirs
   '(("johannes.maier@mailbox.org" . "mailbox/Sent")
     ("johannes.maier@active-group.de" . "ag/Sent")
     (".*" . "sent")))
  (notmuch-saved-searches
   '((:name "work inbox"
            :query "folder:ag/Inbox"
            :key "w"
            :search-type tree)
     (:name "sent"
            :query "folder:ag/Sent or folder:mailbox/Sent"
            :key "s"
            :search-type tree)
     (:name "private inbox"
            :query "folder:mailbox/Inbox"
            :key "p"
            :search-type tree)
     (:name "work archive"
            :query "path:ag/Archives/**"
            :search-type tree)
     (:name "private archive"
            :query "path:mailbox/Archive/**"
            :search-type tree)))
  :bind
  (:map notmuch-show-mode-map
        ("a" . +notmuch-archive)
        ("d" . +notmuch-delete)
        :map notmuch-tree-mode-map
        ("a" . +notmuch-archive)
        ("d" . +notmuch-delete)
        :map notmuch-hello-mode-map
        ("s" . notmuch-tree)))

In order to be able to use notmuch again, I need to rely on saved searches only in a way that I get the same state from a clean maildir sync on each machine. So let’s circumvent the whole idea of notmuch and actually touch our mail to archive, delete, etc. We don’t actually delete things, just move them from maildir to maildir, which requires some small hacks to refresh the notmuch buffers.

(defun +notmuch-get-source-file ()
  "Get the source file for the currently hovered email."
  (car
   (cond
    ((equal major-mode #'notmuch-tree-mode)
     (notmuch-tree-get-prop :filename))
    ((equal major-mode #'notmuch-show-mode)
     (notmuch-show-get-prop :filename))
    ((equal major-mode #'notmuch-search-mode)
     (warn "FIXME: Not implemented for `notmuch-search-mode'!"))
    (warn "cannot find source file for mail"))))

(defun +notmuch-new-without-hooks ()
  "Execute 'notmuch new --no-hooks', circumventing the automatic polling
notmuch does in its preNew hook, yielding quicker refreshes."
  (interactive)
  (if (equal major-mode #'notmuch-tree-mode)
      (notmuch-call-notmuch-process "new" "--no-hooks")))

(defun +notmuch-move-into-maildir (email maildir)
  "Move EMAIL (that is, the corresponding file) into MAILDIR."
  (let* ((parts (split-string (file-truename email) ":"))
         (target-file (concat
                       maildir
                       "/cur/"
                       (org-id-uuid)
                       (when-let (rest (cadr parts))
                         (format ":%s" rest)))))
    (message "[+email] moving %s to %s" email target-file)
    (rename-file email target-file)
    (let ((line (line-number-at-pos)))
      (+notmuch-new-without-hooks)
      (add-hook 'notmuch-tree-process-exit-functions
                (defun +notmuch-restore-point (proc)
                  (goto-line line)
                  (remove-hook 'notmuch-tree-process-exit-functions #'+notmuch-restore-point)))
      (notmuch-refresh-this-buffer))))

(defun +is-work-email (email)
  "Determine whether a given EMAIL belongs to my work account."
  (string-match "/ag/" (file-name-directory email)))

(defun +notmuch-archive ()
  "Archive the current email."
  (interactive)
  (let* ((email (+notmuch-get-source-file))
         (archive-year (caddr (calendar-current-date)))
         ;; TODO: get maildir location from system configuration
         (archive-dir (if (+is-work-email email)
                          (format "~/.mail/ag/Archives/%s" archive-year)
                        (format "~/.mail/mailbox/Archive/%s" archive-year))))
    (+notmuch-move-into-maildir email archive-dir)))

(defun +notmuch-unarchive ()
  "Unarchive the current email."
  (interactive)
  (let* ((email (+notmuch-get-source-file))
         (maildir (if (+is-work-email email)
                      "~/.mail/ag/Inbox"
                    "~/.mail/mailbox/Inbox")))
    (+notmuch-move-into-maildir email maildir)))

(defun +notmuch-delete ()
  "Delete the current email (by moving it into the trash)."
  (interactive)
  (let ((email (+notmuch-get-source-file)))
    (+notmuch-move-into-maildir
     email
     (if (+is-work-email email)
         "~/.mail/ag/Junk"
       "~/.mail/mailbox/Trash"))))

Gnus-alias makes it possible to use different identities when composing mail. I mostly use it to make sure that replies to a mail are sent from the address I’ve received it at.

(use-package gnus-alias
  :defer t
  :config
  (setq gnus-alias-identity-alist
        `(("mailbox"
           nil
           "Johannes Maier <johannes.maier@mailbox.org>"
           nil
           nil
           nil
           nil)
          ("ag"
           nil
           "Johannes Maier <johannes.maier@active-group.de>"
           "Active Group GmbH"
           nil
           nil
           ,(concat
             "Johannes Maier\n"
             "johannes.maier@active-group.de\n\n"
             "+49 (7071) 70896-67\n\n"
             "Active Group GmbH\n"
             "Hechinger Str. 12/1\n"
             "72072 Tübingen\n"
             "Registergericht: Amtsgericht Stuttgart, HRB 224404\n"
             "Geschäftsführer: Dr. Michael Sperber"))))
  (setq gnus-alias-default-identity "mailbox")
  (setq gnus-alias-identity-rules
        '(("ag" ("any" "@active-group.de" both) "ag")))
  :hook
  (message-setup . gnus-alias-determine-identity))

Gnus

As I’ve written before, I’ve never given the mighty gnus the trial it deserves. Getting into this package is really quite scary, for lack of a better word. The reason is that gnus defines abstractions over “news”, where the word nowadays can incorporate everything from feeds, reddit, usenet, email, etc. The result is that one has to learn lots of specialized and often confusing terminology before being able to use gnus (especially for email). Due to the length and comprehensiveness of the manual the learning curve is quite steep.

Plus, I feel like you cannot “just start using gnus” and get used to it, whereas that is an actual path to succees in something like mu4e, for instance. With gnus there’s a lot of configuration to be done before even being able to do anything.

I’m not sure yet what I will have to sync between machines; the automatically created .newsrc.eld file is the most likely candidate. It seems like that the path to this file can (only?) be configured by setting the path to the startup file, meaning the newsreader-agnostic .newsrc file – that I’m not actually using, as I will only be using gnus.

(use-package gnus
  :disabled
  :init
  (setq gnus-directory "~/.gnus/")
  (setq gnus-home-directory "~/.gnus/")
  (setq gnus-startup-file "~/org/newsrc")
  (setq gnus-init-file (locate-user-emacs-file "gnus.el"))
  :config
  (setq user-full-name "Johannes Maier")
  (setq user-mail-address "johannes.maier@mailbox.org")
  (setq message-directory "~/.gnus")
  (setq message-send-mail-function 'message-send-mail-with-sendmail)
  (setq send-mail-function 'message-send-mail-with-sendmail)
  (setq message-sendmail-envelope-from 'header)
  (setq mail-envelope-from 'header)
  (setq mail-specify-envelope-from 'header)
  (setq gnus-check-new-newsgroups t)
  (setq gnus-gcc-mark-as-read t)
  (setq nnml-directory "~/.gnus")
  (setq gnus-interactive-exit t)
  (setq gnus-asynchronous t)
  (setq gnus-use-article-prefetch 15)
  (setq gnus-select-method '(nnnil ""))
  (setq gnus-secondary-select-methods
        '((nntp "news.gwene.org")
          (nnimap "ag"
                  (nnimap-address "imap.active-group.de")
                  (nnimap-server-port 993)
                  (nnimap-stream ssl)
                  (nnimap-inbox "INBOX"))
          (nnimap "mailbox"
                  (nnimap-address "imap.mailbox.org")
                  (nnimap-server-port 993)
                  (nnimap-stream ssl)
                  (nnimap-inbox "INBOX")))))

Mu for Emacs (mu4e)

Mu is what I was using for the longest period of time, with mu4e being its Emacs frontend. It’s not as customizable as notmuch, but part of its charm is that I don’t need to sync anything between my machines, at the cost of mu touching my e-mail (adding custom headers I believe). I don’t mind this at all, and I can use isync and msmtp to receive and send mail on any host.

For writing e-mails mu4e uses message-mode like the other tools. This checks the user-full-name variable to fill in my name.

(setq user-full-name "Johannes Maier")

The actual mu4e configuration is one huge use-package block, but most of it is due to its concept of contexts. Usually there’s one context for each of my e-mail addresses, and switching between them I may set some context-specific variables, or even change the mu4e UI accordingly.

(use-package mu4e
  :disabled
  :straight
  (:local-repo "~/.nix-profile/share/emacs/site-lisp/mu4e"
               :type built-in)
  :defer
  :commands (mu4e)
  :config
  (setq mail-user-agent 'mu4e-user-agent)
  (setq mu4e-completing-read-function #'completing-read)
  ;; I don't sync drafts to either of the accounts
  (setq mu4e-confirm-quit nil)
  (setq mu4e-change-filenames-when-moving t)
  (setq mu4e-drafts-folder "/drafts")
  (setq mu4e-attachment-dir "~/Downloads/")
  (setq mu4e-contexts
        `(,(make-mu4e-context
            :name "mailbox"
            :match-func (lambda (msg)
                          (when msg
                            (string-prefix-p "/mailbox"
                                             (mu4e-message-field msg :maildir)
                                             t)))
            :vars '((user-mail-address . "johannes.maier@mailbox.org")
                    (mu4e-compose-signature . nil)
                    (mu4e-sent-folder . "/mailbox/Sent")
                    (mu4e-trash-folder . "/mailbox/Trash")
                    (mu4e-refile-folder . (lambda (msg)
                                            (let* ((date (mu4e-message-field-at-point :date))
                                                   (year (decoded-time-year (decode-time date))))
                                              (concat "/mailbox/Archive/"
                                                      (number-to-string year)))))))
          ,(make-mu4e-context
            :name "ag"
            :match-func (lambda (msg)
                          (when msg
                            (string-prefix-p "/ag"
                                             (mu4e-message-field msg :maildir)
                                             t)))
            :vars `((user-mail-address . "johannes.maier@active-group.de")
                    ;; FIXME: Signature in a file?
                    (mu4e-compose-signature . ,(concat
                                                "Johannes Maier\n"
                                                "johannes.maier@active-group.de\n\n"
                                                "+49 (7071) 70896-67\n\n"
                                                "Active Group GmbH\n"
                                                "Hechinger Str. 12/1\n"
                                                "72072 Tübingen\n"
                                                "Registergericht: Amtsgericht Stuttgart, HRB 224404\n"
                                                "Geschäftsführer: Dr. Michael Sperber"))
                    (mu4e-sent-folder . "/ag/Sent")
                    (mu4e-refile-folder . (lambda (msg)
                                            (let* ((date (mu4e-message-field-at-point :date))
                                                   (year (decoded-time-year (decode-time date))))
                                              (concat "/ag/Archives/"
                                                      (number-to-string year)))))
                    (mu4e-trash-folder . "/ag/Trash")))))
  (setq mu4e-bookmarks '((:name "Active-Group inbox" :query "maildir:/ag/Inbox" :key ?a)
                         (:name "Mailbox inbox" :query "maildir:/mailbox/Inbox" :key ?m)
                         (:name "Unread messages" :query "flag:unread AND NOT flag:trashed" :key ?u)
                         (:name "Sent" :query "maildir:/ag/Sent OR maildir:/mailbox/Sent" :key ?s)))
  (setf (alist-get 'trash mu4e-marks)
        (list :char '("d" . "")
              :prompt "dtrash"
              :dyn-target (lambda (target msg)
                            (mu4e-get-trash-folder msg))
              :action (lambda (docid msg target)
                        (mu4e~proc-move docid (mu4e~mark-check-target target)) "-N")))
  (setq mu4e-headers-fields '((:human-date . 12)
                              (:flags . 6)
                              (:maildir . 15)
                              (:mailing-list . 10)
                              (:from . 22)
                              (:subject)))
  (setq mu4e-context-policy 'pick-first)
  (setq mu4e-compose-policy 'ask)
  ;; No search limit
  (setq mu4e-search-results-limit -1)
  (setq mu4e-headers-results-limit -1)
  ;; Always show duplicates (so I can clean them up)
  (setq mu4e-search-skip-duplicates nil)
  (setq mu4e-headers-skip-duplicates nil)
  ;; Getting mail via mbsync
  (setq mu4e-get-mail-command "mbsync -a")
  ;; Composing emails
  (setq message-send-mail-function #'message-send-mail-with-sendmail)
  (setq send-mail-function #'message-send-mail-with-sendmail)
  (setq message-sendmail-envelope-from 'header)
  (setq mail-envelope-from 'header)
  (setq mail-specify-envelope-from 'header)
  (setq message-kill-buffer-on-exit t)
  ;; Visuals
  (setq mu4e-headers-thread-single-orphan-prefix '("─> " . "─▶"))
  (setq mu4e-headers-thread-orphan-prefix '("┬> " . "┬▶ "))
  (setq mu4e-headers-thread-child-prefix '("├> " . "├▶"))
  (setq mu4e-headers-thread-connection-prefix '("" . ""))
  (setq mu4e-headers-thread-duplicate-prefix '("= " . ""))
  (setq mu4e-headers-thread-first-child-prefix '("├> " . "├▶"))
  (setq mu4e-headers-thread-last-child-prefix '("└> " . "╰▶"))
  (general-define-key
   :keymaps '(mu4e-view-mode-map mu4e-headers-mode-map)
   :states '(normal motion)
   "R" #'mu4e-compose-wide-reply))

Warn/confirm when trying to send with empty subject

mu4e uses the built-in message-mode for composing mail. In order to receive a warning or yes/no question whenever I try sending without having specified a subject header, I have to hook into this.

(defun +confirm-empty-mail-subject ()
  "Check whether the subject header of the current message is empty,
and abort in this case (https://emacs.stackexchange.com/a/41176)."
  (or (message-field-value "Subject")
      (y-or-n-p "Really send without subject? ")
      (keyboard-quit)))

(add-hook 'message-send-mail-hook #'+confirm-empty-mail-subject)

Window management

Taming popups

One thing I’m missing from Doom Emacs is the way it handled all sorts of popup-like buffers. When using vanilla Emacs with packages, there are some different behaviors w.r.t. popups:

  • Window splits, new buffer is focused
  • Window splits, but new buffer is not focused
  • Popup opens over current buffer
  • The popup may be closed by pressing q
  • The popup needs to be closed by killing the window
  • The popup needs to be closed by killing the buffer
  • … and probably others

Doom makes it so there is a unified way of dealing with these, and they all open and behave the same way. In theory much of this should boil down to good customization of display-buffer-alist, but that’s pretty arcane. Shackle.el seems to make this easier. There’s also popper.el (it goes well together with shackle.el), which can designate windows/buffers meeting certain criteria as popups, which can then be hidden/shown on a whim. I might want that at a later point, but first I have to take the following hurdle:

Some buffers (think helpful or help) should have a “designated” window, that is, create “their” window if it doesn’t already exist, and open subsequent buffers in that one. This is (of course) possible with shackle, yet I’m pretty sure I’d need to understand the inner workings of display-buffer to customize it that way. It seems prudent then to tackle those, and I might end up not needing shackle that way.

(setq display-buffer-alist
      '(("\\*\\(helpful\\|Help\\|Apropos\\)"
         (display-buffer-reuse-mode-window display-buffer-at-bottom)
         (mode . (helpful-mode help-mode apropos-mode))
         (window-height . 0.4))
        ("\\*mu4e-main\\*"
         (display-buffer-same-window))
        ("\\*Native-compile-Log\\*"
         (display-buffer-no-window)
         (allow-no-window . t))
        ("\\*Async-native-compile-log\\*"
         (display-buffer-no-window)
         (allow-no-window . t))
        ("\\*sly-compilation\\*"
         (display-buffer-no-window)
         (allow-no-window . t))))

Untamed popups

  • [ ] REPLs? vterm?
  • [ ] *Backtrace*
  • [ ] Shell command results
  • [ ] LSP / eglot
  • [ ] lispy evaluation results?

Resizing windows in splits

Resizing windows is one of those things that still make me use a mouse, as I find the default bindings awkward to use and especially chain. A hydra might just remedy that:

(defhydra hydra-window-size (:hint nil)
  "\n
  action:   [+]^^     [+]^^
  ----------^^--------^^----
  height:   [_g_]     [_l_]
  width:    [_w_]     [_n_]\n
  "
  ("b" balance-windows "balance windows" :color blue)
  ("g" enlarge-window)
  ("l" shrink-window)
  ("w" (lambda ()
         (interactive)
         (enlarge-window-horizontally 2)))
  ("n" (lambda ()
         (interactive)
         (shrink-window-horizontally 2)))
  ("q" nil "exit"))

(leader "w" 'hydra-window-size/body)

IRC with ERC

I’ve been using weechat for IRC communication in the past. And while my usage of IRC has decreased quite a bit due to a lot of things moving over to Discord, there are some channels and communities that have their sole online presence in IRC. As with anything, it’s worth trying whether just using Emacs might be preferable. So far my experience with ERC has been quite smooth and I don’t regret it yet, so I’ve fully switched over.

(use-package erc
  :defer
  :config
  (setq erc-autojoin-channels-alist
        '((libera "#emacs"
                  "#nyxt"
                  "#systemcrafters"
                  "#org-mode"
                  "#haskell"
                  "#nim"
                  "#notmuch"
                  "#zig"
                  "#crawl"
                  "#guix"
                  "#commonlisp"
                  "#lisp"
                  "#herrhotzenplotz"
                  "#gcli"
                  "#stumpwm"
                  "#voidlinux")))
  (setq erc-track-exclude
        '("#org-mode" "#crawl" "#nim" "#zig"))
  (setq erc-track-exclude-types '("333" "353"))
  (setq erc-hide-list '("NICK" "MODE" "AWAY" "JOIN" "PART" "QUIT" "AWAY"))
  (setq erc-track-exclude-server-buffer t)
  (setq erc-kill-server-buffer-on-quit t)
  (setq erc-kill-buffer-on-part t)
  (setq erc-fill-column 130)
  (setq erc-fill-static-center 20)
  (setq erc-fill-function #'erc-fill-static))

(use-package erc-hl-nicks
  :after erc
  (add-to-list 'erc-modules 'hl-nicks))

(use-package erc-image
  :after erc
  :config
  (setq erc-image-inline-rescale 200)
  (add-to-list 'erc-modules 'image))

(defun start-irc ()
  "Connect to some IRC servers."
  (interactive)
  (erc-tls :id 'libera
           :server "irc.libera.chat"
           :port 6697
           :nick "kenran"
           :full-name "kenran"
           :client-certificate (let ((cert-dir (getenv "KENRAN_IRC_CERTS")))
                                 `(,(concat cert-dir "/kenran.key")
                                   ,(concat cert-dir "/kenran.crt")))))

Multiple cursors

I’m still new to this, and have only scratched the surface of when to successfully use them. In particular I’m not sure about what my most-used commands will be, and if and where to bind those.

(use-package multiple-cursors
  :defer
  :config
  (keymap-unset mc/keymap "<return>")
  :init
  (add-hook 'multiple-cursors-mode-hook
            (defun +work-around-multiple-cursors-issue ()
              "Loads the file multiple-cursors-core.el (probably for the second
time), which makes the cursors work again.  See
https://www.reddit.com/r/emacs/comments/121swxh/multiplecursors_error_on_emacs_29060/."
              (load "multiple-cursors-core.el")
              (remove-hook 'multiple-cursors-mode-hook #'+work-around-multiple-cursors-issue))))

Due to the nature of mc/mark-next-like-this and consorts, a hydra should lend itself very well to this package. It can then be bound to a top-level keybinding for the best of both worlds.

Credit: I found ejmr’s archived Emacs configuration on GitHub; it contains lots of hydras, so I took heavy inspiration there.

(defhydra hydra-multiple-cursors (:hint nil)
  "\n
  ^^forward       ^^backward        region-based
  ^^--------------^^-------------   ^^-------------------^^---------------------
  [_n_]   next    [_p_]   next      [_l_]   lines        [_C-a_] beg. of lines
  [_N_]   skip    [_P_]   skip      [_a_]   all          [_C-e_] end. of lines
  [_M-n_] unmark  [_M-p_] unmark    [_M-w_] all words    [_M-w_] words in defun
  [_w_]   word    [_W_]   word      [_r_]   regexp

  "
  ("n" mc/mark-next-like-this)
  ("N" mc/skip-to-next-like-this)
  ("M-n" mc/unmark-next-like-this)
  ("p" mc/mark-previous-like-this)
  ("P" mc/skip-to-previous-like-this)
  ("M-p" mc/unmark-previous-like-this)
  ("w" mc/mark-next-like-this-word)
  ("W" mc/mark-previous-like-this-word)
  ("r" mc/mark-all-in-region-regexp :exit t)
  ("l" mc/edit-lines :exit t)
  ("a" mc/mark-all-like-this :exit t)
  ("M-w" mc/mark-all-words-like-this :exit t)
  ("C-w" mc/mark-all-words-like-this-in-defun :exit t)
  ("C-a" mc/edit-beginnings-of-lines :exit t)
  ("C-e" mc/edit-ends-of-lines :exit t)
  ("q" nil "exit"))

(keymap-global-set "C-z" #'hydra-multiple-cursors/body)

Package-specific configuration

Mode-specific templates/snippets

(use-package yasnippet
  :init
  (yas-global-mode))

Unified interface for creating code formatters

Another generally useful package by Steve Purcell is reformatter.el. It enables easy definition of commands to format buffers, as well as minor modes that, when active, automatically apply these commands on save.

(use-package reformatter
  :defer)

Terminal emulator in Emacs

vterm is a terminal emulator for Emacs, more feature-rich than the built-in term. This is very useful for quickly spawning a terminal, for instance in the top-level directory of a project.

(use-package vterm
  :commands (vterm)
  :defer
  :config
  (setq vterm-shell "fish"))

C/C++

(setq c-default-style '((awk-mode . "awk")
                        (other . "stroustrup")))

Zig

Zig is a relatively new systems programming language that I could see me learning more in-depth in the near future. It’s a smaller language than, say, Rust, and less safe; but I like its explicit nature and great defaults. The community is very welcoming so far, as well!

(use-package zig-mode
  :defer)

OCaml

tuareg is the standard mode for OCaml editing, providing syntax highlighting, REPL support, etc., similar to what haskell-mode does for Haskell.

(use-package tuareg
  :defer
  :hook (tuareg-mode . (lambda () (setq mode-name "🐫")))
  :config
  (setq tuareg-indent-align-with-first-arg nil)
  (setq tuareg-match-patterns-aligned t)
  (local-leader
    :keymaps 'tuareg-mode-map
    "f" #'+ocamlformat-format-buffer))

To get some IDE features for OCaml in Emacs I use merlin.

;; (use-package merlin
;;   :hook ((tuareg-mode . merlin-mode)))

merlin-eldoc integrates merlin with eldoc-mode, automatically documenting things at point.

(use-package merlin-eldoc
  :after merlin
  :hook (tuareg-mode . merlin-eldoc-setup)
  :config
  (setq merlin-eldoc-max-lines 8)
  (setq merlin-eldoc-type-verbosity 'min)
  (setq merlin-eldoc-function-arguments t)
  (setq merlin-eldoc-doc t))

I use ocamlformat to automatically format files on save (using .dir-locals.el to eval the reformatter-created mode.

(reformatter-define +ocamlformat-format
  :program "ocamlformat"
  :args (list "--name" (buffer-file-name) "-")
  :lighter " +ocamlformat")

The OCaml build tool dune has configuration files written with s-expression syntax. So in the special dune-mode let’s add lispy=/=lispyville as well.

(use-package dune
  :hook (dune-mode . lispy-mode))

Fish

I’m often using the fish shell. It comes with its own, POSIX-incompatible language, but I mainly use it for fish’s configuration (though most of that is done via nix, anyway). It’s nice to have syntax highlighting, though.

(use-package fish-mode
  :defer)

EditorConfig

I want to be able to simply clone and work in projects and adapt to their respective styles of indentation, newlines at the end of files, and the like. EditorConfig comes with a specified file format to describe these things, possible even on per-file basis; all one needs to use these is support of one’s editor. Many editors have out-of-the-box EditorConfig support nowadays. For Emacs, there’s the official editorconfig-emacs package.

(use-package editorconfig
  :config
  (setq editorconfig-mode-lighter " EC")
  (editorconfig-mode 1))

Ini files

I’m not 100 percent happy with this package, as paragraphs seem to be acting strange. Deleting a paragraph via dap for instance often deletes the following one, too, plus sometimes the previous section header.

(use-package ini-mode
  :defer)

JavaScript

(use-package js
  :defer
  :config
  (setq js-indent-level 2))

Purescript

(use-package psc-ide
  :disabled
  :hook (purescript-mode . psc-ide-mode)
  :config
  (setq psc-ide-rebuild-on-save t))

(use-package purescript-mode
  :disabled
  :hook (purescript-mode . turn-on-purescript-indentation))

Nix

I use Nix for tons of things (like the repository you found this Emacs configuration in). As of now there are a couple of widely used formatters, of which I personally like nixfmt the most.

But flake.nix files can nowadays also “declare” which formatter should be used for any .nix files in their respective project; that formatter is then integrated with the Flake CLI, by calling nix fmt <some-file-or-dir>. The “user experience” is still lacking in my opinion, as it doesn’t accept input from stdin (yet?), and is thus harder to integrate with text editors. This is why I still reformatter-define functions for more convenient to use formatters as well as nix fmt here.

(reformatter-define +nix-format
  :program "nix"
  :args (list "fmt" input-file)
  :stdin nil
  :stdout nil
  :lighter " +nix-fmt")

(reformatter-define +nixfmt-format
  :program "nixfmt"
  :lighter " +nixfmt")

(reformatter-define +alejandra-format
  :program "alejandra"
  :lighter " +alejandra")
(use-package nix-mode
  :mode "\\.nix\\'"
  :config
  (local-leader
    :keymaps 'nix-mode-map
    "f" #'+nixfmt-format-buffer))

Markdown

(use-package markdown-mode
  :mode (("README\\.md\\'" . gfm-mode)
         ("\\.md\\'" . markdown-mode)
         ("\\.markdown\\'" . markdown-mode))
  :init (setq markdown-command "pandoc")
  :config
  (mapc #'evil-declare-ignore-repeat
        '(markdown-forward-paragraph
          markdown-backward-paragraph))
  :hook ((markdown-mode gfm-mode) . auto-fill-mode))

Haskell

Provide an interactive mode for writing Haskell. I can work with a REPL, get feedback and compilation errors shown in the code, and so on. I’ve also added a bunch of utility functions and want to load everything lazily, so I’ve created a custom local Emacs package that contains everything.

(use-package +haskell
  :straight nil
  :demand
  :load-path +custom-package-dir)

Dhall

(use-package dhall-mode
  :mode "\\.dhall\\'"
  :config
  (setq dhall-type-check-inactivity-timeout 2))

Docker

I still have to semi-regularly write Dockerfiles. This package comes with syntax highlighting for those.

(use-package dockerfile-mode
  :defer
  :config
  (add-to-list 'evil-emacs-state-modes 'docker-image-mode)
  (add-to-list 'evil-emacs-state-modes 'docker-container-mode)
  (add-to-list 'evil-emacs-state-modes 'docker-volume-mode)
  (add-to-list 'evil-emacs-state-modes 'docker-network-mode)
  (add-to-list 'evil-emacs-state-modes 'docker-context-mode))

I’m trying out this package, as it provides a magit-like UI to control images, containers, volumes, networks, etc. from inside Emacs.

(use-package docker
  :defer)

YAML

(use-package yaml-mode
  :defer)

Clojure

The key to using Clojure effectively with Emacs seems to be CIDER.

(use-package clojure-mode
  :defer)

(use-package cider
  :after clojure-mode
  :defer)

Some of our work projects use cljfmt to enforce a certain code style. It’s a little hard to “get right”, though, as the source files to be formatted need to be inside the project directory.

(reformatter-define +cljfmt
  :program "cljfmt"
  :args (list "fix" input-file)
  :stdin nil
  :stdout nil
  :input-file (reformatter-temp-file-in-current-directory)
  :lighter " +cljfmt")

CSV

(use-package csv-mode
  :defer)

PlantUML

(use-package plantuml-mode
  :defer
  :init
  (add-to-list 'auto-mode-alist
               '("\\.\\(plantuml\\|puml\\)\\'" . plantuml-mode))
  :config
  (setq plantuml-default-exec-mode 'executable))

Common Lisp

SLY seems to be a bit more actively developed and modern than SLIME.

(use-package sly
  :defer
  :config
  (setq inferior-lisp-program "sbcl")
  (add-hook 'sly-macroexpansion-minor-mode-hook #'turn-off-evil-mode)
  (add-to-list 'evil-emacs-state-modes 'sly-mrepl-mode)
  (add-to-list 'evil-emacs-state-modes 'sly-db-mode)
  (add-to-list 'evil-emacs-state-modes 'sly-inspector-mode)
  (add-to-list 'evil-emacs-state-modes 'sly-xref-mode)
  (local-leader
    :keymaps 'lisp-mode-map
    "q" #'sly-quit-lisp))

sly-asdf gives integration with Common Lisp’s package manager, ASDF.

(use-package sly-asdf
  :defer)

I don’t like that sly immediately jumps inside its REPL buffer (and window) it creates. The following works well, at least in the situation where you don’t want to connect to an existing sly session/REPL.

(defun +sly (&optional command coding-system interactive callback)
  (interactive (list nil nil t nil))
  (let ((buf (buffer-name (current-buffer))))
    (add-hook 'sly-mrepl-hook
              (defun +sly-jump-back ()
                (pop-to-buffer buf)
                (remove-hook 'sly-mrepl-hook #'+sly-jump-back)
                (when callback (funcall callback))))
    (sly command coding-system interactive)))

When developing one of my Common Lisp projects, the normal “startup” workflow would be as follows:

  • Open the ASDF file
  • M-x sly
  • Use sly-compile-and-load-file to load the file, making the system definitions known to, for instance, quicklisp
  • Load the system with sly-asdf-load-system
  • Inside the REPL, set the current package via sly-mrepl-set-package

The following utility function +sly-load-project simplifies the above process by only needing to be inside the ASDF file when invoking it. The rest is done automatically.

(defun +sly-set-repl-package (successp notes buffer loadp)
  "Pop to the current MREPL buffer and call `sly-mrepl-set-package'."
  (if successp
      (progn
        (pop-to-buffer (sly-mrepl--find-buffer))
        (sly-mrepl-set-package)
        (remove-hook 'sly-compilation-finished-hook #'+sly-set-repl-package))
    (warn "Compilation has failed. Can't set REPL package.")))

(defun +sly-load-project ()
  "From within an ASDF file, load it as well as the contained package(s),
then switch the current REPL package."
  (interactive)
  (+sly nil nil nil
        (lambda ()
          (sly-load-file (buffer-file-name))
          (let ((system (sly-asdf-find-current-system)))
            (add-hook 'sly-compilation-finished-hook #'+sly-set-repl-package)
            (sly-asdf-load-system system)))))

To have easier access to these functions when inside ASDF buffers, let’s create a specific minor mode (and, implicitly, keymap, to hold bindings for our functions).

(define-minor-mode +asdf-mode
  "A minor mode to signify ASDF's files."
  :init-value nil
  :global nil
  :lighter " +asdf")

To enable +asdf-mode in .asd files, we hook into lisp-mode; note that this is easier for major modes, which can make use of auto-mode-alist.

(defun +possibly-enable-+asdf-mode ()
  "Enable `+asdf-mode' if the current file has the 'asd' extension, and the
buffer's major mode is `lisp-mode'."
  (when (and buffer-file-name
             (string= "asd"
                      (file-name-extension buffer-file-name)))
    (+asdf-mode 1)))

(add-hook 'lisp-mode-hook #'+possibly-enable-+asdf-mode)

To finally set the keybinding in +asdf-mode, we need to circumvent a known evil problem. Note that the keymap below isn’t really a keymap (actually, we don’t even create one for +asdf-mode), but rather the mode’s symbol itself. In conjunction with :definer 'minor-mode this creates a binding that immediately works.

(local-leader
  :definer 'minor-mode
  :keymaps '+asdf-mode
  "l" #'+sly-load-project)

Racket

(use-package racket-mode
  :defer
  :hook ((racket-mode . racket-xp-mode)
         (racket-mode . racket-unicode-input-method-enable)
         (racket-repl-mode . racket-unicode-input-method-enable)))

Rust

(use-package rust-mode
  :defer
  :config
  (setq rust-format-on-save t))

Java

I don’t use Java, but Bob Nystrom’s excellent and free book Crafting Interpreters uses it for the first part. It’s actually quite OK to write Java with meghanada, but it takes a long time to download all its dependencies.

(use-package meghanada
  :disabled
  :defer
  :init
  (add-hook 'java-mode-hook
            (lambda ()
              (meghanada-mode t)
              (flycheck-mode +1)
              (setq c-basic-offset 2)
              (add-hook 'before-save-hook 'meghanada-code-beautify-before-save))))

Lua

From time to time I need to write some Lua code, like for DCSS RC files. I haven’t used much from lua-mode yet, but having syntax highlighting at least is nice.

(use-package lua-mode
  :defer t)

Sometimes I wish to format Lua files. Stylua seems nice.

(reformatter-define +stylua-format
  :program "stylua"
  :args
  (list
   "--search-parent-directories"
   "--stdin-filepath"
   (buffer-file-name)
   "-")
  :lighter " +stylua")

(local-leader
  :keymaps 'lua-mode-map
  "f" #'+stylua-format-buffer)

Python

(use-package anaconda-mode
  :defer
  :hook (python-mode . anaconda-mode))

(use-package pyimport
  :defer)

Formatting with black:

(reformatter-define +black-format
  :program "black"
  :args (list input-file)
  :stdin nil
  :stdout nil
  :input-file (reformatter-temp-file-in-current-directory)
  :lighter " +black")

Sorting imports with isort:

(reformatter-define +isort
  :program "isort"
  :args (list input-file)
  :stdin nil
  :stdout nil
  :input-file (reformatter-temp-file-in-current-directory)
  :lighter " +isort")

Nim

(use-package nim-mode
  :disabled
  :defer)

F#

(use-package fsharp-mode
  :defer
  :config
  (setq fsharp-indent-offset 2)
  (setq fsharp-continuation-offset 2)
  (setq inferior-fsharp-program "dotnet fsi --readline-"))

Go

I neither like nor normally use Go, but it’s nice to have some syntax highlighting at least for when I do have to read it.

(use-package go-mode
  :defer)

Crystal

Crystal is the language I learn/use to write my Twitch bot with, at least for now. So far I’m liking it and it will surely also transfer to my Ruby reading skills.

(use-package crystal-mode
  :defer)

WASM

Sometimes one needs to edit/write WASM text files (.wat) manually. Sadly this package is not on MELPA yet, but that’s no problem with straight.

(use-package wat-mode
  :straight (:type git :host github :repo "devonsparks/wat-mode")
  :mode "\\.wat\\'"
  :defer)

LSP integration

TODO: document

eglot

(use-package eglot
  :defer)

lsp-mode and supporting modes

(use-package lsp-mode
  :init
  (setq lsp-keymap-prefix "C-;")
  :config
  (setq lsp-ui-doc-enable nil)
  (setq lsp-diagnostics-provider :flycheck)
  :defer
  :commands lsp)
(use-package lsp-ui
  :commands lsp-ui-mode)
(use-package consult-lsp
  :defer
  :after lsp-mode)
(use-package flycheck
  :after lsp-mode
  :config
  (mapc #'evil-declare-ignore-repeat
        '(flycheck-next-error
          flycheck-previous-error
          flycheck-first-error))
  :defer)

Better Emacs help and documentation

This gives us better and more readable help pages. We also replace some built-in C-h keybings with helpful-* functions.

(use-package helpful
  :defer
  :config
  (setq helpful-max-buffers 2)
  (setq helpful-switch-buffer-function #'+helpful-switch-to-buffer)
  :bind (("C-h f" . helpful-callable)
         ("C-h v" . helpful-variable)
         ("C-h k" . helpful-key)))

By default, helpful annoys me with its way of continuously spawning new windows in new locations.

(defun +helpful-switch-to-buffer (buffer-or-name)
  (if (eq major-mode 'helpful-mode)
      (switch-to-buffer buffer-or-name)
    (pop-to-buffer buffer-or-name)))

Project management

I’ve used projectile for a while. It’s great, but I found myself not using most of its features. Now that the built-in project.el has been coming along great, I’m giving it a try. I’m very happy with it so far.

The following are utility functions that mostly rely on being in the top-level directory of a known project. project.el is making this possible in a straightforward way.

(defun +add-nix-flakes-envrc-file ()
  "If it doesn't already exist create a .envrc file containing 'use
flake in the current directory."
  (interactive)
  (let ((envrc (expand-file-name ".envrc")))
    (if (file-exists-p envrc)
        (message "%s" "Envrc file already exists")
      (write-region "use flake" nil envrc))))

(defun +project-vterm ()
  "Open a `vterm' session in the project root of the current
project."
  (interactive)
  (let ((default-directory
         (if-let (project (project-current))
             (project-root project)
           default-directory)))
    (vterm)))

(defun +project-edit-dir-local-variable (mode variable value)
  "Edit directory-local variables in the root directory of the
current project."
  (interactive
   ;; Taken from `add-dir-local-variable', as I don't know of a better
   ;; way to simply wrap that command.
   (let (variable)
     (require 'files-x)
     (list
      (read-file-local-variable-mode)
      (setq variable (read-file-local-variable "Add or edit directory-local variable"))
      (read-file-local-variable-value variable))))
  (let ((default-directory (project-root (project-current t))))
    (modify-dir-local-variable mode variable value 'add-or-replace)))

Another thing I sometimes need is quickly navigate into my project directory, so why not write something to open dired there and then add this function to project-prefix-map?

(defun +navigate-to-projects ()
  "Open a `dired' buffer in my personal project directory."
  (interactive)
  (dired "~/projects"))

One tricky thing was making it possible to bind the keymap project-prefix-map to a key. One needs to make it callable via fset.

(use-package project
  :config
  (fset 'project-prefix-map project-prefix-map)
  (setq project-switch-commands
        '((project-find-file "find file")
          (consult-ripgrep "search/grep" ?s)
          (magit-project-status "git status" ?g)
          (project-dired "dired")
          (+project-vterm "vterm" ?t)
          (project-switch-to-buffer "find buffer" ?b)
          (project-vc-dir "vc")))
  (leader "p" project-prefix-map)
  :bind
  (:map project-prefix-map
        ("t" . +project-vterm)
        ("g" . magit-project-status)
        ("d" . project-dired)
        ("s" . consult-ripgrep)
        ("D" . +project-edit-dir-local-variable)
        ("n" . +navigate-to-projects)))

Magit

Not much to say here: magit is awesome and in my top 3 reasons why I can’t ever switch to anything that doesn’t have this. I’ve tried vim-fugitive and neogit for (neo)vim, and while they’re great, I still missed magit. I’m in the process of getting used to the non-=evil= keybindings (again); once that is finished, I might add some custom bindings here. magit-status-here is surely the one I use the most, but I have a hunch it’s not used nearly often enough to warrant a custom key chord; I can just use M-x, where I don’t have to enter much due to savehist-mode.

(use-package magit
  :defer
  :hook ((git-commit-mode . evil-insert-state)
         (git-commit-mode . (lambda () (set-fill-column 70))))
  :config
  ;; No autosave for open buffers, as that might trigger hooks and
  ;; such.
  (setq magit-save-repository-buffers nil)
  (setq magit-diff-refine-hunk t)
  (setq magit-display-buffer-function #'magit-display-buffer-same-window-except-diff-v1)
  (setq magit-bury-buffer-function #'magit-restore-window-configuration)
  (setq magit-section-initial-visibility-alist
        '((stashes . hide)
          (untracked . show)))
  ;; I frequently pull with the autostash option, so add that to the
  ;; transient command list.
  (transient-append-suffix 'magit-pull "-r"
    '("-a" "Autostash" "--autostash")))

I used to have way more magit keybindings when I used evil, but as mentioned above M-x seems like the way to go. I was missing magit-status-here a lot, though, so add it in an accessible spot.

(leader "g" 'magit-status-here)

Interactively browse git history

(use-package git-timemachine
  :defer)

Modes for other kinds of git-related files

It’s useful to have a little bit of syntax highlighting in files like .gitignore or .gitattributes. The git-modes package provides just that, and autoloads the specific modes for the respective file types. Like its readme proposes it’s also possible to reuse the gitignore-mode for other things, in this case .dockerignore files.

(use-package git-modes
  :defer
  :init
  (add-to-list 'auto-mode-alist
               (cons "/.dockerignore\\'" 'gitignore-mode)))

“Modal” Lisp editing with lispy

I’ve grown very fond of the way the lispy package works, especially without evil. The basic idea is that there are certain special positions in Lisp code where it’s very uncommon to insert any letter. When point is in such a special position – most commonly on an opening parenthesis or directly behind a closing parenthesis – letters that you enter execute special Lisp editing commands: > slurps s-expressions, j and k move down and up on the same level, r raises and lots and lots more. After getting used to it for a bit, it feels absolutely great and I continue to discover new bindings gradually. There’s also an awesome video by abo-abo, linked in the readme, where they showcase many things lispy has to offer.

(use-package lispy
  :defer
  :config
  (setq lispy-colon-p nil)
  :hook
  ((emacs-lisp-mode
    lisp-mode
    clojure-mode
    clojurec-mode
    clojurescript-mode
    common-lisp-mode
    racket-mode
    racket-repl-mode
    sly-repl-mode
    slime-repl-mode)
   . lispy-mode))

Integration with evil: lispyville

When I was not using evil for a couple of months, I learned how to use lispy. I really loved that, but going fully “holy” now feels wrong when writing Lisp code. So I’ll try reverting to lispyville, which changes some lispy keybindings (which I don’t like) but also integrates lispy with evil, making it so that keybindings like C, deletions and pastes don’t interfere with the balanced parentheses.

(use-package lispyville
  :hook
  ((lispy-mode . lispyville-mode))
  :init
  (setq lispyville-key-theme
        '(operators
          c-w
          (prettify insert)
          additional-movement
          (atom-movement t)
          slurp/barf-lispy
          additional
          additional-insert)))

Annotate minibuffer completions

Annotate minibuffer completions, like showing the bound keys and docstrings for commands in M-x, variable values in C-h v, file sizes and permissions in C-x C-f, and much more.

(use-package marginalia
  :init
  (marginalia-mode)
  (advice-add #'marginalia-cycle :after
              (lambda () (when (bound-and-true-p selectrum-mode)
                           (selectrum-exhibit 'keep-selected))))
  :config
  (setq marginalia-annotators
        '(marginalia-annotators-heavy marginalia-annotators-light nil))
  :bind
  (:map minibuffer-local-map
        ("M-A" . marginalia-cycle)))

Make Emacs direnv-sensitive

Steve Purcell’s envrc package is an alternative to emacs-direnv. The latter has a long-standing issue where it sometimes loads too late, that is, after packages like lsp-mode would need it. envrc has worked flawlessly so far. Note: this should probably be one of the last modes to load, as the hook function is then placed before the other modes to ensure direnv integration is working as expected.

(use-package envrc
  :defer
  :init (envrc-global-mode))

Fast grepping via ripgrep

I use ripgrep on the command line a lot. This package makes it comfortably usable from within Emacs.

(use-package ripgrep
  :defer)

Interactive window switching

(use-package ace-window
  :defer
  :init
  (setq aw-keys '(?i ?n ?e ?a ?h ?t ?s ?r))
  (general-define-key
   :keymaps 'override
   :states '(normal insert emacs visual motion)
   "C-l" 'ace-window)
  :config
  (set-face-attribute 'aw-leading-char-face nil :height 2.5))

Global font scaling

The default-text-scale package (again by Steve Purcell) makes it a breeze to “globally” scale text in Emacs. That is, it won’t only increase the font size in the current buffer as the text-scale-increase command does, but rather do it everywhere.

(use-package default-text-scale
  :defer
  :after hydra
  :config
  (setq default-text-scale-amount 10))

Now I like increasing/decreasing the font, for instance when presenting or pair programming, untit it “fits”. Let’s create a hydra for this, so that I can press single keys until it quits – either after q or a timeout.

(defhydra hydra-global-zoom (:hint nil :timeout 3)
  "\n
  [_g_]: [+]     [_s_]: set
  [_l_]: [-]     [_r_]: reset\n
  "
  ("g" default-text-scale-increase)
  ("l" default-text-scale-decrease)
  ("r" (lambda ()
         (interactive)
         (setq default-text-scale--complement 0)
         (face-spec-set 'default `((t (:height ,+default-font-height))))
         (set-face-attribute 'default nil
                             :height +default-font-height)))
  ("s" (lambda (height)
         (interactive "nFont size: ")
         (set-face-attribute 'default nil
                             :height height))
   :color blue)
  ("q" nil "exit"))

(leader "z" 'hydra-global-zoom/body)

Edit grep results over multiple buffers

(use-package wgrep
  :defer
  :custom
  ((wgrep-auto-save-buffer t)
   (wgrep-change-readonly-file nil)
   (wgrep-too-many-file-length 15)))

Improve garbage collector behavior

(use-package gcmh
  :init
  (gcmh-mode 1))