From 848892b8d40c033d95701e95a1e54e41bf4101b5 Mon Sep 17 00:00:00 2001 From: Miquel Sabaté Solà Date: Tue, 8 Jul 2025 17:50:42 +0200 Subject: emacs: There and back again MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bring back a hand-rolled GNU Emacs configuration, moving on from Doom Emacs. This is due some recent regressions I've had with my Doom configuration that I was unable to debug/fix. This brings back some much needed simplicity, while having better knowledge on how to keep things modern and snappy. Signed-off-by: Miquel Sabaté Solà --- .config/emacs/init.el | 682 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 682 insertions(+) create mode 100644 .config/emacs/init.el (limited to '.config/emacs/init.el') diff --git a/.config/emacs/init.el b/.config/emacs/init.el new file mode 100644 index 0000000..3762480 --- /dev/null +++ b/.config/emacs/init.el @@ -0,0 +1,682 @@ +;; Copyright (C) 2016-Ω Miquel Sabaté Solà +;; +;; This program is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. +;; +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. +;; +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; +;; Early checks: force Linux and a recent GNU Emacs. + +(unless (string= system-type 'gnu/linux) + (error "You are supposed to be running Linux!")) + +(unless (>= emacs-major-version 30) + (error "You are supposed to be running an up-to-date GNU Emacs")) + +;;; +;; Package manager: straight.el. + +;; Change the repo url for nongnu-elpa as savannah is unreliable. This has been +;; changed in straight.el `develop` branch, but it's not there in the stable +;; branch. +(custom-set-variables + '(straight-recipe-overrides '((nil + (nongnu-elpa :type git + :repo "https://github.com/emacsmirror/nongnu_elpa" + :depth (full single-branch) + :local-repo "nongnu-elpa" + :build nil))))) + +;; As taken from the documentation, this is some fancy Emacs Lisp code to +;; bootstrap straight.el in case it isn't available on the system already. +(defvar bootstrap-version) +(let ((bootstrap-file + (expand-file-name + "straight/repos/straight.el/bootstrap.el" + (or (bound-and-true-p straight-base-dir) + user-emacs-directory))) + (bootstrap-version 7)) + (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)) + +;; But we are actually going to use `use-package' always, so let's integrate +;; straight.el into it. +(straight-use-package 'use-package) +(setq straight-use-package-by-default t) + +;;; +;; Core + +;; There are some packages that expect us to have a `user-full-name' and a +;; `user-mail-address' defined. +(setq user-full-name "Miquel Sabaté Solà" + user-mail-address "mikisabate@gmail.com") + +;;; +;; Key bindings: using Evil mode and general.el for the key binding shenanigans. + +;; Forgive me Father, for I have sinned. +(use-package evil + :custom + (evil-want-keybinding nil) ; Defer Evil's default keybindings, let evil-collection handle it or define manually. + (evil-want-C-u-scroll t) + (evil-want-C-u-delete t) + (evil-want-C-d-scroll t) + (evil-want-C-w-delete t) + :config + (evil-mode 1)) + +;; Add the surround object. +(use-package evil-surround + :after evil + :config + (global-evil-surround-mode 1)) + +;; Carelessly comment out stuff in multiple languages. +(use-package evil-commentary + :after evil + :config + (evil-commentary-mode t)) + +;; Evil +everywhere. +(use-package evil-collection + :after evil + :config + (evil-collection-init)) + +;; Allow me to use C-z as my escape input. +(use-package evil-escape + :after general + :config + (general-define-key + :states '(insert normal) + "C-z" #'evil-escape)) + +;; Sane key-binding management. +(use-package general + :after evil + :config + + ;; Small C-<> mappings. + (general-define-key + :states 'normal + "C-s" #'save-buffer + "C-e" #'evil-end-of-line + "C-2" (lambda (n) + (interactive "p") + (evil-execute-macro n "@q"))) + + ;; And being cheeky with a C-s for saving even in insert mode. + (general-define-key + :states 'insert + "C-s" (lambda () (interactive) (save-buffer) (evil-force-normal-state))) + + ;; Expansions for the Emacs' C-x prefix, handling windows, buffers, etc. + (general-define-key + :prefix "C-x" + "k" #'kill-this-buffer + "K" #'kill-buffer-and-window + "v" #'split-window-right + "V" (lambda () (interactive) (split-window-right) (other-window 1))) + + ;; Expansions for the Emacs' C-c prefix. + (general-define-key + :prefix "C-c" + ;; The least-ambitious use of org-mode ever, I know. + "C-o" + (lambda () + (interactive) + (find-file (concat (file-name-as-directory org-directory) "index.org")))) + + ;; Define my leader key. + (general-create-definer mssola/leader-def + :prefix "SPC") + + ;; Define mappings based on a . + (mssola/leader-def + :states 'normal + :keymaps 'override + "j" 'xref-find-definitions + "m" 'mssola/mu4e + "p" 'project-switch-project + "s" 'consult-ripgrep + "u" 'xref-find-references ; TODO: in vertico buffer + "SPC" 'project-find-file)) + +;;; +;; General + +;; Follow symlinks. +(setq vc-follow-symlinks t) + +;; Default to y/n instead of full yes/no. +(fset 'yes-or-no-p 'y-or-n-p) + +;; Revert buffers automatically when underlying files are changed externally. +(global-auto-revert-mode t) + +;; No backups: they are more of a nuisance that an actual help… +(setq-default make-backup-files nil) +(setq-default auto-save-default nil) + +;; Save custom variables somewhere else +(setq custom-file (expand-file-name "custom.el" user-emacs-directory)) +(when (file-exists-p custom-file) + (load custom-file)) + +;; The crux package offers quite a few goodies, and I'm particularly interested +;; on the `crux-delete-file-and-buffer', which will delete the file related to +;; the current buffer as well as the buffer itself. +(use-package crux + :bind (("C-c D" . crux-delete-file-and-buffer))) + +;; Org mode directory. +(setq org-directory "~/org/") + +;; Show me which functions are available for the key I half-pressed. +(use-package which-key + :custom + (which-key-idle-delay 0.3) + (which-key-side-window-location 'bottom) + :hook (after-init . which-key-mode)) + +;;; +;; UI + +;; Some time ago I created this title format, and now I prefer it over other +;; options. +(setq frame-title-format + (setq icon-title-format '((:eval (concat (user-real-login-name) ": " + (if (buffer-file-name) + (abbreviate-file-name (buffer-file-name)) + "%b")))))) + +;; I’m using “Droid Sans Mono” simply because I’ve grown used to it. +(defconst mssola-font + "Droid Sans Mono Dotted for Powerline-10" + "The font to be used.") + +(add-to-list 'default-frame-alist `(font . ,mssola-font)) + +;; Emacs in daemon mode does not like `set-face-attribute` because this is only +;; applied if there is a frame in place, which doesn't happen when starting the +;; daemon. Thus, we should call that after the frame has been created (e.g. by +;; emacsclient). See: +;; https://lists.gnu.org/archive/html/help-gnu-emacs/2015-03/msg00016.html +(add-hook 'after-make-frame-functions-hook + (lambda () + (set-face-attribute 'default t :font mssola-font))) + +;; Remove the initial message from the scratch buffer. +(setq initial-scratch-message nil) + +;; No welcome screen +(setq-default inhibit-startup-message t) + +;; Minimalistic UI. +(menu-bar-mode -1) +(when (fboundp 'set-scroll-bar-mode) + (set-scroll-bar-mode nil)) +(when (fboundp 'tool-bar-mode) + (tool-bar-mode -1)) +(when (fboundp 'tooltip-mode) + (tooltip-mode 0)) + +;; Display helpful information on where we are. +(line-number-mode 1) +(column-number-mode 1) +(size-indication-mode 1) +(add-hook 'prog-mode-hook 'display-line-numbers-mode) + +;; Let's make scrolling suck less. +(setq scroll-margin 0 + scroll-conservatively 100000 + scroll-preserve-screen-position 1) + +;; There is somewhere someone who likes to be interrupted with stupid beep +;; sounds. That sorry soul is not me. +(setq ring-bell-function 'ignore) + +;; Use the modeline for Doom, which is simply superior to whatever I can +;; half-ass. +(use-package doom-modeline + :custom + (doom-modeline-height 20) + (doom-modeline-persp-name nil) + (doom-modeline-major-mode-icon nil) + (doom-modeline-modal-icon t) + (doom-modeline-icon t) + (doom-modeline-buffer-file-name-style 'relative-from-project) + :hook (after-init . doom-modeline-mode)) + +;; Color theme: try to use my own theme called `soria'. There are two paths +;; where the `soria' theme might reside. The first path we are going to try is +;; my local dev environment, otherwise we will try the global installation path +;; (as pointed out on the documentation of the RPM package). +(let ((dev-dir (concat (getenv "HOME") "/src/github.com/mssola/soria")) + (global-dir "/usr/share/emacs/site-lisp/themes")) + (if (file-exists-p dev-dir) + (add-to-list 'custom-theme-load-path dev-dir) + (when (file-exists-p global-dir) + (add-to-list 'custom-theme-load-path global-dir))) + + ;; If soria could be loaded as a theme, let's set the + ;; `soria-theme-purple-identifiers' hook for the relevant modes. + (when (load-theme 'soria t) + (dolist (lang-hook '(ruby-mode-hook + php-mode-hook + perl-mode-hook + emacs-lisp-mode-hook)) + (add-hook lang-hook 'soria-theme-purple-identifiers)))) + +;; When in Evil mode, it just makes sense to have line numbers relatively. +(setq display-line-numbers-type 'relative) + +;; For whatever reason someone thought that having the tilde fringe as it +;; happens to vi was a good idea. I disagree. +(remove-hook 'text-mode-hook #'vi-tilde-fringe-mode) + +;; Enable number highlighting. +(use-package highlight-numbers + :hook ((prog-mode-hook conf-mode-hook) . highlight-numbers-mode)) + +;; Highlight todos, warnings, et al. +(use-package hl-todo + :hook ((prog-mode yaml-mode) . hl-todo-mode)) + +;;; +;; Core editor. + +;; UTF-8 everywhere. +(prefer-coding-system 'utf-8) +(set-default-coding-systems 'utf-8) +(set-terminal-coding-system 'utf-8) +(set-keyboard-coding-system 'utf-8) +(set-language-environment 'utf-8) + +;; GNU Emacs modes typically provide a standard means to change the indentation +;; width (e.g. c-basic-offset), and that will inevitably conflict with the “tabs +;; vs spaces” question and the myriad of code conventions for each programming +;; language. We will deal with all this later. +(setq-default indent-tabs-mode nil) +(setq-default tab-width 4) + +;; If an .editorconfig is around, use that. +(editorconfig-mode 1) + +;; As for word wrapping and filling we will take a conservative default, and +;; later we will discard it for some modes. For example, it makes sense to +;; perform auto-fill in text-mode (which includes org-mode), but there is no +;; point of doing it in source code. +(setq-default fill-column 80) +(setq-default auto-fill-function 'do-auto-fill) + +;; Do not break lines +(set-default 'truncate-lines t) + +;; It’s pretty bananas that I have to say it, but if I’ve pressed a key when +;; selecting text, I want that text to be nuked and replaced with whatever I’ve +;; typed. This will be overriden anyways with evil-mode, but let’s do this now +;; just in case evil-mode is disabled or something. Delete the selection with a +;; keypress. +(delete-selection-mode t) + +;; There are uncivilized barbarians that do not delete trailing +;; whitespaces. This is not my case. +(add-hook 'before-save-hook #'delete-trailing-whitespace) + +;; And finally, let’s make the cursor a little more relevant. For starters, the +;; default of the cursor blinking is plain idiotic. Secondly, I don’t want +;; special highlighting for the current line. And finally, I’d like matching +;; parenthesis to say something whenever the cursor is on top of one of them. +(blink-cursor-mode 0) +(global-hl-line-mode -1) +(show-paren-mode 1) + +;;; +;; Completion. + +;; Ignore case on completion. +(setq read-file-name-completion-ignore-case t + read-buffer-completion-ignore-case t + completion-ignore-case t) + +;; Code completion. +(use-package company + :hook (after-init . global-company-mode)) + +;; Vertico: vertical completion for search elements. +(use-package vertico + :after general + :custom + (vertico-count 20) + (vertico-resize nil) + (vertico-cycle t) + :init + (vertico-mode) + :bind + (:map vertico-map + ("C-k" . vertico-previous) + ("C-j" . vertico-next))) + +;; Persist history over Emacs restarts. Vertico sorts by history position. +(use-package savehist + :init + (savehist-mode)) + +;; Emacs minibuffer configurations. +(use-package emacs + :custom + ;; Enable context menu. `vertico-multiform-mode' adds a menu in the minibuffer + ;; to switch display modes. + (context-menu-mode t) + ;; Support opening new minibuffers from inside existing minibuffers. + (enable-recursive-minibuffers t) + ;; Hide commands in M-x which do not work in the current mode. Vertico + ;; commands are hidden in normal buffers. This setting is useful beyond + ;; Vertico. + (read-extended-command-predicate #'command-completion-default-include-p) + ;; Do not allow the cursor in the minibuffer prompt + (minibuffer-prompt-properties + '(read-only t cursor-intangible t face minibuffer-prompt))) + +;; Optionally use the `orderless' completion style. +(use-package orderless + :custom + ;; Configure a custom style dispatcher (see the Consult wiki) + ;; (orderless-style-dispatchers '(+orderless-consult-dispatch orderless-affix-dispatch)) + ;; (orderless-component-separator #'orderless-escapable-split-on-space) + (completion-styles '(orderless basic)) + (completion-category-defaults nil) + (completion-category-overrides '((file (styles partial-completion))))) + +;; Needed in order to get `consult-ripgrep', which is great for searching text +;; on the current project. +(use-package consult + :after vertico) + +;; Put abbrevs into my emacs directory so it can be tracked on Git. +(setq abbrev-file-name (expand-file-name "abbrevs.el" user-emacs-directory) + save-abbrevs 'silent) + +;; And apply abbrevs.el where relevant. +(dolist (hook '(org-mode-hook text-mode-hook)) + (add-hook hook #'abbrev-mode)) + +;;; +;; Tools + +;; Enable spelling checks (only on text, not on code). +(use-package flyspell + :defer t + :config + (setq flyspell-issue-welcome-flag nil + flyspell-issue-message-flag nil) + (general-define-key "" 'ispell-word)) + +;; And integrate it with vertico. +(use-package flyspell-correct + :defer t + :commands flyspell-correct-previous + :general ([remap ispell-word] #'flyspell-correct-at-point)) + +(defun mssola/git-commit-hook () + "Things to run upon entering git-commit mode." + + (setq fill-column 72) + (when (and (bound-and-true-p evil-local-mode) + (not (evil-emacs-state-p)) + (bobp) (eolp)) + (evil-insert-state))) + +;; I mostly stick to Git CLI, but it's really nice when staging chunks, checking +;; diffs, showing logs; and similar scenarios where Git can be tedious. +(use-package magit + :defer t + :config + (setq magit-diff-refine-hunk t ; show granular diffs in selected hunk + ;; Don't autosave repo buffers. This is too magical, and saving can + ;; trigger a bunch of unwanted side-effects, like save hooks and + ;; formatters. Trust the user to know what they're doing. + magit-save-repository-buffers nil + ;; Don't display parent/related refs in commit buffers; they are rarely + ;; helpful and only add to runtime costs. + magit-revision-insert-related-refs nil + ;; If two projects have the same project name (e.g. A/src and B/src will + ;; both resolve to the name "src"), Magit will treat them as the same + ;; project and destructively hijack each other's magit buffers. This is + ;; especially problematic if you use workspaces and have magit open in + ;; each, and the two projects happen to have the same name! By unsetting + ;; `magit-uniquify-buffer-names', magit uses the project's full path as + ;; its name, preventing such naming collisions. + magit-uniquify-buffer-names nil) + + ;; Turn ref links into clickable buttons. + (add-hook 'magit-process-mode-hook #'goto-address-mode) + + ;; Git commit mode. + (setq git-commit-summary-max-length 50 + git-commit-style-convention-checks '(overlong-summary-line non-empty-second-line)) + + (add-hook 'git-commit-mode-hook 'mssola/git-commit-hook)) + +;; Nicer diffs. +(use-package magit-delta + :when (executable-find "git-delta") + :hook (magit-mode . magit-delta-mode)) + +;; Highlighting for git-related files. +(use-package git-modes + :defer t) + +;;; +;; Languages + +;; I'm fine with the basic support from GNU Emacs for most languages. Here I'm +;; adding support from modes which are not installed by default. + +(use-package markdown-mode + :defer t) + +(use-package adoc-mode + :defer t) + +(use-package yaml-mode + :defer t) + +(use-package dockerfile-mode + :defer t) + +;; I don't want to go LSP all the way, but for certain languages is almost +;; required (e.g. Go). In any case, let's keep it simple. + +(use-package lsp-mode + :defer t + :config + (setq lsp-enable-folding nil + lsp-enable-text-document-color nil + lsp-enable-on-type-formatting nil + lsp-headerline-breadcrumb-enable nil)) + +;; Better LSP integration, configuration taken from Doom. +(use-package lsp-ui + :hook (lsp-mode . lsp-ui-mode) + :config + (setq lsp-ui-peek-enable nil + lsp-ui-doc-max-height 8 + lsp-ui-doc-max-width 72 + lsp-ui-doc-delay 0.75 + lsp-ui-doc-show-with-mouse nil + lsp-ui-doc-position 'at-point + lsp-ui-sideline-ignore-duplicate t + lsp-ui-sideline-show-hover nil + lsp-ui-sideline-actions-icon lsp-ui-sideline-actions-icon-default)) + +(defun mssola/golang-before-save-hooks () + "Setup before-save-hooks relevant to Go." + + (add-hook 'before-save-hook #'lsp-format-buffer nil t) + (add-hook 'before-save-hook #'lsp-organize-imports nil t)) + +;; Support for Go with LSP integration. +(use-package go-mode + :defer t + :hook ((go-mode . lsp-deferred) + (go-mode . mssola/golang-before-save-hooks))) + +;; Utils + +;; Provide the `xref-find-definitions' interactive function which can be used to +;; jump between definitions in any language. It's basically `rg' on steroids but +;; it works remarkably well for what it is. +(use-package dumb-jump + :defer t) + +;;; +;; Email + +;; I take mu4e not from MELPA but from the openSUSE repositories. Hence, +;; instead of using `use-package' we are going to pin point the installation +;; path and configure `mu4e' whenever I try to launch it. +(add-to-list 'load-path "/usr/share/emacs/site-lisp/mu4e") + +;; Set the maildir directory in any case. +(setq mu4e-maildir "~/data/mail") + +(defun mssola/mu4e () + "Configure mu4e in-place and launch it." + + (interactive) + + (progn + (require 'mu4e) + + ;; Use `msmtp' as the program for sending email. + (setq sendmail-program (executable-find "msmtp") + send-mail-function #'smtpmail-send-it + starttls-use-gnutls t + message-sendmail-f-is-evil t + message-sendmail-extra-arguments '("--read-envelope-from") + message-send-mail-function #'message-send-mail-with-sendmail) + + ;; The default bookmarks are cool and all, but they lack a proper sink that + ;; gathers all inboxes. + (add-to-list 'mu4e-bookmarks + '(:name "Inbox" + :query "maildir:/gmail/inbox OR maildir:/comsuse/inbox OR maildir:/sindicat/inbox OR maildir:/uoc/inbox" + :key ?n)) + + ;; For whatever reason I lost the "messages sent" bookmark. Let's re-add it. + (add-to-list 'mu4e-bookmarks + '(:name "Sent" + :query "from:mikisabate@gmail.com OR from:msabate@suse.com" + :key ?s)) + + ;; General mu4e settings. + (setq message-kill-buffer-on-exit t + mu4e-get-mail-command "mbsync --all" + mu4e-update-interval 600 + mu4e-use-fancy-chars nil + gnus-treat-display-smileys nil + mu4e-sent-messages-behavior 'sent + mu4e-notification-support t + mu4e-hide-index-messages t + mu4e-confirm-quit nil + + mu4e-headers-date-format "%Y-%m-%d %H:%M" + mu4e-headers-skip-duplicates t + mu4e-headers-include-related t + mu4e-headers-auto-update t + mu4e-headers-fields + '((:human-date . 8) + (:from-or-to . 25) + (:subject)) + + message-user-organization nil + message-organization nil + + message-citation-line-format "%N @ %Y-%m-%d %H:%M %Z:\n" + message-citation-line-function 'message-insert-formatted-citation-line + + message-kill-buffer-on-exit t + mu4e-view-show-addresses t + + mu4e-context-policy 'ask-if-none + mu4e-compose-context-policy 'always-ask + mu4e-compose-dont-reply-to-self t + + mu4e-completing-read-function #'completing-read + + mail-user-agent 'mu4e-user-agent + message-mail-user-agent 'mu4e-user-agent + + mu4e-attachment-dir (concat (expand-file-name (or (getenv "XDG_DOWNLOAD_DIR") "www") "~") "/") + mu4e-change-filenames-when-moving t) + + ;; PGP thingies. + (setq mml-secure-openpgp-signers '("0x96BE8C6FD89D6565") + mml-secure-openpgp-sign-with-sender t) + + (add-hook 'message-send-hook 'mml-secure-message-sign-pgpmime) + + ;; And here comes the account shenanigans. + (setq mu4e-contexts + `( + ;; GMail + ,(make-mu4e-context + :name "gmail" + :enter-func (lambda () + (mu4e-message "Switching to gmail.com") + (setq mu4e-sent-messages-behavior 'delete)) + :match-func + (lambda (msg) + (when msg + (string-prefix-p "/gmail" (mu4e-message-field msg :maildir)))) + :vars '( + (user-mail-address . "mikisabate@gmail.com") + (mu4e-reply-to-address . "mikisabate@gmail.com") + (mu4e-drafts-folder . "/gmail/Drafts") + (mu4e-sent-folder . "/gmail/Sent") + (mu4e-refile-folder . "/gmail/All") + (mu4e-trash-folder . "/gmail/Trash"))) + + ;; suse.com + ,(make-mu4e-context + :name "suse" + :enter-func (lambda () + (mu4e-message "Switching to suse.com") + (setq mu4e-sent-messages-behavior 'delete)) + :match-func + (lambda (msg) + (when msg + (string-prefix-p "/comsuse" (mu4e-message-field msg :maildir)))) + :vars `( + (user-mail-address . "msabate@suse.com") + (mu4e-reply-to-address . "msabate@suse.com") + (mu4e-drafts-folder . "/comsuse/Esborranys") + (mu4e-sent-folder . "/comsuse/Elements enviats") + (mu4e-refile-folder . "/comsuse/All") + (mu4e-trash-folder . "/comsuse/Elements suprimits"))))) + ) + + ;; And finally call the real deal. + (mu4e)) + +;; Nice highlighting for patches inside of emails. +(use-package message-view-patch + :hook (gnus-part-display . message-view-patch-highlight)) -- cgit v1.2.3