;;; init.el --- Emacs configuration. -*- lexical-binding: t -*- ;;; Commentary: ;;; Code: (defun tw/xdg-emacs-subdir (type name) "Get the name of a subdirectory called NAME under $XDG__HOME/emacs." (expand-file-name (concat (string-trim-right name "/") "/") (expand-file-name "emacs/" (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)))))) ;; 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.") (native-compile-target-directory ,(tw/xdg-emacs-subdir 'cache "eln") "Put native-compiled binaries in the cache.") (inhibit-startup-screen t "Don't show the startup screen with help links.") (indent-tabs-mode nil "Always use spaces to indent.") (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.") (global-hl-line-mode t "Highlight the current line in all buffers.") (column-number-mode t "Show the column number in the statusline.") (backup-directory-alist (("." . ,(tw/xdg-emacs-subdir 'data "backup"))) "Save all backup files in one place to avoid clutter.") (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.") (package-archives nil "Don't fetch packages from the internet; only get them from Guix.") (recentf-max-saved-items 10000 "Save lots of recently-opened files."))) (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 "." (1+ anything) "rc" eos) . conf-unix-mode) (,(rx ".gnuplot" eos) . gnuplot-mode) (,(rx ".aurora" eos) . python-mode))) ;; Load settings set through Custom. ;; (setq custom-file (locate-user-emacs-file "custom.el")) ;; (when (file-readable-p custom-file) ;; (load custom-file)) ;; `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 :commands (sml/setup) :demand t :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.") :config (sml/setup)) ;; General editor behaviour. (use-package ivy :commands (ivy-mode) :demand t :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 linum :hook (prog-mode . linum-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)) ;; 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 company :hook (prog-mode . company-mode) ;; Use TAB for selecting completions. Config from: ;; https://github.com/company-mode/company-mode/blob/master/company-tng.el :bind (:map company-active-map ([tab] . company-select-next) ([backtab] . company-select-previous)) :custom (company-minimum-prefix-length 2 "Start showing completion candidates slightly earlier.") (company-idle-delay (lambda () (if (company-in-string-or-comment) nil 0.3)) "Automatically show completions when editing non-comment parts of code.") :diminish company-mode) (use-package company-quickhelp :after (company autothemer) ; autothemer needed for `tw/get-catppuccin-color' :hook (company-mode . company-quickhelp-mode) :custom (company-quickhelp-delay 0.2 "Automatically show quickhelp for completions.") (company-quickhelp-color-background (tw/get-catppuccin-color "surface0") "Give the quickhelp tooltip a nicer background colour.") (company-quickhelp-color-foreground (tw/get-catppuccin-color "text") "Give the quickhelp tooltip the default text colour.")) (use-package company-posframe :after (company company-quickhelp) :hook (company-mode . company-posframe-mode) :diminish company-posframe-mode) (use-package flymake :after (evil which-key) :hook (prog-mode . flymake-mode) :init (which-key-add-key-based-replacements "e" '("errors" . "Flymake")) :bind (("el" . flymake-show-buffer-diagnostics) ("ep" . flymake-show-project-diagnostics)) :custom (flymake-suppress-zero-counters t "Don't show severity counters that are zero at all.")) (use-package flymake-collection :after (flymake) ;; 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 (python-mode c-mode c++-mode) :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 (advice-add 'eglot :after #'tw/help-is-eldoc)) ;; Non-LSP language modes. (use-package mmm-mode :commands (mmm-mode)) (use-package ledger-mode :mode (rx ".journal" eos)) (use-package cmake-mode :mode (rx (or (: (or bos "/") "CMakeLists.txt") ".cmake") eos)) (use-package puppet-mode :mode (rx ".pp" eos)) (use-package web-mode :mode (rx ".htm" (? "l") eos)) (use-package yaml-mode :mode (rx ".y" (? "a") "ml" eos)) (use-package rec-mode :mode (rx ".rec" eos)) (use-package hcl-mode :mode (rx "." (or "hcl" "nomad") eos)) (use-package graphviz-dot-mode :mode (rx ".dot" eos) :custom (graphviz-dot-view-command "xdot %s" "Use xdot for previewing graphviz files.")) (use-package geiser :after (evil) :commands (geiser geiser-eval-buffer geiser-eval-definition geiser-eval-region geiser-eval-last-sexp) :hook (scheme-mode . geiser-autodoc-mode) :config (evil-define-key '(normal visual) scheme-mode-map (kbd "i") #'geiser (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-racket :after (geiser)) (use-package geiser-guile :after (geiser)) ;; 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 ; TODO: port to flymake ;; :after (flycheck mmm-mode) ;; :mode (rx (or bos "/") "alidist/" (1+ anything) ".sh" eos) ;; :load-path "include/") (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 org-babel) :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." (switch-to-buffer (other-buffer))) (defun tw/new-buffer () "Open a new, empty buffer." (interactive) (switch-to-buffer (generate-new-buffer "untitled"))) (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.") :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 "bx") #'kill-buffer-and-window (kbd "ff") #'find-file (kbd "fs") #'save-buffer (kbd "h") help-map (kbd "sc") #'evil-ex-nohighlight (kbd "tl") #'toggle-truncate-lines (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)) (use-package evil-org :after (evil org) :hook (org-mode . evil-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 :after (evil) :commands (evil-commentary-mode) :demand t :config (evil-commentary-mode +1)) (use-package evil-expat :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 . evil-smartparens-mode)) (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) :hook (markdown-mode . evil-markdown-mode)) ;; Lisp features (use-package smartparens :commands (smartparens-global-mode) :demand t :config (smartparens-global-mode +1) (defun tw/sp-no-pair-single-quotes-p (id action context) "Return whether ` and ' shouldn't be auto-paired in the current `major-mode'." (memq major-mode (cons 'minibuffer-mode sp-lisp-modes))) ;; Don't auto-pair ` and ' chars in Lisp modes or the minibuffer. ;; Modify the global pair to not apply there. ;; The CLOSE parameter is not used in this case, so it is nil. (sp-pair "`" nil :unless '(tw/sp-no-pair-single-quotes-p)) (sp-pair "'" nil :unless '(tw/sp-no-pair-single-quotes-p sp-in-comment-p sp-in-string-p)) (sp-local-pair 'lisp-mode "`" "'" :actions '(wrap insert autoskip) :when '(sp-in-comment-p sp-in-string-p)) :functions (sp-pair sp-local-pair)) (use-package aggressive-indent :hook (; (scheme-mode . aggressive-indent-mode) ((emacs-lisp-mode common-lisp-mode) . aggressive-indent-mode))) (defun tw/lisp-evil-setup () "Set up evil in general `lisp-mode' buffers." (setq-local evil-symbol-word-search t ;; https://github.com/wcsmith/evil-args#customization evil-args-delimiters '(" "))) (add-hook 'lisp-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) ;;; init.el ends here