:title: Emacs Config
:date: 2025-02-16
:tags: emacs
:identifier: 20250216T190756
:signature: 5
My Emacs Configuration
======================
Add a ``Makefile`` rule to setup the configuration on a new machine
.. highlight:: none
.. code-block:: make
:filename: Makefile
.PHONY: emacs
emacs:
test -L $(HOME)/.emacs.d || ln -s $(shell pwd)/emacs/ $(HOME)/.emacs.d
Put all the noise generated by the custom system into a separate file
.. code-block:: elisp
:filename: emacs/init.el
;;; init.el -- Emacs configuration -*- lexical-binding: t -*-
(setq custom-file (locate-user-emacs-file "custom.el"))
(when (file-exists-p custom-file)
(load custom-file))
Setup package archives
.. code-block:: elisp
:filename: emacs/init.el
(package-initialize)
(add-to-list 'package-archives '("org" . "https://orgmode.org/elpa/"))
(add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/"))
And install some utility libraries
- :gh:`magnars/s.el`: String manipulation library
.. code-block:: elisp
:filename: emacs/init.el
(dolist (pkg '(s))
(unless (package-installed-p pkg)
(unless package-archive-contents
(package-refresh-contents))
(package-install pkg)))
.. admonition:: Work Around
:class: mt-4
I am currently using `Aurora `__ which encourages the use of homebrew to install cli programs.
This means executables are normally placed in ``/home/linuxbrew/.linuxbrew/bin/`` which is usually added to ``PATH`` for my user account.
However, this does not happen when launching Emacs via krunner and so it fails to find any tools installed there.
To work around this, let's manually add it to ``exec-path``
.. code-block:: elisp
:filename: emacs/init.el
(if (file-exists-p "/home/linuxbrew/.linuxbrew/bin")
(add-to-list 'exec-path "/home/linuxbrew/.linuxbrew/bin"))
However, this isn't enough to let functions like ``shell-command`` find the command, we also need to update Emacs' version of ``PATH``
.. code-block:: elisp
:class: mb-2
:filename: emacs/init.el
(setenv "PATH" (string-join exec-path ":"))
Appearance
----------
Fonts
^^^^^
I quite like the Ubuntu family of fonts, but use the "Nerd Font" version to get some extra icons
.. code-block:: elisp
:filename: emacs/init.el
(set-face-attribute 'default nil :family "UbuntuMonoNerdFont" :height 120)
(set-face-attribute 'fixed-pitch nil :family "UbuntuMonoNerdFont" :height 120)
(set-face-attribute 'variable-pitch nil :family "UbuntuSansNerdFont" :weight 'light :height 120)
Make it easy to get relevant nerd icons
.. code-block:: elisp
:filename: emacs/init.el
(use-package nerd-icons
:ensure t)
Line Numbers
^^^^^^^^^^^^
Enable line numbers for programming modes
.. code-block:: elisp
:filename: emacs/init.el
(add-hook 'prog-mode-hook (lambda () (display-line-numbers-mode t)))
Reserve enough space to display a line number that is 4 digits long and when a buffer is narrowed, always display the actual line number.
.. code-block:: elisp
:filename: emacs/init.el
(setq-default display-line-numbers-widen t
display-line-numbers-width 4)
Scrolling
^^^^^^^^^
With Emacs 29 came ``pixel-scroll-precision-mode`` which makes the scrolling with a touchpad experience much nicer overall.
.. code-block:: elisp
:filename: emacs/init.el
(setq pixel-scroll-precision-use-momentum nil
pixel-scroll-precision-interpolate-page t
pixel-scroll-precision-momentum-seconds 0.5)
(pixel-scroll-precision-mode t)
Tab Bar
^^^^^^^
Not to be confused with the tabs you see in editors like VSCode, tabs allow for easy switching between different collections of windows - like workspaces.
As well as using the tab bar to show... well tabs, I also make use of the ``tab-bar-format-global`` variable to show global status information like battery levels.
.. code-block:: elisp
:filename: emacs/init.el
(setq tab-bar-format '(tab-bar-format-tabs-groups
tab-bar-format-align-right
tab-bar-format-global
tab-bar-format-menu-bar))
;; Disable the menu-bar, since it's accessible via the tab bar.
(menu-bar-mode -1)
(add-hook 'after-init-hook #'tab-bar-mode)
**Battery Info**
.. code-block:: elisp
:filename: emacs/init.el
(display-battery-mode)
**Time**
.. code-block:: elisp
:filename: emacs/init.el
(setq display-time-format "%H:%M %d/%m/%y"
display-time-default-load-average nil)
(display-time-mode)
Theme
^^^^^
The excellent `modus-themes `__ are available out of the box in recent Emacs versions.
.. code-block:: elisp
:filename: emacs/init.el
;; Use `setopt' since modus supports automatically reloading the theme when these
;; are changed via the `customize' framework
(setopt modus-themes-bold-constructs t
modus-themes-italic-constructs t
modus-themes-prompts '(bold italic)
modus-themes-variable-pitch-ui nil)
Loading them seems slighty complicated though?
I want to run one of the ``modus-themes-load-`` functions so that the ``modus-themes-after-load-theme-hook`` hook is run however, these functions are not initially loaded by Emacs.
``(require 'modus-themes)`` does not work either, so I first have to run ``(load-theme 'modus- t)`` which will load the relevant module and then load the theme a second time with ``(modus-themes-load-)``.
.. code-block:: elisp
:filename: emacs/init.el
(defun alc-theme-load-theme (variant)
"Load the light or dark theme according to VARIANT."
(if (eq 1 variant)
(progn
(load-theme 'modus-vivendi t)
(modus-themes-load-theme 'modus-vivendi))
(load-theme 'modus-operandi t)
(modus-themes-load-theme 'modus-operandi)))
Specifics of theme loading aside, let's pick a variant based on the current system theme.
(Thanks to `auto-dark-emacs `__ for the DBus specifics.)
.. code-block:: elisp
:filename: emacs/init.el
(when (display-graphic-p)
(alc-theme-load-theme
(caar (dbus-call-method
:session
"org.freedesktop.portal.Desktop"
"/org/freedesktop/portal/desktop"
"org.freedesktop.portal.Settings" "Read"
"org.freedesktop.appearance" "color-scheme"))))
Also, set up a listener so that Emacs automatically updates if the preference changes (again, thanks to ``auto-dark-emacs`` for the DBus incantation)
.. code-block:: elisp
:filename: emacs/init.el
(when (display-graphic-p)
(setq alc-theme-dbus-listener
(dbus-register-signal
:session
"org.freedesktop.portal.Desktop"
"/org/freedesktop/portal/desktop"
"org.freedesktop.portal.Settings"
"SettingChanged"
(lambda (path var val)
(when (and (string= path "org.freedesktop.appearance")
(string= var "color-scheme"))
(alc-theme-load-theme (car val)))))))
If I ever need to, the listener can be disabled by running ``(dbus-unregister-object alc-theme-dbus-listener)``
However, the dbus code fails when I am SSH-ing into my Raspberry Pi so in that case we just want a theme loaded and be done with it.
.. code-block:: elisp
:filename: emacs/init.el
(unless (display-graphic-p)
(alc-theme-load-theme 1)) ; dark
Miscellaneous
^^^^^^^^^^^^^
Disable some GUI elements
.. code-block:: elisp
:filename: emacs/early-init.el
(blink-cursor-mode -1)
(tool-bar-mode -1)
(setq inhibit-x-resources t
inhibit-startup-message t)
And enable others
.. code-block:: elisp
:filename: emacs/early-init.el
(context-menu-mode t)
Basic Editing
-------------
.. code-block:: elisp
:filename: emacs/init.el
(recentf-mode t)
(savehist-mode t)
(save-place-mode t)
Backup Files
^^^^^^^^^^^^
By default Emacs will litter folders with backup files (``filename.txt~``), rather than disable them entirely just put them out of sight.
.. code-block:: elisp
:filename: emacs/init.el
(setq backup-directory-alist `(("." . ,(expand-file-name "backups" user-emacs-directory))))
Repeat Mode
^^^^^^^^^^^
``repeat-mode`` is awesome and while I should really look at trying some of the things shown in `Karthink's guide `_, simply turning the mode on gives you a lot.
.. code-block:: elisp
:filename: emacs/init.el
(repeat-mode 1)
Whitespace
^^^^^^^^^^
Visualise certain whitespace characters
face
Enable visualisations that use faces
``empty`` (requires ``face``)
Highlight empty lines at the start/end of the buffer.
``trailing`` (requires ``face``)
Highlight trailing whitespace
``lines-char`` (requires ``face``)
If a line is too long (according to ``whitespace-line-column``), highlight the first character that crosses the threshold
``tab-mark``
Render tab characters in the buffer (like ``set list`` in vim).
Uses ``whitespace-display-mappings`` to determine the character(s) to use.
.. code-block:: elisp
:filename: emacs/init.el
(setq whitespace-style '(face empty trailing lines-char tab-mark))
(global-whitespace-mode t)
Enable automatic whitespace cleanup on each save
.. code-block:: elisp
:filename: emacs/init.el
(add-hook 'before-save-hook #'whitespace-cleanup)
Indent using spaces by default, and ensure that the TAB key is only used for indentation.
.. code-block:: elisp
:filename: emacs/init.el
(setq tab-always-indent t)
(setq-default indent-tabs-mode nil)
Compilation
^^^^^^^^^^^
Basic settings for Emacs' compilation framework
.. code-block:: elisp
:filename: emacs/init.el
(setq compilation-always-kill t
compilation-scroll-output t)
Make :kbd:`F5` call ``recompile``
.. code-block:: elisp
:filename: emacs/init.el
(keymap-global-set "" 'recompile)
The following adds a simple minor mode which, when enabled, will call ``recompile`` each time the buffer is saved
.. code-block:: elisp
:filename: emacs/init.el
(define-minor-mode recompile-on-save-mode
"When enabled, run `recompile' after the current buffer is saved"
:lighter " тно"
(if recompile-on-save-mode
(add-hook 'after-save-hook 'recompile nil t)
(remove-hook 'after-save-hook 'recompile t)))
Treesitter
----------
.. seealso::
`How To Get Started with Tree Sitter `__
Article on Matering Emacs
`Combobulate: Structured Movement and Editing with Tree-Sitter `__
Another introducing the ``combobulate`` package
As of version ``29.1`` Emacs comes with support out of the box (assuming it has been compiled with the ``--with-tree-sitter`` flag)
.. code-block:: elisp
:filename: emacs/init.el
(use-package alc-treesitter
:load-path "lisp"
:if (treesit-available-p))
Installing Grammars
^^^^^^^^^^^^^^^^^^^
While Emacs supports the tree-sitter library itself, it does not come with any grammars which need to be installed separately.
Thankfully, Emacs does provide a few utilities to help with this
.. code-block:: elisp
:filename: emacs/lisp/alc-treesitter.el
(setq treesit-language-source-alist
'((python "https://github.com/tree-sitter/tree-sitter-python" "v0.20.4")
(typescript "https://github.com/tree-sitter/tree-sitter-typescript" "v0.20.3" "typescript/src")))
The ``treesit-language-source-alist`` variable tells Emacs where it can find a given grammar and what language it is for.
Once defined (and assuming you have the necessary dependencies) you can use ``M-x treesit-install-language-grammar`` command to install one of the grammars you defined.
Alternatively this snippet will install them all in one go
.. code-block:: elisp
(mapc #'treesit-install-language-grammar (mapcar #'car treesit-language-source-alist))
Major Modes
^^^^^^^^^^^
Switching to tree-sitter powered major modes by default would not be backwards compatible, so if we actually want to make use of the tree-sitter support we need to opt into it.
.. code-block:: elisp
:filename: emacs/lisp/alc-treesitter.el
(if (treesit-language-available-p 'python)
(add-to-list 'major-mode-remap-alist '(python-mode . python-ts-mode)))
Combobulate
^^^^^^^^^^^
:gh:`mickeynp/combobulate` provides a nice set of movement and editing commands that leverage the parse tree generated by tree sitter.
.. code-block:: elisp
:filename: emacs/lisp/alc-treesitter.el
(use-package combobulate
:vc (:url "https://github.com/mickeynp/combobulate")
:hook ((python-ts-mode . combobulate-mode)
(typescript-ts-mode . combobulate-mode)))
.. code-block:: elisp
:filename: emacs/lisp/alc-treesitter.el
(provide 'alc-treesitter)
``*Completions*``
-----------------
Use :kbd:`C-n` and :kbd:`C-p` to cycle through completion candidates.
.. code-block:: elisp
:filename: emacs/init.el
(dolist (kmap (list minibuffer-local-map completion-in-region-mode-map))
(keymap-set kmap "C-p" #'minibuffer-previous-completion)
(keymap-set kmap "C-n" #'minibuffer-next-completion))
Only pop open the ``*Completions*`` buffer on the *second* attempt at completing something.
However, once it is open keep it updated and only focus it on the second completion attempt with no progress made.
.. code-block:: elisp
:filename: emacs/init.el
(setq completion-auto-help 'visible
completion-auto-select 'second-tab)
If there are only 3 (or fewer) possible candidates, cycle between them
.. code-block:: elisp
:filename: emacs/init.el
(setq completion-cycle-threshold 3)
Tweak how the ``*Completions*`` buffer is rendered
.. code-block:: elisp
:filename: emacs/init.el
(setq completions-detailed t
completions-format 'one-column
completions-group t
completions-header-format (propertize "%s candidates\n" 'face 'shadow)
completions-max-height 15
completion-show-help nil)
Use the :gh:`oantolin/orderless` completion style
.. code-block:: elisp
:filename: emacs/init.el
(use-package orderless
:ensure t
:custom
(completion-styles '(orderless basic))
(completion-category-defaults nil)
(completion-category-overrides '((file (styles basic partial-completion)))))
Consult
^^^^^^^
Use :gh:`minad/consult`
.. code-block:: elisp
:filename: emacs/init.el
(use-package consult
:ensure t
:custom
(consult-preview-key "M-.")
:bind (("C-x b" . consult-buffer)))
And since I use embark, use the recommended ``emabrk-consult`` package
.. code-block:: elisp
:filename: emacs/init.el
(use-package embark-consult
:after (embark consult)
:ensure t)
Embark
^^^^^^
Use :gh:`oantolin/embark`!
.. code-block:: elisp
:filename: emacs/init.el
(use-package embark
:ensure t
:bind (("C-." . embark-act)
("M-." . embark-dwim)))
Dired
-----
Settings for dired
.. code-block:: elisp
:filename: emacs/init.el
(use-package dired
:hook ((dired-mode . dired-hide-details-mode)
(dired-mode . hl-line-mode))
:config
(setq dired-dwim-target t
;; -A, all files - except '.' & '..'
;; -F, add symbols denoting object type (file, directory, etc.)
;; -G, omit owning group
;; -h, human readable file sizes
;; -l, long listing, required for dired.
;; -v, natural sort of (version) numbers within text
dired-listing-switches "-AFGhlvX --group-directories-first --time-style=long-iso"))
``project.el``
--------------
The built-in project management, ``project.el`` is good enough for my needs.
However, so that Emacs works nicely with monorepo style repositories defining some additional root marker files will get ``project.el`` to consider a sub-directory of a repo a valid project.
.. code-block:: elisp
:filename: emacs/init.el
(setq project-vc-extra-root-markers '("Cargo.toml"
"package.json"
"pyproject.toml"))
This however, introduces a few problems the most obvious of which is that ``project-find-file`` doesn't respect ``.gitignore`` when run from within a subproject.
The following advice ensures that the right vc backend is selected for subprojects
.. code-block:: elisp
:filename: emacs/init.el
(defun alc-project-try-vc-subproject (orig-fun &rest args)
"Advice for `project-try-vc'.
When using `project-vc-extra-root-markers' to teach project.el
about subprojects within a monorepo, `project-try-vc'
successfully finds the subject's root but fails to detect the
backend. But by calling `vc-responsible-backend' on the found
root, we can fill in the blanks.
As a result, commands like `project-find-file' now respect the
parent repo's .gitignore file even when being run from within a
subproject."
(let* ((res (apply orig-fun args))
(dir (nth 2 res))
(backend (or (nth 1 res)
(ignore-errors (vc-responsible-backend dir)))))
(if dir
`(vc ,backend ,dir))))
(advice-add 'project-try-vc :around #'alc-project-try-vc-subproject)
Languages
---------
And :gh:`radian-software/apheleia` for automatic formatting of buffers
.. code-block:: elisp
:filename: emacs/init.el
(use-package apheleia
:ensure t
:config
(apheleia-global-mode))
reStructuredText
^^^^^^^^^^^^^^^^
.. code-block:: elisp
:filename: emacs/init.el
(use-package esbonio
:vc (:url "https://github.com/swyddfa/esbonio.el" :rev "main")
:hook ((rst-mode . esbonio-eglot-ensure)))
TypeScript
^^^^^^^^^^
.. code-block:: elisp
:filename: emacs/init.el
(use-package typescript-ts-mode
:mode "\\.ts\\'"
:ensure t
:hook ((typescript-ts-mode . eglot-ensure)))
YAML
^^^^
.. code-block:: elisp
:filename: emacs/init.el
(use-package yaml-mode
:ensure t)
LLMs
----
.. seealso::
:denote:link:`20241116T120000`
.. code-block:: elisp
:filename: emacs/init.el
(use-package gptel
:ensure t
:bind (("C-c RET" . gptel-send))
:config
(setq gptel-model 'llama3.2:latest)
(setq gptel-backend (gptel-make-ollama "Ollama"
:host "localhost:11434"
:stream t
:models '(llama3.2:latest))))