Skip to content

Instantly share code, notes, and snippets.

@sminez
Created December 6, 2024 16:07
Show Gist options
  • Save sminez/b328c0aab3e541773fc4cd28732a5202 to your computer and use it in GitHub Desktop.
Save sminez/b328c0aab3e541773fc4cd28732a5202 to your computer and use it in GitHub Desktop.
init.org

My Emacs Configuration

Sort out tangle on save for this file

For now, org-babel-tangle is working alright but auto-tangling on save would be good

Track start-up time

Org

[ ] Org itself

[ ] Babel

[ ] Roam

languages

[ ] Rust

[ ] Shell

[ ] JSON / YAML

[ ] Groovy

Overview

This is my handwritten Emacs configuration file that is stolen from based on the things that I like from Doom Emacs (and other personal set-ups from folk such as daviwil, Howard Abrams and Prot), without using the full lazy loading all singing, all dancing set up that doom provides. I really like what doom has to offer but it really is a lot of code under the hood.

I’m definitely not saying I think I can make something that is better, but I do think that I can make something that fits my needs without needing the full generality of doom. That should be a lot easier to reason about and learning more about how all this works is a large part of why I’m doing it.

This file can be tangled out to the corresponding real files by calling org-babel-tangle inside of this buffer.

To look at

Tangled file headers

Make sure that the tangled output files have the correct headers in them

;;; early-init.el -*- lexical-binding: t; -*-
;;; init.el -*- lexical-binding: t; -*-

Early initialisation

Emacs 27.1 introduced early-init.el which runs before init.el, before package initialisation takes place and before site files are loaded. I’m always using a version of Emacs that is at least as new as this so it makes sense to use it to help with improving start-up where possible.

See the manual page for the init file for more details on how all of this works.

Help with startup time

A big contributor to startup times is garbage collection. So up the threshold to temporarily prevent it from running, then reset it later by enabling gcmh-mode (not resetting it will cause stuttering / freezes).

(setq gc-cons-threshold most-positive-fixnum)

In Emacs 27+, package initialisation occurs before user-init-file is loaded, but after early-init-file. I’m using straight.el so prevent Emacs from doing it early!

(setq package-enable-at-startup nil)

In non-interactive sessions, prioritise non-byte-compiled source files to prevent the use of stale byte-code. Otherwise, it saves a little IO time to skip the mtime checks on every *.elc file.

(setq load-prefer-newer noninteractive)

file-name-handler-alist is consulted on each call to require, load and various path/io functions. You get a minor speed up by un-setting this but you need to restore it later because it is needed for handling encrypted or compressed files (among other things).

Premature re-displays can substantially affect startup times and produce ugly flashes of un-styled Emacs.

Site files tend to use load-file, which emits “Loading X…” messages in the echo area, which in turn triggers a re-display. Re-displays can have a big effect on startup times and in this case happens so early that Emacs may flash white while starting up. We only want this on start-up though so we need to unload the advice before init.el is loaded.

(unless (or (daemonp)
            noninteractive
            init-file-debug)
  (let ((original-file-name-handler-alist file-name-handler-alist))
    (setq-default file-name-handler-alist nil)
    (defun sminez/reset-file-handler-alist-h ()
      (setq file-name-handler-alist
            ;; Merge instead of overwrite because there may have been changes to
            ;; `file-name-handler-alist' since startup we want to preserve.
            (delete-dups (append file-name-handler-alist
                                 original-file-name-handler-alist))))
    (add-hook 'emacs-startup-hook #'sminez/reset-file-handler-alist-h 101))

  (setq-default inhibit-redisplay t
                inhibit-message t)
  (add-hook 'window-setup-hook
            (lambda ()
              (setq-default inhibit-redisplay nil
                            inhibit-message nil)
              (redisplay)))

  (define-advice load-file (:override (file) silence)
    (load file nil 'nomessage))

  (define-advice startup--load-user-init-file (:before (&rest _) sminez-init)
    (advice-remove #'load-file #'load-file@silence)))

Tracking initialisation

I’m not massively worried about start-up time for now, but getting things set up for tracking it will probably pay off in the long run when I inevitably end up with something causing things to grind to a halt…

(defun sminez/display-startup-time ()
  (message
   "Emacs loaded in %s."
   (format "%.2f seconds"
           (float-time (time-subtract after-init-time before-init-time)))))

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

System settings

Contrary to what many Emacs users have in their configuration files, you don’t need more than this to make UTF-8 the default coding system. That said, set-language-environment also sets defualt-input-method which we don’t want.

(set-language-environment "UTF-8")
(setq default-input-method nil)

Loading in the core library

At this point, we’re done with the start-up tweaks and it’s time to actually begin loading in the configuration!

(setq user-emacs-directory (file-name-directory load-file-name))
(add-to-list 'load-path (concat (file-name-directory load-file-name) "lib"))

Remember these variables’ initial values, so we can safely reset them at a later time, or consult them without fear of contamination.

(dolist (var '(exec-path load-path process-environment))
  (unless (get var 'initial-value)
    (put var 'initial-value (default-value var))))

The core shared library code lives in ~/.emacs.d/lib/sminez-lib.el. For now, there isn’t much there but lets load it in anyway so that once it does start to flesh out, it’s always available.

(require 'sminez-lib)

Setting up system constants

There are a few things that are helpful to define at this top level so that they can be re-used and referenced everywhere else. For the most part these are things about the specific system that we are running on and (typically) not things that I change all that much.

System properties

(defconst NATIVECOMP (if (fboundp 'native-comp-available-p)
                         (native-comp-available-p)))
(defconst EMACS28+   (> emacs-major-version 27))
(defconst EMACS29+   (> emacs-major-version 28))
(defconst IS-MAC     (eq system-type 'darwin))
(defconst IS-LINUX   (eq system-type 'gnu/linux))

Directories

(defconst sminez-lib-dir (concat user-emacs-directory "lib/")
  "Directory containing core library functionality.")

(defconst sminez-local-dir (concat user-emacs-directory ".local/")
  "Root directory for local storage.
Use this as a storage location for this system's installation of Emacs.
These files should not be shared across systems.")

(defconst sminez-etc-dir (concat sminez-local-dir "etc/")
  "Directory for non-volatile local storage.
Use this for files that don't change much, like server binaries, external
dependencies or long-term shared data.")

(defconst sminez-cache-dir (concat sminez-local-dir "cache/")
  "Directory for volatile local storage.
Use this for files that change often, like cache files.")

(defconst sminez-autoloads-file
  (concat sminez-local-dir "autoloads." emacs-version ".el")
  "Where `sminez-reload-lib-autoloads' stores its core autoloads.
This file is responsible for informing Emacs where to find all of the
autoloaded core functions (in lib/autoload/*.el).")

;; TODO: This needs sorting so that the env file can be generated correctly
(defconst sminez-env-file (concat sminez-local-dir "env")
  "The location of your envvar file, generated by `doom env`.
This file contains environment variables scraped from the shell environment,
which is loaded at startup (if it exists). This is helpful if Emacs can't
easily be launched from the correct shell session (looking at you MacOS).")

Keeping things clean

There is a package called no-littering that helps with keeping your .emacs.d directory free of auto-generated files and other crud. There is a comment in the source code of doom that hints at it being a bit too opinionated about how it goes about that so I just take the setup that doom uses as I’m happy with how that works.

(setq async-byte-compile-log-file  (concat sminez-etc-dir "async-bytecomp.log")
      custom-file                  (concat user-emacs-directory "custom.el")
      desktop-dirname              (concat sminez-etc-dir "desktop")
      desktop-base-file-name       "autosave"
      desktop-base-lock-name       "autosave-lock"
      pcache-directory             (concat sminez-cache-dir "pcache/")
      request-storage-directory    (concat sminez-cache-dir "request")
      shared-game-score-directory  (concat sminez-etc-dir "shared-game-score/"))

FIXME: This defadvice! macro is not ported over yet

(defadvice! sminez--write-to-sane-paths-a (fn &rest args)
  "Write 3rd party files to `sminez-etc-dir' to keep `user-emacs-directory' clean.
Also writes `put' calls for saved safe-local-variables to `custom-file' instead
of `user-init-file' (which `en/disable-command' in novice.el.gz is hardcoded to
do)."
  :around #'en/disable-command
  :around #'locate-user-emacs-file
  (let ((user-emacs-directory sminez-etc-dir)
        (user-init-file custom-file))
    (apply fn args)))

Sorting out some security issues

Emacs is essentially one huge security vulnerability, what with all the dependencies it pulls in from all corners of the globe. While it’s probably fine, let’s try to at least be a little more discerning than the default.

(setq gnutls-verify-error (and (fboundp 'gnutls-available-p)
                               (gnutls-available-p)
                               (not (getenv-internal "INSECURE")))
      gnutls-algorithm-priority
      (when (boundp 'libgnutls-version)
        (concat "SECURE128:+SECURE192:-VERS-ALL"
                (if (>= libgnutls-version 30605)
                    ":+VERS-TLS1.3")
                ":+VERS-TLS1.2"))
      ;; `gnutls-min-prime-bits' is set based on recommendations from
      ;; https://www.keylength.com/en/4/
      gnutls-min-prime-bits 3072
      tls-checktrust gnutls-verify-error
      ;; Emacs is built with `gnutls' by default, so `tls-program' would not be
      ;; used in that case. Otherwise, people have reasons to not go with
      ;; `gnutls', we use `openssl' instead. For more details, see
      ;; https://redd.it/8sykl1
      tls-program '("openssl s_client -connect %h:%p -CAfile %t -nbio -no_ssl3 -no_tls1 -no_tls1_1 -ign_eof"
                    "gnutls-cli -p %p --dh-bits=3072 --ocsp --x509cafile=%t \
--strict-tofu --priority='SECURE192:+SECURE128:-VERS-ALL:+VERS-TLS1.2:+VERS-TLS1.3' %h"
                    ;; compatibility fallbacks
                    "gnutls-cli -p %p %h"))

Oh, and Emacs stores =authinfo in $HOME and in plain-text. Let’s…not? This file stores usernames, passwords etc…

(setq auth-sources (list (concat sminez-etc-dir "authinfo.gpg")
                         "~/.authinfo.gpg"))

Sensible defaults

There are a tonne of dials and knobs to play around with when it comes to configuring the behaviour of a lot of the built in functionality in Emacs. If I’m honest, I probably don’t need all of these but given that I like the behaviour of doom so far I’m just lifting what they set and documenting what each setting is for. That way, if I notice anything funky in the future I can hunt it down and see what is up.

Auto-mode association list

A second, case-insensitive pass over auto-mode-alist is a waste of time, and indicates that things have been configured incorrectly (don’t rely on case insensitivity for file names).

(setq auto-mode-case-fold nil)

No bidirectional text scanning

Disable bidirectional text scanning for a modest performance boost. I’ve set this to `nil’ in the past, but the bidi-display-reordering’s docs say that is an undefined state and suggest this to be just as good: Disabling the BPA makes re-display faster, but might produce incorrect display reordering of bidirectional text with embedded parentheses and other bracket characters whose paired-bracket Unicode property is non-nil.

(setq-default bidi-display-reordering 'left-to-right
              bidi-paragraph-direction 'left-to-right)
(setq bidi-inhibit-bpa t)

Don’t bother rending things we don’t need

We can reduce rendering / line scan work for Emacs by not rendering cursors or regions in non-focused windows. It’s also possible to opt-in to faster scrolling over unfontified regions so long as you don’t mind brief moments of wonky syntax highlighting (which should self correct once scrolling stops).

(setq-default cursor-in-non-selected-windows nil)
(setq highlight-nonselected-windows nil
      fast-but-imprecise-scrolling t)

Resizing the Emacs frame can be a pretty expensive part of changing fonts. Preventing this halves startup times, particularly when using fonts that are larger than the system default (which would cause the frame to resize).

(setq frame-inhibit-implied-resize t)

Emacs “updates” its UI more often than it really needs to. Slowing it down slightly actually makes a noticeable difference.

(setq idle-update-delay 1.0)  ; default is 0.5

Tame the garbage collector

The garbage collector introduces annoying pauses and stuttering into the Emacs experience, but you can use gcmh to keep it at bay while you’re using Emacs, and then invoke it when things are idle. That said, if the idle delay is too long, we run the risk of runaway memory usage in busy sessions. If it’s too low, then we may as well not be using gcmh at all.

(setq gcmh-idle-delay 'auto  ; default is 15s
      gcmh-auto-idle-delay-factor 10
      gcmh-high-cons-threshold (* 16 1024 1024))  ; 16mb

(add-hook 'emacs-startup-hook #'gcmh-mode)

Better interaction with external processes

Increase how much is read from processes in a single chunk (default is 4kb). This is further increased elsewhere, where needed (like our LSP module).

(setq read-process-output-max (* 64 1024))  ; 64kb

Disable useless warnings and output

There are quite a few things that spam debug output and message which aren’t all that helpful: lets just shut them up.

Setting the initial scratch buffer major mode to fundamental takes seconds off startup time, rather than, say, org-mode or text-mode, which pull in a ton of packages.

(setq ad-redefinition-action 'accept
      debug-on-error init-file-debug
      jka-compr-verbose init-file-debug
      inhibit-startup-screen t
      inhibit-startup-echo-area-message user-login-name
      inhibit-default-init t
      initial-major-mode 'fundamental-mode
      initial-scratch-message nil)

(unless (daemonp)
  (advice-add #'display-startup-echo-area-message :override #'ignore))

Y means yes

y/n is more than sufficient thank you very much.

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

Per-system changes

At work I have a Mac-Book and for my personal set up I use Linux. Some things need to be modified a bit depending on which machine I’m on so just check the system type to determine which one we are on. (The IS-LINUX and IS-MAC constants are defined in early-init.el)

(cond (IS-LINUX (setq user-full-name "Innes Anderson-Morrison"
                      user-mail-address "[email protected]"
                      command-line-ns-option-alist nil))
      (IS-MAC   (setq user-full-name "Innes Anderson-Morrison"
                      user-mail-address "[email protected]"
                      command-line-x-option-alist nil)))

Tidying up the UI

Out of the box, the Emacs UI is pretty messy. Lets sort that.

(push '(menu-bar-lines . 0)   default-frame-alist)
(push '(tool-bar-lines . 0)   default-frame-alist)
(push '(vertical-scroll-bars) default-frame-alist)

(setq display-line-numbers-mode 1)
(setq-default display-line-numbers-width 3
              display-line-numbers-widen t)


(menu-bar-mode -1)
(tool-bar-mode -1)
(scroll-bar-mode -1)
(blink-cursor-mode -1)

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

(setq uniquify-buffer-name-style 'forward
      ring-bell-function #'ignore
      visible-bell nil
      blink-matching-paren nil
      x-stretch-cursor nil
      hscroll-margin 2
      hscroll-step 1
      ;; Emacs spends too much effort recentering the screen if you scroll the
      ;; cursor more than N lines past window edges (where N is the settings of
      ;; `scroll-conservatively'). This is especially slow in larger files
      ;; during large-scale scrolling commands. If kept over 100, the window is
      ;; never automatically recentered.
      scroll-conservatively 101
      scroll-margin 0
      scroll-preserve-screen-position t
      ;; Reduce cursor lag by a tiny bit by not auto-adjusting `window-vscroll'
      ;; for tall lines.
      auto-window-vscroll nil)

(setq indicate-buffer-boundaries nil
      indicate-empty-lines nil)

(setq frame-title-format '("%b – Emacs")
      icon-title-format frame-title-format)

;; Don't resize the frames in steps; it looks weird, especially in tiling window
;; managers, where it can leave unseemly gaps.
(setq frame-resize-pixelwise t)

;; But do not resize windows pixelwise, this can cause crashes in some cases
;; when resizing too many windows at once or rapidly.
(setq window-resize-pixelwise nil)

;; The native border "consumes" a pixel of the fringe on righter-most splits,
;; `window-divider' does not. Available since Emacs 25.1.
(setq window-divider-default-places t
      window-divider-default-bottom-width 1
      window-divider-default-right-width 1)

(add-hook 'emacs-startup-hook #'window-divider-mode)

(setq use-dialog-box nil)
(when (bound-and-true-p tooltip-mode)
  (tooltip-mode -1))
(when IS-LINUX
  (setq x-gtk-use-system-tooltips nil))

(setq split-width-threshold 160
      split-height-threshold nil)

Sorting out the mini-buffer

;; Allow for minibuffer-ception. Sometimes we need another minibuffer command
;; while we're in the minibuffer.
(setq enable-recursive-minibuffers t)

;; Show current key-sequence in minibuffer ala 'set showcmd' in vim. Any
;; feedback after typing is better UX than no feedback at all.
(setq echo-keystrokes 0.02)

;; Expand the minibuffer to fit multi-line text displayed in the echo-area. This
;; doesn't look too great with direnv, however...
(setq resize-mini-windows 'grow-only)

;; Typing yes/no is obnoxious when y/n will do
(advice-add #'yes-or-no-p :override #'y-or-n-p)

;; Try to keep the cursor out of the read-only portions of the minibuffer.
(setq minibuffer-prompt-properties '(read-only t intangible t cursor-intangible t face minibuffer-prompt))
(add-hook 'minibuffer-setup-hook #'cursor-intangible-mode)

Misc tweaks and hacks

(setq global-auto-revert-non-file-buffers t)
(global-auto-revert-mode 1)
(setq-default indent-tabs-mode nil
              tab-width 4)

(setq confirm-nonexistent-file-or-buffer nil)

Package management

I really like the reproducible build nature of straight.el (especially compared to the built in package.el workflow). The README for straight is huge and probably something I should take a longer look at in the future. For now though, I’m just following the example for bootstrapping straight.el itself which seems to be significantly simpler than what doom does? (Not sure what doom’s approach gets you over the one recommended by straight itself yet)

Bootstrapping

This is taken straight from the README:

;; make sure that we have straght.el on this system
(defvar bootstrap-version)
(let ((bootstrap-file
       (expand-file-name "straight/repos/straight.el/bootstrap.el" user-emacs-directory))
      (bootstrap-version 5))
  (unless (file-exists-p bootstrap-file)
    (with-current-buffer
        (url-retrieve-synchronously
         "https://raw.githubusercontent.com/raxod502/straight.el/develop/install.el"
         'silent 'inhibit-cookies)
      (goto-char (point-max))
      (eval-print-last-sexp)))
  (load bootstrap-file nil 'nomessage))

;; For everything else, we'll be using `use-package` so bring that in now.
(straight-use-package 'use-package)

Helper macros

doom has a number of helper macros for package management and loading that are pretty convenient. I don’t want the full set up that doom provides but I’m bringing in some of those QOL improvements to keep things neat and as simple as possible when configuring each package that I use.

after!

Evaluate a body after a package (or combination of packages) have loaded.

This is a wrapper around eval-after-load that:

  1. Suppresses warnings for disabled packages at compile-time
  2. No-ops for package that are disabled by the user (via package!)
  3. Supports compound package statements (see below)
  4. Prevents eager expansion pulling in auto-loaded macros all at once

PACKAGE is a symbol or list of symbols: these are package names, not modes, functions or variables. Valid values are:

  • An unquoted package symbol (the name of a package) (after! helm BODY...)
  • An unquoted list of package symbols (i.e. BODY is evaluated once both magit and git-gutter have loaded) (after! (magit git-gutter) BODY...)
  • An unquoted, nested list of compound package lists, using any combination of :or/:any and :and/:all: (after! (:or package-a package-b ...) BODY...) (after! (:and package-a package-b ...) BODY...) (after! (:and package-a (:or package-b package-c) ...) BODY...)

For list values, omitting :or/:any/:and/:all implies :and/:all.

(defmacro after! (package &rest body)
  "Evaluate BODY after PACKAGE have loaded."
  (declare (indent defun) (debug t))
  (if (symbolp package)
      ;; Simple case: `after' a single package is loaded
      ;;   (after! foo BODY...)
      (list (if (or (not (bound-and-true-p byte-compile-current-file))
                    (require package nil 'noerror))
                #'progn
              #'with-no-warnings)
            ;; Avoid `with-eval-after-load' to prevent eager macro expansion
            ;; from pulling (or failing to pull) in autoloaded macros/packages.
            `(eval-after-load ',package ',(macroexp-progn body)))

    ;; One of the following cases:
    ;;   (after! (package-a package-b ...) BODY...)
    ;;   (after! (:or package-a package-b ...)  BODY...)
    ;;   (after! (:and package-a package-b ...) BODY...)
    ;;   (after! (:and package-a (:or package-b package-c) ...) BODY...)
    (let ((p (car package)))
      (cond ((memq p '(:or :any))
             (macroexp-progn
              (cl-loop for next in (cdr package)
                       collect `(after! ,next ,@body))))
            ((memq p '(:and :all))
             (dolist (next (reverse (cdr package)) (car body))
               (setq body `((after! ,next ,@body)))))
            (`(after! (:and ,@package) ,@body))))))

add-load-path!

Add a list of directories to load-path. Paths are resolved relative to the file where add-load-path! is called.

(defmacro add-load-path! (&rest dirs)
  "Add DIRS to `load-path', relative to the current file.
The current file is the file from which `add-to-load-path!' is used."
  `(let ((default-directory ,(dir!))
         file-name-handler-alist)
     (dolist (dir (list ,@dirs))
       (cl-pushnew (expand-file-name dir) load-path :test #'string=))))

Garbage Collection Magic Hack

In early-init.el the GC limit was bumped as high as it can go but now we need to actually sort it out going forward:

(use-package gcmh
  :straight t
  :config (gcmh-mode 1))

Evil mode

Before anything else, evil. I love the extensibility of Emacs but good god do I need the editor itself to be as close to Vim as possible.

(use-package undo-tree
  :straight t
  :init (global-undo-tree-mode 1))

(use-package evil
  :straight t
  :init
  (setq evil-want-C-i-jump nil
        evil-want-keybinding nil
        evil-want-integration t
        evil-undo-system 'undo-tree
        evil-want-Y-yank-to-eol t
        )
  :config
  (evil-mode 1)
  (setq evil-search-module 'rg
        evil-magic 'very-magic
        evil-want-fine-undo t
        evil-want-change-word-to-end t
        evil-split-window-below t
        evil-split-window-right t))

(use-package evil-escape
  :straight t
  :after evil
  :init (setq-default evil-escape-key-sequence "fd")
  :config (evil-escape-mode))

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

(use-package evil-nerd-commenter
  :straight t)

Keybindings

Vim-like Escape

keyboard-quit is too much of a nuclear option. I want ESC/C-g to do-what-I-mean. It serves four purposes (in order):

  1. Quit active states; e.g. highlights, searches, snippets, iedit, multiple-cursors, recording macros, etc.
  2. Close popup windows remotely (if it is allowed to)
  3. Refresh buffer indicators, like git-gutter and flycheck
  4. Or fall back to keyboard-quit

It should do these things incrementally (rather than all at once) and it shouldn’t interfere with recording macros or the mini-buffer. This may require you press ESC/C-g a couple of times to reach keyboard-quit but it’s much more intuitive.

(defvar sminez-escape-hook nil
  "A hook run when C-g is pressed (or ESC in normal mode, for evil users).
More specifically, when `sminez/escape' is pressed. If any hook returns non-nil,
all hooks after it are ignored.")

(defun sminez/escape (&optional interactive)
  "Run `sminez-escape-hook'."
  (interactive (list 'interactive))
  (cond ((minibuffer-window-active-p (minibuffer-window))
         ;; quit the minibuffer if open.
         (when interactive
           (setq this-command 'abort-recursive-edit))
         (abort-recursive-edit))
        ;; Run all escape hooks. If any returns non-nil, then stop there.
        ((run-hook-with-args-until-success 'sminez-escape-hook))
        ;; don't abort macros
        ((or defining-kbd-macro executing-kbd-macro) nil)
        ;; Back to the default
        ((unwind-protect (keyboard-quit)
           (when interactive
             (setq this-command 'keyboard-quit))))))

(global-set-key [remap keyboard-quit] #'sminez/escape)

(with-eval-after-load 'eldoc
  (eldoc-add-command 'sminez/escape))

Which-key

I love which-key. There’s probably a whole bunch that it can do that I’ve not even begun to look into but the fact that it can remind me what on earth I bound everything to without me needing to remember to register each binding is just a life saver.

(use-package which-key
  :straight t
  :config
  (which-key-mode))

Setting up General and top level key bindings

General is a package for providing nicer keybinding definitions with built in support for evil-mode and vim-like key combinations. There is a lot that it can do out of the box, but the doom keybinding system is built on top of this with even more magic. From what I can tell, other than the improved ergonomics of the map! macro, it also does a whole heap of things to try to lazy load bindings and make sure that things can be overwritten and tweaked in a nicer way.

We’ll see if this ends up biting me in the long run but for now I’m just going to stick with general. I tried extracting the map! macro out of doom and after an hour or so it was clear that it’s tied pretty deeply into the guts of all of the module and package management stuff that I’m aiming to avoid.

(use-package general
  :straight t
  :init
  (defalias 'def-key! #'general-def)
  (defalias 'undef-key! #'general-unbind))

(after! general
  (general-create-definer sminez/leader-def
    :prefix "SPC")

  (general-create-definer sminez/local-leader-def
    :prefix "SPC m")

  (sminez/leader-def
   :keymaps 'normal
   "SPC" '(execute-extended-command :wk "M-x")
   "," '(consult-buffer :wk "switch buffer")
   "." '(find-file :wk "find file")
   ";" '(evilnc-comment-or-uncomment-lines :wk "comment line")

   ;; SECTION: Buffers
   "b" '(nil :wk "buffers")
   "bb" '(consult-buffer :wk "switch buffer")
   "bd" '(kill-this-buffer :wk "delete buffer")
   "bi" '(ibuffer :wk "ibuffer")

   ;; SECTION: Files
   "f" '(nil :wk "file")
   "ff" '(find-file :wk "find file")
   "fr" '(consult-recent-file :wk "recent files")
   "fs" '(save-buffer :wk "save buffer")

   ;; SECTION: Search
   "s" '(nil :wk "search")
   "ss" '(consult-line :wk "search current buffer")
   
   ;; SECTION: Windows
   "w" '(nil :wk "window")
   "wc" '(delete-window :wk "close window")
   "wh" '(evil-window-left :wk "window left")
   "wj" '(evil-window-down :wk "window down")
   "wk" '(evil-window-up :wk "window up")
   "wl" '(evil-window-right :wk "window right")
   "w/" '(evil-window-vsplit :wk "vertical split")
   "w-" '(evil-window-split :wk "horizontal split")
   )

  (general-define-key
   "C-SPC" 'company-complete-common)
  )

Completion and searching

Save hist

(use-package savehist
  :straight t
  :custom (savehist-file (concat sminez-cache-dir "savehist"))
  :config
  (setq savehist-save-minibuffer-history t
        savehist-autosave-interval nil     ; save on kill only
        savehist-additional-variables
        '(kill-ring                        ; persist clipboard
          register-alist                   ; persist macros
          mark-ring global-mark-ring       ; persist marks
          search-ring regexp-search-ring)  ; persist searches
        history-length 25)
  (savehist-mode 1))

Recent files

(recentf-mode 1)
(setq recentf-max-menu-items 25
      recentf-max-saved-items 25)

Vertico

There are loads of different completion / search frameworks for Emacs (I’ve tried helm and ivy previously) but for now I’m settled on vertico. It’s pretty bare bones, integrates nicely with the built-in functionality of Emacs itself (as opposed to being a completely new framework) and also plays well with a number of other packages that follow the same design philosophy (see below).

As with a lot of these packages: see the README in GitHub for a lot more information (as and when I have time to go through it…)

(use-package vertico
  :straight t
  :init
  (vertico-mode)
  (setq vertico-cycle t
        vertico-count 17))

(use-package emacs
  :init
  (setq enable-recursive-minibuffers t))

Orderless

Orderless provides a really nice alternative for incremental filtering of completion targets based on fragments of search patterns (it’s a little hard to summarise without an example so see the README for more details). At the moment I’m pretty much just using the “out of the box” default settings and not making that much use of the wider feature set: this is definitely one to read more into and see what it can do…

(use-package orderless
  :straight t
  :init
  (setq completion-styles '(orderless)
        completion-category-defaults nil
        completion-category-overrides '((file (styles partial-completion)))))

Marginalia

Marginalia provides nice help summaries in completion mini-buffers which make it a lot easier to quickly hunt for things that you’ve not used for a while (or at all) without needing to constantly stop and look up the docs. With that in mind, remember to document your variables and functions! Trust me, you’ll thank yourself later when you have to hunt for them. There is some customisation available for marginalia but I just use the defaults for now as they give me all that I need.

(use-package marginalia
  :straight t
  :init
  (marginalia-mode))

Consult

Consult provides some greatly enhanced versions of built in Emacs commands along with a whole heap of extra stuff I’ve not even begun to scratch the surface on.

(use-package consult
  :straight t
  :after company-mode
  :init
  (general-define-key
    [remap apropos]                       #'consult-apropos
    [remap bookmark-jump]                 #'consult-bookmark
    [remap evil-show-marks]               #'consult-mark
    [remap evil-show-jumps]               #'+vertico/jump-list
    [remap evil-show-registers]           #'consult-register
    [remap goto-line]                     #'consult-goto-line
    [remap imenu]                         #'consult-imenu
    [remap locate]                        #'consult-locate
    [remap load-theme]                    #'consult-theme
    [remap man]                           #'consult-man
    [remap recentf-open-files]            #'consult-recent-file
    [remap switch-to-buffer]              #'consult-buffer
    [remap switch-to-buffer-other-window] #'consult-buffer-other-window
    [remap switch-to-buffer-other-frame]  #'consult-buffer-other-frame
    [remap yank-pop]                      #'consult-yank-pop
    [remap persp-switch-to-buffer]        #'+vertico/switch-workspace-buffer))

Helpful

Better help documentation.

(use-package helpful
  :straight t
  :commands helpful--read-symbol
  :init
  ;; Make `apropos' et co search more extensively.
  (setq apropos-do-all t)

  (global-set-key [remap describe-function] #'helpful-callable)
  (global-set-key [remap describe-command]  #'helpful-command)
  (global-set-key [remap describe-variable] #'helpful-variable)
  (global-set-key [remap describe-key]      #'helpful-key)
  (global-set-key [remap describe-symbol]   #'helpful-symbol))

Company

Company is the text completion framework for Emacs: everything integrates with it. So lets set it up! One thing I change from the default behaviour is to prevent it auto-completing as I type. I find it really distracting and actually, needing to hit C-SPC when I want completion candidates is pretty nice: it better fits my mental model of “ok what should this thing be?” instead of being more like having someone sat next to you trying to second guess everything you are typing…

(use-package company
  :straight t
  :commands (company-complete-common
             company-complete-common-or-cycle
             company-manual-begin
             company-grab-line)
  :init
  (setq company-minimum-prefix-length 2
        company-tooltip-limit 14
        company-tooltip-align-annotations t
        company-require-match 'never
        company-global-modes
        '(not erc-mode
              circe-mode
              message-mode
              help-mode
              gud-mode
              vterm-mode)
        company-frontends
        '(company-pseudo-tooltip-frontend  ; always show candidates in overlay tooltip
          company-echo-metadata-frontend)  ; show selected candidate docs in echo area

        ;; Buffer-local backends will be computed when loading a major mode, so
        ;; only specify a global default here.
        company-backends '(company-capf)
        company-auto-commit nil
        company-dabbrev-other-buffers nil
        company-dabbrev-ignore-case nil
        company-dabbrev-downcase nil)

  :config
  (add-hook 'company-mode-hook #'evil-normalize-keymaps)
  (company-mode 1)
  (global-company-mode)
  (setq company-idle-delay nil))

Theme and appearance

Modus

I first tried out the modus themes from Prot on a whim when I was thinking about how “in your face” my (then) theme of choice was. (It was called laserwave which tells you pretty much all you need to know). After switching I wasn’t too sure if I liked it or not but the more I stuck with them the more they have grown on me, even to the extent that I’m using modus-operandi for writing Lisp and Org files as it’s actually a really nice experience.

The themes come with quite a few configuration options which I’ve tinkered around with until I’ve found what works for me quite well at this point. I would quite like to try out the mixed-pitch stuff for Org mode but I can’t for the life of me get it to stop jiggling the focused line a bit every time I move the cursor…

(use-package modus-themes
  :straight t
  :init
  (setq modus-themes-italic-constructs t
        modus-themes-bold-constructs t
        modus-themes-region '(bg-only no-extend accented)
        modus-themes-org-blocks 'gray-background
        modus-themes-deuteranopia nil
        modus-themes-syntax '(alt-syntax green-strings)
        modus-themes-subtle-line-numbers t
        modus-themes-headings
        '((1 . (1.2))
          (2 . (1.15))
          (3 . (1.1))
          (4 . (1.05)))
        modus-themes-diffs 'desaturated
        modus-themes-hl-line '(intense accented))
  (modus-themes-load-themes)
  :config
  (modus-themes-load-operandi))

Font

(set-frame-font "Fira Code 10")
(set-face-attribute 'default nil :family "Fira Code")
(set-face-attribute 'variable-pitch nil :family "Alegreya")

Frame and window tweaks

;; Add frame borders and window dividers
(modify-all-frames-parameters
 '((right-divider-width . 10)
   (internal-border-width . 10)))
(dolist (face '(window-divider
                window-divider-first-pixel
                window-divider-last-pixel))
  (face-spec-reset-face face)
  (set-face-foreground face (face-attribute 'default :background)))
(set-face-background 'fringe (face-attribute 'default :background))

Org

Org itself

(setq org-auto-align-tags nil
      org-tags-column 0
      org-pretty-entities t
      org-ellipsis " [...]"
      org-src-window-setup 'other-window
      org-src-preserve-indentation t
      org-src-tab-acts-natively t
      org-src-fontify-natively t
      org-edit-src-content-indentation 0)

org-modern

(use-package org-modern
  :straight t
  :init
  (add-hook 'org-mode-hook #'org-modern-mode)
  (add-hook 'org-agenda-finalize-hook #'org-modern-mode)
  :config
  (setq org-modern-block t))

Mode line

Minions

(use-package minions
  :straight t
  :config (minions-mode 1))

Snippets

(use-package yasnippet
  :straight t
  :config
  (yas-global-mode 1))

(use-package doom-snippets
  :after yasnippet
  :straight
  (doom-snippets
   :type git
   :host github
   :repo "hlissner/doom-snippets"
   :files ("*.el" "*")))

Highlighting TODO comments

hl-todo is pretty simple but easily configurable and really nice for helping draw a little more attention to TODO comments and similar notes in files.

(use-package hl-todo
  :straight t
  :config
  (setq hl-todo-highlight-punctuation ":"
        hl-todo-keyword-faces
        `(("TODO" warning bold)
          ("NOTE" success bold)
          ("FIXME" error bold)
          ("XXX" font-lock-constant-face bold)
          ("HACK" font-lock-warning-face bold)
          ("REVIEW" font-lock-keyword-face bold)
          ("DEPRECATED" font-lock-doc-face bold)
          ("SECTION" font-lock-comment-face bold)
          ("BUG" error bold)))
  (hl-todo-mode 1)
  (global-hl-todo-mode))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment