Skip to content

Repository files navigation

Emacs: A Literate Configuration

This is a literate Emacs configuration file written in Org mode. The primary goal is to create a setup that is well-documented, modular, and easy to maintain. Saving this file tangles its Emacs Lisp blocks to the generated `lit.el`; restart Emacs to load those changes. The hand-maintained `init.el` bootstraps Straight and Org before loading this file through `org-babel-load-file`.

Core Startup Configuration

These settings run from the generated `lit.el` after `init.el` has bootstrapped Straight, use-package, and Org.

Early UI Tweaks

These settings disable distracting UI elements and set up fundamental frame behavior to ensure a clean and predictable launch.

;; Set frame behavior early
(setq frame-resize-pixelwise t
      frame-inhibit-implied-resize t)

;; Disable UI elements for a minimal appearance
(scroll-bar-mode -1)
(tool-bar-mode -1)
(tooltip-mode -1)
(menu-bar-mode -1)
(set-fringe-mode 10) ; A little breathing room

;; Inhibit startup screens and messages for a faster, quieter launch
(setq inhibit-splash-screen t
      inhibit-startup-screen t
      inhibit-x-resources t
      inhibit-startup-echo-area-message user-login-name
      inhibit-startup-buffer-menu t
      inhibit-startup-message t)

;; Set up the visible bell instead of an audible one
(setq visible-bell t)

;; Use short 'y/n' answers instead of 'yes/no'
(setq use-short-answers t)

;; Reduce native compilation noise
(when (featurep 'native-compile)
  (setq native-comp-async-report-warnings-errors nil))

Package Runtime Configuration

Straight and use-package are bootstrapped once in `init.el`, before Org loads this generated configuration. Runtime package configuration remains here.

;; GCMH - Garbage Collector Magic Hack for better performance
(use-package gcmh
  :config
  (gcmh-mode 1))

;; Ensure Emacs inherits environment variables from shell
;; This sources ~/.secrets via interactive bash, picking up Vertex API keys
(use-package exec-path-from-shell
  :if (or (daemonp) (memq window-system '(mac ns x pgtk)))
  :custom
  (exec-path-from-shell-variables
   '("PATH"
     "MANPATH"
     ;; Claude Code / Vertex AI authentication
     "ANTHROPIC_VERTEX_PROJECT_ID"
     "CLOUD_ML_REGION"
     "CLAUDE_CODE_USE_VERTEX"
     ;; SSH agent for git operations
     "SSH_AUTH_SOCK"
     "SSH_AGENT_PID"
     ;; GPG
     "GPG_TTY"))
  :config
  (exec-path-from-shell-initialize))

Org Mode Setup

This configures Org mode basics and automatically tangles `lit.org` to generated `lit.el` when this file is saved. Tangling does not reload the running Emacs.

(setq org-support-shift-select t)

;; Automatically tangle our lit.org config file when we save it
(defun my/org-babel-tangle-config ()
  "Auto-tangle config file on save."
  (condition-case err
      (when (and buffer-file-name
                 (file-equal-p buffer-file-name
                               (expand-file-name "lit.org" user-emacs-directory)))
        (let ((org-confirm-babel-evaluate nil))
          (org-babel-tangle)))
    (error
     (message "Error tangling config: %s" (error-message-string err)))))

(add-hook 'org-mode-hook
          (lambda ()
            (add-hook 'after-save-hook #'my/org-babel-tangle-config nil 'local)))


(with-eval-after-load 'org
  (require 'org-tempo) ;; Needed as of Org 9.2 for structure templates
  (add-to-list 'org-structure-template-alist '("sh" . "src shell"))
  (add-to-list 'org-structure-template-alist '("el" . "src emacs-lisp"))
  (add-to-list 'org-structure-template-alist '("py" . "src python")))

Core Emacs Behavior

This section configures the fundamental, non-UI behavior of Emacs, from user information to editing enhancements and file handling.

Core Emacs Configuration

Consolidated configuration for user settings, file handling, editing behavior, and built-in features.

(use-package emacs
  :ensure nil
  :bind (("M-o" . other-window)
         ("M-j" . duplicate-dwim)
         ("RET" . newline-and-indent)
         ;; Unbind some keys to use for other purposes
         ("C-z" . nil)
         ("C-x C-z" . nil)
         ("C-x C-k RET" . nil))
  :custom
  ;; User information
  (user-full-name "Sean Mooney")
  (user-mail-address "sean@seanmooney.info")
  ;; Use UTF-8 everywhere
  (coding-system-for-read 'utf-8)
  (coding-system-for-write 'utf-8)
  (ad-redefinition-action 'accept)
  ;; Don't create lockfiles
  (create-lockfiles nil)
  ;; Disable backup files
  (make-backup-files nil)
  (backup-inhibited t)
  ;; Disable auto-save files (#filename#)
  (auto-save-default nil)
  ;; Editing behavior
  (completion-ignore-case t)
  (completions-detailed t)
  (help-window-select t)
  ;; Don't store duplicate entries in the kill ring
  (kill-do-not-save-duplicates t)
  ;; Default width for text wrapping
  (fill-column 80)
  (column-number-mode 1)
  ;; Completion settings
  (completions-max-height 15)
  (tab-always-indent 'complete)
  ;; Improve line spacing for better readability
  (line-spacing 0.2)
  :config
  ;; Highlight the current line in programming, text, and org modes.
  (global-hl-line-mode 1)
  ;; When pasting, overwrite the currently selected region.
  (delete-selection-mode 1)
  ;; Font lock (syntax highlighting)
  (global-font-lock-mode 1)
  (setq font-lock-maximum-decoration t))

Additional Editing Packages

Additional packages that enhance the text editing experience beyond built-in functionality.

;; Enable line numbers for modes where it's most useful.
(dolist (mode '(text-mode-hook
                prog-mode-hook
                conf-mode-hook))
  (add-hook mode #'display-line-numbers-mode))

;; But disable them for modes where they are distracting.
(dolist (mode '(org-mode-hook
                term-mode-hook
                shell-mode-hook
                treemacs-mode-hook
                eshell-mode-hook))
  (add-hook mode (lambda () (display-line-numbers-mode -1))))


;; Automatically pair delimiters like parentheses and quotes.
(use-package elec-pair
  :ensure nil
  :hook (after-init . electric-pair-mode)
  :bind ("C-c d" . delete-pair)
  :custom
  (delete-pair-blink-delay 0.0))

;; Visually highlight matching parentheses.
(use-package paren
  :ensure nil
  :hook (after-init . show-paren-mode)
  :custom
  (show-paren-style 'mixed)
  (show-paren-context-when-offscreen t))

;; Allows repeating commands with C-x z.
(use-package repeat
  :ensure nil
  :config
  (repeat-mode 1))

;; Color-code matching delimiters for better code readability
(use-package rainbow-delimiters
  :hook (prog-mode . rainbow-delimiters-mode))

;; Move text (lines or regions) up and down
(use-package move-text
  :bind (("M-<up>" . move-text-up)
         ("M-<down>" . move-text-down)))

;; Multiple cursors for simultaneous editing
(use-package multiple-cursors
  :bind (("C-S-c C-S-c" . mc/edit-lines)                    ; Add cursor to each line in region
         ("C->" . mc/mark-next-like-this)                   ; Mark next occurrence
         ("C-<" . mc/mark-previous-like-this)               ; Mark previous occurrence
         ("C-c C-<" . mc/mark-all-like-this)                ; Mark all occurrences
         ("C-S-<mouse-1>" . mc/add-cursor-on-click))        ; Add cursor with mouse
  :config
  ;; Don't warn about commands that haven't been used with multiple cursors
  (setq mc/always-run-for-all t))

File Handling & Saving

This configures how Emacs handles files, symlinks, and saving state.

;; Prompt inside Emacs when decrypting files such as auth-source credentials.
(setq epa-pinentry-mode 'loopback)

(use-package files
  :ensure nil
  :straight (:type built-in)
  :custom
  ;; Prefer newer versions of files when loading Lisp code.
  (load-prefer-newer t)
  ;; Don't warn me about large files. I know what I'm doing.
  (large-file-warning-threshold nil)
  ;; When visiting a file, resolve symlinks to the true path.
  (find-file-visit-truename t))

;; Remember the cursor position in files between sessions.
(use-package saveplace
  :ensure nil
  :hook (after-init . save-place-mode))

;; Remember minibuffer history between sessions.
(use-package savehist
  :ensure nil
  :hook (after-init . savehist-mode)
  :custom (history-length 300))

;; Remember recently opened files.
(use-package recentf
  :ensure nil
  :hook (after-init . recentf-mode)
  :custom
  (recentf-max-saved-items 100)
  (recentf-exclude '("/tmp/" "/ssh:" "/sudo:" "\\.git/")))

;; Automatically revert file buffers when they change on disk.
(use-package autorevert
  :ensure nil
  :custom
  (auto-revert-interval 10)                   ; Check every 10 seconds (reduces background git load)
  (auto-revert-check-vc-info t)              ; Also check version control info
  (auto-revert-verbose t)                    ; Show messages when reverting
  (global-auto-revert-non-file-buffers t)   ; Also revert non-file buffers like Dired
  (auto-revert-avoid-polling nil)            ; Use file notifications when available
  :config
  (global-auto-revert-mode 1))

;; Prevent background git operations from acquiring the index lock.
;; GIT_OPTIONAL_LOCKS=0 tells git that read-only operations (status, diff)
;; should not take optional locks, avoiding conflicts with rebase/merge.
;; This affects all git subprocesses spawned by Emacs (diff-hl, treemacs, vc, etc.)
(setenv "GIT_OPTIONAL_LOCKS" "0")

Persistent Undo

This setup enables undo-tree-mode, a more powerful way of handling undo/redo that visualizes the history as a tree. More importantly, it configures Emacs to save the undo history of files to a dedicated directory (~/.config/emacs/undo/), so you can undo changes even after closing and reopening a file.

(use-package undo-tree
  :hook (after-init . global-undo-tree-mode)
  :bind (("C-z" . undo-tree-undo)
         ("C-S-z" . undo-tree-redo))
  :custom
  ;; Save undo history across sessions
  (undo-tree-auto-save-history t)
  ;; Create the undo directory if it doesn't exist
  (undo-tree-history-directory-alist
   `(("." . ,(expand-file-name "undo/" user-emacs-directory))))
  ;; Bound undo history memory while retaining Undo-tree's tiered cleanup.
  (undo-tree-limit 8388608)         ; 8 MiB
  (undo-tree-strong-limit 12582912) ; 12 MiB
  (undo-tree-outer-limit 37748736)  ; 36 MiB
  :config
  ;; Unbind C-/ from undo-tree to allow our comment binding
  (define-key undo-tree-map (kbd "C-/") nil))

Version Control

Settings for Emacs’s built-in version control integration.

(use-package vc
  :ensure nil
  :custom
  ;; VC should follow symbolic links.
  (vc-follow-symlinks t)
  :config
  (add-to-list 'vc-directory-exclusion-list ".venv"))

Version Control (Magit)

settings for magit for more powerful git integration

(use-package magit
  :bind (("C-x g" . magit-status)
         ("C-x M-g" . magit-dispatch))
  :custom
  (magit-display-buffer-function #'magit-display-buffer-same-window-except-diff-v1))

;; Show git diff indicators in the fringe
(use-package diff-hl
  :hook ((prog-mode . diff-hl-mode)
         (dired-mode . diff-hl-dired-mode))
  :config
  ;; Integration with magit - refresh diff-hl when magit updates
  (with-eval-after-load 'magit
    (add-hook 'magit-pre-refresh-hook #'diff-hl-magit-pre-refresh)
    (add-hook 'magit-post-refresh-hook #'diff-hl-magit-post-refresh)))

User Interface

This section covers all visual aspects of Emacs, from fonts and colors to window layouts and completion UIs.

Fonts (Fontaine)

I use the `fontaine` package to easily switch between predefined font configurations. It prefers the Nerd Fonts build of Source Code Pro, then standard Source Code Pro, for the default face, with a generic monospace fallback. Variable-pitch text uses a generic serif face.

(use-package fontaine
  :demand t
  :init
  (let* ((families (font-family-list))
         (default-font
          (cond
           ((member "SauceCodePro Nerd Font" families)
            "SauceCodePro Nerd Font")
           ((member "Source Code Pro" families)
            "Source Code Pro")
           (t "Monospace")))
         (dyslexia-font (if (member "OpenDyslexic Nerd Font" families)
                            "OpenDyslexic Nerd Font"
                          "OpenDyslexic")))
    (setq fontaine-presets
          `((small
           :default-height 90)
          (regular
           :default-weight normal
           :default-height 120)
          (medium
           :default-weight semilight
           :default-height 140)
          (large
           :default-weight semilight
           :default-height 180
           :bold-weight extrabold)
          (dyslexia-friendly
           :default-family ,dyslexia-font
           :variable-pitch-family ,dyslexia-font
           :default-height 130
           :variable-pitch-height 1.1)
          (t ; our shared fallback properties
           :default-family ,default-font
           :default-weight semilight
           :default-height 100
           :variable-pitch-family "Serif"
           :variable-pitch-weight normal
           :variable-pitch-height 1.05
           :bold-weight bold
           :italic-slant italic))))
  :bind ("C-c f" . fontaine-set-preset)
  :config
  (fontaine-set-preset 'regular))

;; Pulsar briefly highlights the current line after certain commands
;; Excellent accessibility feature for tracking cursor movement
(use-package pulsar
  :custom
  ;; Highlight line after these commands for better cursor tracking
  (pulsar-pulse-functions '(isearch-repeat-forward
                            isearch-repeat-backward
                            recenter-top-bottom
                            move-to-window-line-top-bottom
                            reposition-window
                            bookmark-jump
                            other-window
                            delete-window
                            delete-other-windows
                            forward-page
                            backward-page
                            scroll-up-command
                            scroll-down-command
                            windmove-right
                            windmove-left
                            windmove-up
                            windmove-down))
  :config
  (pulsar-global-mode 1))

Theming (ef-themes)

I use the `ef-themes` collection by Protesilaos Stavrou for its excellent contrast and beautiful color palettes. I define a dark (`ef-cherie`) and light (`ef-summer`) theme to toggle between.

(use-package ef-themes
  :config
  ;; Define the pair of themes to toggle between.
  (setq ef-themes-to-toggle '(ef-cherie ef-summer))
  ;; Disable all other themes to avoid awkward blending.
  (mapc #'disable-theme custom-enabled-themes)
  ;; Load the default dark theme.
  (load-theme 'ef-cherie :no-confirm))

Frame and Window Management

These settings control the appearance of the Emacs frame, windows, and how they are split.

;; Enable smooth, pixel-based scrolling.
(use-package pixel-scroll
  :ensure nil
  :straight (:type built-in)
  :custom
  (pixel-scroll-precision-use-momentum nil)
  :config
  (pixel-scroll-precision-mode 1))

;; Add a hint of transparency and maximize the frame on startup.
(add-to-list 'default-frame-alist '(alpha-background . 93))
(add-to-list 'default-frame-alist '(fullscreen . maximized))

;; Improve display characters in terminal mode.
(when standard-display-table
  (set-display-table-slot standard-display-table 'vertical-border ?\u2502)
  (set-display-table-slot standard-display-table 'truncation ?\u2192))

;; Custom function to toggle a 2-window split between vertical and horizontal.
(defun my/toggle-window-split ()
  "Switch between horizontal and vertical split window layout."
  (interactive)
  (if (= (count-windows) 2)
      (let* ((window (selected-window))
             (other-window (next-window))
             (window-edges (window-edges window))
             (other-edges (window-edges other-window))
             (window-second (or (> (car window-edges) (car other-edges))
                                (> (cadr window-edges) (cadr other-edges))))
             (window-buffer (window-buffer window))
             (other-buffer (window-buffer other-window))
             (splitter (if (= (car window-edges) (car other-edges))
                           #'split-window-right
                         #'split-window-below)))
        (delete-other-windows)
        (let ((new-window (funcall splitter)))
          (if window-second
              (progn
                (set-window-buffer window other-buffer)
                (set-window-buffer new-window window-buffer)
                (select-window new-window))
            (set-window-buffer new-window other-buffer))))
    (message "This command only works when there are exactly two windows.")))
(global-set-key (kbd "C-c j") #'my/toggle-window-split)

Minibuffer & Completion Framework

I use a modern completion system composed of several packages that work together.

  • vertico provides the core vertical minibuffer UI.
  • marginalia adds rich annotations (file permissions, command docs) to completions.
  • orderless enables powerful out-of-order matching.
  • consult enhances built-in commands like `find-file` and `switch-to-buffer` with previews.
  • corfu provides an in-buffer completion popup.
(use-package vertico
  :init (vertico-mode)
  :custom
  (vertico-cycle t)
  (vertico-resize nil))

(use-package marginalia
  :after vertico
  :init (marginalia-mode))

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

(use-package corfu
    :custom
    (corfu-auto nil)
    (corfu-auto-delay 0.1)
    (corfu-quit-no-match 'separator)
    ;; Enable Corfu globally except in terminal buffers.
    (global-corfu-modes
     '((not eshell-mode shell-mode term-mode ghostel-mode eat-mode) t))
    :init
    (global-corfu-mode))

;; Adds more completion sources (backends) for Corfu
(use-package cape
  :init
  (add-to-list 'completion-at-point-functions #'cape-file))

(use-package consult
  :bind (("C-x f" . consult-find)
         ("M-s M-o" . consult-outline)
         ("C-f" . consult-line)
         ("C-x b" . consult-buffer) ; a powerful switch-to-buffer
         ("C-j" . consult-imenu)
         ("C-x p b" . consult-project-buffer)
         ("M-y" . consult-yank-pop)
         ("M-g g" . consult-goto-line)
         ("C-c m" . consult-man)
         ("C-c i" . consult-info)
         ("C-c h" . consult-history)
         ("M-s c" . consult-locate)
         ("M-s g" . consult-grep)
         ("M-s G" . consult-git-grep)
         ("M-s r" . consult-ripgrep)
         ;; Isearch integration
         ("M-s e" . consult-isearch-history)
         :map isearch-mode-map
         ("M-e" . consult-isearch-history)
         ("M-s e" . consult-isearch-history)
         ("M-s l" . consult-line)
         ("M-s L" . consult-line-multi))
  :init
  ;; Add consult bindings to org-mode and org-agenda
  (with-eval-after-load "org"
    (keymap-set org-mode-map "C-j" #'consult-org-heading))
  (with-eval-after-load "org-agenda"
    (keymap-set org-agenda-mode-map "C-j" #'consult-org-agenda))
  :custom
  (consult-line-start-from-top nil)
  :config
  ;; Integrate with xref for "find definitions/references"
  (with-eval-after-load "xref"
    (require 'consult-xref)
    (setq xref-show-xrefs-function #'consult-xref)
    (setq xref-show-definitions-function #'consult-xref)))

Dired (File Manager)

Configuration for Dired, Emacs’s built-in file manager.

(use-package dired
  :ensure nil
  :straight (:type built-in)
  :hook ((dired-mode . hl-line-mode)
         (dired-mode . dired-hide-details-mode))
  :custom
  (dired-listing-switches "-alFh") ; ls-like output
  (dired-dwim-target t)            ; Smart target for copying/renaming
  (dired-recursive-copies 'always)
  (dired-recursive-deletes 'always))

;; dired-x provides extra functionality for dired
(use-package dired-x
  :ensure nil
  :straight (:type built-in)
  :after dired
  :bind (("C-x C-j" . dired-jump))         ; Jump to dired of current file
  :custom
  ;; Only omit system/backup files, not regular dot files
  (dired-omit-files "^\\.\\.$\\|\\.DS_Store$\\|\\.localized$\\|~$\\|#.*#$")
  (dired-guess-shell-gnutar "tar"))


;; Ranger-style file browser with three-pane layout and previews.
;; Avoid the observed Hydra-related failure in Ranger's compiled output.
(use-package ranger
  :straight (ranger :type git :host github
                    :repo "punassuming/ranger.el"
                    :local-repo "ranger.el"
                    :build (:not compile))
  :bind (("C-x r d" . ranger)
         ("C-x r j" . deer))          ; Minimal ranger mode
  :custom
  (ranger-cleanup-eagerly t)          ; Clean up ranger buffers
  (ranger-cleanup-on-disable t)
  ;; Show dotfiles initially; `zh' cycles the supported visibility states.
  (ranger-show-hidden 'format)
  (ranger-preview-file t)             ; Show file previews
  (ranger-max-preview-size 10))       ; Limit preview to 10MB files

;; Modern icons for dired
(use-package nerd-icons-dired
  :after (dired nerd-icons)
  :hook (dired-mode . nerd-icons-dired-mode))

Ibuffer (Buffer Manager)

I use Ibuffer to manage open buffers, with custom groups to keep things organized.

(use-package ibuffer
  :ensure nil
  :bind ("C-x C-b" . ibuffer)
  :custom
  (ibuffer-show-empty-filter-groups nil)
  (ibuffer-saved-filter-groups
   '(("default"
      ("org" (or (mode . org-mode) (name . "^\\*Org Src")))
      ("emacs" (or (name . "^\\*scratch\\*$") (name . "^\\*Messages\\*$")))
      ("dired" (mode . dired-mode))
      ("terminal" (or (mode . term-mode) (mode . shell-mode)))
      ("help" (or (name . "^\\*Help\\*$") (name . "^\\*helpful"))))))
  :config
  (add-hook 'ibuffer-mode-hook
            (lambda () (ibuffer-switch-to-saved-filter-groups "default"))))

Helper UI (which-key, helpful, treemacs)

Additional UI packages that help with discoverability and navigation.

;; `which-key` displays available keybindings in a popup.
(use-package which-key
  :ensure nil
  :straight (:type built-in)
  :config
  (which-key-mode))

;; Enhanced help system with more detailed information and better formatting
(use-package helpful
  :bind (("C-h f" . helpful-callable)   ; Enhanced function help
         ("C-h v" . helpful-variable)   ; Enhanced variable help
         ("C-h k" . helpful-key)        ; Enhanced key help
         ("C-h x" . helpful-command))   ; Enhanced command help
  :custom
  ;; Show source code for elisp functions
  (helpful-switch-buffer-function #'helpful-switch-to-buffer))

;; Transient menu framework (used by Magit and other packages)
(use-package transient
  :straight t)

;; Clean up mode line by hiding/shortening minor mode names
(use-package diminish
  :config
  ;; Hide these minor modes from the mode line
  (diminish 'which-key-mode)
  (diminish 'eldoc-mode)
  (diminish 'auto-revert-mode)
  (diminish 'visual-line-mode)
  (diminish 'subword-mode)
  (diminish 'rainbow-delimiters-mode)
  (diminish 'flyspell-mode)
  (diminish 'writegood-mode))

;; `treemacs` provides a file tree sidebar.
(use-package treemacs
  :defer t
  :bind (("C-x t w"   . treemacs-select-window)
         ("C-x t 1"   . treemacs-delete-other-windows)
         ("C-x t t"   . treemacs)
         ("C-x t d"   . treemacs-select-directory))
  :custom
  (treemacs-display-in-side-window t)
  (treemacs-follow-after-init t)
  (treemacs-expand-after-init t)
  (treemacs-git-command-pipe "")
  (treemacs-hide-dot-git-directory t)
  (treemacs-indentation 2)
  (treemacs-litter-directories '("/node_modules" "/.venv" "/.cask"))
  (treemacs-position 'left)
  (treemacs-show-hidden-files t)
  (treemacs-width 35)
  :config
  (setq treemacs-collapse-dirs (if treemacs-python-executable 3 0))
  (treemacs-follow-mode t)
  (treemacs-filewatch-mode t)
  (treemacs-fringe-indicator-mode 'always)
  ;; Enable automatic project following
  (treemacs-project-follow-mode t))

;; Custom integration with project.el for single-project display
(defun my/treemacs-display-current-project-exclusively (&optional directory)
  "Display only the project in DIRECTORY in treemacs."
  (let ((default-directory (or directory default-directory)))
    (when (project-current)
      (treemacs-add-and-display-current-project-exclusively))))

;; Advice to automatically update treemacs when switching projects
(defun my/treemacs-project-switch-advice (directory)
  "Update treemacs after switching to the project in DIRECTORY."
  (when (and (featurep 'treemacs)
             (treemacs-current-workspace))
    (run-with-idle-timer
     0.1 nil #'my/treemacs-display-current-project-exclusively
     directory)))

;; Add advice to project-switch-project
(with-eval-after-load 'project
  (advice-add 'project-switch-project :after #'my/treemacs-project-switch-advice))

;; Git integration for treemacs
(use-package treemacs-magit
  :after (treemacs magit)
  :defer t)

;; Modern icon support for better readability
(use-package nerd-icons
  :defer t
  :config
  (defun my/nerd-icons-font-installed-p ()
    "Check if Nerd Fonts are installed."
    (or (member "Symbols Nerd Font Mono" (font-family-list))
        (member "Nerd Icons" (font-family-list))))

  ;; Run M-x nerd-icons-install-fonts after first install.  Keep the
  ;; interactive check for normal startup, but avoid prompting in batch mode
  ;; where there is no stdin to answer the question.  Also skip the prompt on
  ;; NixOS, where fonts should be installed declaratively.
  (unless (or noninteractive
              (my/nerd-icons-font-installed-p)
              (file-exists-p "/etc/NIXOS"))
    (when (y-or-n-p "Nerd fonts not found. Install them? ")
      (nerd-icons-install-fonts))))

(use-package treemacs-nerd-icons
  :after (treemacs nerd-icons)
  :config
  (treemacs-load-theme "nerd-icons"))

Reading and Writing Support

Configuration for packages that enhance reading comprehension and writing quality, particularly beneficial for dyslexic users.

Distraction-Free Writing (Olivetti)

Creates a focused writing environment with comfortable margins and reduced visual clutter.

(use-package olivetti
  :bind ("C-c o" . olivetti-mode)
  :custom
  (olivetti-body-width 80)
  (olivetti-minimum-body-width 60)
  (olivetti-recall-visual-line-mode-entry-state t))

Enhanced Writing Analysis (Writegood)

Helps improve writing clarity and catch common errors beyond spell-checking.

(use-package writegood-mode
  :hook (text-mode . writegood-mode))

Code Spell Checking (Codespell)

Codespell catches common spelling errors in code, comments, and documentation. Particularly useful for catching typos in variable names and comments.

;; Codespell integration for catching spelling errors in code
(defun my/codespell-buffer ()
  "Run codespell on the current buffer."
  (interactive)
  (if (executable-find "codespell")
      (let ((temp-file (make-temp-file "codespell-")))
        (write-region (point-min) (point-max) temp-file)
        (with-current-buffer (get-buffer-create "*codespell*")
          (erase-buffer)
          (call-process "codespell" nil t nil temp-file)
          (if (> (buffer-size) 0)
              (progn
                (display-buffer (current-buffer))
                (message "Codespell found issues - see *codespell* buffer"))
            (message "No spelling errors found by codespell")))
        (delete-file temp-file))
    (message "Codespell not found. Install with: pip install codespell")))

(defun my/codespell-region (start end)
  "Run codespell on the selected region."
  (interactive "r")
  (if (executable-find "codespell")
      (let ((temp-file (make-temp-file "codespell-region-")))
        (write-region start end temp-file)
        (with-current-buffer (get-buffer-create "*codespell*")
          (erase-buffer)
          (call-process "codespell" nil t nil temp-file)
          (if (> (buffer-size) 0)
              (progn
                (display-buffer (current-buffer))
                (message "Codespell found issues in region"))
            (message "No spelling errors found in region")))
        (delete-file temp-file))
    (message "Codespell not found. Install with: pip install codespell")))

(defun my/codespell-project ()
  "Run codespell on the current project."
  (interactive)
  (if (executable-find "codespell")
      (if-let ((project-root (project-root (project-current))))
          (let ((default-directory project-root))
            (compile "codespell --skip=.git,*.lock,*.json"))
        (message "Not in a project"))
    (message "Codespell not found. Install with: pip install codespell")))

Development Environment

This section configures Emacs for software development, including linters, language servers, and language-specific setups.

General Tooling (LSP, Linters, Compilation)

These are language-agnostic tools that form the foundation of the IDE experience.

;; Use Emacs's bundled cross-reference, diagnostics, and LSP clients.
(use-package xref
  :ensure nil
  :straight (:type built-in))

(use-package flymake
  :ensure nil
  :straight (:type built-in)
  :bind (("C-c l n" . flymake-goto-next-error)
         ("C-c l p" . flymake-goto-prev-error)))

(use-package eglot
  :ensure nil
  :straight (:type built-in)
  :hook ((python-mode . my/python-eglot-ensure)
         (python-ts-mode . my/python-eglot-ensure)
         (js-mode . eglot-ensure)
         (js-ts-mode . eglot-ensure)
         (typescript-mode . eglot-ensure)
         (typescript-ts-mode . eglot-ensure)
         (tsx-ts-mode . eglot-ensure)
         (rust-ts-mode . eglot-ensure)
         (zig-mode . eglot-ensure)
         (zig-ts-mode . eglot-ensure))
  :bind (("C-c l c" . eglot-reconnect)
         ("C-c l d" . flymake-show-buffer-diagnostics)
         ("C-c l f f" . eglot-format)
         ("C-c l f b" . eglot-format-buffer)
         ("C-c l l" . eglot)
         ("C-c l r n" . eglot-rename)
         ("C-c l s" . eglot-shutdown)
         ("C-c l i" . eglot-inlay-hints-mode))
  :custom
  ;; Shutdown LSP server when the last managed buffer is killed.
  (eglot-autoshutdown t))


;; Configuration for Emacs's compilation interface.
(use-package compile
  :ensure nil
  :bind (("C-c b" . compile)
         ("C-c B" . recompile)) ; Removed C-c t conflict
  :custom
  (compilation-scroll-output 'first-error)
  (compilation-skip-threshold 2)) ; Skip warnings

Spell Checking (Flyspell)

Traditional spell checker that’s reliable and doesn’t interfere with syntax highlighting.

;; Flyspell for spell checking
(use-package flyspell
  :ensure nil
  :hook ((text-mode . flyspell-mode)
         (org-mode . flyspell-mode)
         (markdown-mode . flyspell-mode)
         (prog-mode . flyspell-prog-mode))  ; Only check comments/strings in code
  :bind (("M-$" . flyspell-correct-word-before-point)
         ("C-M-$" . ispell-change-dictionary))
  :custom
  (flyspell-issue-message-flag nil)  ; Don't show messages for every word
  (flyspell-issue-welcome-flag nil)  ; Don't show welcome message
  :config
  ;; Better visual feedback
  (set-face-attribute 'flyspell-incorrect nil :underline '(:color "red" :style wave))
  (set-face-attribute 'flyspell-duplicate nil :underline '(:color "orange" :style wave)))

Tree-sitter

Tree-sitter provides faster and more accurate syntax parsing, which improves highlighting and code analysis. `treesit-auto` manages the installation of parsers.

(use-package treesit-auto
  :custom
  (treesit-auto-install 'prompt)
  :config
  ;; Only add tree-sitter modes for languages that benefit from it
  (treesit-auto-add-to-auto-mode-alist '(python bash javascript typescript json yaml rust zig nix))
  (global-treesit-auto-mode))

Language: Python

Python uses separate host-local tool environments and project runtime environments. Managed environments live outside project checkouts under the target host’s XDG data home. Environment creation and updates are always explicit; visiting a file only reuses environments that are already ready.

  (require 'json)

  (defvar my/python-environment-state-file
    (expand-file-name
     "emacs/python-environments.json"
     (file-name-as-directory
      (expand-file-name (or (getenv "XDG_STATE_HOME") "~/.local/state"))))
    "Local JSON file storing explicit Python environment selections.")

  (defvar my/python-tool-environments (make-hash-table :test #'equal)
    "Selected Python tool environments, keyed by local or TRAMP host.")

  (defvar my/python-runtime-environments (make-hash-table :test #'equal)
    "Selected Python runtime environments, keyed by project root.")

  (defun my/python--save-environment-selections ()
    "Atomically save explicit Python environment selections as JSON."
    (let (tools runtimes temporary)
      (maphash
       (lambda (host path)
         (when (and (stringp host) (stringp path))
           (push `((host . ,host) (path . ,path)) tools)))
       my/python-tool-environments)
      (maphash
       (lambda (project record)
         (let ((path (plist-get record :path))
               (tox-environment (plist-get record :tox-env)))
           (when (and (stringp project) (stringp path))
             (push (append `((project . ,project)
                             (path . ,path)
                             (managed . ,(if (plist-get record :managed)
                                              t :false)))
                           (when (stringp tox-environment)
                             `((tox-env . ,tox-environment))))
                   runtimes))))
       my/python-runtime-environments)
      (make-directory (file-name-directory my/python-environment-state-file) t)
      (unwind-protect
          (progn
            (setq temporary
                  (make-temp-file
                   (expand-file-name ".python-environments-"
                                     (file-name-directory
                                      my/python-environment-state-file))))
            (with-temp-file temporary
              (set-buffer-file-coding-system 'utf-8-unix)
              (insert (json-serialize
                       `((version . 1)
                         (tools . ,(vconcat tools))
                         (runtimes . ,(vconcat runtimes)))
                       :false-object :false)))
            (set-file-modes temporary #o600)
            (rename-file temporary my/python-environment-state-file t)
            (setq temporary nil))
        (when (and temporary (file-exists-p temporary))
          (delete-file temporary)))))

  (defun my/python--load-environment-selections ()
    "Load valid selection metadata without accessing environment paths."
    (when (file-readable-p my/python-environment-state-file)
      (condition-case error-data
          (let ((data
                 (with-temp-buffer
                   (insert-file-contents my/python-environment-state-file)
                   (json-parse-buffer :object-type 'alist :array-type 'list
                                      :false-object :false))))
            (unless (equal (alist-get 'version data) 1)
              (error "Unsupported state version"))
            (dolist (entry (alist-get 'tools data))
              (let ((host (alist-get 'host entry))
                    (path (alist-get 'path entry)))
                (when (and (stringp host) (stringp path))
                  (puthash host path my/python-tool-environments))))
            (dolist (entry (alist-get 'runtimes data))
              (let ((project (alist-get 'project entry))
                    (path (alist-get 'path entry))
                    (managed (alist-get 'managed entry))
                    (tox-environment (alist-get 'tox-env entry)))
                (when (and (stringp project)
                           (stringp path)
                           (memq managed '(t :false))
                           (or (null tox-environment)
                               (stringp tox-environment)))
                  (puthash project
                           (append (list :path path :managed (eq managed t))
                                   (when tox-environment
                                     (list :tox-env tox-environment)))
                           my/python-runtime-environments)))))
        (error
         (message "Ignoring invalid Python environment state: %s"
                  (error-message-string error-data))))))

  (my/python--load-environment-selections)

  (defun my/python--host (&optional path)
    "Return the local or TRAMP host identity for PATH."
    (or (file-remote-p (or path default-directory)) "local"))

  (defun my/python--native-path (path)
    "Return PATH without its TRAMP prefix for a target-host process."
    (if (file-remote-p path) (file-local-name path) path))

  (defun my/python--command-output (program &rest arguments)
    "Run PROGRAM with ARGUMENTS and return its trimmed output."
    (with-temp-buffer
      (let ((status (apply #'process-file program nil t nil arguments)))
        (unless (and (integerp status) (zerop status))
          (error "%s failed: %s" program (string-trim (buffer-string))))
        (string-trim (buffer-string)))))

  (defun my/python--data-home ()
    "Return the target host's XDG data directory as an Emacs path."
    (if-let* ((remote (file-remote-p default-directory)))
        (concat remote
                (file-name-as-directory
                 (my/python--command-output
                  "sh" "-lc"
                  "printf '%s' \"${XDG_DATA_HOME:-$HOME/.local/share}\"")))
      (file-name-as-directory
       (expand-file-name (or (getenv "XDG_DATA_HOME") "~/.local/share")))))

  (defun my/python--managed-root ()
    "Return the target host's managed Python environment directory."
    (expand-file-name "emacs/python/" (my/python--data-home)))

  (defun my/python--default-tool-environment ()
    "Return the target host's default managed tool environment."
    (expand-file-name "tools/default/" (my/python--managed-root)))

  (defun my/python--executable (environment executable)
    "Return EXECUTABLE inside virtual ENVIRONMENT."
    (expand-file-name (concat "bin/" executable) environment))

  (defun my/python--same-host-p (path)
    "Return non-nil when PATH belongs to the current host."
    (equal (my/python--host path) (my/python--host)))

  (defun my/python--valid-tool-environment-p (environment)
    "Return non-nil when ENVIRONMENT contains pylsp and tox."
    (and environment
         (my/python--same-host-p environment)
         (file-executable-p (my/python--executable environment "pylsp"))
         (file-executable-p (my/python--executable environment "tox"))))

  (defun my/python--tool-environment (&optional required)
    "Return this host's selected or default tool environment.
Signal an error when REQUIRED is non-nil and none is ready."
    (let* ((host (my/python--host))
           (selected (gethash host my/python-tool-environments))
           (default (my/python--default-tool-environment))
           (environment (cond ((my/python--valid-tool-environment-p selected) selected)
                              ((my/python--valid-tool-environment-p default) default))))
      (when (and required (not environment))
        (user-error "No Python tool environment; create or select one first"))
      environment))

  (defun my/python-select-tool-environment (directory)
    "Select an existing tool virtual environment at DIRECTORY for this host."
    (interactive
     (list (read-directory-name "Python tool environment: "
                                (my/python--default-tool-environment) nil t)))
    (setq directory (file-name-as-directory (expand-file-name directory)))
    (unless (my/python--same-host-p directory)
      (user-error "Tool environment must be on the current host"))
    (unless (my/python--valid-tool-environment-p directory)
      (user-error "%s must contain executable bin/pylsp and bin/tox" directory))
    (puthash (my/python--host) directory my/python-tool-environments)
    (my/python--save-environment-selections)
    (message "Selected Python tool environment: %s" directory))

  (defun my/python--copy-tool-requirements ()
    "Copy tool-requirements.txt to the target host and return its path."
    (let* ((source (expand-file-name "tool-requirements.txt" user-emacs-directory))
           (destination (expand-file-name "tools/tool-requirements.txt"
                                          (my/python--managed-root))))
      (make-directory (file-name-directory destination) t)
      (copy-file source destination t)
      destination))

  (defun my/python--shell-command (arguments)
    "Quote and join ARGUMENTS as a shell command."
    (mapconcat #'shell-quote-argument arguments " "))

  (defun my/python--compilation-buffer-name (name _mode)
    "Return a target-specific compilation buffer NAME."
    (format "*%s*" name))

  (defun my/python--compile (name commands)
    "Run shell COMMANDS in compilation buffer NAME."
    (compilation-start
     (mapconcat #'my/python--shell-command commands " && ")
     'compilation-mode
     (apply-partially #'my/python--compilation-buffer-name name)))

  (defun my/python--venv-command (environment &optional clear)
    "Return a command to create ENVIRONMENT, optionally CLEARing it first."
    (let ((path (my/python--native-path environment)))
      (if-let* ((uv (executable-find "uv" t)))
          (append (list (my/python--native-path uv) "venv")
                  (when clear (list "--clear")) (list path))
        (let ((python3 (or (executable-find "python3" t)
                           (user-error "python3 is unavailable on this host"))))
          (append (list (my/python--native-path python3) "-m" "venv")
                  (when clear (list "--clear")) (list path))))))

  (defun my/python--install-command (environment requirements &optional upgrade)
    "Return a command to install REQUIREMENTS into ENVIRONMENT.
UPGRADE requests newer versions satisfying the tracked minimums."
    (let ((python (my/python--native-path
                   (my/python--executable environment "python")))
          (requirements (my/python--native-path requirements)))
      (if-let* ((uv (executable-find "uv" t)))
          (append (list (my/python--native-path uv) "pip" "install"
                        "--python" python)
                  (when upgrade (list "--upgrade"))
                  (list "-r" requirements))
        (append (list python "-m" "pip" "install")
                (when upgrade (list "--upgrade"))
                (list "-r" requirements)))))

  (defun my/python-create-tool-environment (&optional rebuild)
    "Create this host's default tool environment.
With prefix argument REBUILD, clear and recreate the managed environment."
    (interactive "P")
    (let* ((environment (my/python--default-tool-environment))
           (requirements (my/python--copy-tool-requirements)))
      (when (and (file-directory-p environment) (not rebuild))
        (user-error "%s already exists; update it or rebuild with a prefix argument"
                    environment))
      (when (and rebuild
                 (not (yes-or-no-p (format "Rebuild managed tool environment %s? "
                                           environment))))
        (user-error "Tool environment rebuild cancelled"))
      (make-directory (file-name-directory environment) t)
      (puthash (my/python--host) environment my/python-tool-environments)
      (my/python--save-environment-selections)
      (my/python--compile
       (format "Python tools %s" (my/python--host))
       (list (my/python--venv-command environment rebuild)
             (my/python--install-command environment requirements)))))

  (defun my/python-update-tool-environment ()
    "Update the selected tool environment without recreating it."
    (interactive)
    (let ((environment (my/python--tool-environment t))
          (requirements (my/python--copy-tool-requirements)))
      (my/python--compile
       (format "Python tools %s" (my/python--host))
       (list (my/python--install-command environment requirements t)))))

  (defun my/python-rebuild-tool-environment ()
    "Clear and recreate this host's managed tool environment."
    (interactive)
    (my/python-create-tool-environment t))

  (defun my/python--project-root (&optional noerror)
    "Return the current project root, or nil when NOERROR is non-nil."
    (when-let* ((project (project-current (not noerror))))
      (file-name-as-directory (project-root project))))

  (defun my/python--project-environment-root (project)
    "Return the managed environment directory for PROJECT."
    (let ((name (file-name-nondirectory (directory-file-name project)))
          (identity (secure-hash 'sha256 project)))
      (expand-file-name
       (format "projects/%s-%s/"
               (replace-regexp-in-string "[^[:alnum:]_.-]" "-" name)
               identity)
       (my/python--managed-root))))

  (defun my/python--runtime-record (&optional project)
    "Return the ready runtime record for PROJECT."
    (when-let* ((root (or project (my/python--project-root t)))
                (record (gethash root my/python-runtime-environments))
                (environment (plist-get record :path))
                ((my/python--same-host-p environment))
                ((file-executable-p (my/python--executable environment "python")))
                ((or (not (plist-get record :managed))
                     (file-exists-p (expand-file-name ".emacs-ready" environment)))))
      record))

  (defun my/python-select-runtime-environment (directory)
    "Select existing runtime DIRECTORY for the current project."
    (interactive
     (let ((project (my/python--project-root)))
       (list (read-directory-name "Python runtime environment: "
                                  (my/python--project-environment-root project)
                                  nil t))))
    (let ((project (my/python--project-root)))
      (setq directory (file-name-as-directory (expand-file-name directory)))
      (unless (my/python--same-host-p directory)
        (user-error "Runtime environment must be on the project host"))
      (unless (file-executable-p (my/python--executable directory "python"))
        (user-error "%s does not contain executable bin/python" directory))
      (puthash project (list :path directory :managed nil)
               my/python-runtime-environments)
      (my/python--save-environment-selections)
      (my/python--apply-runtime)
      (message "Selected Python runtime environment: %s" directory)))

  (defun my/python--file-matches-p (file regexp)
    "Return non-nil when FILE contains REGEXP."
    (and (file-readable-p file)
         (with-temp-buffer
           (insert-file-contents file)
           (re-search-forward regexp nil t))))

  (defun my/python--tox-project-p (project)
    "Return non-nil when PROJECT contains a tox configuration."
    (or (file-exists-p (expand-file-name "tox.ini" project))
        (file-exists-p (expand-file-name ".tox.ini" project))
        (file-exists-p (expand-file-name "tox.toml" project))
        (my/python--file-matches-p (expand-file-name "setup.cfg" project)
                                   "^\\[tox:tox\\]")
        (my/python--file-matches-p (expand-file-name "pyproject.toml" project)
                                   "^\\[tool\\.tox\\(?:\\.|\\]\\)")))

  (defun my/python--tox-environments (project tool)
    "Return tox environments for PROJECT using TOOL."
    (let ((default-directory project)
          (tox (my/python--native-path (my/python--executable tool "tox"))))
      (seq-filter
       (lambda (line)
         (and (string-match-p "\\`[[:alnum:]_.-]+\\'" line)
              (not (member line '("." "..")))))
       (split-string (my/python--command-output tox "list" "--no-desc")
                     "\n" t "[[:space:]]+"))))

  (defun my/python--managed-tox-environment (base tox-environment)
    "Return a safe managed path under BASE for TOX-ENVIRONMENT."
    (when (member tox-environment '("." ".."))
      (user-error "Unsafe tox environment name: %s" tox-environment))
    (let ((environment
           (expand-file-name
            (format "%s-%s/"
                    (replace-regexp-in-string "[^[:alnum:]_.-]" "-" tox-environment)
                    (secure-hash 'sha256 tox-environment))
            base)))
      (unless (string-prefix-p (file-name-as-directory (expand-file-name base))
                               (file-name-as-directory environment))
        (error "Managed environment escaped its project directory: %s" environment))
      environment))

  (defun my/python--prepare-runtime (rebuild choose)
    "Prepare the current runtime environment.
REBUILD recreates a managed environment; CHOOSE prompts for a tox environment."
    (let* ((project (my/python--project-root))
           (current (gethash project my/python-runtime-environments))
           (managed (plist-get current :managed)))
      (when (and rebuild (not managed))
        (user-error "Select or prepare a managed project environment before rebuilding"))
      (when (and rebuild
                 (not (yes-or-no-p (format "Rebuild managed environment for %s? " project))))
        (user-error "Runtime environment rebuild cancelled"))
      (if (my/python--tox-project-p project)
          (let* ((tool (my/python--tool-environment t))
                 (environments (my/python--tox-environments project tool))
                 (old-tox (plist-get current :tox-env))
                 (tox-environment
                  (if (and old-tox (member old-tox environments) (not choose)) old-tox
                    (completing-read "Tox environment: " environments nil t
                                     nil nil (and (member "py3" environments) "py3"))))
                 (base (my/python--project-environment-root project))
                 (environment (my/python--managed-tox-environment
                               base tox-environment))
                 (workdir (expand-file-name "tox-work/" base))
                 (marker (expand-file-name ".emacs-ready" environment))
                 (tox (my/python--native-path (my/python--executable tool "tox"))))
            (puthash project
                     (list :path environment :managed t :tox-env tox-environment)
                     my/python-runtime-environments)
            (my/python--save-environment-selections)
            (let ((default-directory project))
              (my/python--compile
               (format "Python runtime %s" (substring (secure-hash 'sha1 project) 0 8))
               (list (list "rm" "-f" (my/python--native-path marker))
                     (append (list tox "--workdir" (my/python--native-path workdir))
                             (when rebuild (list "--recreate"))
                             (list "devenv" "-e" tox-environment
                                   (my/python--native-path environment)))
                     (list "touch" (my/python--native-path marker))))))
        (let* ((base (my/python--project-environment-root project))
               (environment (expand-file-name "venv/" base))
               (marker (expand-file-name ".emacs-ready" environment)))
          (puthash project (list :path environment :managed t)
                   my/python-runtime-environments)
          (my/python--save-environment-selections)
          (make-directory base t)
          (my/python--compile
           (format "Python runtime %s" (substring (secure-hash 'sha1 project) 0 8))
           (list (list "rm" "-f" (my/python--native-path marker))
                 (my/python--venv-command environment rebuild)
                 (list "touch" (my/python--native-path marker))))))))

  (defun my/python-prepare-runtime-environment (&optional choose)
    "Prepare an out-of-tree runtime environment.
With prefix argument CHOOSE, prompt for a different tox environment."
    (interactive "P")
    (my/python--prepare-runtime nil choose))

  (defun my/python-rebuild-runtime-environment ()
    "Recreate the managed runtime environment for the current checkout."
    (interactive)
    (my/python--prepare-runtime t nil))

  (defun my/python-eglot-contact (&optional _interactive _project)
    "Return pylsp from the selected tool environment with runtime settings."
    (let* ((tool (my/python--tool-environment t))
           (runtime (or (my/python--runtime-record)
                        (user-error "Prepare or select a project runtime environment first")))
           (environment (my/python--native-path (plist-get runtime :path))))
      (list (my/python--native-path (my/python--executable tool "pylsp"))
            :initializationOptions
            `(:pylsp (:plugins (:jedi (:environment ,environment)))))))

  (defun my/python-eglot-ensure ()
    "Start Eglot only when this project already has ready environments."
    (when (and (my/python--project-root t)
               (my/python--tool-environment)
               (my/python--runtime-record))
      (eglot-ensure)))

  (defun my/python--apply-runtime ()
    "Use the selected runtime environment in the current Python buffer."
    (when-let* ((runtime (my/python--runtime-record))
                (environment (plist-get runtime :path)))
      (setq-local python-shell-interpreter
                  (my/python--native-path
                   (my/python--executable environment "python")))))

  (defun my/python-eglot ()
    "Start or join Eglot using the selected Python environments."
    (interactive)
    (my/python--tool-environment t)
    (unless (my/python--runtime-record)
      (user-error "Prepare or select a project runtime environment first"))
    (my/python--apply-runtime)
    (call-interactively #'eglot))

  (defun my/python-mode-setup ()
    "Configure indentation and the selected runtime for Python buffers."
    (setq-local tab-width 4)
    (setq-local python-indent-offset 4)
    (my/python--apply-runtime))

  (defun my/python-restart-eglot ()
    "Restart Eglot with the currently selected Python environments."
    (interactive)
    (unless (derived-mode-p 'python-base-mode)
      (user-error "This command requires a Python buffer"))
    (my/python--tool-environment t)
    (unless (my/python--runtime-record)
      (user-error "Prepare or select a project runtime environment first"))
    (when-let* ((server (and (featurep 'eglot)
                             (eglot-managed-p)
                             (eglot-current-server))))
      (eglot-shutdown server))
    (my/python-eglot))

  (defun my/python-environment-status ()
    "Describe selected Python environments and current Eglot state."
    (interactive)
    (let* ((host (my/python--host))
           (project (my/python--project-root t))
           (selected-tool (or (gethash host my/python-tool-environments)
                              (my/python--default-tool-environment)))
           (tool-ready (my/python--valid-tool-environment-p selected-tool))
           (runtime (and project
                         (gethash project my/python-runtime-environments)))
           (runtime-path (plist-get runtime :path))
           (runtime-ready (and project (my/python--runtime-record project)))
           (managed (and (featurep 'eglot) (eglot-managed-p))))
      (with-help-window "*Python environments*"
        (princ (format "Host: %s\n" host))
        (princ (format "Project: %s\n\n" (or project "none")))
        (princ (format "Selected tool environment: %s\n"
                       (or selected-tool "none")))
        (princ (format "Tool ready: %s\n\n" (if tool-ready "yes" "no")))
        (princ (format "Selected runtime environment: %s\n"
                       (or runtime-path "none")))
        (princ (format "Runtime ready: %s\n" (if runtime-ready "yes" "no")))
        (princ (format "Managed by Emacs: %s\n"
                       (if (plist-get runtime :managed) "yes" "no")))
        (princ (format "Tox environment: %s\n\n"
                       (or (plist-get runtime :tox-env) "none")))
        (princ (format "Current buffer managed by Eglot: %s\n"
                       (if managed "yes" "no")))
        (when managed
          (princ "A running server may predate the selections above.\n"))
        (princ "Use C-c v e to restart Eglot with the selected environments.\n")
        (princ "Use C-c v l to inspect Eglot's actual initialization exchange.\n"))))

  (transient-define-prefix my/python-environment-menu ()
    "Manage Python tool and project environments."
    [["Tool environment"
      ("c" "Create" my/python-create-tool-environment)
      ("R" "Rebuild" my/python-rebuild-tool-environment)
      ("u" "Update" my/python-update-tool-environment)
      ("s" "Select" my/python-select-tool-environment)]
     ["Project runtime"
      ("p" "Prepare" my/python-prepare-runtime-environment)
      ("r" "Rebuild" my/python-rebuild-runtime-environment)
      ("S" "Select" my/python-select-runtime-environment)]
     ["Eglot"
      ("i" "Environment info" my/python-environment-status)
      ("e" "Restart with selections" my/python-restart-eglot)
      ("l" "Show events" eglot-events-buffer)]])

  (defvar-keymap my/python-tool-environment-map
    :doc "Commands for host-local Python tool environments."
    "c" #'my/python-create-tool-environment
    "r" #'my/python-rebuild-tool-environment
    "s" #'my/python-select-tool-environment
    "u" #'my/python-update-tool-environment)

  (defvar-keymap my/python-project-environment-map
    :doc "Commands for project Python runtime environments."
    "c" #'my/python-prepare-runtime-environment
    "r" #'my/python-rebuild-runtime-environment
    "s" #'my/python-select-runtime-environment)

  (defvar-keymap my/python-environment-map
    :doc "Commands for managed Python virtual environments."
    "e" #'my/python-restart-eglot
    "i" #'my/python-environment-status
    "l" #'eglot-events-buffer
    "m" #'my/python-environment-menu
    "p" my/python-project-environment-map
    "t" my/python-tool-environment-map)

  (global-set-key (kbd "C-c v") my/python-environment-map)

  (with-eval-after-load 'which-key
    (which-key-add-key-based-replacements
      "C-c v" '("virtual environments" . "Python environments")
      "C-c v e" "restart Eglot with selections"
      "C-c v i" "show environment info"
      "C-c v l" "show Eglot events"
      "C-c v m" "open environment menu"
      "C-c v p" '("project" . "Project runtime environment")
      "C-c v p c" "create or prepare"
      "C-c v p r" "rebuild"
      "C-c v p s" "select existing"
      "C-c v t" '("tool" . "Host tool environment")
      "C-c v t c" "create"
      "C-c v t r" "rebuild"
      "C-c v t s" "select existing"
      "C-c v t u" "update"))

  (use-package python
    :ensure nil
    :straight (:type built-in)
    :hook ((python-mode . my/python-mode-setup)
           (python-ts-mode . my/python-mode-setup))
    :bind (:map python-base-mode-map
                ("C-c l l" . my/python-eglot))
    :custom
    (python-check-command "ruff check --ignore-noqa"))

  (with-eval-after-load 'eglot
    (add-to-list 'eglot-server-programs
                 '((python-mode python-ts-mode) . my/python-eglot-contact)))

Language: Markdown

This section configures the Markdown syntax highlighting.

(use-package markdown-mode
  :mode ("README\\.md\\'" . gfm-mode)
  :bind (:map markdown-mode-map
         ("C-c C-e" . markdown-do))
  :custom
  (markdown-command "multimarkdown"))

Language: Zig

This section configures Zig development support with syntax highlighting, LSP integration, and Tree-sitter parsing.

(defun my/zig-mode-setup ()
  "Use four-column, space-only indentation in Zig buffers."
  (setq-local tab-width 4)
  (setq-local indent-tabs-mode nil))

(use-package zig-mode
  :hook ((zig-mode . my/zig-mode-setup)
         (zig-ts-mode . my/zig-mode-setup)))

Language: Nix

Nix expression file editing with tree-sitter powered syntax highlighting. The nix tree-sitter grammar will be prompted for installation on first use via treesit-auto.

(use-package nix-ts-mode
  :mode "\\.nix\\'")

Project-Specific Environment (envrc)

envrc provides buffer-local environment variable management via direnv. Unlike global direnv-mode, each buffer gets its own environment from the .envrc file in its project root. This is essential for Nix devshells, where different projects may provide different tool versions.

With nix-direnv installed on the system, Nix environment evaluations are cached so loads are fast after the first run. Projects just need an .envrc containing use flake and a flake.nix with a devShells.default.

(use-package envrc
  :if (executable-find "direnv")
  :bind (:map envrc-mode-map
         ("C-c e" . envrc-command-map))
  :config
  (envrc-global-mode))

Project Configuration (editorconfig)

EditorConfig helps maintain consistent coding styles across different editors and IDEs.

(use-package editorconfig
  :config
  (editorconfig-mode 1))

Enhanced Project Management

Enhanced project.el integration with useful keybindings for project-based workflows. Custom helper functions provide integrated workflows leveraging treemacs, eat, magit, and consult.

;; Custom project helper functions
(defun my/project-workspace-setup ()
  "Setup 2-pane workspace: dired on left, ghostel terminal on right (50:50 split)."
  (interactive)
  (let ((project-root (project-root (project-current))))
    ;; Close treemacs if it's loaded and has a workspace
    (when (and (featurep 'treemacs)
               (fboundp 'treemacs-current-workspace)
               (treemacs-current-workspace))
      (treemacs-kill-buffer))
    ;; Start with a clean slate
    (delete-other-windows)
    ;; Open dired in project root
    (dired project-root)
    ;; Split window vertically (50:50)
    (split-window-right)
    ;; Move to right pane and open terminal
    (other-window 1)
    (ghostel-project)
    ;; Terminal should be the active buffer (already is from other-window)
    ))

(defun my/project-dev-setup ()
  "Open terminal + magit for development workflow."
  (interactive)
  (let ((project-root (project-root (project-current))))
    (eat-project)
    (magit-status project-root)))

(defun my/project-smart-compile ()
  "Compile the current project with a command inferred from its root."
  (interactive)
  (let* ((project-root (project-root (project-current t)))
         (default-directory project-root)
         (compile-cmd (cond
                       ((file-exists-p "Makefile") "make")
                       ((file-exists-p "package.json") "npm run build")
                       ((file-exists-p "Cargo.toml") "cargo build")
                       ((file-exists-p "pyproject.toml") "python -m build")
                       ((file-exists-p "CMakeLists.txt") "cmake --build build")
                       ((file-exists-p "flake.nix") "nix build")
                       (t "make"))))
    (compile compile-cmd)))

(defun my/consult-project-ripgrep ()
  "Ripgrep in current project with better defaults."
  (interactive)
  (consult-ripgrep (project-root (project-current))))

(defun my/project-show-treemacs ()
  "Show treemacs for current project."
  (interactive)
  (if (treemacs-current-workspace)
      (treemacs-select-window)
    (treemacs)))

(defun my/project-ranger ()
  "Open ranger in current project root."
  (interactive)
  (let ((project-root (project-root (project-current))))
    (ranger project-root)))

(defun my/project-recent-files ()
  "Show recent files in current project using consult."
  (interactive)
  (unless (bound-and-true-p recentf-mode)
    (recentf-mode 1))
  (let* ((project-root (project-root (project-current)))
         (project-files (when (and project-root
                                  (bound-and-true-p recentf-list))
                         (seq-filter
                          (lambda (file)
                            (string-prefix-p project-root file))
                          recentf-list))))
    (if project-files
        (find-file (completing-read "Recent project files: " project-files))
      (message "No recent files found in this project"))))

(defun my/project-remember-projects-under (dir max-depth)
  "Remember projects under DIR, descending at most MAX-DEPTH levels."
  (interactive "DDirectory: \nnMaximum depth: ")
  (let ((directories (list (cons (expand-file-name dir) 0))))
    (while directories
      (let* ((entry (pop directories))
             (directory (car entry))
             (depth (cdr entry)))
        (when-let ((project (project-current nil directory)))
          (project-remember-project project nil t))
        (when (< depth max-depth)
          (dolist (child
                   (directory-files
                    directory t directory-files-no-dot-files-regexp))
            (when (and (file-directory-p child)
                       (not (file-symlink-p child)))
              (push (cons child (1+ depth)) directories))))))))

(use-package project
  :ensure nil
  :straight (:type built-in)
  :bind (("C-x p p" . project-switch-project)
         ("C-x p f" . project-find-file)
         ("C-x p g" . project-find-regexp)
         ("C-x p d" . project-find-dir)
         ("C-x p t" . eat-project)      ; Modern terminal for project
         ("C-x p G" . ghostel-project)  ; Ghostty-powered terminal for project
         ("C-x p a" . ansi-term))       ; Alternative terminal option
  :custom
  ;; Enhanced project switching menu with streamlined, logically grouped actions
  (project-switch-commands
   '(;; Core File Operations (most frequent)
     (project-find-file "Find file" ?f)
     (consult-find "Find externally" ?F)
     (my/project-recent-files "Recent files" ?r)
     (consult-project-buffer "Project buffers" ?b)
     ;; Navigation & Browsing
     (my/project-ranger "Browse (ranger)" ?d)
     (my/consult-project-ripgrep "Search project" ?s)
     ;; Development Workflow
     (eat-project "Terminal" ?t)
     (ghostel-project "Ghostty terminal" ?G)
     (magit-status "Git status" ?g)
     (my/project-smart-compile "Compile" ?c)
     ;; Layout & Cleanup
     (my/project-workspace-setup "Workspace setup" ?w)
     (my/project-show-treemacs "Treemacs" ?T)
     (project-kill-buffers "Kill buffers" ?k))))

Shell & Terminals

Configuration for various terminal emulators inside Emacs. I use `eat`, a modern term-mode replacement. Terminal commands are bound under the `C-c t` prefix to avoid conflicts with spell-checking commands.

(straight-use-package
 '(eat :type git
       :host codeberg
       :repo "akib/emacs-eat"
       :files ("*.el" ("term" "term/*.el") "*.texi"
               "*.ti" ("terminfo/e" "terminfo/e/*")
               ("terminfo/65" "terminfo/65/*")
               ("integration" "integration/*")
               (:exclude ".dir-locals.el" "*-tests.el"))))

(use-package eat
  :ensure nil ; It's installed by `straight-use-package` above
  :bind (("C-c t t" . eat)
         ("C-c t a" . ansi-term)     ; Fallback terminal
         ("C-c t p" . eat-project))  ; Project-specific terminal
  :hook (eat-mode . (lambda ()
                      ;; Prevent Emacs from recentering (jumping cursor to middle)
                      ;; when large terminal output moves point off-screen
                      (setq-local scroll-conservatively most-positive-fixnum)
                      (setq-local scroll-margin 0)
                      ;; Disable visual features that interfere with terminal display
                      (hl-line-mode -1)
                      (pulsar-mode -1))))

;; ghostel: fast terminal emulator powered by libghostty-vt (same engine as Ghostty).
;; Native module is auto-downloaded on first use.
(use-package ghostel
  :straight (:host github :repo "dakra/ghostel")
  :bind (("C-c t g" . ghostel)
         ("C-c t G" . ghostel-project))
  :hook (ghostel-mode . (lambda ()
                          (setq-local scroll-conservatively most-positive-fixnum)
                          (setq-local scroll-margin 0)
                          (hl-line-mode -1)
                          (pulsar-mode -1))))

AI Terminal Launcher

Lightweight replacement for claude-code.el. Launches AI coding tools (Claude, OpenCode, Droid, Pi) in a right-side terminal panel at ~30% width, with per-project instance tracking. The terminal backend is configurable: ghostel (default), eat, or ansi-term.

Ghostel launches its supported shell entry point, preserving Ghostel shell integration and the target buffer’s environment. The command is sent through Ghostel’s public input API as soon as the shell process is created; no readiness timer is used.

;; Also install this helper on machines without envrc/direnv.
(use-package inheritenv :defer t)

;;; --- Configuration ---

(defcustom my/ai-term-backend 'ghostel
  "Terminal backend to use for AI tools: ghostel, eat, or ansi-term."
  :type '(radio (const ghostel) (const eat) (const ansi-term))
  :group 'tools)

(defcustom my/ai-term-window-width 0.30
  "Width of the AI terminal side window as a fraction of the frame."
  :type 'number
  :group 'tools)

;;; --- Internal helpers ---

;; These values identify launcher-owned buffers.  Buffer names are only
;; cosmetic; the full target directory and TRAMP prefix are the session key.
(defvar-local my/ai-term-directory-identity nil)
(defvar-local my/ai-term-tool nil)
(defvar-local my/ai-term-originating-backend nil)

(defun my/ai-term--directory ()
  "Return the working directory for the AI tool.
Prefers project root, then the buffer file's directory, then `default-directory'."
  (or (when (project-current) (project-root (project-current)))
      (when buffer-file-name (file-name-directory buffer-file-name))
      default-directory))

(defun my/ai-term--directory-identity (dir)
  "Return the complete local or remote identity for target directory DIR."
  (let ((directory (file-name-as-directory (expand-file-name dir))))
    (list :directory directory :remote (file-remote-p directory))))

(defun my/ai-term--project-name (dir)
  "Return the short, cosmetic project name for DIR."
  (file-name-nondirectory (directory-file-name (expand-file-name dir))))

(defun my/ai-term--buffer-name (tool dir)
  "Return a cosmetic, identity-distinct buffer name for TOOL in DIR."
  (let* ((identity (my/ai-term--directory-identity dir))
         (digest (substring (secure-hash 'sha1 (prin1-to-string identity)) 0 10)))
    (format "*AI %s:%s@%s*" tool (my/ai-term--project-name dir) digest)))

(defun my/ai-term--buffer-alive-p (buf)
  "Return non-nil if BUF has a live terminal lifecycle process."
  (and (buffer-live-p buf)
       (let ((process (get-buffer-process buf)))
         (and process (process-live-p process)))))

(defun my/ai-term--owned-buffer-p (buf identity &optional tool)
  "Return non-nil if BUF is an owned AI terminal for IDENTITY and TOOL."
  (and (buffer-live-p buf)
       (with-current-buffer buf
         (and (local-variable-p 'my/ai-term-directory-identity buf)
              (equal my/ai-term-directory-identity identity)
              (or (null tool) (equal my/ai-term-tool tool))))))

(defun my/ai-term--find-buffer (dir &optional tool live-only)
  "Find an owned AI terminal for DIR and optional TOOL.
When LIVE-ONLY is non-nil, only return a buffer with a live process."
  (let ((identity (my/ai-term--directory-identity dir)))
    (seq-find (lambda (buf)
                (and (my/ai-term--owned-buffer-p buf identity tool)
                     (or (not live-only) (my/ai-term--buffer-alive-p buf))))
              (buffer-list))))

(defun my/ai-term--tag-buffer (buf identity tool backend)
  "Record the owning session identity, TOOL, and BACKEND in BUF."
  (with-current-buffer buf
    (setq-local my/ai-term-directory-identity identity)
    (setq-local my/ai-term-tool tool)
    (setq-local my/ai-term-originating-backend backend))
  buf)

(defun my/ai-term--display (buf)
  "Display BUF in a right-side window at `my/ai-term-window-width'."
  (let ((win (display-buffer buf
               `((display-buffer-in-side-window)
                 (side . right)
                 (window-width . ,my/ai-term-window-width)))))
    (when win
      (set-window-parameter win 'left-fringe-width 0)
      (set-window-parameter win 'right-fringe-width 0))
    win))

;;; --- Backend implementations ---

(defun my/ai-term--shell-command (program)
  "Return PROGRAM as a shell command string.
PROGRAM may be a string or a list of program/argument strings."
  (if (listp program)
      (mapconcat #'shell-quote-argument program " ")
    program))

(defun my/ai-term--backend-name (buf-name)
  "Return a fresh backend buffer base name derived from cosmetic BUF-NAME.
Eat and ansi-term address buffers by name, so never let either reuse an
unrelated buffer which happens to have the same cosmetic label."
  (let ((base (string-trim buf-name "\\*" "\\*"))
        (number 2))
    (while (get-buffer (format "*%s*" base))
      (setq base (format "%s<%d>" (string-trim buf-name "\\*" "\\*") number)
            number (1+ number)))
    base))

(defun my/ai-term--make-ghostel (buf-name dir program)
  "Create a Ghostel shell in DIR and send PROGRAM through its public API."
  (require 'ghostel)
  (let ((default-directory dir))
    (let ((buf (ghostel-create buf-name nil)))
      (with-current-buffer buf
        (ghostel-send-string (concat (my/ai-term--shell-command program) "\n")))
      buf)))

(defun my/ai-term--make-eat (buf-name dir program)
  "Create an Eat buffer in DIR running PROGRAM."
  (require 'eat)
  (let* ((default-directory dir)
         (argv (if (listp program)
                   program
                 (list shell-file-name shell-command-switch program)))
         (name (my/ai-term--backend-name buf-name)))
    (apply #'eat-make name (car argv) nil (cdr argv))))

(defun my/ai-term--make-ansi-term (buf-name dir program)
  "Create an ansi-term buffer in DIR and send PROGRAM."
  (require 'term)
  (let ((default-directory dir))
    (with-current-buffer (ansi-term shell-file-name
                                     (my/ai-term--backend-name buf-name))
      (term-send-raw-string (concat (my/ai-term--shell-command program) "\n"))
      (current-buffer))))

(defun my/ai-term--make-buffer (buf-name dir program)
  "Dispatch creation with the originating buffer's local environment."
  (require 'inheritenv)
  (inheritenv-apply
   (lambda ()
     (pcase my/ai-term-backend
       ('ghostel   (my/ai-term--make-ghostel buf-name dir program))
       ('eat       (my/ai-term--make-eat buf-name dir program))
       ('ansi-term (my/ai-term--make-ansi-term buf-name dir program))
       (_ (error "Unknown my/ai-term-backend: %s" my/ai-term-backend))))))

;;; --- Core launch ---

(defun my/ai-term-launch (tool program)
  "Launch PROGRAM as TOOL in the configured terminal.
Reuse a live owned session for the same full directory and tool, regardless
of later changes to `my/ai-term-backend'.  A stale owned buffer is only
replaced after `kill-buffer' succeeds."
  (let* ((dir (my/ai-term--directory))
         (identity (my/ai-term--directory-identity dir))
         (live (my/ai-term--find-buffer dir tool t))
         (stale (and (not live) (my/ai-term--find-buffer dir tool))))
    (cond
     (live
      (my/ai-term--display live))
     (stale
      (unless (kill-buffer stale)
        (user-error "AI terminal %s was not killed" (buffer-name stale)))
      (when (buffer-live-p stale)
        (user-error "AI terminal %s is still live" (buffer-name stale)))
      (let ((buf (my/ai-term--make-buffer
                  (my/ai-term--buffer-name tool dir) dir program)))
        (my/ai-term--tag-buffer buf identity tool my/ai-term-backend)
        (my/ai-term--display buf)
        buf))
     (t
      (let ((buf (my/ai-term--make-buffer
                  (my/ai-term--buffer-name tool dir) dir program)))
        (my/ai-term--tag-buffer buf identity tool my/ai-term-backend)
        (my/ai-term--display buf)
        buf)))))

;;; --- Toggle ---

(defun my/ai-term-toggle ()
  "Toggle a live owned AI terminal side window for the current project."
  (interactive)
  (let ((buf (my/ai-term--find-buffer (my/ai-term--directory) nil t)))
    (cond
     ((and buf (get-buffer-window buf))
      (delete-window (get-buffer-window buf)))
     (buf
      (my/ai-term--display buf))
     (t
      (message "No AI terminal running for this project")))))

;;; --- Send commands ---

(defun my/ai-term--send (buf string)
  "Send STRING followed by a newline to owned AI terminal BUF."
  (with-current-buffer buf
    (pcase my/ai-term-originating-backend
      ('ghostel (ghostel-send-string (concat string "\n")))
      ('eat (eat-term-send-string eat-terminal (concat string "\n")))
      ('ansi-term (term-send-raw-string (concat string "\n")))
      (_ (user-error "Buffer %s is not an AI terminal" (buffer-name buf))))))

(defun my/ai-term-send-command ()
  "Prompt for a string and send it to the current project's live AI terminal."
  (interactive)
  (let ((buf (my/ai-term--find-buffer (my/ai-term--directory) nil t)))
    (if buf
        (my/ai-term--send buf (read-string "Send to AI: "))
      (message "No AI terminal running for this project"))))

;;; --- Tool launchers ---

(defun my/claude ()
  "Launch Claude Code in the current project's AI terminal."
  (interactive)
  (my/ai-term-launch "claude" "claude"))

(defun my/opencode ()
  "Launch OpenCode in the current project's AI terminal."
  (interactive)
  (my/ai-term-launch "opencode" "opencode"))

(defun my/droid ()
  "Launch Factory.ai Droid in the current project's AI terminal."
  (interactive)
  (my/ai-term-launch "droid" "droid"))

(defun my/pi ()
  "Launch Pi in the current project's AI terminal."
  (interactive)
  (my/ai-term-launch "pi" "pi"))

(defun my/pi-no-sandbox ()
  "Launch Pi with --no-sandbox in the current project's AI terminal."
  (interactive)
  (my/ai-term-launch "pi-no-sandbox" '("pi" "--no-sandbox")))

;;; --- Keymap ---

(defvar my/ai-term-map
  (let ((m (make-sparse-keymap)))
    (define-key m (kbd "c") #'my/claude)
    (define-key m (kbd "o") #'my/opencode)
    (define-key m (kbd "d") #'my/droid)
    (define-key m (kbd "p") #'my/pi)
    (define-key m (kbd "P") #'my/pi-no-sandbox)
    (define-key m (kbd "t") #'my/ai-term-toggle)
    (define-key m (kbd "s") #'my/ai-term-send-command)
    m)
  "Keymap for AI terminal launcher commands, bound to \\[my/ai-term-map].")

(global-set-key (kbd "C-c a") my/ai-term-map)

Knowledge Management

Org-roam

Org-roam provides a non-hierarchical note-taking system built on top of Org-mode. It is only loaded when the notes vault directory (~~/repos/notes~) exists, so machines without the vault start cleanly with no errors and no C-c n bindings.

(use-package org-roam
  :if (file-directory-p (expand-file-name "~/repos/notes"))
  :custom
  (org-roam-directory (expand-file-name "~/repos/notes"))
  (org-roam-db-location (expand-file-name "~/repos/notes/.org-roam.db"))
  (org-roam-capture-templates
   '(("p" "public" plain "%?"
      :target (file+head "public/%<%Y%m%d%T>-${slug}.org"
                         "#+title: ${title}\n#+filetags: :public:\n")
      :unnarrowed t)
     ("r" "private" plain "%?"
      :target (file+head "private/%<%Y%m%d%T>-${slug}.org"
                         "#+title: ${title}\n#+filetags: :private:\n")
      :unnarrowed t)))
  :bind (("C-c n f" . org-roam-node-find)
         ("C-c n i" . org-roam-node-insert)
         ("C-c n c" . org-roam-capture)
         ("C-c n l" . org-roam-buffer-toggle)
         ("C-c n g" . org-roam-graph))
  :config
  (org-roam-db-autosync-mode))

Vault Git Workflow

Org-roam does not alter version control for the notes vault. Use Magit or another Git client to review, commit, and push note changes manually.

Custom Commands & Bindings

This section is for custom functions and global keybindings that don’t belong to a specific package.

;; Bury the current buffer instead of killing it.
(global-set-key (kbd "C-c k") #'bury-buffer)

;; A convenient key for replacing text via regexp.
(global-set-key (kbd "C-c r") #'replace-regexp)

;; Toggles whitespace visibility.
(global-set-key (kbd "C-c w") #'whitespace-mode)

;; Custom function to comment/uncomment line or region without moving cursor
(defun my/comment-dwim-line-or-region ()
  "Toggle the region, enclosing comment, or current line without moving point."
  (interactive)
  (condition-case err
      (if (region-active-p)
          ;; If region is selected, comment/uncomment the region
          (comment-or-uncomment-region (region-beginning) (region-end))
        (save-excursion
          (comment-normalize-vars)
          (if-let ((beginning (comment-beginning)))
              (let ((end (progn
                           (goto-char beginning)
                           (comment-forward 1)
                           (point))))
                (uncomment-region beginning end))
            ;; Outside a comment, toggle the entire current line.
            (comment-line 1))))
    (error
     (message "Error commenting: %s" (error-message-string err)))))

;; Toggle comment/uncomment for region or line
;; Clear any existing binding for C-/ first
(global-unset-key (kbd "C-/"))
(global-set-key (kbd "C-/") #'my/comment-dwim-line-or-region)

;; Keybinding for the restart command.
(global-set-key (kbd "C-c x r") #'restart-emacs)

;; Writing assistance and accessibility keybindings
(global-set-key (kbd "C-c W") #'writegood-mode)         ; Toggle writing analysis

;; Spell checking keybindings (C-c s prefix)
(global-set-key (kbd "C-c s c") #'flyspell-correct-word-before-point)  ; Quick spell correction
(global-set-key (kbd "C-c s n") #'flyspell-goto-next-error)            ; Next spelling error
(global-set-key (kbd "C-c s l") #'ispell-change-dictionary)            ; Change dictionary

;; Codespell keybindings (C-c s prefix)
(global-set-key (kbd "C-c s b") #'my/codespell-buffer)   ; Check buffer
(global-set-key (kbd "C-c s r") #'my/codespell-region)   ; Check region
(global-set-key (kbd "C-c s p") #'my/codespell-project)  ; Check project

Package Management

This section contains package management functions for straight.el, version control integration, and a transient menu interface.

Core Package Operations

Functions for basic package management operations like freezing versions and updating packages.

;; Core package management functions
(defun my/straight-freeze-versions ()
  "Freeze current package versions to lock file."
  (interactive)
  (condition-case err
      (progn
        (message "Freezing package versions...")
        (straight-freeze-versions)
        (message "Package versions frozen to configured lockfiles"))
    (error
     (message "Error freezing packages: %s" (error-message-string err)))))

(defun my/straight-update-all ()
  "Update and rebuild all straight packages."
  (interactive)
  (condition-case err
      (progn
        (message "Pulling all packages...")
        (straight-pull-all)
        (message "Rebuilding all packages...")
        (straight-rebuild-all)
        (message "All packages updated and rebuilt successfully"))
    (error
     (message "Error updating packages: %s" (error-message-string err)))))

Package Maintenance

`straight-check-all` checks for package modifications and rebuilds affected packages; it is not a repository-health report. Pruning deletes build artifacts and must only be run after a complete, successful init has established which packages are in use. It always asks for confirmation and is never run automatically.

;; Package maintenance functions use straight.el's public interactive APIs.
(defun my/straight-check-modifications ()
  "Rebuild packages that straight.el detects as modified."
  (interactive)
  (straight-check-all))

(defun my/straight-prune-build ()
  "Prune unused build artifacts after a complete successful init."
  (interactive)
  (when (yes-or-no-p
         "Prune unused build artifacts after a complete successful init? ")
    (straight-prune-build)
    (message "Pruned unused build artifacts")))

(defun my/straight-normalize-all ()
  "Fix repository states by normalizing all packages."
  (interactive)
  (condition-case err
      (when (yes-or-no-p "This will reset all package repositories. Continue? ")
        (message "Normalizing all package repositories...")
        (straight-normalize-all)
        (message "All packages normalized successfully"))
    (error
     (message "Error normalizing packages: %s" (error-message-string err)))))

(defun my/straight-rebuild-package ()
  "Select and rebuild a package using straight.el's interactive command."
  (interactive)
  (call-interactively #'straight-rebuild-package))

Version Control Integration

Functions for managing lockfile versions, including backup, restore, review, and diff operations. Restoring copies a selected backup over the current profile’s lockfile; it does not change package checkouts. After reviewing a restored lockfile, run `M-x straight-thaw-versions` explicitly to restore its pinned checkouts. The review command opens Magit rather than creating an automatic, timestamp-only commit.

;; Version control integration functions
(defun my/straight-lockfile ()
  "Return the lockfile for `straight-current-profile'."
  (straight--versions-lockfile straight-current-profile))

(defun my/straight-lockfile-backup-directory ()
  "Return the backup directory dedicated to `straight-current-profile'."
  (expand-file-name (format "versions/backups/%s" straight-current-profile)
                    (straight--dir)))

(defun my/straight-backup-lockfile ()
  "Create a timestamped backup of the current profile's lockfile."
  (interactive)
  (condition-case err
      (let* ((lockfile (my/straight-lockfile))
             (backup-dir (my/straight-lockfile-backup-directory))
             (timestamp (format-time-string "%Y%m%d-%H%M%S"))
             (backup-file (expand-file-name (format "lockfile-%s.el" timestamp) backup-dir)))
        (unless (file-directory-p backup-dir)
          (make-directory backup-dir t))
        (if (file-exists-p lockfile)
            (progn
              (copy-file lockfile backup-file)
              (message "Lockfile backed up to: %s" backup-file))
          (message "No lockfile found to backup")))
    (error
     (message "Error backing up lockfile: %s" (error-message-string err)))))

(defun my/straight-restore-lockfile ()
  "Restore the current profile's lockfile from one of its backups."
  (interactive)
  (condition-case err
      (let* ((backup-dir (my/straight-lockfile-backup-directory))
             (lockfile (my/straight-lockfile)))
        (if (file-directory-p backup-dir)
            (let* ((backups (directory-files backup-dir nil "lockfile-.*\\.el$"))
                   (backup (when backups
                            (completing-read "Restore from backup: " backups nil t))))
              (when backup
                (let ((backup-path (expand-file-name backup backup-dir)))
                  (when (yes-or-no-p (format "Restore lockfile from %s? This will overwrite current lockfile." backup))
                    (copy-file backup-path lockfile t)
                    (message "Lockfile restored from: %s" backup)))))
          (message "No backup directory found")))
    (error
     (message "Error restoring lockfile: %s" (error-message-string err)))))

(defun my/straight-commit-lockfile ()
  "Open the current profile's lockfile repository in Magit for review and commit."
  (interactive)
  (let ((lockfile (my/straight-lockfile)))
    (if (file-exists-p lockfile)
        (progn
          (require 'magit-status)
          (let ((repository-root (magit-toplevel (file-name-directory lockfile))))
            (if repository-root
                (magit-status repository-root)
              (user-error "Lockfile is not in a Git repository: %s" lockfile))))
      (message "Lockfile does not exist: %s" lockfile))))

(defun my/straight-diff-lockfile ()
  "View changes in the current profile's lockfile compared to the last commit."
  (interactive)
  (condition-case err
      (let* ((lockfile (my/straight-lockfile))
             (backend (and (file-exists-p lockfile) (vc-backend lockfile)))
             (default-directory (file-name-directory lockfile)))
        (if backend
            (vc-diff nil t (list backend (list lockfile)))
          (message "Lockfile not under version control or doesn't exist")))
    (error
     (message "Error viewing lockfile diff: %s" (error-message-string err)))))

Transient Menu Interface

A magit-style transient menu that provides organized access to all package management functions.

;; Transient menu for package management
(defun my/straight-transient ()
  "Open the package management transient menu."
  (interactive)
  (transient-setup 'my/straight-transient))

(transient-define-prefix my/straight-transient ()
  "Package management with straight.el"
  :info-manual "(straight) Top"
  [["Actions"
    ("f" "Freeze versions" my/straight-freeze-versions)
    ("u" "Update all packages" my/straight-update-all)
    ("r" "Rebuild package" my/straight-rebuild-package)]
   ["Maintenance"
    ("c" "Check modified packages" my/straight-check-modifications)
    ("p" "Prune build artifacts" my/straight-prune-build)
    ("n" "Normalize repositories" my/straight-normalize-all)]
   ["Version Control"
    ("b" "Backup lockfile" my/straight-backup-lockfile)
    ("B" "Restore lockfile" my/straight-restore-lockfile)
    ("C" "Review lockfile in Magit" my/straight-commit-lockfile)
    ("d" "Diff lockfile" my/straight-diff-lockfile)]
   ["Quit"
    ("q" "Quit" transient-quit-one)]])

Keybindings

All package management functions are bound under the `C-c p` prefix for easy access.

;; Package management keybindings (C-c p prefix)
(define-prefix-command 'my/package-map)
(global-set-key (kbd "C-c p") 'my/package-map)
(define-key my/package-map (kbd "m") #'my/straight-transient)        ; Main transient menu
(define-key my/package-map (kbd "f") #'my/straight-freeze-versions)  ; Freeze versions
(define-key my/package-map (kbd "u") #'my/straight-update-all)       ; Update all packages
(define-key my/package-map (kbd "c") #'my/straight-check-modifications) ; Check modified packages
(define-key my/package-map (kbd "p") #'my/straight-prune-build)      ; Prune build artifacts
(define-key my/package-map (kbd "r") #'my/straight-rebuild-package)  ; Rebuild package
(define-key my/package-map (kbd "b") #'my/straight-backup-lockfile)  ; Backup lockfile
(define-key my/package-map (kbd "R") #'my/straight-restore-lockfile) ; Restore lockfile
(define-key my/package-map (kbd "C") #'my/straight-commit-lockfile)  ; Review lockfile in Magit
(define-key my/package-map (kbd "d") #'my/straight-diff-lockfile)    ; Diff lockfile

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Contributors

Languages