Skip to content

SeanMooney/emacs

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. All configuration is tangled into `init.el` upon saving this file.

Early Init

This section contains critical setup that must run at the very beginning of the Emacs startup process, before any packages are loaded.

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 Management (straight.el)

I use `straight.el` for package management, which works directly with Git repositories. It is paired with `use-package` for declarative configuration. This block bootstraps `straight.el` if it’s not already installed.

(setq straight-use-package-by-default t)
;; Ensure we can get packages from nongnu.org
(custom-set-variables
 '(straight-recipe-overrides '((nil
                              (nongnu-elpa :type git
                                           :repo "https://github.com/emacsmirror/nongnu_elpa"
                                           :depth (full single-branch)
                                           :local-repo "nongnu-elpa"
                                           :build nil)))))

(defvar bootstrap-version)
(let ((bootstrap-file
       (expand-file-name "straight/repos/straight.el/bootstrap.el"
                         (or (bound-and-true-p straight-base-dir)
                             user-emacs-directory)))
      (bootstrap-version 7))
  (unless (file-exists-p bootstrap-file)
    (with-current-buffer
        (url-retrieve-synchronously
         "https://raw.githubusercontent.com/radian-software/straight.el/develop/install.el"
         'silent 'inhibit-cookies)
        (goto-char (point-max))
        (eval-print-last-sexp)))
  (load bootstrap-file nil 'nomessage))

(straight-use-package 'use-package)
(setq use-package-always-ensure t)

;; 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))

Early Org Mode Setup

This configures Org mode basics, especially the function to automatically tangle this configuration file (`lit.org` -> `init.el`) every time it is saved.

(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)
                 (string-equal (file-name-directory (buffer-file-name))
                               (expand-file-name user-emacs-directory)))
        ;; Dynamic scoping to the rescue
        (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))
  :config
  (move-text-default-bindings))

;; 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.

(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))))
  ;; Increase the amount of history stored
  (undo-tree-buffer-size-limit (* 1024 1024 8)) ; 8MB
  (undo-tree-max-history-size 1000)
  :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. My default is `Source Code Pro` for code and `FiraGO` for proportional text.

(use-package fontaine
  :demand t
  :init
  (setq fontaine-latest-state-file
        (locate-user-emacs-file "fontaine-latest-state.eld"))
  (setq fontaine-presets
        '((small
           :default-height 90)
          (regular
           :default-height 120)
          (medium
           :default-weight semilight
           :default-height 140)
          (large
           :default-weight semilight
           :default-height 180
           :bold-weight extrabold)
          (dyslexia-friendly
           :default-family "OpenDyslexic"
           :variable-pitch-family "OpenDyslexic"
           :default-height 130
           :variable-pitch-height 1.1)
          (t ; our shared fallback properties
           :default-family "Source Code Pro"
           :default-weight semilight
           :default-height 100
           :variable-pitch-family "FiraGO"
           :variable-pitch-weight normal
           :variable-pitch-height 1.05
           :bold-weight bold
           :italic-slant italic)))
  :bind ("C-c f" . fontaine-set-preset))

;; 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* ((other-win (next-window))
             ;; Is the split vertical? (i.e. do windows share a left edge)
             (is-vertical-split (= (nth 0 (window-edges))
                                   (nth 0 (window-edges other-win)))))
        ;; Delete the other window, which collapses the split
        (delete-other-windows)
        ;; And re-split in the other direction
        (if is-vertical-split
            (split-window-horizontally)
          (split-window-vertically)))
    (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)
    ;; Disable corfu in modes where it's disruptive
    (corfu-mode-modes '(not eshell-mode shell-mode term-mode))
    :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
(use-package ranger
  :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)
  (ranger-show-dotfiles t)
  (ranger-preview-file t)             ; Show file previews
  (ranger-max-preview-size 10)        ; Limit preview to 10MB files
  :config
  ;; Don't show hidden files by default (toggle with zh)
  (setq ranger-show-hidden nil))

;; 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
  :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 (required for claude-code)
(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 ()
  "Display only the current project in treemacs, removing all others."
  (when-let* ((project (project-current))
              (project-root (project-root project)))
    (treemacs-block-refresh
      ;; Remove all projects from workspace
      (dolist (proj (treemacs-workspace->projects (treemacs-current-workspace)))
        (treemacs-do-remove-project-from-workspace proj t))
      ;; Add and display only the current project
      (treemacs-add-and-display-current-project-exclusively))))

;; Advice to automatically update treemacs when switching projects
(defun my/treemacs-project-switch-advice (&rest _args)
  "Advice function to update treemacs when switching projects."
  (when (and (featurep 'treemacs)
             (treemacs-current-workspace))
    (run-with-idle-timer 0.1 nil #'my/treemacs-display-current-project-exclusively)))

;; 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))))

  ;; Prompt to install fonts on non-NixOS systems
  (unless (or (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)
  :custom
  (writegood-weasel-words-length 5))

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-temp-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-temp-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.

;; `flymake` is the built-in alternative.
;; I bind keys for navigating its diagnostics.
(use-package flymake
  :ensure nil
  :bind (:map flymake-mode-map
         ("C-c n" . flymake-goto-next-error)
         ("C-c N" . flymake-goto-prev-error)))

;; `eglot` is a minimal, built-in LSP client.
(use-package eglot
  :ensure nil
  :hook ((python-mode . eglot-ensure)
         (python-ts-mode . eglot-ensure)
         (js-mode . eglot-ensure)
         (typescript-mode . eglot-ensure)
         (zig-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 zig nix))
  (global-treesit-auto-mode))

Language: Python

This section configures the Python development environment, including virtual environment management with `pyvenv` and linting with `ruff`.

(use-package pyvenv
  :config
  (pyvenv-mode 1)
  ;; Set correct Python interpreter when a virtual env is activated/deactivated.
  (setq pyvenv-post-activate-hooks
        (list (lambda ()
                (setq python-shell-interpreter (concat pyvenv-virtual-env "bin/python3")))))
  (setq pyvenv-post-deactivate-hooks
        (list (lambda ()
                (setq python-shell-interpreter "python3")))))

(use-package python
  :ensure nil
  :hook (python-mode . (lambda ()
                        (setq-local tab-width 4)
                        (setq-local python-indent-offset 4)))
  :custom
  ;; Use the fast and powerful `ruff` linter for checking Python code.
  (python-check-command "ruff check --ignore-noqa"))

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.

(use-package zig-mode
  :hook (zig-mode . (lambda ()
                     (setq-local tab-width 4)
                     (setq-local indent-tabs-mode nil))))

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, eat 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)
    (eat-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 using project-appropriate command based on detected project type."
  (interactive)
  (let* ((project-root (project-root (project-current)))
         (compile-cmd (cond
                      ((file-exists-p (expand-file-name "Makefile" project-root)) "make")
                      ((file-exists-p (expand-file-name "package.json" project-root)) "npm run build")
                      ((file-exists-p (expand-file-name "Cargo.toml" project-root)) "cargo build")
                      ((file-exists-p (expand-file-name "pyproject.toml" project-root)) "python -m build")
                      ((file-exists-p (expand-file-name "CMakeLists.txt" project-root)) "cmake --build build")
                      ((file-exists-p (expand-file-name "flake.nix" project-root)) "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"))))

(use-package project
  :ensure nil
  :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 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)
     (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))))

GPT & AI

gptel

Configuration for `gptel`, a client for interacting with Large Language Models. The default backend is Z.AI (Zhipu AI GLM-5) with a local Ollama instance as a fallback.

(use-package gptel
  :custom
  (gptel-default-mode 'org-mode)
  (epa-pinentry-mode 'loopback)
  :config
  ;; Define Z.AI (Zhipu AI) backend using OpenAI-compatible API
  ;; API key is read securely from ~/.authinfo.gpg via auth-source
  (setq gptel-backend (gptel-make-openai "Z.AI"
                        :host "api.z.ai"
                        :endpoint "/api/coding/paas/v4/chat/completions"
                        :stream t
                        :key #'gptel-api-key-from-auth-source
                        :models '(glm-4.7
                                  glm-5
                                  glm-4.7-flash))
        gptel-model 'glm-4.7)

  ;; Keep local Ollama as a secondary backend (switchable via C-c RET b)
  (let ((ollama-host (or (getenv "OLLAMA_HOST") "192.168.16.172:11434")))
    (gptel-make-ollama "Ollama"
      :host ollama-host
      :stream t
      :models '("hf.co/unsloth/gemma-3-4b-it-qat-GGUF:UD-Q8_K_XL"
                "hf.co/unsloth/DeepSeek-R1-0528-Qwen3-8B-GGUF:UD-Q4_K_XL"
                "hf.co/unsloth/Magistral-Small-2506-GGUF:Q3_K_XL"
                "omaciel/ticketeer-granite3.3"
                "hf.co/unsloth/GLM-Z1-9B-0414-GGUF:Q5_K_XL")))

  ;; Add error handling for unavailable backends
  (defun my/gptel-check-backend ()
    "Check if gptel backend is available and provide feedback."
    (condition-case err
        (gptel--model-capable-p 'stream)
      (error
       (message "GPTel backend unavailable: %s" (error-message-string err))
       nil)))
  (add-hook 'gptel-pre-request-hook #'my/gptel-check-backend))
(require 'gptel-integrations)

Integrating the Model Context Protocol (MCP)

This configures Emacs as a client for the Model Context Protocol (MCP), allowing gptel to automatically pull in context from external sources like the project’s file system (a simple RAG setup). This provides the language model with relevant information about the project you’re working on.

(use-package mcp
  :straight (mcp :type git :host github :repo "lizqwerscott/mcp.el")
  :after gptel
  :custom
  (mcp-hub-servers
   `(;; 1. A Filesystem Server (with error checking)
     ;; This server exposes the root directory of your current project to the LLM.
     ;; Only enabled if npx is available.
     ,@(when (executable-find "npx")
         `(("filesystem" . (:command "npx"
                           :args ("-y" "@modelcontextprotocol/server-filesystem"
                                  ,(or (ignore-errors (project-root (project-current)))
                                       default-directory))))))

     ;; 2. A Fetch Server (with error checking)
     ;; This server can fetch content from URLs.
     ;; Only enabled if uvx is available.
     ,@(when (executable-find "uvx")
         `(("fetch" . (:command "uvx" :args ("mcp-server-fetch")))))))
  :hook (after-init . (lambda ()
                        (when mcp-hub-servers
                          (condition-case err
                              (mcp-hub-start-all-server)
                            (error
                             (message "MCP servers failed to start: %s" (error-message-string err)))))))
  :config
  ;; Load the hub functionality and tell gptel to use MCP as a context provider.
  (require 'mcp-hub))

claude-code

(use-package claude-code
  :straight (:type git :host github :repo "stevemolitor/claude-code.el" :branch "main"
             :files ("*.el" (:exclude "demo.gif")))
  :bind-keymap
  ("C-c a" . claude-code-command-map)  ; Choose your preferred prefix
  :custom
  ;; Configure to use claude installation from PATH
  (claude-code-program (or (executable-find "claude")
                           (executable-find "claude-code")
                           "claude"))
  :config
  (claude-code-mode)
  ;; Configure for your wide screen setup
  (add-to-list 'display-buffer-alist
               '("^\\*claude"
                 (display-buffer-in-side-window)
                 (side . right)
                 (window-width . 0.33))))

Custom Commands & Bindings

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

  ;; Helper for AI functions to display response in a dedicated buffer
  (defun my/gptel--display-response (response info)
    "Display gptel RESPONSE in a dedicated buffer.
INFO is the plist of request metadata."
    (if (stringp response)
        (with-current-buffer (get-buffer-create "*gptel-response*")
          (erase-buffer)
          (insert response)
          (goto-char (point-min))
          (display-buffer (current-buffer)))
      (message "gptel request failed: %s" (plist-get info :status))))

  ;; AI-enhanced development commands
  (defun my/explain-code-with-ai ()
    "Explain selected code using AI."
    (interactive)
    (if (region-active-p)
        (gptel-request (concat "Explain this code:\n\n"
                               (buffer-substring-no-properties
                                (region-beginning) (region-end)))
          :callback #'my/gptel--display-response)
      (message "Please select code to explain")))

  (defun my/review-code-with-ai ()
    "Review selected code for improvements."
    (interactive)
    (if (region-active-p)
        (gptel-request (concat "Review this code for best practices and suggest improvements:\n\n"
                               (buffer-substring-no-properties
                                (region-beginning) (region-end)))
          :callback #'my/gptel--display-response)
      (message "Please select code to review")))

  (defun my/document-code-with-ai ()
    "Generate documentation for selected code using AI."
    (interactive)
    (if (region-active-p)
        (gptel-request (concat "Generate documentation for this code:\n\n"
                               (buffer-substring-no-properties
                                (region-beginning) (region-end)))
          :callback #'my/gptel--display-response)
      (message "Please select code to document")))

  ;; AI-enhanced writing assistance functions
  (defun my/improve-writing-with-ai ()
    "Improve selected text for clarity, grammar, and style using AI."
    (interactive)
    (if (region-active-p)
        (gptel-request (concat "Improve this text for clarity, grammar, and style. "
                               "Keep the meaning and intent intact:\n\n"
                               (buffer-substring-no-properties
                                (region-beginning) (region-end)))
          :callback #'my/gptel--display-response)
      (message "Please select text to improve")))

  (defun my/adjust-tone-with-ai (tone)
    "Adjust the tone of selected text (professional, casual, academic, friendly)."
    (interactive "sTone (professional/casual/academic/friendly): ")
    (if (region-active-p)
        (gptel-request (concat (format "Rewrite this text in a %s tone while keeping the core message:\n\n" tone)
                               (buffer-substring-no-properties
                                (region-beginning) (region-end)))
          :callback #'my/gptel--display-response)
      (message "Please select text to adjust tone")))

  (defun my/summarize-text-with-ai ()
    "Summarize selected text using AI."
    (interactive)
    (if (region-active-p)
        (gptel-request (concat "Provide a clear, concise summary of this text:\n\n"
                               (buffer-substring-no-properties
                                (region-beginning) (region-end)))
          :callback #'my/gptel--display-response)
      (message "Please select text to summarize")))

  (defun my/expand-text-with-ai ()
    "Expand selected text with more detail using AI."
    (interactive)
    (if (region-active-p)
        (gptel-request (concat "Expand this text with more detail and examples while maintaining clarity:\n\n"
                               (buffer-substring-no-properties
                                (region-beginning) (region-end)))
          :callback #'my/gptel--display-response)
      (message "Please select text to expand")))

  (defun my/proofread-with-ai ()
    "Proofread selected text for grammar, spelling, and style issues using AI."
    (interactive)
    (if (region-active-p)
        (gptel-request (concat "Proofread this text for grammar, spelling, punctuation, and style issues. "
                               "Provide specific corrections and explanations:\n\n"
                               (buffer-substring-no-properties
                                (region-beginning) (region-end)))
          :callback #'my/gptel--display-response)
      (message "Please select text to proofread")))

  ;; 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 ()
    "Comment or uncomment current line or region without moving cursor."
    (interactive)
    (condition-case err
        (if (region-active-p)
            ;; If region is selected, comment/uncomment the region
            (comment-or-uncomment-region (region-beginning) (region-end))
          ;; If no region, comment/uncomment current line without moving cursor
          (save-excursion
            (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)

  ;; AI-enhanced development keybindings (C-c g prefix for all AI functions)
  (global-set-key (kbd "C-c g e") #'my/explain-code-with-ai)
  (global-set-key (kbd "C-c g r") #'my/review-code-with-ai)
  (global-set-key (kbd "C-c g d") #'my/document-code-with-ai)

  ;; AI-enhanced writing keybindings (C-c g prefix)
  (global-set-key (kbd "C-c g i") #'my/improve-writing-with-ai)     ; Improve text
  (global-set-key (kbd "C-c g t") #'my/adjust-tone-with-ai)        ; Adjust tone
  (global-set-key (kbd "C-c g s") #'my/summarize-text-with-ai)     ; Summarize
  (global-set-key (kbd "C-c g x") #'my/expand-text-with-ai)        ; Expand text
  (global-set-key (kbd "C-c g p") #'my/proofread-with-ai)          ; Proofread

  ;; 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 advanced package management functions for straight.el, including health checking, 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 straight/versions/default.el"))
    (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 Health & Maintenance

Functions for checking package health, cleaning up build artifacts, and maintaining package repositories.

;; Package health and maintenance functions
(defun my/straight-check-all ()
  "Check for broken or problematic packages."
  (interactive)
  (condition-case err
      (progn
        (message "Checking package health...")
        (let ((issues '()))
          ;; Check for missing repositories
          (dolist (package (hash-table-keys straight--recipe-cache))
            (let ((repo-dir (straight--repos-dir (symbol-name package))))
              (unless (file-directory-p repo-dir)
                (push (format "Missing repository: %s" package) issues))))

          ;; Check for build issues
          (dolist (package (hash-table-keys straight--recipe-cache))
            (let ((build-dir (straight--build-dir (symbol-name package))))
              (when (and (file-directory-p build-dir)
                        (= 0 (length (directory-files build-dir nil "\\.el\\'"))))
                (push (format "Empty build directory: %s" package) issues))))

          (if issues
              (with-current-buffer (get-buffer-create "*Package Health*")
                (erase-buffer)
                (insert "Package Health Issues:\n\n")
                (dolist (issue issues)
                  (insert "" issue "\n"))
                (display-buffer (current-buffer))
                (message "Found %d package issues - see *Package Health* buffer" (length issues)))
            (message "All packages appear healthy"))))
    (error
     (message "Error checking package health: %s" (error-message-string err)))))

(defun my/straight-prune-build ()
  "Clean up unused build artifacts."
  (interactive)
  (condition-case err
      (progn
        (message "Pruning build artifacts...")
        (let ((pruned 0))
          (dolist (build-dir (directory-files (straight--build-dir) t))
            (when (and (file-directory-p build-dir)
                      (not (member (file-name-nondirectory build-dir) '("." "..")))
                      (not (gethash (intern (file-name-nondirectory build-dir))
                                   straight--recipe-cache)))
              (delete-directory build-dir t)
              (setq pruned (1+ pruned))))
          (message "Pruned %d unused build directories" pruned)))
    (error
     (message "Error pruning build artifacts: %s" (error-message-string err)))))

(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 ()
  "Rebuild a specific package interactively."
  (interactive)
  (condition-case err
      (let* ((packages (hash-table-keys straight--recipe-cache))
             (package (intern (completing-read "Rebuild package: "
                                             (mapcar #'symbol-name packages)))))
        (message "Rebuilding package: %s" package)
        (straight-rebuild-package package)
        (message "Package %s rebuilt successfully" package))
    (error
     (message "Error rebuilding package: %s" (error-message-string err)))))

Version Control Integration

Functions for managing lockfile versions, including backup, restore, commit, and diff operations.

;; Version control integration functions
(defun my/straight-backup-lockfile ()
  "Create a timestamped backup of the current lockfile."
  (interactive)
  (condition-case err
      (let* ((lockfile (straight--versions-lockfile))
             (backup-dir (expand-file-name "versions/backups" (straight--dir)))
             (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 from a previous lockfile backup."
  (interactive)
  (condition-case err
      (let* ((backup-dir (expand-file-name "versions/backups" (straight--dir)))
             (lockfile (straight--versions-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))))
              (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 ()
  "Commit lockfile changes with a descriptive message."
  (interactive)
  (condition-case err
      (let* ((lockfile (straight--versions-lockfile))
             (default-directory user-emacs-directory))
        (if (and (file-exists-p lockfile)
                 (vc-backend lockfile))
            (let ((commit-msg (format "Pin package versions (%s)"
                                    (format-time-string "%Y-%m-%d %H:%M"))))
              (vc-checkin (list lockfile) nil commit-msg)
              (message "Lockfile committed: %s" commit-msg))
          (message "Lockfile not under version control or doesn't exist")))
    (error
     (message "Error committing lockfile: %s" (error-message-string err)))))

(defun my/straight-diff-lockfile ()
  "View changes in the lockfile compared to the last commit."
  (interactive)
  (condition-case err
      (let* ((lockfile (straight--versions-lockfile))
             (default-directory user-emacs-directory))
        (if (and (file-exists-p lockfile)
                 (vc-backend lockfile))
            (vc-diff nil t (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 package health" my/straight-check-all)
    ("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" "Commit lockfile" 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-all)        ; Check package health
(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)  ; Commit lockfile
(define-key my/package-map (kbd "d") #'my/straight-diff-lockfile)    ; Diff lockfile

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Contributors 2

  •  
  •