Skip to content

fr33zing/fr33macs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

62 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Fr33macs, my Nix flake Emacs config

Read this document in this repository's github.io page or in Emacs for the best experience.

Links

About

This document is tangled to create my Emacs configuration. If you want to try it for yourself, click here.

Mostly intended for use with Wayland on Linux, this configuration uses the pure-gtk version of Emacs 29. The build process, including fetching gathering all external dependencies, is handled by the Nix flake.

Build steps

  1. Fetch and build external dependencies, including any developer tools needed for the languages I use, such as: compilers, libraries, formatters, linters, language servers.
  2. Use sass to convert export.scss into CSS. That CSS is then used to style this document when exported to HTML.
  3. Tangle configuration from config.org.
  4. Substitute variables from Nix flake (including the export CSS) into tangled configuration.
  5. Build Emacs with tangled and subtituted configuration.
  6. Wrap Emacs binary to give it exclusive access to its external dependencies.

Scripts

Utility scripts are located in ~util/~ and they’re used to cover parts of the Nix flake build that wouldn’t be worth it to implement in Nix.

Tangling

This file is not tangled using org-babel-table. Instead, it’s tangled using a bare-bones implementation in ~util/tangle.py~, and thus no fancy tangle features are supported. The header-args property at the top of the file is not even parsed, it’s only there to maintain compatibility with other tangle implementations.

Substitution

The function getnix is used to get a variable from the Nix flake, sort of like getenv but not exactly. It isn’t actually a function that exists. Instead, ~util/substitute.py~ is used to substitute all instances of (getnix "variableName") with the appropriate Elisp literal.

Formatting

Blank lines

Different elements, (i.e. text, blocks, headings, etc), should be separated with blank lines except between multiple headings. Omitting the blank line between multiple headings is necessary for heading visibility cycling to work as expected.

Tags in comments

While reading this document you may see what look to be XML tags, e.g.:

(defun something-spanning-multiple-src-blocks ()
  (beginning-of-the-defun))
;; <defun>
(middle-of-the-defun)
  (end-of-the-defun)) ; <-- closes the defun
;; </defun>

These tags are not parsed. They are only used to indicate to the reader that code in the same scope may be spread across multiple src blocks.

Try it yourself

Warning

Back up your .emacs.d directory if you already have a configuration you care about. I don’t think that this will affect it, but back it up anyway just to be safe.

Instructions

If you don’t have Nix installed, click here.

If you have the experimental features nix-command and flakes enabled:

nix run github:fr33zing/fr33macs

If you don’t have them enabled but you want to enable them, click here.

nix --experimental-features 'nix-command flakes' run github:fr33zing/fr33macs

Basic setup

Disable unwanted UI elements and startup messages.

(menu-bar-mode -1)
(tool-bar-mode -1)
(scroll-bar-mode -1)
(setq inhibit-startup-message t
      use-dialog-box nil)

Enable some things which should probably be default.

(setq require-final-newline t)
(setq frame-inhibit-implied-resize t)
(global-hl-line-mode)

Enable relative line numbers globally.

(setq display-line-numbers-type 'relative)
(global-display-line-numbers-mode)

Try to always split window vertically.

(setq split-width-threshold 0)
(setq split-height-threshold nil)

Theme

(add-to-list 'custom-theme-load-path (getnix "themeDir"))
(load-theme 'catppuccin t)
(setq catppuccin-flavor (getnix "themeVariant"))
(catppuccin-reload)

Variable definitions

(defvar my:cursor-color (catppuccin-get-color 'sapphire)
  "Default cursor color.")

(defvar my:switch-window-cursor-color (catppuccin-get-color 'red)
  "Cursor color when switching windows.")

(defvar my:code-color (catppuccin-get-color 'text)
  "Default color for text that is considered code.")

Customization

Notice: Not all theming is done in this section.

(set-face-attribute 'default t :font (getnix "font"))

(set-cursor-color my:cursor-color)
(set-face-attribute 'region nil
                    :background (catppuccin-get-color 'surface2))

Dash

Dash is a library for dealing with lists.

(use-package dash :ensure t)

Map

Map provides generic map-manipulation functions that work on alists, plists, hash-tables, and arrays.

(use-package map)

Subr-x

Less commonly used functions that complement basic APIs.

(use-package subr-x)

My library

Notice: Not all functions are defined in this section. There are some less general / more context-specific functions that are scattered around with no particular rhyme or reason.

Advice

(defun my:advice:be-quiet (orig-fun &rest args)
  "Advice function: Suppress messages in ORIG-FUN.

Example:

(advice-add 'svg-tag-mode-on :around #'my:advice:be-quiet)"
  (let ((inhibit-message t)
        (message-log-max nil))
    (apply orig-fun args)))
(defun my:advice:nav-flash-show (&rest _)
  "Advice function: Call `nav-flash-show' after a command.

Example:

(advice-add 'avy-goto-char :after #'my:advice:nav-flash-show)"
  (nav-flash-show))
(defun my:advice:always-force-save (&rest _)
  (set-buffer-modified-p t))

(advice-add 'save-buffer :before #'my:advice:always-force-save)
(defun my:advice:save-after-code-actions (&rest _)
  (save-buffer))

(advice-add 'lsp-ui-sideline-apply-code-actions :after #'my:advice:save-after-code-actions)

Timing

  • TODO Add debounce-eval-immediately
(setq my:debounce-timers (make-hash-table :test 'equal))
(defun my:debounce (name secs form)
  "Begin a timer for SECS seconds named NAME to delay the evaluation
of FORM, typically to prevent something expensive from running
too frequently. If a pending debounce timer exists with the same
NAME it is cancelled and replaced.

Example:

(my:debounce \"print-it\" 0.5 '(print \"it\"))"
  (if-let ((old-timer (map-elt my:debounce-timers name)))
          (cancel-timer old-timer))
  (let* ((new-timer-fn `(progn
          (setq my:debounce-timers (map-delete my:debounce-timers ,name))
          (eval ,form)))
         (new-timer (run-with-timer secs nil 'eval new-timer-fn)))
        (map-put! my:debounce-timers name new-timer)))

Windows

This function was adapted from a StackOverflow answer by Zhro.

(defun my:delete-window-maybe-kill-buffer (&optional window)
  "Delete the window.
If WINDOW is nil, delete the current window.
If no other window shows its buffer, kill the buffer too."
  (interactive)
  (let ((buf (window-buffer window)))
    (if (> (length (window-list)) 1)
        (delete-window window)
      (unless (get-buffer-window buf 'visible) (kill-buffer buf))
      (kill-buffer buf))))

It’s irritating to need to switch to a temporary window to close it. These functions are used to make closing temporary windows more convenient.

(defun my:temporary-window-p (&optional window)
  "Decide if the window is temporary.
If WINDOW is nil, use the current window."
  (let ((buffer (window-buffer window)))
    (with-current-buffer buffer
      (not (or (buffer-file-name buffer)
               (-contains? my:not-temporary-major-modes major-mode)
               (--some (-contains? my:not-temporary-minor-modes it)
                       local-minor-modes))))))

(defun my:delete-window (window)
  "Delete WINDOW or abort if WINDOW is a minibuffer."
  (if (window-minibuffer-p window)
      (abort-recursive-edit)
      (my:delete-window-maybe-kill-buffer window)))

(defun my:next-temporary-window ()
  "Attempt to find a minibuffer first. If no minibuffer is found,
then starting at the current window, find the next temporary
window."
  (interactive)
  (if-let ((minibuffer (active-minibuffer-window)))
    minibuffer
    (-first #'my:temporary-window-p (window-list))))

(defun my:delete-next-temporary-window ()
  "Starting at the current window, find the next temporary window
and delete it."
  (interactive)
  (if-let ((window (my:next-temporary-window)))
    (my:delete-window window)))

(defun my:delete-next-temporary-window-or-kb-quit ()
  "Starting at the current window, find the next temporary window
and delete it. If there is none, send keyboard-quit instead."
  (interactive)
  (if-let ((window (my:next-temporary-window)))
    (my:delete-window window)
    (keyboard-quit)))

When a window’s major or minor mode matches any in these lists, that window will never be considered temporary.

(setq my:not-temporary-major-modes
  '(lisp-interaction-mode magit-status-mode))

(setq my:not-temporary-minor-modes
  '(org-src-mode))

Errors

(defun my:fix-next-error ()
  "Jump to the next error and open code actions"
  (interactive)
  (next-error)
  (call-interactively 'lsp-ui-sideline-apply-code-actions)
  (save-buffer))

(defun my:fix-previous-error ()
  "Jump to the previous error and open code actions"
  (interactive)
  (previous-error)
  (call-interactively 'lsp-ui-sideline-apply-code-actions)
  (save-buffer))

Major-mode setup

(defmacro my:set-formatter (major-mode lang-name formatter)
  (let ((hook (intern (concat (symbol-name major-mode) "-hook"))))
  `(add-hook ',hook (lambda ()
     (setq-local format-all-formatters '((,lang-name ,formatter)))))))

Backups

Prevent clobbering symlinks.

(setq backup-by-copying t)

Use versioned backups.

(setq version-control t)

Prevent backup files from being littered all over the place.

(setq backup-directory-alist
      '(("." . "~/.emacs-backups/")))

Delete excess backup versions silently.

(setq delete-old-versions t
      kept-new-versions 6
      kept-old-versions 2)

Key bindings

Hydras

Pretty-hydra is used to create action menus. I’ve chosen to use hydras as the foundation for my custom keybind scheme to improve discoverability and reduce cognitive load.

(use-package major-mode-hydra
  :ensure t)

(use-package pretty-hydra
  :ensure t
  :config
  (set-face-attribute 'hydra-face-red nil
    :foreground (catppuccin-get-color 'red))
  (set-face-attribute 'hydra-face-blue nil
    :foreground (catppuccin-get-color 'sapphire))
  (set-face-attribute 'hydra-face-pink nil
    :foreground (catppuccin-get-color 'pink))
  (set-face-attribute 'hydra-face-teal nil
    :foreground (catppuccin-get-color 'teal))
  (set-face-attribute 'hydra-face-amaranth nil
    :foreground (catppuccin-get-color 'mauve))
    ;; <config>

Notice: Not all hydras are defined in this section. Other sections that define hydra(s):

Code

(pretty-hydra-define hydra:code
  (:exit t :idle 0.25)
  ("Action"
   (("." lsp-ui-sideline-apply-code-actions "Code actions at point")
    ("o" lsp-organize-imports               "Organize imports")
    ("r" lsp-rename                         "Rename symbol"))
   "Find"
   (("n" next-error                         "Next error"     :exit nil)
    ("p" previous-error                     "Previous error" :exit nil)
    ("d" lsp-ui-peek-find-definitions       "Definitions")
    ("R" lsp-ui-peek-find-references        "References"))))

File

(pretty-hydra-define hydra:file
  (:exit t :idle 0.25)
  ("Switch"
   (("f" find-file         "Find file")
    ("p" project-find-file "Find file in project"))))

Buffer

(defun my:last-buffer ()
  (interactive)
  (switch-to-buffer (other-buffer (current-buffer) 1)))

(pretty-hydra-define hydra:buffer
  (:exit t :idle 0.25)
  ("Switch"
   (("b" bufler-switch-buffer "Quick switch")
    ("i" bufler-list          "List buffers")
    ("]" next-buffer          "Next buffer"     :exit nil)
    ("[" previous-buffer      "Previous buffer" :exit nil)
    ("l" my:last-buffer       "Last buffer"))
   "Actions"
   (("s" save-buffer         "Save buffer")
    ("k" kill-current-buffer "Kill buffer")
    ("K" kill-buffer         "Kill other buffer")
    ("r" revert-buffer       "Revert current buffer")
    ("R" rename-buffer       "Rename current buffer"))))

Window

(pretty-hydra-define hydra:window
  (:exit t :idle 0.25)
  ("Switch"
   (("w" my:switch-window "Quick switch"))
   "Actions"
   (("k" delete-window             "Kill window")
    ("K" switch-window-then-delete "Kill other window")
    ("s" evil-window-new           "Split window horizontally")
    ("v" evil-window-vnew          "Split window vertically"))))

Toggle

(pretty-hydra-define hydra:toggle
  (:exit t :idle 0.25)
  ("Toggle"
   (("w" whitespace-mode      "Whitespace visualization")
    ("f" format-all-mode      "Format on save")
    ("h" lsp-inlay-hints-mode "Inlay hints"))))

Leader key

  (pretty-hydra-define hydra:leader
    (:exit t :idle 0.25)
    ("Hydra"
     (("m" major-mode-hydra  "Mode-specific")
      ("t" hydra:toggle/body "Toggle")
      ("c" hydra:code/body   "Code")
      ("f" hydra:file/body   "File")
      ("b" hydra:buffer/body "Buffer")
      ("w" hydra:window/body "Window")
      ("a" hydra:avy/body    "Avy")
      ("g" hydra:git/body    "Git"))
     "Shortcut"
     (("SPC" project-find-file                  "Find file in project")
      ("."   lsp-ui-sideline-apply-code-actions "Code actions at point")
      (">"   next-error                         "Next error")
      ("<"   previous-error                     "Previous error")
      ("s"   save-buffer                        "Save buffer")
      ("q"   save-buffer-kill-emacs             "Quit")))))
;; </config>

General

General provides a convenient way to bind keys.

(use-package general
  :ensure t
  :init
  (setq general-override-states
        '(insert emacs hybrid normal visual motion operator replace))
  :config
  ;; <config>

Use escape to close temporary windows.

(general-define-key
  :keymaps 'minibuffer-mode-map
  (kbd "<escape>") 'abort-minibuffers)

(general-define-key
  :states '(normal)
  :keymaps 'override
  (kbd "<escape>") 'my:delete-next-temporary-window)

Use spacebar to open the Leader key hydra.

  (general-define-key
    :states '(normal visual motion)
    :keymaps 'override
    "SPC" 'hydra:leader/body))
;; </config>

Which-key

Which-key shows all the possible completions of a partially-input keybind.

(use-package which-key
  :ensure t
  :delight
  :config
  (which-key-mode))

Evil

Evil (extensible vi layer) emulates the main features of Vim.

(use-package evil
  :ensure t
  :init
  (setq evil-want-keybinding nil
        evil-want-C-u-scroll t
        evil-undo-system 'undo-redo)
  :config (evil-mode 1))

Evil collection is a collection of Evil bindings for the parts of Emacs that Evil does not cover properly by default.

(use-package evil-collection
  :ensure t
  :after evil
  :custom
  (evil-collection-want-unimpaired-p nil)
  :init
  (evil-collection-init))

Completion

Orderless

The orderless completion style divides the pattern into space-separated components and matches candidates that match all of the components in any order.

(use-package orderless
  :ensure t
  :custom
  (completion-styles '(orderless basic))
  (completion-category-overrides '((file (styles partial-completion)))))

Company

(use-package yasnippet
  :ensure t
  :delight yas-minor-mode
  :hook ((lsp-mode . yas-minor-mode)))

  (use-package company
  :ensure t
  :delight
  :custom
  (company-idle-delay 0.1))

Minibuffer

Vertico

Vertico is a minibuffer completion interface.

(use-package vertico
  :ensure t
  :init
  (vertico-mode)
  :config
  (setq vertico-cycle t)
  :bind (:map vertico-map
         ("C-j" . vertico-next)
         ("C-k" . vertico-previous)
         ("TAB" . vertico-insert)))

Persist history over Emacs restarts. Vertico sorts by history position.

(use-package savehist
  :init
  (savehist-mode))

Do not allow the cursor in the minibuffer prompt.

(use-package emacs
  :init
  (setq minibuffer-prompt-properties
        '(read-only t cursor-intangible t face minibuffer-prompt))
  (add-hook 'minibuffer-setup-hook #'cursor-intangible-mode)
  ;; <init>

Hide commands in M-x which do not work in the current mode. Vertico commands are hidden in normal buffers.

  (setq read-extended-command-predicate
        #'command-completion-default-include-p))
;; </init>

Marginalia

Marginalia adds helpful information to entries in Vertico. For example, if you type M-x, Vertico will open a list of usable commands. Marginalia will add each command’s description next to it.

(use-package marginalia
  :ensure t
  :init
  (marginalia-mode))

WIP Mode line

(use-package mini-modeline
  :ensure t
  :delight mini-modeline-mode
  :init
  (mini-modeline-mode t)
  :custom
  (mini-modeline-enhance-visual t)
  (mini-modeline-right-padding 6))

(use-package delight
  :ensure t)

(use-package autorevert
  :delight auto-revert-mode)

Motion

Avy

Avy is a convenient tool to jump around and perform some actions based on a short filter.

(use-package avy
  :ensure t
  :after evil
  :config
  (setq avy-timeout-seconds 0.375)
  (advice-add 'avy-process :after #'my:advice:nav-flash-show))

(pretty-hydra-define hydra:avy
  (:exit t :idle 0.25)
  ("Filter"
   (("a" avy-goto-char-timer           "Char(s) with timer")
    ("c" avy-goto-char-in-line         "Char in line")
    ("w" avy-goto-word-1               "Word by first char")
    ("s" avy-goto-symbol-1             "Symbol by first char")
    ("h" avy-org-goto-heading-timer    "Org heading with timer"))
   "Repeat"
   ((";" avy-next                      "Next match"     :exit nil)
    ("," avy-previous                  "Previous match" :exit nil)
    ("p" avy-pop-mark                  "Pop mark"       :exit nil))
   "Actions"
   (("y" avy-copy-line                 "Copy line")
    ("Y" avy-copy-region               "Copy region")
    ("m" avy-move-line                 "Move line")
    ("M" avy-move-region               "Move region")
    ("d" avy-kill-ring-save-whole-line "Delete line")
    ("D" avy-kill-ring-save-region     "Delete region"))))

Nav-flash

Nav-flash makes it easier to track the cursor across large movements by flashing the current line.

(use-package nav-flash
  :ensure t
  :config
  (setq nav-flash-delay 0.125)
  (set-face-attribute 'nav-flash-face nil
    :background (face-attribute 'cursor :background)
    :foreground (face-attribute 'default :background))

  (--map (advice-add it :after #'my:advice:nav-flash-show)
         '(evil-scroll-up evil-scroll-down)))

WIP Version control

Magit

(use-package magit :ensure t
  :init
  (with-eval-after-load 'magit-mode
    (add-hook 'after-save-hook 'magit-after-save-refresh-status t))
  :config
  (setq git-commit-style-convention-checks
        '(non-empty-second-line overlong-summary-line)))

(pretty-hydra-define hydra:git
  (:exit t :idle 0.25) 
  ("Git"
   (("g" magit "Open Magit"))))

Search

Misc. packages

Eldoc

(use-package markdown-mode)

(defun my:open-docs ()
  (call-interactively 'eldoc)
  (with-current-buffer (eldoc-doc-buffer)
    (markdown-mode)))

(use-package eldoc
  :after evil
  :delight
  :custom
  (global-eldoc-mode t)
  (eldoc-echo-area-use-multiline-p nil)
  (evil-lookup-func 'my:open-docs))

Try

Try allows you to try Emacs packages without installing them.

(use-package try :ensure t)

Switch-window

Switch-window provides a nice way to choose which window to switch to when there are more than two windows. When a switch-window command is used, a number character appears in the corner of each window. Typing one of the characters then switches focus to that window.

(use-package switch-window
  :ensure t
  :init
  :config
  (defun my:switch-window ()
    "Switch window and change cursor color."
    (interactive)
    (set-cursor-color my:switch-window-cursor-color)
    (switch-window))
  (defun my:switch-window-finish-hook ()
    (set-cursor-color my:cursor-color))
  (add-hook 'switch-window-finish-hook #'my:switch-window-finish-hook)

  (setq switch-window-input-style 'minibuffer)
  (set-face-attribute 'switch-window-label nil :height 5.0))

(defun my:advice:always-show-switch-window-overlay (orig-fun &rest args)
  (setq switch-window-threshold 1)
  (apply orig-fun args)
  (setq switch-window-threshold 2))

(advice-add 'switch-window-then-delete :around #'my:advice:always-show-switch-window-overlay)

Solaire mode

Solaire mode helps to visually distinguish “real” buffers (code buffers) from “unreal” buffers (popups, sidebars, log buffers, terminals, etc) by giving the latter a different background.

(use-package solaire-mode
  :ensure t
  :after general
  :config
  (solaire-global-mode +1))

Bufler

Bufler is an ibuffer alternative.

(use-package bufler
  :ensure t
  :after evil
  :config
  (evil-define-key 'motion bufler-list-mode-map
    (kbd "RET") 'bufler-list-buffer-switch
    (kbd "SPC") 'bufler-list-buffer-peek
    (kbd "C-s") 'bufler-list-buffer-save
    "d" 'bufler-list-buffer-kill
    "x" 'bufler-list-buffer-kill))

WIP SVG tag mode

SVG tag mode replaces keywords or regular expression with SVG tags. It can be used as an extra-fancy alternative to hl-todo.

(use-package svg-tag-mode
  :ensure t
  :config
  ;; <config>

Define some simple SVG tags. The regular expressions are case sensitive, so there’s going to be some redundant patterns.

(defconst my:svg-tags '(
  ("TODO" . sapphire)               ; TODO
  ("WIP" . teal)                    ; WIP
  ("DONE" . green)                  ; DONE
  ("TEMP" . red)                    ; TEMP
  ("\\(Example:?\\)" . blue)        ; Example Example:
  ("\\(HACK:?\\)" . mauve)          ; HACK HACK:
  ("\\(Hack:?\\)" . mauve)          ; Hack Hack:
  ("\\(IMPORTANT[:!]?\\)" . peach)  ; IMPORTANT IMPORTANT! IMPORTANT:
  ("\\(Important[:!]?\\)" . peach)  ; Important Important! Important:
  ("WARN" . yellow)                 ; WARN
  ("\\(Warning[:!]?\\)" . yellow))) ; Warning Warning! Warning:

Define the default style. It’s necessary to use (getnix "tagsFontFamily") instead of (getnix "fontFamily") because my default font causes some strange kerning issues.

(setq svg-lib-style-default `(
  :background ,(catppuccin-get-color 'text)
  :foreground ,(catppuccin-get-color 'base)
  :font-family (getnix "tagsFontFamily")
  :font-size (getnix "fontSize")
  :font-weight 800
  :padding 1
  :margin 0
  :stroke 0
  :radius 5.5
  :alignment 0.5
  :width 20
  :height 1.0
  :scale 1.0
  :ascent center
  :collection "material"))

Create the tags by mapping the previously defined alist of patterns and color.

(setq svg-tag-tags (--map
  `(,(car it) . ((lambda (tag) (svg-lib-tag tag svg-lib-style-default
    :background ,(catppuccin-get-color (cdr it))))))
  my:svg-tags))

Add advice to suppress the constant “SVG tag mode on” messages.

(advice-add 'svg-tag-mode-on :around #'my:advice:be-quiet)

Finally, enable the SVG tags globally.

  (global-svg-tag-mode))
;; </config>

Add progress bar, tags, priority

https://github.com/rougier/svg-tag-mode/blob/main/examples/example-2.el

Format-all

(use-package format-all
  :ensure t
  :delight
  :hook (prog-mode . format-all-mode)
  :config
  (setq format-all-show-errors 'never))

LSP

(use-package lsp-mode
  :ensure t
  :delight
  :after eldoc
  :hook ((lsp-mode . lsp-diagnostics-mode)
	 (lsp-mode . lsp-enable-which-key-integration))
  :custom
  (lsp-enable-on-type-formatting nil)
  (lsp-auto-execute-action nil)
  (lsp-headerline-breadcrumb-enable nil)
  (lsp-lens-enable nil)
  (lsp-diagnostics-provider :flycheck)
  (lsp-keep-workspace-alive nil)
  (lsp-enable-xref t)
  (lsp-auto-configure t)
  (lsp-eldoc-enable-hover t)
  (lsp-eldoc-render-all t)
  (lsp-enable-symbol-highlighting t))

(use-package lsp-ui
  :ensure t
  :after (lsp-mode evil)
  :bind (("C-K" . lsp-ui-doc-focus-frame))
  :custom
  (lsp-ui-peek-enable t)

  (lsp-ui-sideline-show-hover nil) 
  (lsp-ui-sideline-show-code-actions t)

  (lsp-ui-doc-enable nil)
  (lsp-ui-doc-include-signature t)
  (lsp-ui-doc-show-with-cursor nil)
  (lsp-ui-doc-show-with-mouse nil)
  (lsp-ui-doc-position 'at-point)
  (lsp-ui-doc-max-height 30))

Flycheck

Flycheck is a modern on-the-fly syntax checking extension.

(use-package flycheck
  :ensure t
  :init
  (global-flycheck-mode)
  (set-face-attribute 'flycheck-info nil
    :underline `(:color ,(catppuccin-get-color 'green) :style wave)
    :weight 'bold)
  (set-face-attribute 'flycheck-error nil
    :underline `(:color ,(catppuccin-get-color 'red) :style wave)
    :weight 'bold)
  (set-face-attribute 'flycheck-warning nil
    :underline `(:color ,(catppuccin-get-color 'peach) :style wave)
    :weight 'bold))

Flycheck-inline implements a minor-mode for displaying errors from Flycheck right below their reporting location

;; (use-package flycheck-inline
;;   :ensure t
;;   :after flycheck
;;   :hook (flycheck-mode . flycheck-inline-mode))
;; (use-package flycheck-eglot
;;   :ensure t
;;   :after (flycheck eglot)
;;   :config
;;   (global-flycheck-eglot-mode 1))

Treesit-auto

Treesit-auto provides an easy way to automatically install and use tree-sitter major modes.

(use-package treesit-auto
  :ensure t
  :config
  (setq treesit-auto-install t)
  (global-treesit-auto-mode))

Rainbow-delimiters

(use-package rainbow-delimiters
  :ensure t
  :init (rainbow-delimiters-mode))

Dimmer

(use-package dimmer
  :ensure t
  :init
  (dimmer-configure-which-key)
  (dimmer-configure-company-box)
  (dimmer-configure-hydra)
  (dimmer-configure-magit)
  (dimmer-configure-org)
  (dimmer-mode t))

Sublimity-attractive

Centers each buffer horizontally.

(use-package sublimity
  :ensure t
  :init (sublimity-mode 1))

(use-package sublimity-attractive
  :custom
  (sublimity-attractive-centering-width 110))

Languages

WIP English

(use-package langtool
  :ensure t
  :init
  (setq langtool-bin (executable-find "languagetool-commandline"))
  :config
  (add-hook 'langtool-error-exists-hook 'my:remove-langtool-overlays))

(setq my:code-properties '(font-lock-fontified
                           src-block))

(defun my:has-code-property-p (pos)
  (--any? (get-text-property pos it) my:code-properties))

(defun my:remove-langtool-overlays-p (overlay)
  (let* ((pos (overlay-start overlay))
         (faces (get-text-property pos 'face)))
    (my:has-code-property-p pos)
))

(defun my:remove-langtool-overlays ()
  (-each (langtool--overlays-region (buffer-end -1) (buffer-end +1))
    (lambda (overlay)
      (when (my:remove-langtool-overlays-p overlay)
        (delete-overlay overlay)))))

Emacs Lisp

(use-package elisp-def :ensure t)

(use-package highlight-quoted
  :ensure t
  :config
  (add-hook 'emacs-lisp-mode-hook 'highlight-quoted-mode))

Org

Org is like Markdown but a lot better.

(use-package htmlize :ensure t)
(use-package org
  :ensure t
  :after evil htmlize
  :config
  ;; <config>

Append custom css to make exporting from org into html match our editor theme.

(setq org-html-head
    (concat "<style>" (getnix "exportCSS") "</style>"))

Change the default appearance of org documents.

(setq org-indent-mode-turns-on-hiding-stars nil
      org-startup-indented t
      org-startup-folded 'content)

(set-face-attribute 'org-block nil
  :foreground my:code-color))
;; </config>

Fix odd indentation behavior in src blocks.

(setq org-src-preserve-indentation t
      org-edit-src-content-indentation 0)

Change the html export preamble and postamble.

  • TODO Figure out how to hide the title for html exports only.
(setq org-export-with-title nil)
(setq org-html-preamble-format
      '(("en" "<h1 class=\"title\">%t</h1>\n<p class=\"author\">by %a</p>")))

(setq org-html-postamble t
      org-html-postamble-format '(("en" "<p class=\"date\">Export time: %T</p>")))

(setq org-todo-keywords
      '((sequence "TODO" "WIP" "|" "DONE")))

WIP Hydra

(defun my:org-toggle-emphasis-markers ()
  "Toggle emphasis markers [*/_=~+]"
  (interactive)
  (setq org-hide-emphasis-markers (not org-hide-emphasis-markers)))

(major-mode-hydra-define org-mode
  (:exit t :idle 0.25 :quit-key "<escape>")
  ("Toggle"
   (("l" org-toggle-link-display        "Link display")
    ("E" my:org-toggle-emphasis-markers "Emphasis markers")
    ("c" org-toggle-checkbox            "Checkbox")
    ("i" org-toggle-inline-images       "Inline images")
    ("n" org-toggle-narrow-to-subtree   "Narrow to subtree")
    ("p" org-toggle-pretty-entities     "Pretty entities"))
   "Insert"
   (("L" org-insert-link             "Link")
    ("s" org-store-link              "(Store link to here)")
    ("S" org-insert-last-stored-link "Last stored link")
    ("t" org-time-stamp              "Timestamp")
    ("d" org-insert-drawer           "Drawer")
    ("T" org-table-create            "Table"))
   "Table"
   (("a" org-table-align         "Align")
    ("R" org-table-insert-row    "Insert row"    :exit nil)
    ("C" org-table-insert-column "Insert column" :exit nil)
    ("k" org-table-kill-row      "Kill row")
    ("K" org-table-kill-column   "Kill column"))
   "Hydra"
   (("e" hydra:org-emphasize/body "Emphasize"))))

(pretty-hydra-define hydra:org-emphasize
  (:exit t :idle 0.25)
  ("Mnemonic"
   (("x" (org-emphasize ?\s) "Clear")
    ("b" (org-emphasize ?*)  "Bold")
    ("i" (org-emphasize ?/)  "Italic")
    ("u" (org-emphasize ?_)  "Underlined")
    ("v" (org-emphasize ?=)  "Verbatim")
    ("c" (org-emphasize ?~)  "Code")
    ("s" (org-emphasize ?+)  "Strike-through"))
   "Literal"
   (("*" (org-emphasize ?*)  "Bold")
    ("/" (org-emphasize ?/)  "Italic")
    ("_" (org-emphasize ?_)  "Underlined")
    ("=" (org-emphasize ?=)  "Verbatim")
    ("~" (org-emphasize ?~)  "Code")
    ("+" (org-emphasize ?+)  "Strike-through"))))

Nix

(use-package nix-mode
  :ensure t
  :mode "\\.nix\\'"
  :hook (nix-mode . my:nix-mode-hook))

(defun my:nix-mode-hook ()
  (setq-local format-all-formatters '(("Nix" nixfmt))))

Shell script

Sh, bash, zsh

Syntax checking and linting is provided by Shellcheck via flycheck.

(add-hook 'sh-mode-hook 'flycheck-mode)
(setq bash-ts-mode-hook sh-mode-hook)

Fish

(use-package fish-mode
  :ensure t
  :hook (fish-mode . flycheck-mode))

Python

(my:set-formatter python-mode "Python" black)
(setq python-ts-mode-hook python-mode-hook)

Rust

(use-package project)

(use-package rustic
  :ensure t
  :custom
  (rustic-rustfmt-config-alist '(("edition" . "2021")))
  (rustic-format-trigger 'on-save)
  (rustic-format-on-save-method #'my:rust-format-on-save)
  (add-hook 'server-after-make-frame-hook #'catppuccin-reload)

  (lsp-inlay-hint-enable nil)
  (lsp-rust-analyzer-completion-add-call-argument-snippets nil)
  (lsp-rust-analyzer-completion-add-call-parenthesis nil)
  (lsp-rust-analyzer-display-lifetime-elision-hints-enable "skip_trivial")
  (lsp-rust-analyzer-display-chaining-hints t)
  (lsp-rust-analyzer-display-lifetime-elision-hints-use-parameter-names t)
  (lsp-rust-analyzer-display-closure-return-type-hints t)
  (lsp-rust-analyzer-display-parameter-hints t)
  (lsp-rust-analyzer-display-reborrow-hints t))

For some unknown reason, format-all-mode does not work with rustic. Setting rustic-format-trigger enables formatting on save, but it needs to check for format-all-mode in order to respect the “Format on save” option in the Toggle hydra.

(defun my:rust-format-on-save ()
  "Format the file, respecting format-all-mode"
  (if format-all-mode
    (rustic-format-file)))

WIP Hydra

(major-mode-hydra-define rustic-mode
  (:exit t :idle 0.25 :quit-key "<escape>")
  ("Cargo"
   (("c" rustic-cargo-check                    "Check")
    ("m" rustic-cargo-add-missing-dependencies "Add missing dependencies")
    ("a" rustic-cargo-add                      "Add dependency")
    ("r" rustic-cargo-rm                       "Remove dependency"))))

WGSL

(use-package wgsl-mode
  :ensure t)