#+title: Nathan's Doom Emacs Configuration #+author: Nathan McCarty #+PROPERTY: header-args:emacs-lisp :tangle yes My doom emacs configuration * Basic Doom stuff The provided default ~config.el~ #+begin_src emacs-lisp ;;; $DOOMDIR/config.el -*- lexical-binding: t; -*- ;; Place your private configuration here! Remember, you do not need to run 'doom ;; sync' after modifying this file! ;; Some functionality uses this to identify you, e.g. GPG configuration, email ;; clients, file templates and snippets. It is optional. (setq user-full-name "Nathan McCarty" user-mail-address "nathan@mccarty.io") ;; Doom exposes five (optional) variables for controlling fonts in Doom: ;; ;; - `doom-font' -- the primary font to use ;; - `doom-variable-pitch-font' -- a non-monospace font (where applicable) ;; - `doom-big-font' -- used for `doom-big-font-mode'; use this for ;; presentations or streaming. ;; - `doom-unicode-font' -- for unicode glyphs ;; - `doom-serif-font' -- for the `fixed-pitch-serif' face ;; ;; See 'C-h v doom-font' for documentation and more examples of what they ;; accept. For example: ;; (setq doom-font (font-spec :family "FiraCode Nerd Font" :size 11 :weight 'semi-light) doom-variable-pitch-font (font-spec :family "Fira Sans" :size 15)) ;; ;; If you or Emacs can't find your font, use 'M-x describe-font' to look them ;; up, `M-x eval-region' to execute elisp code, and 'M-x doom/reload-font' to ;; refresh your font settings. If Emacs still can't find your font, it likely ;; wasn't installed correctly. Font issues are rarely Doom issues! ;; There are two ways to load a theme. Both assume the theme is installed and ;; available. You can either set `doom-theme' or manually load a theme with the ;; `load-theme' function. This is the default: ;; (setq doom-theme 'doom-solarized-dark) (use-package! solarized-theme :demand t :config (setq solarized-distinct-fringe-background t solarized-distinct-doc-face t solarized-scale-markdown-headlines t solarized-scale-org-headlines t) (load-theme 'solarized-selenized-dark t)) ;; This determines the style of line numbers in effect. If set to `nil', line ;; numbers are disabled. For relative line numbers, set this to `relative'. (setq display-line-numbers-type t) ;; If you use `org' and don't want your org files in the default location below, ;; change `org-directory'. It must be set before org loads! (setq org-directory "~/Org/") ;; Whenever you reconfigure a package, make sure to wrap your config in an ;; `after!' block, otherwise Doom's defaults may override your settings. E.g. ;; ;; (after! PACKAGE ;; (setq x y)) ;; ;; The exceptions to this rule: ;; ;; - Setting file/directory variables (like `org-directory') ;; - Setting variables which explicitly tell you to set them before their ;; package is loaded (see 'C-h v VARIABLE' to look up their documentation). ;; - Setting doom variables (which start with 'doom-' or '+'). ;; ;; Here are some additional functions/macros that will help you configure Doom. ;; ;; - `load!' for loading external *.el files relative to this one ;; - `use-package!' for configuring packages ;; - `after!' for running code after a package has loaded ;; - `add-load-path!' for adding directories to the `load-path', relative to ;; this file. Emacs searches the `load-path' when you load packages with ;; `require' or `use-package'. ;; - `map!' for binding new keys ;; ;; To get information about any of these functions/macros, move the cursor over ;; the highlighted symbol at press 'K' (non-evil users must press 'C-c c k'). ;; This will open documentation for it, including demos of how they are used. ;; Alternatively, use `C-h o' to look up a symbol (functions, variables, faces, ;; etc). ;; ;; You can also try 'gd' (or 'C-c c d') to jump to their definition and see how ;; they are implemented. #+end_src * Appearance and UI ** Centaur tabs #+begin_src emacs-lisp (after! centaur-tabs (setq centaur-tabs-set-icons t centaur-tabs-set-bar 'underflow centaur-tabs-style "alternate") (centaur-tabs-headline-match) (centaur-tabs-group-by-projectile-project)) #+end_src ** Mixed Pitch Mode Use mixed pitch mode in prose writing modes, to make the writing experience a bit more pleasant. This tweak applies to: - ~org-mode~ - ~markdown-mode~ #+begin_src emacs-lisp (use-package! mixed-pitch :hook (org-mode . mixed-pitch-mode) (markdown-mode . mixed-pitch-mode) :config (setq mixed-pitch-set-height t)) #+end_src Setting ~mixed-pitch-set-height~ is required to get ~mixed-pitch-mode~ to render fonts with the correct size in doom emacs, apparently. * Basic Editing ** Navigation *** Avy More modern ace-jump-mode Set up our key bindings #+begin_src emacs-lisp (after! avy (define-key! "C-:" 'avy-goto-char "C-'" 'avy-goto-char-2 "M-g f" 'avy-goto-line "M-g w" 'avy-goto-word-1 "M-g e" 'avy-goto-word-0) (cheatsheet-add-group 'Avy '(:key "C-:" :description "Goto Char") '(:key "C-'" :description "Goto Char (2)") '(:key "M-g f" :description "Goto line") '(:key "M-g w" :description "Goto word") '(:key "M-g e" :description "Goto word (0)"))) #+end_src *** Swiper Better isearch Override old isearch #+begin_src emacs-lisp (after! swiper (define-key! "C-s" 'swiper)) #+end_src ** Crux Smarter replacements for emacs built ins, with the following in use: - ~crux-smart-kill-line~ - Smart ~C-k~ replacement - ~crux-top-join-line~ - ~C-c ^~ Join two lines #+begin_src emacs-lisp (use-package! crux :bind (("C-k" . crux-smart-kill-line) ("C-c ^" . crux-top-join-line))) #+end_src ** string-inflection Automatically cycle case of names #+begin_src emacs-lisp (global-unset-key (kbd "C-q")) (use-package! string-inflection :bind (("C-q" . string-inflection-all-cycle))) (cheatsheet-add-group 'string-inflection '(:key "C-q" :description "Rotate case")) #+end_src ** Smart Hungry Delete Gobble up whitespace in a smarter way #+begin_src emacs-lisp (use-package! smart-hungry-delete :bind (("M-" . smart-hungry-delete-backward-char))) #+end_src ** Search *** Deadgrep Ripgrep, but from within emacs #+begin_src emacs-lisp (use-package! deadgrep :bind ("C-c s r" . deadgrep)) #+end_src * Org Mode Improvements to the best mode in emacs Setup some basic cosmetic improvements - Disable showing of emphasis markers - Show entities as utf-8 ~test~ #+begin_src emacs-lisp (setq org-hide-emphasis-markers t org-pretty-entities t) #+end_src Setup font lock for normal (non-heading) list items, to make things a bit more pleasnt to look at #+begin_src emacs-lisp (font-lock-add-keywords 'org-mode '(("^ *\\([-]\\) " 0 (prog1 () (compose-region (match-beginning 1) (match-end 1) ""))) ("^ *\\([+]\\) " 0 (prog1 () (compose-region (match-beginning 1) (match-end 1) ""))))) #+end_src Automatically add all files in the org dir to the agenda. This performs some filtering of the files returned from ~directory-files~ to exclude some things that would confuse org-agenda. We also setup an idle timer, with a short duration, only 30 seconds, to update the ~org-agenda-files~ list, as well as a longer regular timer with a duration of 300 seconds (5 minutes) to keep the agenda up to date even when we are actively using emacs. #+begin_src emacs-lisp ;; A function to update the org-agenda files (defun nm/update-org-agenda-files () (setq org-agenda-files (seq-filter (lambda (item) (and ;; Only accept things that are a directory, or an org file (or (file-directory-p item) (string-match-p ".*org$" item)) ;; Exclude the syncthing folder (not (string-match-p ".*stfolder$" item)) ;; Exclude the elfeed data folder (not (string-match-p (concat "^" (regexp-quote org-directory) "elfeed/.*") item)))) (directory-files-recursively org-directory directory-files-no-dot-files-regexp)))) (after! org ;; Set the agenda files on first start (nm/update-org-agenda-files) ;; Set up a timer to automatically update them in the background ;; This first one is an idle timer that runs when emacs has been idle for 30 seconds (run-with-idle-timer 25 t 'nm/update-org-agenda-files) ;; Then setup the non-idle timer for 5 minutes (run-with-timer "5 min" t 'nm/update-org-agenda-files)) #+end_src Set up two different timers for updating the org-agenda buffer. + Idle timer The idle timer simply updates the views unconditionally, and is set with a slightly higher timeout than our idle time that updates the org agenda files. This idle time can safely modify the state of the buffer without any other checks, as if the user is idle, they aren't doing anything in the buffer + Timer timer Setup a timer that attempts to update the org-agenda buffer every 5 minutes. This timer is a little bit unsafe, so it _could_ end up annyoing the user by updating the state while they are in the middle of doing something, so it cancels out and does nothing if the user is currently focused on the agenda buffer. #+begin_src emacs-lisp ;; Helper function to refresh the buffer (defun nm/org-agenda-refresh () ;; Attempt to get the org agenda buffer (when-let ((buffer (get-buffer org-agenda-buffer-name))) ;; Update the views (with-current-buffer buffer (org-agenda-redo-all)))) ;; Helper function to only refresh the buffer if it isn't focused, we don't want to annoy ourselves ;; by refreshing in the middle of faffing around, the idle timer will clean us up when we are done ;; anyway (defun nm/org-agenda-refresh-conditional () ;; Attempt to get the org agenda buffer (when-let ((buffer (get-buffer org-agenda-buffer-name))) ;; Get the currently selected window and see if it contains the org-agenda buffer, we don't want ;; to trample the current state if the user (me) is currently active in the buffer (when (not (eq (window-buffer (selected-window)) buffer)) ;; Since we are not in the org-agenda-buffer it is safe to rebuild the views (with-current-buffer buffer (org-agenda-redo-all))))) (after! org ;; Set up an idle time that's slighty longer than our update-org-agenda-files idle timer (run-with-idle-timer 30 t 'nm/org-agenda-refresh) (run-with-timer "5 min" t 'nm/org-agenda-refresh-conditional)) #+end_src Log state changes into a drawer #+begin_src emacs-lisp (after! org (setq org-log-into-drawer t)) #+end_src ** org-roam A second brain in emacs Here we: - Set the roam directory to be a sub-directory of the org directory, which I have in syncthing - Use a more informative display template, as we use ivy - Turn on db autosync - Setup dalies to add the time of the capture to the note #+begin_src emacs-lisp (use-package! org-roam :custom (org-roam-directory (concat org-directory "Roam/")) (org-roam-complete-everywhere t) :bind (("C-c r l" . org-roam-buffer-toggle) ("C-c r f" . org-roam-node-find) ("C-c r g" . org-roam-graph) ("C-c r i" . org-roam-node-insert) ("C-c r c" . org-roam-capture) ("C-c r T" . org-roam-dailies-capture-today) ("C-c r t" . org-roam-dailies-goto-today) :map org-mode-map ("C-M-i" . completion-at-point)) :config (setq org-roam-node-display-template (concat "${title:*} " (propertize "${tags:10}" 'face 'org-tag))) (org-roam-db-autosync-mode) (setq org-roam-dailies-capture-templates '(("d" "default" entry "* %<%I:%M %p>: %?" :if-new (file+head "%<%Y-%m-%d>.org" "#+title: %<%Y-%m-%d>\n"))))) #+end_src * General Modes ** Magit Further configuration for magit *** magit-todos Count the number of todos in the project in the ~magit-status~ buffer #+begin_src emacs-lisp (use-package! magit-todos :hook (magit-mode . magit-todos-mode)) #+end_src *** magit-delta Use delta for git diff display #+begin_src emacs-lisp (use-package! magit-delta :hook (magit-mode . magit-delta-mode)) #+end_src *** magit-wip-mode Stash autosaves inside of git #+begin_src emacs-lisp (magit-wip-mode) #+end_src ** System integration Various tools for interacting with the system from within emacs *** Terminal Doom already provides pretty nice vterm support, but lets take us a step further, using ~multi-vterm~ to provide ergonomic support for multiple terminals. Vterm really doesn't like being installed through emacs on nix, so proper support for it in my setup requires installing it through nix like so: #+begin_src nix :tangle no let emacsPackage = (emacsPackagesFor emacs).emacsWithPackages (epgks: with epkgs; [ vterm ]); in { environment.systemPackages = [ emacsPackage ]; } #+end_src **** [[https://github.com/suonlight/multi-vterm][multi-vterm]] Add ergonomic support for multiple vterm terminals #+begin_src emacs-lisp (use-package! multi-vterm :bind (("C-c o M" . multi-vterm) ("C-c o m" . multi-vterm-project))) #+end_src * Programming ** General Editing *** Sepraedit Edit indirect for comments Set the default mode to github flavored markdown, turn on smart use of fill column, and bind to the normal edit-indirect keybinding. #+begin_src emacs-lisp (use-package! separedit :bind (:map prog-mode-map ("C-c '" . separedit)) :config (setq separedit-default-mode 'gfm-mode separedit-continue-fill-column t)) #+end_src *** Rainbow delimiters Makes pairs of delimiters into pretty colors. Hook this into prog-mode #+begin_src emacs-lisp (use-package! rainbow-delimiters :hook (prog-mode . rainbow-delimiters-mode)) #+end_src ** LSP Mode Custom configuration for lsp-mode *** LSP UI Turn on the UI features we want **** Sideline Show as much as possible in the sideline #+begin_src emacs-lisp (after! lsp-ui (setq lsp-ui-sideline-show-diagnostics t lsp-ui-sideline-show-hover t lsp-ui-sideline-show-code-actions t)) #+end_src **** Peeking Turn on peeking, and show us the directory as well #+begin_src emacs-lisp (after! lsp-ui (setq lsp-ui-peek-enable t lsp-ui-peek-show-directory t)) #+end_src **** Documentation Show the documentation in a popup frame in the top right corner #+begin_src emacs-lisp (after! lsp-ui (setq lsp-ui-doc-enable t lsp-ui-doc-position 'top lsp-ui-doc-show-with-cursor t)) #+end_src ** Rust Configuration specific for rust *** LSP Tweaks Most of these are defaults, but I like having them explicit for my sanity #+begin_src emacs-lisp (after! lsp-mode (setq lsp-auto-configure t lsp-lens-enable t lsp-rust-analyzer-cargo-watch-command "clippy" lsp-rust-analyzer-cargo-watch-args ["--all-features"] lsp-rust-analyzer-experimental-proc-attr-macros t lsp-rust-analyzer-proc-macro-enable t lsp-rust-analyzer-use-rustc-wrapper-for-build-scripts t lsp-rust-analyzer-import-enforce-granularity t)) #+end_src * Composition Modes for handling plain text and prose ** Markdown Everybody's favorite markup format *** Markdown mode Make the following configuration tweaks to result in a better markdown experience: - Use a variable pitch font (this is prose after all) - Turn on header scaling - Default to gfm mode for readmes - Turn on auto-fill mode - Hide mark up - Fontify code blocks with the language's native mode The goal here is to create a more pretty and fluid composition environment for prose, closer to what you would get in a word processor, but without the horrors of wysiwyg. #+begin_src emacs-lisp (use-package! markdown-mode :mode ("README\\.md" . gfm-mode) :hook (markdown-mode . variable-pitch-mode) (markdown-mode . auto-fill-mode) :config (setq markdown-header-scaling t markdown-hide-markup t markdown-fontify-code-blocks-natively t)) #+end_src *** Grip mode Provide a live, rendered preview when editing markdown readmes using [[https://github.com/joeyespo/grip][grip]]. #+begin_src emacs-lisp (use-package! grip-mode :bind (:map markdown-mode-command-map ("g" . grip-mode))) #+end_src * Applications Emacs is good for more than just editing text ** RSS Use ~elfeed~ for RSS. Doom provides most of the configuration, but we'll make a few minor tweaks: - Automatically update the feed when opening elfeed - Set default filter to only show unread posts - Put the elfeed directory in the org dir (I have it in syncthing) - Create a global keybinding for elfeed (~C-x w~) #+begin_src emacs-lisp (use-package! elfeed :hook (elfeed-search-mode . elfeed-update) :hook (elfeed-show-mode . variable-pitch-mode) :hook (elfeed-show-mode . visual-line-mode) :bind ("C-x w" . elfeed) :config (setq elfeed-search-filter "@4-weeks-ago +unread" elfeed-db-directory (concat org-directory "elfeed/db/") elfeed-enclosure-default-dir (concat org-directory "elfeed/enclosures/") shr-max-width nil) (make-directory elfeed-db-directory t)) #+end_src