#+TITLE: Emacs Configuration #+AUTHOR: Thomas Voss #+DATE: <2023-08-09 Wed> #+DESCRIPTION: My Emacs configuration — before I ran ‘rm -rf ~/.config’ that is #+STARTUP: overview * Headers The first thing we need in any configuration is a header setting ~lexical-binding~ to ~t~. This will allow us to have non-autistic behavior when scoping variables. #+BEGIN_SRC elisp :tangle early-init.el ;;; early-init.el --- Emacs early configuration file -*- lexical-binding: t -*- #+END_SRC #+BEGIN_SRC elisp :tangle init.el ;;; init.el --- Emacs configuration file -*- lexical-binding: t -*- #+END_SRC * Early Init File :PROPERTIES: :header-args: :tangle early-init.el :END: ** Numerical Constants These are not really important, but nice to have to make the following configuration more readable. The reason they’re defined in the ~early-init.el~ is simply because I need to use them here. #+BEGIN_SRC elisp (defconst 1-KiB 1024 "The number of bytes in 1 kibibyte.") (defconst 1-MiB (* 1-KiB 1024) "The number of bytes in 1 mebibyte.") (defconst 1-GiB (* 1-MiB 1024) "The number of bytes in 1 gibibyte.") #+END_SRC ** XDG Directories We want to define variables for the main XDG directories for Emacs to use. These are nice for keeping things organized and out of the way. #+BEGIN_SRC elisp (defconst mango-cache-directory (expand-file-name "emacs" (or (getenv "XDG_CACHE_HOME") (expand-file-name ".cache" (getenv "HOME")))) "The XDG-conformant cache directory that Emacs should use.") (defconst mango-config-directory (expand-file-name "emacs" (or (getenv "XDG_CONFIG_HOME") (expand-file-name ".config" (getenv "HOME")))) "The XDG-conformant config directory that Emacs should use.") (defconst mango-data-directory (expand-file-name "emacs" (or (getenv "XDG_DATA_HOME") (expand-file-name ".local/share" (getenv "HOME")))) "The XDG-conformant data directory that Emacs should use.") #+END_SRC We also need to ensure our directories actually exist. #+BEGIN_SRC elisp (mapc (lambda (x) (make-directory x t)) (list mango-cache-directory mango-config-directory mango-data-directory)) #+END_SRC We don’t want to have all sorts of random garbage populating the configuartion directory; let’s throw it all in the cache directory instead. #+BEGIN_SRC elisp (setq user-emacs-directory mango-cache-directory auto-save-list-file-prefix (expand-file-name "auto-save-list/" mango-cache-directory) backup-directory-alist `(("." . ,(expand-file-name "backups" mango-cache-directory)))) #+END_SRC ** Default Limits These are just some limits Emacs abides by as far as garbage-collection and process communication go. They’re pretty low by default though; any modern system is capable of much higher. To improve startup performance the garbage collection theshold is set to the max until Emacs is initialized. Additionally the documentation for ~read-process-output-max~ gives us a hint at how large we can set it: #+BEGIN_QUOTE On GNU/Linux systems, the value should not exceed =/proc/sys/fs/pipe-max-size=. See pipe(7) manpage for details. #+END_QUOTE #+BEGIN_SRC elisp (setq gc-cons-threshold most-positive-fixnum) (add-hook 'after-init-hook (lambda () (setq gc-cons-threshold (* 512 1-MiB)))) (with-temp-buffer (insert-file-contents "/proc/sys/fs/pipe-max-size") (setq read-process-output-max (number-at-point))) #+END_SRC ** Package Management Finally, we want to completely disable ~package.el~ since we’re going to be using ~straight.el~ in the main configuration. #+BEGIN_SRC elisp (setq package-enable-at-startup nil) #+END_SRC * Init File :PROPERTIES: :header-args: :tangle init.el :END: ** Helper Functions and -Macros *** Shorter Lambdas It’s annoying to have to write out ~(lambda () BODY)~ every time I want to write a lambda that takes no args — a very common operation. The solution is to just use an actual lambda. If I ever forget how to enter a lambda, it’s =C-x 8 RET=. #+BEGIN_SRC elisp (defmacro λ (&rest body) "Convenience macro to create lambda functions that take no arguments with much short and concise syntax. Calling ‘λ’ with BODY is equivalent to calling ‘lambda’ with an empty argument list and BODY." (declare (pure t) (side-effect-free t)) `(lambda () ,@body)) #+END_SRC *** Hooks from Modes It is very often that I have a mode symbol and I want to extract the corresponding hook from it. Luckily there’s a pretty standard naming convention here. #+BEGIN_SRC elisp (defun mango-mode-to-hook (mode) "Get the hook corresponding to MODE." (declare (pure t) (side-effect-free t)) (intern (concat (symbol-name mode) "-hook"))) #+END_SRC *** Tree-Sitter Modes As I was writing this configuration, Emacs 29 released on the Arch repositories with native support for ~tree-sitter~. As a result many major-modes now have two versions — a regular version and a ~tree-sitter~ version. I should have pretty equal configurations for both versions of a mode so it’s useful to be able to grab one from the other. #+BEGIN_SRC elisp (defun mango-mode-to-ts-mode (mode) "Get the tree-sitter mode corresponding to MODE." (declare (pure t) (side-effect-free t)) (intern (concat (string-remove-suffix "-mode" (symbol-name mode)) "-ts-mode"))) #+END_SRC ** Package Management For package management I like to use ~straight.el~. Before setting that up though it’s probably best to disable native-compilation warnings; we’ll get a whole lot of those when ~straight.el~ is installing packages. #+BEGIN_SRC elisp (setq comp-async-report-warnings-errors nil native-comp-async-report-warnings-errors nil) #+END_SRC After doing that, we can bootstrap ~straight.el~. The two options enabled at the end are just configuring ~use-package~ to always use ~straight.el~ by default, and to always ensure packages unless stated otherwise. #+BEGIN_SRC elisp (defvar bootstrap-version) (let ((bootstrap-file (expand-file-name "straight/repos/straight.el/bootstrap.el" user-emacs-directory)) (bootstrap-version 6)) (unless (file-exists-p bootstrap-file) (with-current-buffer (url-retrieve-synchronously "https://raw.githubusercontent.com/radian-software/straight.el/develop/install.el" 'silent 'inhibit-cookies) (goto-char (point-max)) (eval-print-last-sexp))) (load bootstrap-file nil 'nomessage)) (setq straight-use-package-by-default t use-package-always-ensure t) #+END_SRC ** Documentation Documentation is absolutely essential. The ~helpful~ package gives us much better documentation for things, so let’s use it. I’m mostly just overriding the bindings for the standard ~describe-*~ functions. #+BEGIN_SRC elisp (use-package helpful :bind (("C-h f" . #'helpful-callable) ("C-h v" . #'helpful-variable) ("C-h k" . #'helpful-key) ("C-h o" . #'helpful-symbol) ("C-c C-d" . #'helpful-at-point))) #+END_SRC ** Key Bindings *** Editing This Config This configuration is one of the files I visit the most. Not just for making large customizations, but also for simply editing the tab-width for a mode I happen to be using. For that reason there should probably be a binding to get here. #+BEGIN_SRC elisp (keymap-global-set "C-c e" (λ (interactive) (find-file (expand-file-name "config.org" mango-config-directory)))) #+END_SRC *** Evil Mode The default Emacs keybindings are horrible and dangerous. Feel free to use them if you want to develop genuine problems with your hands. For this reason, ~evil-mode~ bindings are the way to go. Also as I was writing this, Bram Moolenaar, the creator of Vim died. RIP. Here we setup bindings in the ~:bind~ section; the functions bound to that don’t exist yet will be created shortly. You also need to set all these variables /before/ ~evil-mode~ is loaded — it’s just how ~evil~ works I suppose. Finally I like to have ~visual-line-mode~ enabled as I find it far more intuitive. #+BEGIN_SRC elisp (use-package evil :bind (:map evil-normal-state-map ("€" . #'evil-end-of-visual-line) ("h" . #'evil-window-left) ("j" . #'evil-window-down) ("k" . #'evil-window-up) ("l" . #'evil-window-right) ("a" . #'mango--evil-align-regexp) ("s" . #'mango--evil-sort-lines) (";" . #'mango--evil-comment-or-uncomment-region)) :init ;; We need to set these variables before loading ‘evil-mode’ (setq evil-want-Y-yank-to-eol t evil-v$-excludes-newline t evil-respect-visual-line-mode t evil-split-window-below t evil-vsplit-window-right t evil-want-fine-undo t evil-undo-system 'undo-redo evil-flash-delay 1 evil-want-keybinding nil) (evil-mode) (global-visual-line-mode) :config (evil-set-leader nil (kbd "SPC"))) #+END_SRC *** Evil Surround This is probably one of the more useful Vim/Emacs extensions out there. It let’s you easy add-, remove-, and change surrounding pairs such as quotation marks and parenthesis from a /text object/. I like to use unicode single- and double quotation marks — so we also want to add support for those. #+BEGIN_SRC elisp (use-package evil-surround :after evil :config (global-evil-surround-mode)) #+END_SRC Supporting custom pairs is a bit tricky since we need to define an evil /text object/ to make them work properly. Also we add some custom Jinja pairs! #+BEGIN_SRC elisp (defmacro mango--evil-define-and-bind-quoted-text-object (name key start-regex end-regex) (let ((inner-name (make-symbol (concat "evil-inner-" name))) (outer-name (make-symbol (concat "evil-a-" name)))) `(progn (evil-define-text-object ,inner-name (count &optional beg end type) (evil-select-paren ,start-regex ,end-regex beg end type count nil)) (evil-define-text-object ,outer-name (count &optional beg end type) (evil-select-paren ,start-regex ,end-regex beg end type count t)) (define-key evil-inner-text-objects-map ,key #',inner-name) (define-key evil-outer-text-objects-map ,key #',outer-name)))) (mango--evil-define-and-bind-quoted-text-object "single-quote-open" "‘" "‘" "’") (mango--evil-define-and-bind-quoted-text-object "single-quote-close" "’" "‘" "’") (mango--evil-define-and-bind-quoted-text-object "double-quote-open" "“" "“" "”") (mango--evil-define-and-bind-quoted-text-object "double-quote-close" "”" "“" "”") (setq-default evil-surround-pairs-alist (append '((?‘ . ("‘ " . " ’")) (?’ . ("‘" . "’")) (?“ . ("“ " . " ”")) (?“ . ("“" . "”"))) evil-surround-pairs-alist)) (add-hook 'html-mode-hook (λ (setq-local evil-surround-pairs-alist (append '((?% . ("{% " . " %}")) (?# . ("{# " . " #}")) (?{ . ("{{ " . " }}"))) evil-surround-pairs-alist)) (mango--evil-define-and-bind-quoted-text-object "jinja-action" "%" "{%" "%}") (mango--evil-define-and-bind-quoted-text-object "jinja-comment" "#" "{#" "#}") (mango--evil-define-and-bind-quoted-text-object "jinja-code" "{" "{{" "}}"))) #+END_SRC *** Evil Collection This is a super-simple to setup package that adds ~evil~ bindings to many other custom packages like ~magit~ and ~mu4e~. #+BEGIN_SRC elisp (use-package evil-collection :after evil :config (evil-collection-init)) #+END_SRC *** Aligning Text I absolutely love aligning text with ~align-regexp~. It is in my opinion one of the most underrated Emacs functions. Unfortunately, it aligns with tabs or -spaces based on the setting of ~indent-tabs-mode~. Personally I am almost always indenting with tabs, but I prefer to align with spaces. Luckily we can use some advice to force the usage of spaces. #+BEGIN_SRC elisp (defun mango--align-regexp-with-spaces (old-function &rest args) "Advice to force ‘align-regexp’ to always align with spaces, regardless of the value of ‘indent-tabs-mode’." (let (indent-tabs-mode) (apply old-function args))) (advice-add 'align-regexp :around #'mango--align-regexp-with-spaces) #+END_SRC Now that it’s behaving properly, it should also be turned into an ~evil~ operator so it can be used with Vim motions. #+BEGIN_SRC elisp (evil-define-operator mango--evil-align-regexp (beg end regexp repeat) "Wrapper around ‘align-regexp’ to allow for use as an ‘evil-mode’ operator." (interactive (let ((range (evil-operator-range))) (list (car range) (cadr range) (concat "\\(\\s-*\\)" (read-string "Align regexp: ")) (y-or-n-p "Repeat? ")))) (align-regexp beg end regexp 1 1 repeat)) #+END_SRC *** Sorting Text Another very common operation is sorting text. So why not make an operator for that too? #+BEGIN_SRC elisp (evil-define-operator mango--evil-sort-lines (beg end) "Wrapper around ‘sort-lines’ to allow for use as an ‘evil-mode’ operator." (sort-lines nil beg end)) #+END_SRC *** Commenting Code Commenting code is a super common task — so make it an operator! #+BEGIN_SRC elisp (evil-define-operator mango--evil-comment-or-uncomment-region (beg end) "Wrapper around ‘comment-or-uncomment-region’ to allow for use as an ‘evil-mode’ operator." (comment-or-uncomment-region beg end)) #+END_SRC *** Tetris Tetris is epic, but the bindings are not so epic. #+BEGIN_SRC elisp (defun mango-tetris-rotate-mirror () (interactive) (tetris-rotate-next) (tetris-rotate-next)) (use-package tetris :hook (tetris-mode . (lambda () (evil-local-mode -1))) :bind (:map tetris-mode-map ("a" . #'tetris-move-left) ("d" . #'tetris-move-right) ("k" . #'tetris-rotate-next) (";" . #'tetris-rotate-prev) ("l" . #'tetris-move-down) ("o" . #'mango-tetris-rotate-mirror) ("SPC" . #'tetris-move-bottom))) #+END_SRC ** Completions *** Savehist-Mode This mode is super handy. It let’s you preserve your history in minibuffer prompts. #+BEGIN_SRC elisp (savehist-mode) #+END_SRC *** Vertico & Marginalia Vertico is a great package for enhanced completions in the minibuffer. It’s minimal and works great. We also want to configure the highlighting of the current line to match up with what is used for ~hl-line-mode~. Vertico also doesn’t offer a builtin function to go up a directory when typing out a path, so that’s what the ~mango-minibuffer-backward-kill~ is for. #+BEGIN_SRC elisp (defun mango-minibuffer-backward-kill (arg) "When minibuffer is completing a file name delete up to parent folder, otherwise delete a word." (interactive "p") (if minibuffer-completing-file-name (if (string-match-p "/." (minibuffer-contents)) (zap-up-to-char (- arg) ?/) (delete-minibuffer-contents)) (backward-kill-word arg))) (use-package vertico :bind (:map vertico-map ("C-j" . vertico-next) ("C-k" . vertico-previous) ("C-l" . vertico-insert) :map minibuffer-local-map ("C-h" . mango-minibuffer-backward-kill)) :custom (vertico-cycle t) :init (vertico-mode)) #+END_SRC Marginalia is kind of a suppliment I like to use with Vertico. It adds, well… /marginalia/ to the completions in the minibuffer. This includes things like file sizes and permissions when looking at files, function docstrings when looking at those, etc. #+BEGIN_SRC elisp (use-package marginalia :after vertico :init (marginalia-mode)) #+END_SRC *** Orderless Orderless is another pretty neat package. It allows for better completion candidate filtering. I personally prefer to use the ~orderless-prefixes~ completion style where entering the string “foo bar baz” will match the options that have components beginning with /foo/, /bar/, and /baz/ in that order. #+BEGIN_SRC elisp (use-package orderless :custom (completion-styles '(orderless basic)) (orderless-matching-styles '(orderless-prefixes)) (completion-category-overrides '((file (styles basic partial-completion))))) #+END_SRC *** Company Company is a package to give me actual completion popups; it’s super useful for autocompleting code but has other uses too I guess. #+BEGIN_SRC elisp (defun mango--company-require-prefix (candidates) "Transformer for ‘company-mode’ that requires that all candidates begin with ‘company-prefix’." (seq-filter (lambda (s) (string-prefix-p company-prefix s)) candidates)) (defun mango--company-select-candidate (pred) "Select either the next or previous candidate in the candidate list based on the comparison of the ‘company-pseudo-tooltip-overlay’ height and 0 using PRED." (let ((ov company-pseudo-tooltip-overlay)) (if (and ov (apply pred (list (overlay-get ov 'company-height) 0))) (company-select-previous) (company-select-next)))) (defun mango-company-next-candidate () "Select the next available candidate, taking into account if the candidate list is flipped or not." (interactive) (mango--company-select-candidate #'<)) (defun mango-company-previous-candidate () "Select the previous available candidate, taking into account if the candidate list is flipped or not." (interactive) (mango--company-select-candidate #'>)) (use-package company :bind (:map company-active-map ("C-j" . #'mango-company-next-candidate) ("C-k" . #'mango-company-previous-candidate)) :hook ((conf-mode prog-mode) . company-mode) :custom (company-minimum-prefix-length 1) (company-idle-delay (lambda () (unless (company-in-string-or-comment) 0))) (company-selection-wrap-around t) (company-tooltip-align-annotations t) (company-tooltip-flip-when-above t) (company-frontends '(company-pseudo-tooltip-unless-just-one-frontend company-preview-frontend company-echo-metadata-frontend)) (company-transformers '(mango--company-require-prefix company-sort-by-backend-importance))) #+END_SRC ** Math and Numbers *** Calc The built-in emacs calculator ~calc~ is genuinely the best calculator program I have ever used. Annoyingly though, it has a ‘trail’ that is always around. I don’t like it. #+BEGIN_SRC elisp (setq calc-display-trail nil) #+END_SRC *** Increment- and Decrement Number at Point This is a pretty standard Vim feature that I dearly miss having. #+BEGIN_SRC elisp (defun mango-increment-number-at-point (&optional arg) "Increment the number at point by ARG or 1 if ARG is nil. If called interactively, the universal argument can be used to specify ARG. If the number at point has leading zeros then the width of the number is preserved." (interactive "p*") (save-excursion (save-match-data (skip-chars-backward "0123456789") (when (eq (char-before (point)) ?-) (goto-char (1- (point)))) (when (re-search-forward "-?\\([0-9]+\\)" nil t) (let ((answer (+ (string-to-number (match-string 0) 10) (or arg 1))) (width (length (match-string 1)))) (replace-match (format (concat "%0" (int-to-string (if (< answer 0) (1+ width) width)) "d") answer))))))) (defun mango-decrement-number-at-point (&optional arg) "The same as ‘mango-increment-number-at-point’, but ARG is negated." (interactive "p*") (mango-increment-number-at-point (- (or arg 1)))) (keymap-global-set "C-c a" #'mango-increment-number-at-point) (keymap-global-set "C-c x" #'mango-decrement-number-at-point) #+END_SRC ** Programming *** Indentation Indentation in Emacs is a royal pain in the ass. Not only is there a ~tab-width~ variable and the ~indent-tabs-mode~ mode, but lots of modes just have an extra ~tab-width~-esque variable for some reason? I try to fix this all with a custom function that reads a list of mode-specific indentation settings. #+BEGIN_SRC elisp (setq-default tab-width 8 evil-shift-width 8 indent-tabs-mode t) (defvar mango-indentation-settings '((bash-ts-mode :width 4) (c-mode :width 4 :extra-vars (c-basic-offset)) (css-mode :extra-vars (css-indent-offset)) (emacs-lisp-mode :spaces t) (graphviz-dot-mode :extra-vars (graphviz-dot-indent-width)) (lisp-mode :spaces t) (org-mode :spaces t) (python-mode :width 4 :extra-vars (python-indent-offset)) (rust-mode :width 4) (sgml-mode :width 2 :extra-vars (sgml-basic-offset)) (sh-mode :width 4 :extra-vars (sh-basic-offset)) (xml-mode :width 4)) "A list of per-mode indentation settings. Each list contains a major-mode and the 3 optional keyword arguments of :spaces, :width, and :extra-vars. When setting the settings for a given major-mode, the settings will also be applied for that modes tree-sitter variant. If :spaces is non-nil, then indentation will be performed with spaces instead of tabs characters. If :width is non-nil, then it will override the modes given tab-width. If :extra-vars is non-nill, then it shall be a list of additional mode-specific variables that need to be assigned the desired indentation-width.") (defun mango-set-indentation-settings () "Apply the indentation settings specified by ‘mango-indentation-settings’." (interactive) (dolist (plist mango-indentation-settings) (let* ((mode (car plist)) (args (cdr plist)) (width (or (plist-get args :width) (default-value 'tab-width))) (spaces (or (plist-get args :spaces) (not (default-value 'indent-tabs-mode)))) (extra (plist-get args :extra-vars)) (callback (λ (indent-tabs-mode (when spaces -1)) (setq-local tab-width width evil-shift-width width) (dolist (var extra) (set var width))))) (add-hook (mango-mode-to-hook mode) callback 95) (add-hook (mango-mode-to-hook (mango-mode-to-ts-mode mode)) callback 95)))) (mango-set-indentation-settings) #+END_SRC *** Git Integration I like to use Magit for my ~git~ integration. I do believe it is the best ~git~ client ever made for any editor ever. Anyone who disagrees has simply never used Emacs before. The only command that really needs binding is ~magit-status~. All other commands I will end up executing from there with the transient commands. I also install ~magit-todos~. It’s a super minimal package that simply finds all the TODOs in a repository and displays them in the ~magit-status~ buffer so that I don’t forget them. #+BEGIN_SRC elisp (use-package magit :bind ("C-c g" . magit-status) :custom (magit-display-buffer-function #'magit-display-buffer-same-window-except-diff-v1)) (use-package magit-todos :after magit :init (magit-todos-mode)) (defun mango--magit-status () (interactive) (thread-last (project-current t) (project-root) (magit-status))) (require 'project) (add-to-list 'project-switch-commands '(mango--magit-status "Git Status" ?g)) #+END_SRC *** Tree-Sitter Emacs 29 brings native support for Tree-Sitter! This doesn’t just mean better- and faster syntax highlighting, but also things such as structured editing. In order to make use of Tree-Sitter the language parsers /do/ need to be installed, so let’s do that. Tree-Sitter doesn’t check to see if the language grammars are already installed unfortunately, but it’s easy enough to do manually. #+BEGIN_SRC elisp (setq treesit-language-source-alist '((bash "https://github.com/tree-sitter/tree-sitter-bash") (c "https://github.com/tree-sitter/tree-sitter-c") (cpp "https://github.com/tree-sitter/tree-sitter-cpp") (css "https://github.com/tree-sitter/tree-sitter-css") (elisp "https://github.com/Wilfred/tree-sitter-elisp") (go "https://github.com/tree-sitter/tree-sitter-go") (gomod "https://github.com/camdencheek/tree-sitter-go-mod.git") (html "https://github.com/tree-sitter/tree-sitter-html") (json "https://github.com/tree-sitter/tree-sitter-json") (make "https://github.com/alemuller/tree-sitter-make") (markdown "https://github.com/ikatyang/tree-sitter-markdown") (python "https://github.com/tree-sitter/tree-sitter-python") (toml "https://github.com/tree-sitter/tree-sitter-toml") (yaml "https://github.com/ikatyang/tree-sitter-yaml"))) (defun mango-treesit-install () "Install all the tree-sitter grammars in ‘treesit-language-source-alist’. This function does not assert whether or not the grammar is already installed, making it useful for updating existing grammars." (interactive) (dolist (spec treesit-language-source-alist) (treesit-install-language-grammar (car spec)))) ;; Automatically install missing grammars (thread-last (mapcar #'car treesit-language-source-alist) (seq-remove #'treesit-language-available-p) (mapc #'treesit-install-language-grammar)) #+END_SRC I also prefer to have as many things syntax highlighted as possible when using tree-sitter, with the ability to fully customize faces (or turn off faces I don’t want highlighted). #+BEGIN_SRC elisp (setq treesit-font-lock-level 4) #+END_SRC *** Language Server Protocol LSP is an absolute necessity when programming. Luckily ~lsp-mode~ has us covered. There is Eglot which is now built in to Emacs 29, but it was giving me some really weird issues so I won’t be using that for now. ~lsp-bridge~ also looked promising but I didn’t like it too much. Also ~yasnippet~ is dead, so why they won’t just move on from it I have no idea. I’ll just disable snippets. #+BEGIN_SRC elisp (use-package lsp-mode :commands (lsp lsp-deferred) :custom (lsp-enable-snippet nil) :init (setq lsp-keymap-prefix "C-c l")) #+END_SRC I use ~pyright~ as my Python LSP only because I know no better option. It does need some this custom package though. #+BEGIN_SRC elisp (use-package lsp-pyright :hook (python-ts-mode . (lambda () (require 'lsp-pyright) (lsp)))) #+END_SRC *** Emmet-Mode This mode is super useful for writing HTML. It lets me expand something like ‘figure>(figcaption>code)+pre’ into an actual HTML structure. #+BEGIN_SRC elisp (use-package emmet-mode :hook (html-mode . emmet-mode)) #+END_SRC *** Compilation Buffer Emacs allows you to compile your project with =C-x p c= which is cool and all, but it annoyingly creates a compilation buffer that I need to manually close every time. I would like to have this buffer, but only when things go wrong. #+BEGIN_SRC elisp (defun mango--compilation-make-window () "Make a new vertical split for the compilation buffer if such a buffer doesn’t already exist." (unless (get-buffer "*compilation*") (split-window-vertically))) (defun mango--compilation-exit-autoclose (status code msg) "Automatically bury the compilation buffer and delete the compilation window if compilation was successful." (when (and (eq status 'exit) (zerop code)) (bury-buffer) (delete-window (get-buffer-window "*compilation*"))) (cons msg code)) (add-hook 'compilation-mode-hook #'mango--compilation-make-window) (setq compilation-exit-message-function #'mango--compilation-exit-autoclose) #+END_SRC *** Projects I want to have all my projects automatically added to the project list. #+BEGIN_SRC elisp (project-remember-projects-under (or (getenv "REPODIR") (expand-file-name "code/repo" (getenv "HOME"))) 'recursive) #+END_SRC ** User Interface The default Emacs UI is fucking atrocious — we need to make it better. *** Cursor Blink I personally don’t like cursors blinking, so let’s disable that. #+BEGIN_SRC elisp (blink-cursor-mode -1) #+END_SRC *** Shorter Prompts For some reason emacs has both the ~y-or-n-p~ and ~yes-or-no-p~ functions. I do not like having to spell out full words — /y/ or /n/ is good enough for me — so let’s just redefine ~yes-or-no-p~. #+BEGIN_SRC elisp (fset #'yes-or-no-p #'y-or-n-p) #+END_SRC *** Disable Basic UI Modes Emacs has a lot of UI modes that are enabled by default to make life easier for the novice user. I am not the novice user and would rather these modes never turned on ever again. #+BEGIN_SRC elisp (menu-bar-mode -1) (scroll-bar-mode -1) (tool-bar-mode -1) (tooltip-mode -1) #+END_SRC *** Warnings I am typically not a fan around being warned about things unless something is actually breaking or going wrong. In order, these options disable the following warnings: 1. Opening large files 2. Following symbolic links 3. Adding advice to functions #+BEGIN_SRC elisp (setq large-file-warning-threshold nil vc-follow-symlinks t ad-redefinition-action 'accept) #+END_SRC *** Visible Bell Why not? I might disable this later. #+BEGIN_SRC elisp (setq visible-bell t) #+END_SRC *** Scrolling By default, scrolling is really bad. Luckily we can improve it a lot; there’s even a pixel-precision scrolling mode! #+BEGIN_SRC elisp (setq mouse-wheel-scroll-amount '(1 ((shift) . 1)) mouse-wheel-progressive-speed nil mouse-wheel-follow-mouse t scroll-step 1) ;; (pixel-scroll-precision-mode) #+END_SRC *** Auto Reverting Buffers This is just good to have all of the time; you should never be looking at a file whose state was changed by an external process, and not see those changes *instantly*. #+BEGIN_SRC elisp (setq global-auto-revert-non-file-buffers t) (global-auto-revert-mode) #+END_SRC *** Highlight Matching Parenthesis This is generally a good thing — especially when writing lisp code — but I don’t want this /everywhere/. #+BEGIN_SRC elisp (defvar mango-highlight-matching-parenthesis-modes '(prog-mode conf-mode) "A list of modes for which the parenthesis that pairs up with the parenthesis at point should be highlighted.") (show-paren-mode -1) (dolist (mode mango-highlight-matching-parenthesis-modes) (add-hook (mango-mode-to-hook mode) #'show-paren-local-mode)) #+END_SRC *** Line- and Column Numbers I like to have line- and column numbers in my modeline. I find them to be very useful to have. I used to also like to have the current line number on the left of the screen, but I don’t really need that information. #+BEGIN_SRC elisp (line-number-mode) (column-number-mode) #+END_SRC *** Fonts My favorite monospace font has got to be /Iosevka/. It’s good looking, it’s far more compact than the usual american-sized monospace fonts, and you can customize just about every character from loads of variants. I have my own custom compiled variant called /Iosevka Smooth/. On the proportional side of things, I am not really sure what font to use. /Vollkorn/ tends to be my go-to serif-font on the web, but I dunno how well it translates to Emacs. I am also a bit fan of Ysabeau for sans-serif. I need to play around with this. #+BEGIN_SRC elisp (defvar mango-monospace-font '("Iosevka Smooth" :weight regular :height 162) "The default monospace font to use. This is a list containing a font name, font weight, and font height in that order.") (defvar mango-proportional-font '("Ysabeau" :weight light :height 180) "The default proportional font to use. This is a list containing a font name, font weight, and font height in that order.") #+END_SRC Actually /setting/ the fonts is a bit tricky. I don’t really fully understand why it works like this, but something with the whole server/client model of Emacs is really fucking with this, so we need to add a hook to set the font for every frame. We also can’t forget the frame that’s actually running this code. #+BEGIN_SRC elisp (defun mango-set-fonts () "Set the fonts specified by ‘mango-monospace-font’ and ‘mango-proportional-font’." (interactive) (let* ((mono-family (car mango-monospace-font)) (mono-props (cdr mango-monospace-font)) (prop-family (car mango-proportional-font)) (prop-props (cdr mango-proportional-font)) (mono-weight (plist-get mono-props :weight)) (mono-height (plist-get mono-props :height)) (prop-weight (plist-get prop-props :weight)) (prop-height (plist-get prop-props :height))) (set-face-attribute 'default nil :font mono-family :weight mono-weight :height mono-height) (set-face-attribute 'fixed-pitch nil :font mono-family :weight mono-weight :height mono-height) (set-face-attribute 'variable-pitch nil :font prop-family :weight prop-weight :height prop-height))) (add-hook 'after-make-frame-functions (lambda (_) (mango-set-fonts))) (mango-set-fonts) #+END_SRC *** Emacs Theme I previously ran the ~sanityinc-tomorrow-eighties~ theme, but I now run my own custom theme. I do like to keep the older theme around though as a reference. #+BEGIN_SRC elisp (use-package color-theme-sanityinc-tomorrow) (load-theme 'mango t) #+END_SRC *** Fringes It’s also nice to have fringes! #+BEGIN_SRC elisp (set-fringe-style (cons 32 32)) #+END_SRC *** Line Highlighting This is just something I personally like having. It makes it very easy for me to figure out where my point is at all times. #+BEGIN_SRC elisp (global-hl-line-mode) #+END_SRC *** Frame Management Prot has made a fantastic package known as ~beframe~. It allows you to have each frame have its own buffer list. This is insanely useful for keeping my buffers nice and organized. It’s also useful for having a way to (for example) kill all buffers you were using in a frame before closing it. #+BEGIN_SRC elisp (defun mango-beframe-delete-frame () "Kill all the buffers within the current beframe buffer list and then destroy the frame." (interactive) (cl-loop with buffers = (cl-loop for frame in (frame-list) unless (eq frame (selected-frame)) append (beframe-buffer-list frame)) for buffer in (beframe-buffer-list) unless (member buffer buffers) do (kill-buffer buffer)) (delete-frame)) (use-package beframe :bind ([remap delete-frame] . mango-beframe-delete-frame) :init (beframe-mode)) #+END_SRC *** Transparency Transparency is totally useless, but it looks cool. Luckily transparent background support was added in Emacs 29! #+BEGIN_SRC elisp (defvar mango-alpha-background 90 "The opacity of a graphical Emacs frame, ranging from 0–100. A value of 0 is fully transparent while a value of 100 is fully opaque.") (defun mango-set-alpha-background (value) "Set the current frames background opacity to VALUE." (interactive "NOpacity: ") (set-frame-parameter nil 'alpha-background value)) (add-to-list 'default-frame-alist (cons 'alpha-background mango-alpha-background)) #+END_SRC ** Extra Modes Some modes aren’t installed by default with Emacs, so let’s fetch them #+BEGIN_SRC elisp (use-package git-modes) (use-package graphviz-dot-mode) (use-package markdown-mode) #+END_SRC ** Auto-Start Modes Some modes need to be manually configured to automatically start when opening a file with a certain file extension. #+BEGIN_SRC elisp (require 'go-ts-mode) (require 'rust-ts-mode) (require 'yaml-ts-mode) (dolist (pair '(("\\.go\\'" . go-ts-mode) ("\\.py\\'" . python-ts-mode) ("\\.rs\\'" . rust-ts-mode) ("\\.ya?ml\\'" . yaml-ts-mode))) (add-to-list 'auto-mode-alist pair)) #+END_SRC ** Customization Settings Emacs has a /customization variable/ that contains various configuration-related settings that are set by different commands, as well as the customization UI. I would rather have these saved in a temporary file since any customizations I make that I would like to have be persistent will be explicitly written into this file. #+BEGIN_SRC elisp (setq custom-file (expand-file-name (format "emacs-custom-%s.el" (user-uid)) temporary-file-directory)) (load custom-file t) #+END_SRC ** Backup Files It’s always good to have backups. I would know — I’ve wiped both =~= and =~/.config= before! Despite the name, ~version-control~ actually just adds version numbers to the backup names — it doesn’t start using a VCS. #+BEGIN_SRC elisp (setq delete-old-versions t version-control t kept-new-versions 2 kept-old-versions 6) #+END_SRC ** Mode Specific Settings *** Org-Mode **** Fontify Blockquotes I want to be able to customize the faces of blockquotes, so we need to enable that setting with ~org-mode~ #+BEGIN_SRC elisp (setopt org-fontify-quote-and-verse-blocks t) #+END_SRC **** Visual Intentation I really enjoy using ~org-indent-mode~. It indents the contents of the org-buffer to keep everything looking nice and structured. #+BEGIN_SRC elisp (add-hook 'org-mode-hook #'org-indent-mode) #+END_SRC *** Auto-Fill-Mode I do like to use this mode, especially when writing. I also want it when coding /sometimes/, but not always. Overall it’s a pretty useful mode — but I am super mixed on when I do and -don’t want it. I always want it in ~org-mode~ though. #+BEGIN_SRC elisp (setq-default fill-column 80) (add-hook 'org-mode-hook #'auto-fill-mode) #+END_SRC ** Auto-Directories When creating new files, the parent directories often don’t exist and I need to make them manually with =M-x make-directory RET RET=. I would prefer that these directories just get created automatically. #+BEGIN_SRC elisp (defun mango--auto-create-directories (original-function filename &rest args) "Automatically create and delete parent directories of files. This is an ‘:override’ advice for ‘find-file’ and friends. It automatically creates the parent directory (or directories) of the file being visited, if necessary. It also sets a buffer-local variable so that the user will be prompted to delete the newly created directories if they kill the buffer without saving it." (let (dirs-to-delete) (let* ((dir-to-create (file-name-directory filename)) (current-dir dir-to-create)) ;; We want to go up each directory component and add them to ;; ‘dirs-to-delete’ individually. (while (not (file-exists-p current-dir)) (push current-dir dirs-to-delete) (setq current-dir (file-name-directory (directory-file-name current-dir)))) (unless (file-exists-p dir-to-create) (make-directory dir-to-create t))) ;; Use ‘prog1’ so that we maintain the original return value (prog1 (apply original-function filename args) (when dirs-to-delete (setq-local mango--dirs-to-delete (reverse dirs-to-delete)) ;; When we kill the buffer we want to ask if we should delete parent ;; directories *unless* the buffer was saved, in which case we don’t ;; want to do anything. (add-hook 'kill-buffer-hook #'mango--delete-directories-if-appropriate t t) (add-hook 'after-save-hook #'mango--remove-auto-directory-hooks t t))))) (dolist (command #'(find-file find-alternate-file write-file)) (advice-add command :around #'mango--auto-create-directories)) (defun mango--delete-directories-if-appropriate () "Delete parent directories if appropriate. This is a function for ‘kill-buffer-hook’. If ‘mango--auto-create-directories’ created the directory containing the file for the current buffer automatically, then offer to delete it. Otherwise, do nothing. Also clean up related hooks." (unless (file-exists-p buffer-file-name) (dolist (dir-to-delete mango--dirs-to-delete) (when (and (stringp dir-to-delete) (file-exists-p dir-to-delete) (y-or-n-p (format "Also delete directory ‘%s’?" (directory-file-name dir-to-delete)))) (delete-directory dir-to-delete))))) (defun mango--remove-auto-directory-hooks () "Clean up directory-deletion hooks, if necessary." (remove-hook 'kill-buffer-hook #'mango--delete-directories-if-appropriate t) (remove-hook 'after-save-hook #'mango--remove-auto-directory-hooks t)) #+END_SRC ** Email #+BEGIN_SRC elisp (use-package mu4e :ensure nil :custom (user-full-name "Thomas Voss") (mu4e-change-filenames-when-moving t) (mu4e-get-mail-command "mbsync -a -c /home/thomas/.config/isync/mbsyncrc") (mu4e-maildir "~/mail") (sendmail-program "/usr/bin/msmtp") (send-mail-function 'smtpmail-send-it) (message-sendmail-f-is-evil t) (message-sendmail-extra-arguments '("--read-envelope-from")) (message-send-mail-function 'message-send-mail-with-sendmail) :config (setq mango--mu4e-personal-context (make-mu4e-context :name "Personal" :match-func (lambda (msg) (when msg (string-prefix-p "/mail@thomasvoss.com" (mu4e-message-field msg :maildir)))) :vars '((user-mail-address . "mail@thomasvoss.com") (mu4e-drafts-folder . "/mail@thomasvoss.com/Drafts") (mu4e-sent-folder . "/mail@thomasvoss.com/Sent") (mu4e-refile-folder . "/mail@thomasvoss.com/Archive") (mu4e-trash-folder . "/mail@thomasvoss.com/Junk") (mu4e-maildir-shortcuts . '((:name "Inbox" :maildir "/mail@thomasvoss.com/Inbox" :key ?i) (:name "Archive" :maildir "/mail@thomasvoss.com/Archive" :key ?a) (:name "Drafts" :maildir "/mail@thomasvoss.com/Drafts" :key ?d) (:name "Sent" :maildir "/mail@thomasvoss.com/Sent" :key ?s) (:name "Junk" :maildir "/mail@thomasvoss.com/Junk" :key ?j)))))) (setq mango--mu4e-legacy-context (make-mu4e-context :name "Legacy" :match-func (lambda (msg) (when msg (string-prefix-p "/thomasvoss@live.com" (mu4e-message-field msg :maildir)))) :vars '((user-mail-address . "thomasvoss@live.com") (user-full-name . "Thomas Voss") (mu4e-drafts-folder . "/thomasvoss@live.com/Drafts") (mu4e-sent-folder . "/thomasvoss@live.com/Sent") (mu4e-refile-folder . "/thomasvoss@live.com/Archive") (mu4e-trash-folder . "/thomasvoss@live.com/Junk") (mu4e-maildir-shortcuts . '((:name "Inbox" :maildir "/thomasvoss@live.com/Inbox" :key ?i) (:name "POP" :maildir "/thomasvoss@live.com/POP" :key ?p) (:name "Archive" :maildir "/thomasvoss@live.com/Archive" :key ?a) (:name "Drafts" :maildir "/thomasvoss@live.com/Drafts" :key ?d) (:name "Sent" :maildir "/thomasvoss@live.com/Sent" :key ?s) (:name "Junk" :maildir "/thomasvoss@live.com/Junk" :key ?j)))))) (setq mango--mu4e-humanwave-context (make-mu4e-context :name "Humanwave" :match-func (lambda (msg) (when msg (string-prefix-p "/thomas.voss@humanwave.nl" (mu4e-message-field msg :maildir)))) :vars '((user-mail-address . "thomas.voss@humanwave.nl") (user-full-name . "Thomas Voss") (mu4e-drafts-folder . "/thomas.voss@humanwave.nl/[Gmail]/Drafts") (mu4e-sent-folder . "/thomas.voss@humanwave.nl/[Gmail]/Sent Mail") (mu4e-refile-folder . "/thomas.voss@humanwave.nl/[Gmail]/All Mail") (mu4e-trash-folder . "/thomas.voss@humanwave.nl/[Gmail]/Trash") (mu4e-maildir-shortcuts . '((:name "Inbox" :maildir "/thomas.voss@humanwave.nl/Inbox" :key ?i) (:name "Archive" :maildir "/thomas.voss@humanwave.nl/[Gmail]/All Mail" :key ?a) (:name "Drafts" :maildir "/thomas.voss@humanwave.nl/[Gmail]/Drafts" :key ?d) (:name "Sent" :maildir "/thomas.voss@humanwave.nl/[Gmail]/Sent Mail" :key ?s) (:name "Junk" :maildir "/thomas.voss@humanwave.nl/[Gmail]/Junk" :key ?j)))))) (setq mu4e-contexts (list mango--mu4e-personal-context mango--mu4e-legacy-context mango--mu4e-humanwave-context))) #+END_SRC