#+TITLE: My Ever Changing Literate Emacs Configuration #+AUTHOR: xulfer #+PROPERTY: header-args :tangle init.el :noweb yes :exports code # For PDF export allow line wrapping. #+LATEX_HEADER: \usepackage{listings} #+LATEX_HEADER: \lstset{breaklines=true, breakatwhitespace=true, basicstyle=\ttfamily\footnotesize, columns=fullflexible} #+begin_src emacs-lisp :exports none :tangle no ;; This asks for a file which it uses to store the processed org ;; data with the includes into. It can be anything and can safely ;; be deleted afterwards. I think README.org is a good file to ;; pick if using a repository. (defun tangle-literate-config () "Tangle config files with proper include handling." (interactive) (let ((org-confirm-babel-evaluate nil)) ;; Create a temporary buffer with expanded includes (with-temp-buffer (insert-file-contents "config.org") (org-mode) ;; This expands all #+INCLUDE directives (org-export-expand-include-keyword) ;; Now tangle from this buffer which has everything in one place (org-babel-tangle nil "init.el")) (message "Configuration tangled!"))) #+end_src #+BEGIN_SRC emacs-lisp :exports none ;;; init.el -*- lexical-binding: t; -*- ;;; Code: #+END_SRC #+begin_src emacs-lisp :tangle early-init.el :exports none ;;; early-init.el -*- lexical-binding: t; -*- ;;; Code: #+end_src * Quick Look [[./img/preview.png]] Just the usual flex of doing as little editing as possible in a screenshot. * Motivation Over a surprisingly short amount of time my emacs configuration has become quite a mess. Almost every visit to it requiring some digging to get to the relevant configuration. So as a result I've decided to make a literate configuration. Mostly for my own mental house keeping. However, if some poor soul reads this and finds it useful, then that's a bonus. ** Inspirations, and sometimes outright copy/paste sources - [[https://github.com/progfolio/.emacs.d][Progfolio's Config]] * Initial Bits and Bobs Before anything else the appearance, and some performance settings need to be tweaked as they can cause issues if done mid-start. ** Initial setup The LSP packages perform better with plists so an environment variable needs to be set to inform them that this is intended. I've also removed default package handling as I intend to use another package manager. Lastly I suppress some compilation warnings. #+begin_src emacs-lisp :tangle early-init.el (setq package-enable-at-startup nil) (setq inhibit-default-init nil) (setq native-comp-async-report-warnings-errors nil) (setenv "LSP_USE_PLISTS" "true") #+end_src ** Appearance I like a minimal look, so I disable menu bars, tool bars, all the bars. I have Emacs loading as a blank slate with only the scratch buffer open. #+begin_src emacs-lisp :tangle early-init.el (defvar default-file-name-handler-alist file-name-handler-alist) (setq file-name-handler-alist nil) (push '(menu-bar-lines . 0) default-frame-alist) (push '(tool-bar-lines . 0) default-frame-alist) (push '(vertical-scroll-bars) default-frame-alist) (setq server-client-instructions nil) (when (and (fboundp 'startup-redirect-eln-cache) (fboundp 'native-comp-available-p) (native-comp-available-p)) (startup-redirect-eln-cache (convert-standard-filename (expand-file-name "var/eln-cache/" user-emacs-directory)))) (setq frame-inhibit-implied-resize t) (advice-add #'x-apply-session-resources :override #'ignore) (setq desktop-restore-forces-onscreen nil) (setq ring-bell-function #'ignore inhibit-startup-screen t) (push '(font . "Victor Mono-13") default-frame-alist) (set-face-font 'default "Victor Mono-13") (set-face-font 'variable-pitch "Victor Mono-13") (copy-face 'default 'fixed-pitch) (provide 'early-init) ;;; early-init.el ends here ;; Local Variables: ;; no-byte-compile: t ;; no-native-compile: t ;; no-update-autoloads: t ;; End: #+end_src #+INCLUDE: "config/editing.org" :minlevel 1 * Package Management I am using [[https://github.com/progfolio/elpaca][Elpaca]] as my package manager. I've found it to be quite quick, and easy to use. #+name: elpaca-boilerplate #+begin_src emacs-lisp :exports none :tangle no (defvar elpaca-installer-version 0.11) (defvar elpaca-directory (expand-file-name "elpaca/" user-emacs-directory)) (defvar elpaca-builds-directory (expand-file-name "builds/" elpaca-directory)) (defvar elpaca-repos-directory (expand-file-name "repos/" elpaca-directory)) (defvar elpaca-order '(elpaca :repo "https://github.com/progfolio/elpaca.git" :ref nil :depth 1 :inherit ignore :files (:defaults "elpaca-test.el" (:exclude "extensions")) :build (:not elpaca--activate-package))) (let* ((repo (expand-file-name "elpaca/" elpaca-repos-directory)) (build (expand-file-name "elpaca/" elpaca-builds-directory)) (order (cdr elpaca-order)) (default-directory repo)) (add-to-list 'load-path (if (file-exists-p build) build repo)) (unless (file-exists-p repo) (make-directory repo t) (when (<= emacs-major-version 28) (require 'subr-x)) (condition-case-unless-debug err (if-let* ((buffer (pop-to-buffer-same-window "*elpaca-bootstrap*")) ((zerop (apply #'call-process `("git" nil ,buffer t "clone" ,@(when-let* ((depth (plist-get order :depth))) (list (format "--depth=%d" depth) "--no-single-branch")) ,(plist-get order :repo) ,repo)))) ((zerop (call-process "git" nil buffer t "checkout" (or (plist-get order :ref) "--")))) (emacs (concat invocation-directory invocation-name)) ((zerop (call-process emacs nil buffer nil "-Q" "-L" "." "--batch" "--eval" "(byte-recompile-directory \".\" 0 'force)"))) ((require 'elpaca)) ((elpaca-generate-autoloads "elpaca" repo))) (progn (message "%s" (buffer-string)) (kill-buffer buffer)) (error "%s" (with-current-buffer buffer (buffer-string)))) ((error) (warn "%s" err) (delete-directory repo 'recursive)))) (unless (require 'elpaca-autoloads nil t) (require 'elpaca) (elpaca-generate-autoloads "elpaca" repo) (let ((load-source-file-function nil)) (load "./elpaca-autoloads")))) (add-hook 'after-init-hook #'elpaca-process-queues) (elpaca `(,@elpaca-order)) (if debug-on-error (setq use-package-verbose t use-package-expand-minimally nil use-package-compute-statistics t) (setq use-package-verbose nil use-package-expand-minimally t)) #+end_src Elpaca supports =use-package= so hook that up, and add a =use-feature= macro that adds a similar construct for configuring already loaded emacs packages and features. #+begin_src emacs-lisp <> (defmacro use-feature (name &rest args) "Like `use-package' but accounting for asynchronous installation. NAME and ARGS are in `use-package'." (declare (indent defun)) `(use-package ,name :ensure nil ,@args)) (elpaca elpaca-use-package (require 'elpaca-use-package) (elpaca-use-package-mode) (setq use-package-always-ensure t)) #+end_src * Garbage Collection There's a lot of clashes that can happen with regards to performance, and garbage collection. There are a lot of [[https://emacsredux.com/blog/2025/03/28/speed-up-emacs-startup-by-tweaking-the-gc-settings/][settings]] improvements that can make a huge difference in this regard. Especially when it comes to LSPs, completion, and the like. I've chosen to let GCCH (Garbage Collector Magic Hack) to handle this. It seems to work pretty well. #+begin_src emacs-lisp ;; Garbage collection (use-package gcmh :ensure t :hook (after-init . gcmh-mode) :custom (gcmh-idle-delay 'auto) (gcmh-auto-idle-delay-factor 10)) #+end_src * Keeping things tidy I'd like to keep all of my configuration, and emacs files in one place. I've found the [[https://github.com/emacscollective/no-littering][no-littering]] package does this well. This keeps everything under the .emacs.d directory rather than littering $HOME. #+begin_src emacs-lisp (use-package no-littering :ensure t :config (no-littering-theme-backups) (let ((dir (no-littering-expand-var-file-name "lock-files/"))) (make-directory dir t) (setq lock-file-name-transforms `((".*" ,dir t)))) (setq custom-file (expand-file-name "custom.el" user-emacs-directory))) #+end_src * Auth sources I make sure the auth sources are within the emacs directory. I use gpg, but in case there's a plain text one laying around I'll use that too. Finally as I use pass I've enabled password-store as well; Though I'm not sure this actually works currently. #+begin_src emacs-lisp (auth-source-pass-enable) (setq auth-sources '("~/.emacs.d/.authinfo.gpg" "~/.emacs.d/.authinfo" "password-store")) #+end_src * Path Rather than having to manage potential paths in the configuration I'll use the [[https://github.com/purcell/exec-path-from-shell][exec-path-from-shell]] package. This pulls in =$PATH= from various different shells and operating systems. At least BSD, Linux, and MacOS are supported anyway. #+BEGIN_SRC emacs-lisp (use-package exec-path-from-shell :ensure t :config (when (memq window-system '(mac ns)) (exec-path-from-shell-initialize))) #+end_src * Profiling Sometimes if I experience slow start times I've found [[https://github.com/jschaf/esup][esup]] does this quickly and without having to quit Emacs. #+begin_src emacs-lisp (use-package esup :ensure t :config (setq esup-depth 0)) #+end_src * General Settings I have some configuration tweaks on existing features in emacs. ** Fancy Compile Output Just want to add a bit of color to compilation output. This also will scroll to the first error if there is one. #+begin_src emacs-lisp (use-feature compile :commands (compile recompile) :custom (compilation-scroll-output 'first-error) :config (defun +compilation-colorize () "Colorize from `compilation-filter-start' to `point'." (require 'ansi-color) (let ((inhibit-read-only t)) (ansi-color-apply-on-region (point-min) (point-max)))) (add-hook 'compilation-filter-hook #'+compilation-colorize)) #+end_src ** General Emacs Settings These are my overall emacs settings. More settings could be moved here. Though keep the loading order in mind when doing so. #+begin_src emacs-lisp (use-feature emacs :demand t :config (epa-file-enable) (setq epg-pinentry-mode 'loopback) (setq epa-file-encrypt-to '("xulfer@cheapbsd.net")) :custom (scroll-conservatively 101 "Scroll just enough to bring text into view") (enable-recursive-minibuffers t "Allow minibuffer commands in minibuffer") (frame-title-format '(buffer-file-name "%f" ("%b")) "Make frame title current file's name.") (find-library-include-other-files nil) (indent-tabs-mode nil "Use spaces, not tabs") (inhibit-startup-screen t) (history-delete-duplicates t "Don't clutter history") (pgtk-use-im-context-on-new-connection nil "Prevent GTK from stealing Shift + Space") (sentence-end-double-space nil "Double space sentence demarcation breaks sentence navigation in Evil") (tab-stop-list (number-sequence 2 120 2)) (tab-width 2 "Shorter tab widths") (completion-styles '(flex basic partial-completion emacs22)) (report-emacs-bug-no-explanations t) (report-emacs-bug-no-confirmation t) (setq shr-use-xwidgets-for-media t)) #+end_src ** Diffs I have a slight tweak to diff output here. Mainly making the diff horizontally split by default. #+begin_src emacs-lisp (use-feature ediff :defer t :custom (ediff-window-setup-function #'ediff-setup-windows-plain) (ediff-split-window-function #'split-window-horizontally) :config (add-hook 'ediff-quit-hook #'winner-undo)) #+end_src ** Minibuffer The minibuffer is already pretty well sorted by other packages that will be discussed later. However, there is still a bit of tidying that can be done with long paths, and some helpful file based completion. #+begin_src emacs-lisp (use-feature minibuffer :custom (read-file-name-completion-ignore-case t) :config (defun +minibuffer-up-dir () "Trim rightmost directory component of `minibuffer-contents'." (interactive) (unless (minibufferp) (user-error "Minibuffer not selected")) (let* ((f (directory-file-name (minibuffer-contents))) (s (file-name-directory f))) (delete-minibuffer-contents) (when s (insert s)))) (define-key minibuffer-local-filename-completion-map (kbd "C-h") #'+minibuffer-up-dir) (minibuffer-depth-indicate-mode)) #+end_src ** Remote Editing There are a lot of solutions for editing files, and projects remotely. At the moment [[https://www.gnu.org/software/tramp/][tramp]] still seems to work perfectly well... albeit somewhat slower than I'd like. #+begin_src emacs-lisp (use-feature tramp :config ;; Enable full-featured Dirvish over TRAMP on ssh connections ;; https://www.gnu.org/software/tramp/#Improving-performance-of-asynchronous-remote-processes (connection-local-set-profile-variables 'remote-direct-async-process '((tramp-direct-async-process . t))) (connection-local-set-profiles '(:application tramp :protocol "ssh") 'remote-direct-async-process) ;; Tips to speed up connections (setq tramp-verbose 0) (setq tramp-chunksize 2000) (setq tramp-ssh-controlmaster-options nil)) #+end_src * Blocks, Parentheses and Formatting Oh My! ** Parentheses, and Structural Editing Sometimes if I delete a parenthesis out of hand I spend the next minute or two kicking myself as I count the parentheses here and there. Well no more! With [[https://shaunlebron.github.io/parinfer/][Parinfer]] structural editing, and taming parentheses becomes a breeze. #+begin_src emacs-lisp (use-package parinfer-rust-mode :ensure t :init (setq parinfer-rust-auto-download t) :hook (emacs-lisp-mode . parinfer-rust-mode)) #+end_src I also have =smart-parens= for parentheses matching in modes where =parinfer= would be overkill. #+begin_src emacs-lisp (use-package smartparens :ensure t :hook (prog-mode text-mode markdown-mode) :config (require 'smartparens-config)) #+end_src Might as well highlight the parentheses to make them easier to spot. #+begin_src emacs-lisp (use-package highlight-parentheses :ensure t :hook (prog-mode . highlight-parentheses-mode)) #+end_src ** Indentation Level #+begin_src emacs-lisp ;; Indent guides (use-package highlight-indent-guides :defer t :hook (prog-mode . highlight-indent-guides-mode)) #+end_src ** List, and String Improvements #+begin_src emacs-lisp (use-package dash :ensure t) (use-package s :ensure t) #+end_src * Documentation If possible I like to have documentation within the editor itself so I can easily read and look at the material easily. ** devdocs.el This [[https://github.com/astoff/devdocs.el][plugin]] browses documentation from [[https://devdocs.io][devdocs.io]]. I haven't used this enough to vouch for it's accuracy, but it seems fine. #+begin_src emacs-lisp (use-package devdocs :ensure t :bind ("C-h D" . devdocs-lookup)) #+end_src * Socializing Here are some things I use to optionally communicate with the rest of the world via Emacs. I keep them in separate files so I can optionally load them easily on a system by system basis. #+begin_src emacs-lisp ;;; Extra optional files (defun maybe-load-rel (relpath) "Loads a file relative to the user-emacs-directory fi it exists." (let ((path (concat (file-name-as-directory user-emacs-directory) relpath))) (when (file-exists-p path) (load-file path)))) (maybe-load-rel "extra/email.el") (maybe-load-rel "extra/feed.el") (maybe-load-rel "extra/social.el") ;;; #+end_src # For now I'm not using headers here and letting them be defined in the org files # themselves. #+PROPERTY: header-args :tangle email.el Incoming email is handled by [[https://notmuchmail.org][notmuch]]. Outgoing is via [[https://github.com/marlam/msmtp][msmtp]] This is a pretty simple implementation without a lot of search queries, and list handling. As I get more comfortable with using emacs as an email client I'll try to get fancier. #+BEGIN_SRC emacs-lisp ;; Email/notmuch settings -*- lexical-binding: t; -*- (use-package notmuch :init (autoload 'notmuch "notmuch" "notmuch mail" t) :config (define-key notmuch-show-mode-map "d" (lambda () "toggle deleted tag for message" (interactive) (if (member "deleted" (notmuch-show-get-tags)) (notmuch-show-tag (list "-deleted")) (notmuch-show-tag (list "+deleted"))))) (define-key notmuch-search-mode-map "d" (lambda () "toggle deleted tag for message" (interactive) (if (member "deleted" (notmuch-search-get-tags)) (notmuch-search-tag (list "-deleted")) (notmuch-search-tag (list "+deleted"))))) (define-key notmuch-tree-mode-map "d" (lambda () "toggle deleted tag for message" (interactive) (if (member "deleted" (notmuch-tree-get-tags)) (notmuch-tree-tag (list "-deleted")) (notmuch-tree-tag (list "+deleted")))))) ;; Saved searches (setq notmuch-saved-searches '((:name "cheapbsd" :query "tag:inbox and tag:cheapbsd" :count-query "tag:inbox and tag:cheapbsd and tag:unread") (:name "icloud" :query "tag:inbox and tag:icloud" :count-query "tag:inbox and tag:icloud and tag:unread") (:name "sdf" :query "tag:inbox and tag:sdf" :count-query "tag:inbox and tag:sdf and tag:unread"))) (setq mml-secure-openpgp-sign-with-sender t) ;; Sign messages by default. (add-hook 'message-setup-hook 'mml-secure-sign-pgpmime) ;; Use msmtp (setq send-mail-function 'sendmail-send-it sendmail-program "msmtp" mail-specify-envelope-from t message-sendmail-envelope-from 'header mail-envelope-from 'header) (defun message-recipients () "Return a list of all recipients in the message, looking at TO, CC and BCC. Each recipient is in the format of `mail-extract-address-components'." (mapcan (lambda (header) (let ((header-value (message-fetch-field header))) (and header-value (mail-extract-address-components header-value t)))) '("To" "Cc" "Bcc"))) (defun message-all-epg-keys-available-p () "Return non-nil if the pgp keyring has a public key for each recipient." (require 'epa) (let ((context (epg-make-context epa-protocol))) (catch 'break (dolist (recipient (message-recipients)) (let ((recipient-email (cadr recipient))) (when (and recipient-email (not (epg-list-keys context recipient-email))) (throw 'break nil)))) t))) (defun message-sign-encrypt-if-all-keys-available () "Add MML tag to encrypt message when there is a key for each recipient. Consider adding this function to `message-send-hook' to systematically send encrypted emails when possible." (when (message-all-epg-keys-available-p) (mml-secure-message-sign-encrypt))) (add-hook 'message-send-hook #'message-sign-encrypt-if-all-keys-available) (setq notmuch-crypto-process-mime t) (defvar notmuch-hello-refresh-count 0) (defun notmuch-hello-refresh-status-message () (let* ((new-count (string-to-number (car (process-lines notmuch-command "count")))) (diff-count (- new-count notmuch-hello-refresh-count))) (cond ((= notmuch-hello-refresh-count 0) (message "You have %s messages." (notmuch-hello-nice-number new-count))) ((> diff-count 0) (message "You have %s more messages since last refresh." (notmuch-hello-nice-number diff-count))) ((< diff-count 0) (message "You have %s fewer messages since last refresh." (notmuch-hello-nice-number (- diff-count))))) (setq notmuch-hello-refresh-count new-count))) (add-hook 'notmuch-hello-refresh-hook 'notmuch-hello-refresh-status-message) (defun color-inbox-if-unread () (interactive) (save-excursion (goto-char (point-min)) (let ((cnt (car (process-lines "notmuch" "count" "tag:inbox and tag:unread")))) (when (> (string-to-number cnt) 0) (save-excursion (when (search-forward "inbox" (point-max) t) (let* ((overlays (overlays-in (match-beginning 0) (match-end 0))) (overlay (car overlays))) (when overlay (overlay-put overlay 'face '((:inherit bold) (:foreground "green"))))))))))) (add-hook 'notmuch-hello-refresh-hook 'color-inbox-if-unread) (custom-set-variables ;; custom-set-variables was added by Custom. ;; If you edit it by hand, you could mess it up, so be careful. ;; Your init file should contain only one such instance. ;; If there is more than one, they won't work right. '(notmuch-search-oldest-first nil)) (custom-set-faces) ;; custom-set-faces was added by Custom. ;; If you edit it by hand, you could mess it up, so be careful. ;; Your init file should contain only one such instance. ;; If there is more than one, they won't work right. #+end_src #+PROPERTY: header-args :tangle "feed.el" :noweb yes I get a lot of my news, and updates via Atom/RSS feeds. If I'm going to browse them in emacs I use elfeed. #+name: header #+begin_src emacs-lisp :exports none ;;; elfeed -- Just my elfeed config. -*- lexical-binding: t; -*- ;;; Commentary: ;;; Nothing yet. ;;; Code: #+end_src #+name: footer #+begin_src emacs-lisp :exports none (provide 'feed) ;;; feed.el ends here #+end_src #+name: feed-src #+begin_src emacs-lisp :exports code (use-package elfeed :defer t :bind (("C-c F" . elfeed))) (use-package elfeed-org :after (elfeed) :config (elfeed-org) (setq rmh-elfeed-org-files (list "~/.emacs.d/extra/elfeed.org"))) (use-package elfeed-tube :after (elfeed) :config (elfeed-tube-setup) :bind (:map elfeed-show-mode-map ("F" . elfeed-tube-fetch) ([remap save-buffer] . elfeed-tube-save) :map elfeed-search-mode-map ("F" . elfeed-tube-fetch) ([remap save-buffer] . elfeed-tube-save))) (use-package mpv :ensure (:host github :repo "kljohann/mpv.el")) (use-package elfeed-tube-mpv :after (elfeed-tube mpv) :bind (:map elfeed-show-mode-map ("C-c C-f" . elfeed-tube-mpv-follow-mode) ("C-c C-w" . elfeed-tube-mpv-where))) (use-package elfeed-goodies :after (elfeed) :config (elfeed-goodies/setup)) <