Skip to content

hnarayanan/dotemacs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

210 Commits
 
 
 
 
 
 
 
 

Repository files navigation

GNU Emacs Configuration

I’ve always had a natural affinity for the native text-editing interface in Emacs. The shortcuts (called key-bindings in Emacs terminology) and mnemonic patterns make intuitive sense to me. So for nearly 2.5 decades, I happily used Emacs as “just a text editor.”

In late 2023, I decided to immerse myself in the world of Emacs Lisp. This really wasn’t driven by any specific goal; it was simply me encouraging my curiosity. It was around this time that it hit me:

Emacs is not a text editor, it’s a tool construction kit that lets you build any text-based tool you want. It is the embodiment of a philosophy of user empowerment.

As I’ve explored the implications of this idea, my Emacs configuration has evolved significantly. It’s no longer limited to a handful of snippets I’ve cribbed from the internet. It now involves multiple packages, relies on a package repository, leans into powerful systems built upon Emacs (like Magit and Org Mode), and most importantly, adapts Emacs to be exactly what I need it to be.

This file gathers all these ideas in one place, and it aims to be two things simultaneously:

  • Literature that is readable and understandable for humans.
  • A resource that can be directly exported (C-c C-v t in Org Mode) to my actual Emacs configuration files.

The structure of this file was inspired by a much richer literate configuration by Prot.

Please write to me if you need any help with how it’s used, or if you’d like to suggest any improvements.

Early Initialisation

There are some things (mostly pertinent to the base UI) that need to be set really early in Emacs’ startup. This is so that the UI doesn’t first show up uncustomised, and then flash as it redraws based on any later UI customisation (such as a change of theme).

This early initialisation configuration goes into a handily-named file called early-init.el.

File Header

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

Clean Slate

Depending on your system, there might be some default configuration shared by other Emacs users in a file called default.el. To ensure our Emacs behaves consistently everywhere, we ignore this and start from a blank slate.

(setq inhibit-default-init t)

Minimal Interface

There are a few elements like a graphical menu and a scroll-bar that are useful for beginners when first getting acquainted with Emacs. But as you get more experienced navigating the app with a keyboard, they get less useful. As a more advanced user, I remove them from the UI to let me focus more on the content.

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

In the snippet above, -1 is convention for disabling the corresponding mode.

macOS System Integration

I generally use Emacs on macOS, and this needs me to tweak it a tiny bit to my liking.

Resolve native compilation issues

To be as fast as possible, modern Emacs byte compiles Emacs Lisp code into an efficient format for use at runtime. For some reason, on my Mac, Emacs fails to fully understand where GCC is installed, and then complains that it cannot find libgccjit.so very many times when installing packages.

No matter, we help it along by explicitly telling it where to look.

(when (eq system-type 'darwin)
  (add-to-list 'exec-path "/opt/local/bin")
  (setenv "PATH" (concat "/opt/local/bin:" (getenv "PATH"))))

The paths above are hard-coded for MacPorts and you can tweak them for your system if it’s different.

Set the Command key to function as the Meta key

Emacs has two primary modifier keys, the Control key (C) and the Meta key (M). M is traditionally mapped to Alt on most keyboards, but on a Mac, Command is so much more comfortable.

(when (eq system-type 'darwin)
  (setq mac-command-modifier 'meta
        mac-option-modifier 'none))

Reduce window decoration

These settings make Emacs’ titlebar blend more seamlessly with the dark theme. Enforcing night mode ensures that the title is in a colour that is visible against the dark titlebar.

(when (eq system-type 'darwin)
  (add-to-list 'default-frame-alist '(ns-transparent-titlebar . t))
  (add-to-list 'default-frame-alist '(ns-appearance . dark)))

Performance Optimisation

Reduce pauses from garbage collection

As Emacs runs, it allocates memory from the operating system to store the data it needs. When tasks complete, not all of this data needs to persist and can be discarded. To find and reclaim this unused memory, Emacs runs a garbage collection process after:

  • a certain number of bytes have been allocated, or
  • a certain fraction of the current heap size has been used up

since the previous garbage collection.

The trouble is, the default thresholds for these are quite low, e.g. 800,000 bytes (~800 kB) on a 64-bit computer. This means garbage collection triggers unnecessarily often, causing noticeable pauses that slow things down.

To address this, we follow an approach that traces back to Andrea Corallo. We defer garbage collection entirely during startup and minibuffer activity to keep the experience snappy, then restore it to a reasonable threshold (64 MB) for normal editing.

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

(defconst hn/gc-normal-threshold (* 64 1024 1024)) ;; 64MB
(defconst hn/gc-normal-percentage 0.1)

(add-hook 'emacs-startup-hook
          (lambda ()
            (setq gc-cons-threshold hn/gc-normal-threshold
                  gc-cons-percentage hn/gc-normal-percentage)))

(add-hook 'minibuffer-setup-hook
          (lambda () (setq gc-cons-threshold most-positive-fixnum)))

(add-hook 'minibuffer-exit-hook
          (lambda () (setq gc-cons-threshold hn/gc-normal-threshold
                           gc-cons-percentage hn/gc-normal-percentage)))

Reduce number of files touched during startup

(defvar hn/file-name-handler-alist-backup file-name-handler-alist)
(setq file-name-handler-alist nil)
(add-hook 'emacs-startup-hook
          (lambda ()
            (setq file-name-handler-alist
                  (delete-dups (append file-name-handler-alist
                                       hn/file-name-handler-alist-backup)))))

Meta Configuration

File Header

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

Separate customize settings

In addition to being configured with the source code in this file, Emacs can also be configured using a graphical interface (M-x customize). When using this GUI, the standard behaviour is to persist these settings directly by editing the default Emacs config file.

The following configuration puts this into its own file, so we can clearly separate these two concepts.

(setq custom-file (locate-user-emacs-file "custom.el"))
(load custom-file 'noerror 'nomessage)

Package Management

In addition to the packages that come built-in with Emacs, there is a lot out there that can add to its functionality. We turn to a popular, community-driven package repository called Melpa to access this goodness.

Repository Configuration

(require 'package)
(add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t)

use-package Setup

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

Optimisation Notes

In the sections that follow, I use use-package to install, configure and load multiple packages. Loading all packages at startup can slow things down significantly (adding multiple seconds to startup time).

To identify which packages are slowing things down, I temporarily enable statistics collection by adding (setq use-package-compute-statistics t) at the top of init.el. After restarting Emacs, I run M-x use-package-report to see a table of package load times sorted by duration.

Armed with this data, I selectively defer expensive packages so they only load when actually needed. The configurations below reflect this optimisation. I’ve deferred heavy packages to keep startup fast.

Appearance and Interface

Frame and Window Behaviour

Frame title format

I like seeing the current buffer name and the machine I’m on right in the title bar, so I don’t lose track when I have multiple frames or hosts open.

(setq frame-title-format
      (concat  "%b - emacs@" (system-name)))

Startup screen preferences

I prefer a relatively clean and empty state as I start, so the following removes a startup splash screen and brings up an empty plain text buffer, so I can start typing immediately.

(setq inhibit-startup-screen t)
(setq initial-scratch-message "")
(setq initial-major-mode 'text-mode)
(setq default-major-mode 'text-mode)

Window splitting preferences

;; (setq split-width-threshold 0)

Prevent accidental zooming of text

On my Mac, I sometimes accidentally trigger zooming of text because I have my hand on the C key and scroll on the trackpad. The following configuration ignores this input.

(when (eq system-type 'darwin)
  (define-key key-translation-map (kbd "C-<wheel-up>")   (kbd "<wheel-up>"))
  (define-key key-translation-map (kbd "C-<wheel-down>") (kbd "<wheel-down>"))
  (define-key special-event-map [pinch] #'ignore))

Theme Configuration

The basic visual styling of my Emacs is controlled by a highly accessible theme called Modus Vivendi Tritanopia. A version of this theme comes built-in with Emacs, but it is not the most up-to-date version. We fetch the most recent version from the package repository and customise it a little. This is a highly customisable theme with many options, but I try to keep it simple.

(use-package modus-themes
  :demand t
  :custom
  (modus-themes-italic-constructs t)
  (modus-themes-bold-constructs t)
  (modus-themes-prompts '(bold))
  (modus-themes-to-toggle '(modus-operandi-tritanopia modus-vivendi-tritanopia))
  (modus-themes-common-palette-overrides
   '((border-mode-line-active bg-mode-line-active)
     (border-mode-line-inactive bg-mode-line-inactive)))
  (modus-themes-headings
   '((1 . (1.2))
     (2 . (1.1))
     (agenda-date . (1.1))
     (agenda-structure . (1.2))
     (t . (1.0))))
  :config
  (modus-themes-load-theme 'modus-vivendi-tritanopia)
  :bind (("<f5>" . modus-themes-toggle)))

Visual Feedback Systems

Selection Behaviour

(transient-mark-mode 1)
(delete-selection-mode 1)

Editor Visual Aids

(setq show-paren-delay 0)
(show-paren-mode 1)
(column-number-mode 1)

Smooth Scrolling

(pixel-scroll-precision-mode 1)

Whitespace and Buffer Boundaries

(setq-default show-trailing-whitespace t
              indicate-empty-lines t
              indicate-buffer-boundaries 'right
              sentence-end-double-space nil)

Mouse Selection

(setq mouse-drag-copy-region t)

Editing Foundations

Indentation and Whitespace

;; prevent extraneous tabs and use 2 spaces
(setq-default indent-tabs-mode nil
              tab-width 2)

Text Manipulation

;; enable up- and down-casing
(put 'downcase-region 'disabled nil)
(put 'upcase-region 'disabled nil)

Spelling

Emacs comes with a built-in spell-checker called Flyspell mode that automatically checks your spelling as you type. I have used this for many years, but in large documents it can be very slow and hampers smooth scrolling.

The community has largely moved onto a third party package called =jinx.el=, which is a really fast just-in-time spell-checker, and that’s what I am switching to too.

For this to work, you need a few system packages installed. For me (using MacPorts), these are:

  • aspell
  • aspell-dict-en
  • enchant2 +aspell
(use-package jinx
  :hook ((text-mode . jinx-mode)
         (prog-mode . jinx-mode))
  :bind ([remap ispell-word] . jinx-correct)
  :custom
  (jinx-languages "en_GB"))

Minibuffer and Completion

Minibuffer

The minibuffer is the small interface at the bottom of the Emacs window where you can enter commands, input parameters, see results of these commands and so on.

;; Vertical completion interface
(use-package vertico
  :custom
  (vertico-resize t)
  :init
  (vertico-mode))

;; Rich annotations in the minibuffer
(use-package marginalia
  :init
  (marginalia-mode))

;; Flexible matching
(use-package orderless
  :custom
  (completion-styles '(orderless basic))
  (completion-category-defaults nil)
  (completion-category-overrides '((file (styles partial-completion)))))

;; Persist minibuffer history across sessions
(use-package savehist
  :ensure nil
  :init
  (savehist-mode))

;; Track recently opened files
(use-package recentf
  :ensure nil
  :init
  (recentf-mode)
  :custom
  (recentf-max-saved-items 100))

In-buffer completion

;; In-buffer completion popup
(use-package corfu
  :custom
  (corfu-cycle t)
  (corfu-preselect 'prompt)
  (corfu-scroll-margin 5)
  (corfu-separator ?\s)
  :init
  (global-corfu-mode)
  :config
  (corfu-popupinfo-mode))

(use-package emacs
  :ensure nil
  :custom

  (tab-always-indent 'complete)
  (completion-cycle-threshold 3)
  (enable-recursive-minibuffers t)
  (read-extended-command-predicate #'command-completion-default-include-p)
  (minibuffer-prompt-properties
   '(read-only t cursor-intangible t face minibuffer-prompt)))

Programming Infrastructure

Basic Indentation

(setq c-default-style "bsd")
(setq-default c-basic-offset 2)
(setq-default sgml-basic-offset 2)

Syntax Highlighting

;; setup tree-sitter
(use-package tree-sitter
  :config
  (global-tree-sitter-mode)
  (add-hook 'tree-sitter-after-on-hook #'tree-sitter-hl-mode))

(use-package tree-sitter-langs
  :ensure t
  :after tree-sitter)

Language Server Protocol

Language-Specific Configuration

Python

;; configure a development environment for python
(use-package python
  :hook ((python-mode . eglot-ensure)
         (python-mode . tree-sitter-hl-mode)))

Scheme

(use-package geiser
  :defer t
  :custom
  (geiser-active-implementations '(mit guile))
  :config
  (setenv "DISPLAY" ":0")
  (add-hook 'geiser-repl-mode-hook 'hn/disable-trailing-whitespace-and-empty-lines))

(use-package geiser-guile
  :defer t
  :custom
  (geiser-guile-binary "/opt/local/bin/guile"))

(use-package geiser-mit
  :defer t
  :custom
  (geiser-mit-binary "/Users/harish/Applications/mit-scheme/bin/mit-scheme")
  :config
  (setenv "MITSCHEME_HEAP_SIZE" "100000")
  (setenv "MITSCHEME_LIBRARY_PATH" "/Users/harish/Applications/mit-scheme/lib/mit-scheme-svm1-64le-12.1")
  (setenv "MITSCHEME_BAND" "mechanics.com"))

LaTeX

(use-package tex
  :ensure auctex
  :defer t
  :hook (tex-mode . auto-fill-mode))

Other Languages

(use-package go-mode :defer t)
(use-package julia-mode :defer t)
(use-package php-mode :defer t)
(use-package markdown-mode :defer t)
(use-package yaml-mode :defer t)
(use-package graphviz-dot-mode :defer t)

Consider something for HTML, CSS and JS

File Associations

(add-to-list 'auto-mode-alist '("\\.m\\'" . octave-mode))

Version Control

Magit

(use-package magit
  :defer t)

Diff and Merge

(setq ediff-split-window-function 'split-window-horizontally)
(setq ediff-window-setup-function 'ediff-setup-windows-plain)

Org Mode

TODO: Consider https://github.com/minad/org-modern

Core Behaviour

(setq org-edit-src-content-indentation 0)
(use-package org-bullets
  :hook (org-mode . org-bullets-mode))

Literate Programming

(org-babel-do-load-languages
 'org-babel-load-languages
 '((scheme . t)))

(defun hn/org-confirm-babel-evaluate (lang body)
  (not (string= lang "scheme")))
(setq org-confirm-babel-evaluate #'hn/org-confirm-babel-evaluate)

Task Management

(global-set-key (kbd "C-c a") 'org-agenda)
;; consider https://github.com/minad/org-modern
(setq org-agenda-files '("~/Notes/todo.org"))

Export Settings

(setq org-export-with-smart-quotes t)

(use-package htmlize
  :defer t)

Additional Tools

Text Manipulation

(use-package unfill)

AI Integration

(use-package gptel
  :defer t)

Social Media

;; (use-package mastodon
;;   :config (setq mastodon-instance-url "https://hachyderm.io/"
;;                mastodon-active-user "harish"))

Custom Functions

These are specific to my needs, and are likely not useful for other people. They are prefixed with my initials, hn/.

Journal Management

(defun hn/journal-todo (start-date end-date &optional prefix)
  "Generate a todo list for journal entries from START-DATE to END-DATE with an optional PREFIX."
  (interactive
   (list
    (read-string "Enter start date (YYYY-MM-DD): ")
    (read-string "Enter end date (YYYY-MM-DD): ")
    (read-string "Enter prefix: " "Write a journal entry for ")))
  (let* ((start-time (date-to-time start-date))
         (end-time (date-to-time end-date))
         (one-day (seconds-to-time 86400)) ; 24 hours * 60 minutes * 60 seconds
         (current-time start-time))
    (while (time-less-p current-time (time-add end-time one-day))
      (let ((entry-date (format-time-string "%A %d-%m-%Y" current-time)))
        (insert (format "%s%s\n" (or prefix "** Write entry for ") entry-date)))
      (setq current-time (time-add current-time one-day)))))

Buffer Display Customisation

(defun hn/disable-trailing-whitespace-and-empty-lines ()
  "Disable showing trailing whitespace and indicating empty lines in the current buffer."
  (setq-local show-trailing-whitespace nil)
  (setq-local indicate-empty-lines nil))

Future Enhancements

Native Treesit Migration

Moving from tree-sitter package to Emacs 30’s built-in treesit with treesit-auto for automatic grammar installation.

Better Keyboard Quit Behaviour

(defun prot/keyboard-quit-dwim ()
  "Do-What-I-Mean behaviour for a general `keyboard-quit'.

The generic `keyboard-quit' does not do the expected thing when
the minibuffer is open.  Whereas we want it to close the
minibuffer, even without explicitly focusing it.

The DWIM behaviour of this command is as follows:

- When the region is active, disable it.
- When a minibuffer is open, but not focused, close the minibuffer.
- When the Completions buffer is selected, close it.
- In every other case use the regular `keyboard-quit'."
  (interactive)
  (cond
   ((region-active-p)
    (keyboard-quit))
   ((derived-mode-p 'completion-list-mode)
    (delete-completion-window))
   ((> (minibuffer-depth) 0)
    (abort-recursive-edit))
   (t
    (keyboard-quit))))

(define-key global-map (kbd "C-g") #'prot/keyboard-quit-dwim)

Package Management Alternatives

straight integrates well with use-package and replaces the internal packaging system.

Further refine completion workflows

  • We also need some way to use vertico-mouse.
  • Consider adding consult, embark, embark-consult, wgrep and cape.

Safer deletes in dired

(delete-by-moving-to-trash t)

Properly colour ediff

About

My local emacs configuration

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published