;;; init.el --- Emacs configuration. -*- lexical-binding: t -*- ;;; Commentary: ;;; Code: ;; Load settings set through Custom. ;; (setq custom-file (locate-user-emacs-file "custom.el")) ;; (when (file-readable-p custom-file) ;; (load custom-file)) (defun tw/xdg-emacs-subdir (type name) "Get the name of a subdirectory called NAME under $XDG__HOME/emacs." (expand-file-name (concat "emacs/" (string-trim-right name "/")) (pcase type ('cache (or (getenv "XDG_CACHE_HOME") "~/.cache/")) ('config (or (getenv "XDG_CONFIG_HOME") "~/.config/")) ('data (or (getenv "XDG_DATA_HOME") "~/.local/share/")) ;; The following two are Guix/GuixSD extensions. ('log (or (getenv "XDG_LOG_HOME") "~/.local/var/log/")) ('state (or (getenv "XDG_STATE_HOME") "~/.local/var/lib/")) (_ (error "Unknown XDG directory type: %S" type))))) ;; Put native-compiled binaries in the cache. This is NOT a `customize' option. (require 'comp) ; for `native-compile-target-directory' (setq native-compile-target-directory (tw/xdg-emacs-subdir 'cache "eln/")) ;; Global/built-in Custom settings ;; Apply these as early as possible so that e.g. the native-comp files go to the right place. (mapc (apply-partially #'apply #'customize-set-variable) `((native-comp-async-report-warnings-errors silent "Don't pop up Warnings buffer for native compilation.") ;; Emacs GUI customization. (inhibit-startup-screen t "Don't show the startup screen with help links.") (menu-bar-mode nil "Hide the menu bar globally.") (tool-bar-mode nil "Hide the tool bar globally.") (tooltip-mode nil "Show tooltips in the echo area instead.") (max-mini-window-height 3 "Let the echo area grow to a maximum of 3 lines, e.g. when using `eldoc-mode'.") (scroll-up-aggressively 0.0 "Don't recenter the window if the point moves off the page.") (scroll-down-aggressively 0.0 "Don't recenter the window if the point moves off the page.") ;; Indentation. (indent-tabs-mode nil "Always use spaces to indent.") ;; Niceties. (global-hl-line-mode t "Highlight the current line in all buffers.") (column-number-mode t "Show the column number in the statusline.") (electric-pair-mode t "Auto-pair suitable characters like parentheses.") (completion-cycle-threshold 6 "Allow cycling through completions if there are 6 or fewer of them.") ;; Text formatting. (sentence-end-double-space nil "Use a single space after a sentence.") ;; Make Emacs a good Guix citizen. (package-archives nil "Don't fetch packages from the internet; only get them from Guix.") ;; Don't clutter the disk with backup files. (make-backup-files nil "Don't create backup files. Backup files break hardlinks.") ;; Recent files and history. (recentf-max-saved-items 1000 "Save lots of recently-opened files.") (recentf-save-file ,(tw/xdg-emacs-subdir 'data "recentf.el") "Save recently-opened files here.") (recentf-mode t "Save recently-opened files.") (savehist-file ,(tw/xdg-emacs-subdir 'data "savehist.el") "Save minibuffer history here.") (savehist-mode t "Save minibuffer history on quit."))) (defalias 'yes-or-no-p #'y-or-n-p "Always use `y-or-n-p' when asking for confirmation.") ;; Custom modes depending on file names. (mapc (apply-partially #'add-to-list 'auto-mode-alist) `((,(rx (or bos "/") "PKGBUILD" eos) . sh-mode) (,(rx ".install" eos) . sh-mode) (,(rx bos "/tmp/neomutt-") . mail-mode) (,(rx ".eml" eos) . mail-mode) (,(rx "." (1+ anything) "rc" eos) . conf-unix-mode))) (add-to-list 'magic-mode-alist `(,(rx "#!" (* (not space)) (? "env" (+ space) (? "-S" (+ space))) (or "guile" "racket")) . scheme-mode)) (add-hook 'mail-mode-hook #'auto-fill-mode) ;; `use-package' requirements. (require 'package) (package-initialize t) (eval-when-compile (require 'use-package)) (use-package diminish) ; for using :diminish later (use-package bind-key) ; for using :bind later ;; Some packages below have `:commands (...) :demand t'. ;; We need :commands for the byte-compiler, but we want to load the package immediately. ;; Look and feel (set-face-attribute 'default nil :family "Hermit" :height 100) (use-package autothemer ;; The "catppuccin" theme is linked to the appropriate variant by guix home. :config (load-theme 'catppuccin-mocha t)) (defun tw/get-catppuccin-color (name) "Get the hex code of the Catppuccin color named NAME." (cl-loop for color in (autothemer--theme-colors autothemer-current-theme) when (string= (autothemer--color-name color) name) return (autothemer--color-value color))) (custom-set-faces `(cursor ((default :background ,(tw/get-catppuccin-color "rosewater"))) t "Make the cursor \"rosewater\", as recommended by Catppuccin upstream, overriding catppuccin/emacs.")) (use-package smart-mode-line :hook (after-init . sml/setup) :custom (sml/no-confirm-load-theme t "Stop Emacs from constantly asking for user confirmation.") (sml/mode-width 'right "Move the minor-mode list to the right of the modeline.") (sml/theme 'respectful "Make `smart-mode-line' blend in with the active theme.")) ;; General editor behaviour. (use-package ivy :commands (ivy-mode) :demand t :custom (ivy-use-selectable-prompt t "Allow selecting the ivy input as-is.") (ivy-use-virtual-buffers t "Show recentf and bookmarks in buffers list.") :config (ivy-mode +1) :diminish ivy-mode) (use-package counsel ; extra niceties for `ivy-mode' :after (ivy evil) ; evil for :bind-ing to :bind (("SPC" . counsel-M-x) ; doesn't work ("fr" . counsel-buffer-or-recentf) :map evil-visual-state-map ("SPC" . counsel-M-x)) :commands (counsel-mode) :demand t :config (counsel-mode +1) :diminish counsel-mode) (defun tw/counsel-dash-is-help () "Install `counsel-dash-at-point' as `evil-lookup-func'." (setq-local evil-lookup-func #'counsel-dash-at-point)) (use-package counsel-dash :bind (("K" . counsel-dash-at-point) ; TODO: just install as `evil-lookup-func'? ("dK" . counsel-dash) ("di" . counsel-dash-install-docset) ("da" . counsel-dash-activate-docset) ("dd" . counsel-dash-deactivate-docset)) :hook (python-mode . tw/counsel-dash-is-help) :custom (counsel-dash-docsets-path (tw/xdg-emacs-subdir 'data "dash-docsets/") "Store docsets in the XDG data directory.") (counsel-dash-browser-func 'eww "Open documentation pages using `eww' instead of an external browser.") (counsel-dash-enable-debugging nil "Disable popping up useless warnings.")) (use-package rainbow-mode :after (evil) :bind (("tR" . rainbow-mode))) (use-package form-feed :commands (global-form-feed-mode) :demand t :config (global-form-feed-mode +1) :diminish form-feed-mode) (use-package display-line-numbers ;; Included in Emacs >= 26. Better than `linum-mode'. ;; There is also `global-display-line-numbers-mode', but that also ;; enables line numbers in help windows, which I don't want. :hook (prog-mode conf-mode alidist-mode)) (use-package which-key :commands (which-key-mode) :demand t :config (which-key-mode +1) :diminish which-key-mode) (use-package undo-tree :after (evil) ; for our :bind-ing :bind (("U" . undo-tree-visualize)) :custom (undo-tree-history-directory-alist `(("." . ,(tw/xdg-emacs-subdir 'data "undo-tree-history/"))) "Store all `undo-tree' history in a single directory, instead of next to the associated file.") :commands (global-undo-tree-mode) :demand t ; this is required so that the :config stanza is actually run asap despite :bind :config (global-undo-tree-mode +1) :diminish undo-tree-mode) ;; IDE-like features. (use-package vc :after (which-key evil) :init (which-key-add-key-based-replacements "g" '("vc" . "Version control") "gM" '("merge" . "Version control merging")) :bind-keymap ("g" . vc-prefix-map)) (use-package corfu ; https://github.com/minad/corfu :hook (prog-mode ledger-mode shell-mode eshell-mode) :custom (corfu-auto t "Show completion popup after a few seconds automatically.") :diminish corfu-mode) (use-package corfu-doc :after (corfu) :hook (corfu-mode) :diminish corfu-doc-mode) (use-package flyspell :hook mail-mode) (use-package flymake :after (evil which-key) :demand t ; needed for `flymake-collection' :hook prog-mode :init (which-key-add-key-based-replacements "e" '("errors" . "Flymake")) :bind (("eb" . flymake-start) ("ec" . display-local-help) ; Show the error message at point in the minibuffer. ; `flymake' also shows it using `eldoc', but documentation ; seems to override error messages. ; `flymake-show-diagnostic' only says "Nothing at point". ("el" . flymake-show-buffer-diagnostics) ("ep" . flymake-show-project-diagnostics) ("en" . flymake-goto-next-error) ("eN" . flymake-goto-prev-error) ("ev" . flymake-running-backends) ("eV" . flymake-disabled-backends)) :custom (flymake-suppress-zero-counters nil "Show severity counters even when they are zero.")) (use-package flymake-collection :after (flymake) :demand t ; we need it loaded now ;; This needs to be called in `after-init-hook' so that all other ;; packages' `:flymake-hook's are processed before f-c-hook-setup is ;; called. See https://github.com/mohkale/flymake-collection. :hook (after-init . flymake-collection-hook-setup)) ;; Language Server Protocol. (defun tw/help-is-eldoc (&rest _) "Set up `evil-lookup-func' to display the `eldoc' buffer." (setq-local evil-lookup-func #'eldoc-doc-buffer)) (use-package eglot ;; I have clang (for clangd) and python-lsp-server installed. ;; `:hook' adds `-mode' to the package name, but `eglot-mode' doesn't exist. :hook ((python-mode c-mode c++-mode) . eglot-ensure) :commands (eglot) :custom (eglot-autoshutdown t "Shut down language servers after deleting their last associated buffer.") (eglot-sync-connect 0.1 "Wait for the language server in the background if it takes longer than 100ms.") :config ;; TODO: only run `tw/help-is-eldoc' if `eglot-managed-p' is true. (add-hook 'eglot-managed-mode-hook #'tw/help-is-eldoc)) ;; Non-LSP language modes. (use-package cmake-mode :mode (rx (or (: (or bos "/") "CMakeLists.txt") ".cmake") eos)) (use-package gnuplot :commands (gnuplot-mode gnuplot-make-buffer) :mode ((rx ".gnuplot" eos) . gnuplot-mode)) (use-package graphviz-dot-mode :mode (rx ".dot" eos) :custom (graphviz-dot-view-command "xdot %s" "Use xdot for previewing graphviz files.")) (use-package hcl-mode :mode (rx "." (or "hcl" "nomad") eos)) (use-package mmm-mode :commands (mmm-mode) ;; Don't highlight submodes specially at all. The default background is annoying. :custom-face (mmm-default-submode-face ((t (:background nil))))) (use-package puppet-mode :mode (rx ".pp" eos)) (use-package python :after (flymake-collection) :commands (python-mode) :mode (((rx ".py" (? (or ?\i ?\w)) eos) . python-mode) ((rx ".aurora" eos) . python-mode)) ;; :flymake-hook would be better, but it fails with error: (void-variable ;; for). Somehow `cl-loop' is getting parsed wrong... ;; https://github.com/mohkale/flymake-collection#associating-checkers-with-major-modes :init (add-to-list 'flymake-collection-config '(python-mode flymake-collection-flake8 flymake-collection-mypy (flymake-collection-pycodestyle :disabled t) flymake-collection-pylint))) (use-package rec-mode :mode (rx ".rec" eos)) (use-package sh-script ; built-in :custom (sh-basic-offset 2 "Use 2 spaces for `sh-mode' indents.")) (use-package tcl :mode ((rx ".tcl" eos) . tcl-mode) :magic ((rx "#%Module1.0") . tcl-mode)) (use-package web-mode :mode (rx "." (or "htm" "html" "js" "css" "scss") eos)) (use-package yaml-mode :mode (rx ".y" (? "a") "ml" eos)) (use-package ledger-mode :mode (rx ".journal" eos) :custom (ledger-default-date-format ledger-iso-date-format "Use hledger-style dates.") (ledger-reconcile-default-date-format ledger-iso-date-format "Use hledger-style dates.") (ledger-reconcile-default-commodity "€" "Make euros the default currency.") (ledger-post-account-alignment-column 2 "Use 2-space indents.") (ledger-post-amount-alignment-at :decimal "Align amounts at decimal points/commas.") (ledger-post-amount-alignment-column 52 "Align amounts' decimal points to the 52nd column.") (ledger-highlight-xact-under-point nil "Don't highlight the transaction at point.")) (use-package geiser :after (evil) :commands (geiser geiser-eval-buffer geiser-eval-definition geiser-eval-region geiser-eval-last-sexp geiser-mode-switch-to-repl geiser-mode-switch-to-repl-and-enter) :hook (scheme-mode . geiser-autodoc-mode) :config (evil-define-key '(normal visual) scheme-mode-map (kbd "i") #'geiser-mode-switch-to-repl (kbd "I") #'geiser-mode-switch-to-repl-and-enter (kbd "eb") #'geiser-eval-buffer (kbd "ef") #'geiser-eval-definition (kbd "er") #'geiser-eval-region (kbd "el") #'geiser-eval-last-sexp) :defines scheme-mode-map) (use-package geiser-guile :after (geiser)) ;; Org-mode (use-package org) (use-package ob ; org-babel :after (org)) (use-package outline :commands (outline-mode outline-minor-mode) :custom ;; Mirror the default "C-c @" binding for `outline-minor-mode'. (outline-minor-mode-prefix (kbd "@") "Use localleader for `outline-minor-mode' keybindings.")) ;; My own custom packages, and stuff that isn't on MELPA. ;; (use-package actionlint ; TODO: port to flymake ;; :after (flycheck) ;; :load-path "include/") (use-package alidist-mode :load-path "include/" :commands (alidist-mode) :mode (rx (or bot "/") "alidist/" (1+ (not ?\/)) ".sh" eot)) (use-package bemscript-mode :load-path "include/" :mode (rx ".bem" eos)) (use-package ifm-mode :load-path "include/" :mode (rx ".ifm" eos)) (use-package pam-env-mode :load-path "include/" :mode (rx (or bos "/") (or "pam_env.conf" ".pam_environment") eos)) (use-package environmentd-mode :load-path "include/" :mode (rx (or bos "/") (or (: (? "etc/") "environment") (: ".environment.d/" (1+ (not ?\/)) ".conf")) eos)) (use-package org-latex-classes :after (ox-latex) :load-path "include/") (use-package ob-rec ;; `org-babel' hooks for `rec-mode' :after (org ob rec-mode) :load-path "include/") (use-package vcard-mode :load-path "include/" :mode (rx "." (or "vcf" "vcard") eos)) ;; Vim keybindings. (defun tw/switch-to-other-buffer () "Switch to the last-used buffer." (interactive) (switch-to-buffer (other-buffer))) (defun tw/new-buffer () "Open a new, empty buffer." (interactive) (switch-to-buffer (generate-new-buffer "untitled"))) (defun tw/delete-current-buffer-file () "Ask for confirmation, then delete the file associated with the current buffer." (interactive) (let ((buffer (current-buffer))) (when (yes-or-no-p (concat "Delete `" (buffer-file-name buffer) "'?")) (delete-file (buffer-file-name buffer)) (kill-buffer buffer)))) (use-package evil :after (which-key) :commands (evil-mode evil-ex-nohighlight) :init (setq evil-want-keybinding nil) ; evil-collection needs this :custom (evil-undo-system 'undo-tree "Use `undo-tree' for evil's undo-redo function.") (evil-want-minibuffer t "Use evil bindings in the minibuffer too.") (evil-want-C-u-scroll t "Scroll on C-u in normal mode, not `universal-argument'.") (evil-want-C-u-delete t "Delete line on C-u in insert mode, not `universal-argument'.") (evil-want-Y-yank-to-eol t "Yank from point to end-of-line on Y.") (evil-symbol-word-search t "Always search by full variable names when using * and #.") :config (evil-mode +1) (evil-set-leader '(normal visual) (kbd "SPC")) ; (evil-set-leader '(normal visual) (kbd "\\") t) ; (evil-define-key '(normal insert visual replace) 'global (kbd "C-s") #'save-buffer) ;; Global major-mode-independent keys should be defined here. Major ;; mode-dependent keys (e.g. for launching a REPL) should go under ;; instead. Use `use-package' `:bind' for those. (evil-define-key '(normal visual) 'global ;; These keybindings mirror the default Spacemacs ones because I have ;; muscle memory of those. (kbd ":") #'eval-expression (kbd "TAB") #'tw/switch-to-other-buffer (kbd "bb") #'switch-to-buffer (kbd "bd") #'kill-current-buffer (kbd "bn") #'tw/new-buffer (kbd "br") #'revert-buffer-quick (kbd "bw") #'read-only-mode (kbd "bx") #'kill-buffer-and-window (kbd "fd") #'tw/delete-current-buffer-file (kbd "ff") #'find-file (kbd "fs") #'save-buffer (kbd "h") help-map (kbd "sc") #'evil-ex-nohighlight (kbd "tf") #'auto-fill-mode (kbd "tl") #'toggle-truncate-lines (kbd "tn") #'display-line-numbers-mode (kbd "u") #'universal-argument (kbd "w") evil-window-map (kbd "wd") #'evil-window-delete ; analogous to "bd" (kbd "wx") #'kill-buffer-and-window) ; analogous to "bx" (which-key-add-key-based-replacements ;; Names are a `cons' of a short name and a long name. ;; E.g. for b, "buffer" is shown under "b" in the "" menu, ;; while "Buffers" is shown as the title in the "b" menu. "b" '("buffer" . "Buffers") "f" '("file" . "Files") "h" '("help" . "General help and documentation") "q" '("quit" . "Finish editing the current buffer in emacsclient") "s" '("search" . "Search operations and options") "t" '("toggle" . "Toggles and quick settings") "w" '("window" . "Windows")) :functions (evil-define-key evil-set-leader evil-define-key* evil-window-delete evil-delay) :defines (evil-visual-state-map)) (use-package evil-collection :after (evil) :commands (evil-collection-init) :demand t :config (evil-collection-init) :diminish evil-collection-unimpaired-mode) (use-package evil-org :after (evil org) :hook org-mode :config (evil-define-key '(normal visual) org-mode-map (kbd "\\") #'org-ctrl-c-ctrl-c (kbd "'") #'org-edit-src-code (kbd "e") #'org-export-dispatch) (evil-define-key '(normal visual) org-src-mode-map (kbd "'") #'org-edit-src-exit (kbd "\\") #'org-edit-src-save (kbd "a") #'org-edit-src-abort)) (use-package evil-replace-with-register :after (evil) :commands (evil-replace-with-register-install) :demand t ;; :custom (evil-replace-with-register-key "gR" "Use the default key.") :config (evil-replace-with-register-install)) (use-package evil-commentary ; e.g. "gcc" / "gcap" to comment out blocks of text :after (evil) :commands (evil-commentary-mode) :demand t :config (evil-commentary-mode +1) :diminish evil-commentary-mode) (use-package evil-expat ; for :reverse, :remove, :rename, :colo, :g*, ... ex commands :after (evil)) (use-package evil-surround :after (evil) :commands (global-evil-surround-mode) :demand t :config (global-evil-surround-mode +1)) (use-package evil-smartparens :after (evil smartparens) :hook smartparens-enabled-hook) (use-package evil-multiedit ;; See: https://github.com/hlissner/evil-multiedit#usage :commands (evil-multiedit-default-keybinds) :demand t :config (evil-multiedit-default-keybinds)) (use-package evil-args :after (evil) :config ;; Bind evil-args text objects only. ;; See https://github.com/wcsmith/evil-args for more bindings. (define-key evil-inner-text-objects-map "a" 'evil-inner-arg) (define-key evil-outer-text-objects-map "a" 'evil-outer-arg)) (use-package evil-numbers :after (evil) :bind (("+" . evil-numbers/inc-at-pt) ("-" . evil-numbers/dec-at-pt))) (use-package evil-goggles ; visual previews for edit operations :after (evil) :commands (evil-goggles-mode evil-goggles-use-diff-faces) :demand t :config (evil-goggles-mode +1) (evil-goggles-use-diff-faces) :diminish evil-goggles-mode) (use-package evil-traces ; visual previews for :ex commands :after (evil) :commands (evil-traces-mode evil-traces-use-diff-faces) :demand t :config (evil-traces-mode +1) (evil-traces-use-diff-faces) :diminish evil-traces-mode) (use-package evil-markdown :after (evil markdown-mode) :hook markdown-mode) (use-package evil-tex :after (evil tex) :hook tex-mode) (use-package evil-text-object-python :after (evil python) :hook (python-mode . evil-text-object-python-add-bindings)) ;; Lisp features (use-package aggressive-indent :hook (scheme-mode emacs-lisp-mode common-lisp-mode sh-mode)) (defun tw/lisp-evil-setup () "Set up evil in general `lisp-mode' buffers." ;; https://github.com/wcsmith/evil-args#customization (setq-local evil-args-delimiters '(" "))) ;; Sadly, not all Lisp modes derive from `lisp-mode'. (add-hook 'lisp-mode-hook #'tw/lisp-evil-setup) (add-hook 'lisp-data-mode-hook #'tw/lisp-evil-setup) ; for elisp (add-hook 'scheme-mode-hook #'tw/lisp-evil-setup) ;; buffer-locally set `evil-lookup-func' (used on K keys) for ;; languages where something better than man pages is available ;; (e.g. `describe-symbol' for elisp). (defun tw/elisp-lookup-func () "Show help in `emacs-lisp-mode' buffers." (let ((sym (symbol-at-point))) (if sym (describe-symbol sym) (call-interactively #'describe-symbol)))) (defun tw/emacs-lisp-evil-setup () "Set up evil in `emacs-lisp-mode' buffers." (setq-local evil-lookup-func #'tw/elisp-lookup-func)) (add-hook 'emacs-lisp-mode-hook #'tw/emacs-lisp-evil-setup) (evil-define-key '(normal visual) emacs-lisp-mode-map (kbd "eb") #'eval-buffer (kbd "ef") #'eval-defun (kbd "er") #'eval-region (kbd "el") #'eval-last-sexp (kbd "ep") #'eval-print-last-sexp) ;; For some reason, in `diff-mode', space isn't assigned to the leader key ;; automatically, unlike in other modes. (evil-define-key '(normal visual) diff-mode-shared-map ; not `diff-mode-map', else toggling `read-only-mode' destroys the binding (kbd "SPC") #'evil-send-leader) ;; Guix-related .dir-locals.el entries. These are fine; don't prompt every time. (mapc (apply-partially #'add-to-list 'safe-local-eval-forms) '((modify-syntax-entry 126 "'") (modify-syntax-entry 36 "'") (modify-syntax-entry 43 "'") (progn (require 'lisp-mode) (defun emacs27-lisp-fill-paragraph (&optional justify) (interactive "P") (or (fill-comment-paragraph justify) (let ((paragraph-start (concat paragraph-start "\\|\\s-*\\([(;\"]\\|\\s-:\\|`(\\|#'(\\)")) (paragraph-separate (concat paragraph-separate "\\|\\s-*\".*[,\\.]$")) (fill-column (if (and (integerp emacs-lisp-docstring-fill-column) (derived-mode-p 'emacs-lisp-mode)) emacs-lisp-docstring-fill-column fill-column))) (fill-paragraph justify)) t)) (setq-local fill-paragraph-function #'emacs27-lisp-fill-paragraph)) ;; Forms used by Guix upstream. (add-to-list 'completion-ignored-extensions ".go") (setq-local guix-directory (locate-dominating-file default-directory ".dir-locals.el")) (with-eval-after-load 'yasnippet (let ((guix-yasnippets (expand-file-name "etc/snippets/yas" (locate-dominating-file default-directory ".dir-locals.el")))) (unless (member guix-yasnippets yas-snippet-dirs) (add-to-list 'yas-snippet-dirs guix-yasnippets) (yas-reload-all)))) (let ((root-dir-unexpanded (locate-dominating-file default-directory ".dir-locals.el"))) (when root-dir-unexpanded (let* ((root-dir (expand-file-name root-dir-unexpanded)) (root-dir* (directory-file-name root-dir))) (unless (boundp 'geiser-guile-load-path) (defvar geiser-guile-load-path 'nil)) (make-local-variable 'geiser-guile-load-path) (require 'cl-lib) (cl-pushnew root-dir* geiser-guile-load-path :test #'string-equal)))))) ;;; init.el ends here