Emacs¶
Add a Makefile
rule to setup the configuration on a new machine
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
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
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
magnars/s.el: String manipulation library
emacs/init.el
(dolist (pkg '(s))
(unless (package-installed-p pkg)
(unless package-archive-contents
(package-refresh-contents))
(package-install pkg)))
Work Around
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
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
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
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
emacs/init.el
(use-package nerd-icons
:ensure t)
Line Numbers¶
Enable line numbers for programming modes
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.
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.
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.
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
emacs/init.el
(display-battery-mode)
Time
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.
emacs/init.el
;; Use `setopt' has 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 t)
Loading them seems slighty complicated though?
I want to run one of the modus-themes-load-<variant>
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-<variant> t)
which will load the relevant module and then load the theme a second time with (modus-themes-load-<variant>)
.
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.)
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)
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.
emacs/init.el
(unless (display-graphic-p)
(alc-theme-load-theme 1)) ; dark
Miscellaneous¶
Disable some GUI elements
emacs/early-init.el
(blink-cursor-mode -1)
(tool-bar-mode -1)
(setq inhibit-x-resources t
inhibit-startup-message t)
And enable others
emacs/early-init.el
(context-menu-mode t)
Basic Editing¶
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.
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.
emacs/init.el
(repeat-mode 1)
Whitespace¶
Visualise certain whitespace characters
- face
Enable visualisations that use faces
empty
(requiresface
)Highlight empty lines at the start/end of the buffer.
trailing
(requiresface
)Highlight trailing whitespace
lines-char
(requiresface
)If a line is too long (according to
whitespace-line-column
), highlight the first character that crosses the thresholdtab-mark
Render tab characters in the buffer (like
set list
in vim). Useswhitespace-display-mappings
to determine the character(s) to use.
emacs/init.el
(setq whitespace-style '(face empty trailing lines-char tab-mark))
(global-whitespace-mode t)
Enable automatic whitespace cleanup on each save
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.
emacs/init.el
(setq tab-always-indent t)
(setq-default indent-tabs-mode nil)
Compilation¶
Basic settings for Emacs’ compilation framework
emacs/init.el
(setq compilation-always-kill t
compilation-scroll-output t)
Make F5 call recompile
emacs/init.el
(keymap-global-set "<f5>" 'recompile)
The following adds a simple minor mode which, when enabled, will call recompile
each time the buffer is saved
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¶
See also
- 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)
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
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
(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.
emacs/lisp/alc-treesitter.el
(if (treesit-language-available-p 'python)
(add-to-list 'major-mode-remap-alist '(python-mode . python-ts-mode)))
Combobulate¶
mickeynp/combobulate provides a nice set of movement and editing commands that leverage the parse tree generated by tree sitter.
emacs/lisp/alc-treesitter.el
(use-package combobulate
;; This assumes that the `mickeynp/combobulate` package has already been cloned to
;; ~/.emacs.d/elpa/combobulate.
;; TODO: Look into using the `:vc` tag when Emacs 30 is released
:load-path ("elpa/combobulate/")
:hook ((python-ts-mode . combobulate-mode)))
emacs/lisp/alc-treesitter.el
(provide 'alc-treesitter)
*Completions*
¶
Use C-n and C-p to cycle through completion candidates.
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.
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
emacs/init.el
(setq completion-cycle-threshold 3)
Tweak how the *Completions*
buffer is rendered
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 oantolin/orderless completion style
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 minad/consult
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
emacs/init.el
(use-package embark-consult
:after (embark consult)
:ensure t)
Embark¶
Use oantolin/embark!
emacs/init.el
(use-package embark
:ensure t
:bind (("C-." . embark-act)
("M-." . embark-dwim)))
Dired¶
Settings for dired
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.
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
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 radian-software/apheleia for automatic formatting of buffers
emacs/init.el
(use-package apheleia
:ensure t
:config
(apheleia-global-mode))
reStructuredText¶
emacs/init.el
(use-package esbonio
:vc (:url "https://github.com/swyddfa/esbonio.el" :rev "main")
:hook ((rst-mode . esbonio-eglot-ensure)))
TypeScript¶
emacs/init.el
(use-package typescript-ts-mode
:mode "\\.ts\\'"
:ensure t
:hook ((typescript-ts-mode . eglot-ensure)))
YAML¶
emacs/init.el
(use-package yaml-mode
:ensure t)
LLMs¶
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))))